diff --git a/.circleci/config.yml b/.circleci/config.yml index e7566bd764b..40076c3c7f6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -532,7 +532,7 @@ jobs: command: | pwd ls - python -m pytest -vv tests/router_unit_tests --cov=litellm --cov-report=xml -x -s -v --junitxml=test-results/junit.xml --durations=5 + python -m pytest -vv tests/router_unit_tests --cov=litellm --cov-report=xml -x -s --junitxml=test-results/junit.xml --durations=5 no_output_timeout: 120m - run: name: Rename the coverage files @@ -1164,7 +1164,7 @@ jobs: command: | pwd ls - python -m pytest -vv tests/test_litellm --cov=litellm --cov-report=xml -s -v --junitxml=test-results/junit-litellm.xml --durations=10 -n 8 + python -m pytest -vv tests/test_litellm --cov=litellm --cov-report=xml -v --junitxml=test-results/junit-litellm.xml --durations=10 -n 8 no_output_timeout: 120m - run: name: Rename the coverage files @@ -1396,7 +1396,7 @@ jobs: command: | pwd ls - python -m pytest -vv tests/image_gen_tests --cov=litellm --cov-report=xml -x -s -v --junitxml=test-results/junit.xml --durations=5 + python -m pytest -vv tests/image_gen_tests --cov=litellm --cov-report=xml -x -v --junitxml=test-results/junit.xml --durations=5 no_output_timeout: 120m - run: name: Rename the coverage files diff --git a/.circleci/requirements.txt b/.circleci/requirements.txt index 8e0f1dfe7e9..2294c84813c 100644 --- a/.circleci/requirements.txt +++ b/.circleci/requirements.txt @@ -15,4 +15,5 @@ fastapi-sso==0.16.0 uvloop==0.21.0 mcp==1.10.1 # for MCP server semantic_router==0.1.10 # for auto-routing with litellm -fastuuid==0.12.0 \ No newline at end of file +fastuuid==0.12.0 +responses==0.25.7 # for proxy client tests \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6ab78d85e33..d9ea0d9a471 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,6 +65,10 @@ COPY --from=builder /wheels/ /wheels/ # Install the built wheel using pip; again using a wildcard if it's the only file RUN pip install *.whl /wheels/* --no-index --find-links=/wheels/ && rm -f *.whl && rm -rf /wheels +# Remove test files and keys from dependencies +RUN find /usr/lib -type f -path "*/tornado/test/*" -delete && \ + find /usr/lib -type d -path "*/tornado/test" -delete + # Install semantic_router and aurelio-sdk using script RUN chmod +x docker/install_auto_router.sh && ./docker/install_auto_router.sh diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 2e886915203..f95f540a7a5 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -57,6 +57,9 @@ USER root # Install only runtime dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ libssl3 \ + libatomic1 \ + nodejs \ + npm \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index 6be115ba71b..0cbdf761fe8 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -69,6 +69,10 @@ RUN pip install *.whl /wheels/* --no-index --find-links=/wheels/ \ && rm -f *.whl \ && rm -rf /wheels +# Remove test files and keys from dependencies +RUN find /usr/lib -type f -path "*/tornado/test/*" -delete && \ + find /usr/lib -type d -path "*/tornado/test" -delete + # Install semantic_router and aurelio-sdk using script RUN chmod +x docker/install_auto_router.sh && ./docker/install_auto_router.sh diff --git a/docs/my-website/docs/benchmarks.md b/docs/my-website/docs/benchmarks.md index 40285b63d71..f00732450d1 100644 --- a/docs/my-website/docs/benchmarks.md +++ b/docs/my-website/docs/benchmarks.md @@ -48,47 +48,6 @@ In these tests the baseline latency characteristics are measured against a fake- - High-percentile latencies drop significantly: P95 630 ms → 150 ms, P99 1,200 ms → 240 ms. - Setting workers equal to CPU count gives optimal performance. -## LiteLLM vs Portkey Performance Comparison - -**Test Configuration**: 4 CPUs, 8 GB RAM per instance | Load: 1k concurrent users, 500 ramp-up - -### Multi-Instance (4×) Performance - -| Metric | Portkey (no DB) | LiteLLM (with DB) | -| ------------------- | --------------- | ----------------- | -| **Total Requests** | 293,796 | 312,405 | -| **Failed Requests** | 0 | 0 | -| **Median Latency** | 100 ms | 100 ms | -| **p95 Latency** | 230 ms | 150 ms | -| **p99 Latency** | 500 ms | 240 ms | -| **Average Latency** | 123 ms | 111 ms | -| **Current RPS** | 1,170.9 | 1,170 | - -### Technical Insights - -**Portkey** - -**Pros** - -* Low memory footprint -* Stable latency with minimal spikes - -**Cons** - -* CPU utilization capped around ~40%, indicating underutilization of available compute resources -* Experienced three I/O timeout outages - -**LiteLLM** - -**Pros** - -* Fully utilizes available CPU capacity -* Strong connection handling and low latency after initial warm-up spikes - -**Cons** - -* High memory usage during initialization and per request - ## Machine Spec used for testing Each machine deploying LiteLLM had the following specs: @@ -163,6 +122,48 @@ class MyUser(HttpUser): ``` +## LiteLLM vs Portkey Performance Comparison + +**Test Configuration**: 4 CPUs, 8 GB RAM per instance | Load: 1k concurrent users, 500 ramp-up + +### Multi-Instance (4×) Performance + +| Metric | Portkey (no DB) | LiteLLM (with DB) | +| ------------------- | --------------- | ----------------- | +| **Total Requests** | 293,796 | 312,405 | +| **Failed Requests** | 0 | 0 | +| **Median Latency** | 100 ms | 100 ms | +| **p95 Latency** | 230 ms | 150 ms | +| **p99 Latency** | 500 ms | 240 ms | +| **Average Latency** | 123 ms | 111 ms | +| **Current RPS** | 1,170.9 | 1,170 | + +### Technical Insights + +**Portkey** + +**Pros** + +* Low memory footprint +* Stable latency with minimal spikes + +**Cons** + +* CPU utilization capped around ~40%, indicating underutilization of available compute resources +* Experienced three I/O timeout outages + +**LiteLLM** + +**Pros** + +* Fully utilizes available CPU capacity +* Strong connection handling and low latency after initial warm-up spikes + +**Cons** + +* High memory usage during initialization and per request + + ## Logging Callbacks diff --git a/docs/my-website/docs/completion/image_generation_chat.md b/docs/my-website/docs/completion/image_generation_chat.md index 58ae70e2fff..98b718ef4ce 100644 --- a/docs/my-website/docs/completion/image_generation_chat.md +++ b/docs/my-website/docs/completion/image_generation_chat.md @@ -15,16 +15,22 @@ Supported Providers: - Google AI Studio (`gemini`) - Vertex AI (`vertex_ai/`) -LiteLLM will standardize the `image` response in the assistant message for models that support image generation during chat completions. +LiteLLM will standardize the `images` response in the assistant message for models that support image generation during chat completions. ```python title="Example response from litellm" "message": { ... "content": "Here's the image you requested:", - "image": { - "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", - "detail": "auto" - } + "images": [ + { + "image_url": { + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "detail": "auto" + }, + "index": 0, + "type": "image_url" + } + ] } ``` @@ -47,7 +53,7 @@ response = completion( ) print(response.choices[0].message.content) # Text response -print(response.choices[0].message.image) # Image data +print(response.choices[0].message.images) # List of image objects ``` @@ -103,10 +109,16 @@ curl http://0.0.0.0:4000/v1/chat/completions \ "message": { "content": "Here's the image you requested:", "role": "assistant", - "image": { - "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", - "detail": "auto" - } + "images": [ + { + "image_url": { + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "detail": "auto" + }, + "index": 0, + "type": "image_url" + } + ] } } ], @@ -141,8 +153,8 @@ response = completion( ) for chunk in response: - if hasattr(chunk.choices[0].delta, "image") and chunk.choices[0].delta.image is not None: - print("Generated image:", chunk.choices[0].delta.image["url"]) + if hasattr(chunk.choices[0].delta, "images") and chunk.choices[0].delta.images is not None: + print("Generated image:", chunk.choices[0].delta.images[0]["image_url"]["url"]) break ``` @@ -175,7 +187,7 @@ data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1723323084 data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1723323084,"model":"gemini/gemini-2.5-flash-image-preview","choices":[{"index":0,"delta":{"content":"Here's the image you requested:"},"finish_reason":null}]} -data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1723323084,"model":"gemini/gemini-2.5-flash-image-preview","choices":[{"index":0,"delta":{"image":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...","detail":"auto"}},"finish_reason":null}]} +data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1723323084,"model":"gemini/gemini-2.5-flash-image-preview","choices":[{"index":0,"delta":{"images":[{"image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...","detail":"auto"},"index":0,"type":"image_url"}]},"finish_reason":null}]} data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1723323084,"model":"gemini/gemini-2.5-flash-image-preview","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} @@ -200,8 +212,8 @@ async def generate_image(): ) print(response.choices[0].message.content) # Text response - print(response.choices[0].message.image) # Image data - + print(response.choices[0].message.images) # List of image objects + return response # Run the async function @@ -215,18 +227,28 @@ asyncio.run(generate_image()) | Google AI Studio | `gemini/gemini-2.5-flash-image-preview` | | Vertex AI | `vertex_ai/gemini-2.5-flash-image-preview` | -## Spec +## Spec -The `image` field in the response follows this structure: +The `images` field in the response follows this structure: ```python -"image": { - "url": "data:image/png;base64,", - "detail": "auto" -} +"images": [ + { + "image_url": { + "url": "data:image/png;base64,", + "detail": "auto" + }, + "index": 0, + "type": "image_url" + } +] ``` -- `url` - str: Base64 encoded image data in data URI format -- `detail` - str: Image detail level (always "auto" for generated images) +- `images` - List[ImageURLListItem]: Array of generated images + - `image_url` - ImageURLObject: Container for image data + - `url` - str: Base64 encoded image data in data URI format + - `detail` - str: Image detail level (always "auto" for generated images) + - `index` - int: Index of the image in the response + - `type` - str: Type identifier (always "image_url") -The image is returned as a base64-encoded data URI that can be directly used in HTML `` tags or saved to a file. +The images are returned as base64-encoded data URIs that can be directly used in HTML `` tags or saved to files. diff --git a/docs/my-website/docs/exception_mapping.md b/docs/my-website/docs/exception_mapping.md index 2342f444e17..efdada2a1eb 100644 --- a/docs/my-website/docs/exception_mapping.md +++ b/docs/my-website/docs/exception_mapping.md @@ -112,6 +112,85 @@ except openai.APITimeoutError as e: print(f"should_retry: {should_retry}") ``` +## Advanced + +### Accessing Provider-Specific Error Details + +LiteLLM exceptions include a `provider_specific_fields` attribute that contains additional error information specific to each provider. This is particularly useful for Azure OpenAI, which provides detailed content filtering information. + +#### Azure OpenAI - Content Policy Violation Inner Error Access + +When Azure OpenAI returns content policy violations, you can access the detailed content filtering results through the `innererror` field: + +```python +import litellm +from litellm.exceptions import ContentPolicyViolationError + +try: + response = litellm.completion( + model="azure/gpt-4", + messages=[ + { + "role": "user", + "content": "Some content that might violate policies" + } + ] + ) +except ContentPolicyViolationError as e: + # Access Azure-specific error details + if e.provider_specific_fields and "innererror" in e.provider_specific_fields: + innererror = e.provider_specific_fields["innererror"] + + # Access content filter results + content_filter_result = innererror.get("content_filter_result", {}) + + print(f"Content filter code: {innererror.get('code')}") + print(f"Hate filtered: {content_filter_result.get('hate', {}).get('filtered')}") + print(f"Violence severity: {content_filter_result.get('violence', {}).get('severity')}") + print(f"Sexual content filtered: {content_filter_result.get('sexual', {}).get('filtered')}") +``` + +**Example Response Structure:** + +When calling the LiteLLM proxy, content policy violations will return detailed filtering information: + +```json +{ + "error": { + "message": "litellm.ContentPolicyViolationError: AzureException - The response was filtered due to the prompt triggering Azure OpenAI's content management policy...", + "type": null, + "param": null, + "code": "400", + "provider_specific_fields": { + "innererror": { + "code": "ResponsibleAIPolicyViolation", + "content_filter_result": { + "hate": { + "filtered": true, + "severity": "high" + }, + "jailbreak": { + "filtered": false, + "detected": false + }, + "self_harm": { + "filtered": false, + "severity": "safe" + }, + "sexual": { + "filtered": false, + "severity": "safe" + }, + "violence": { + "filtered": true, + "severity": "medium" + } + } + } + } + } +} + ## Details To see how it's implemented - [check out the code](https://github.com/BerriAI/litellm/blob/a42c197e5a6de56ea576c73715e6c7c6b19fa249/litellm/utils.py#L1217) diff --git a/docs/my-website/docs/moderation.md b/docs/my-website/docs/moderation.md index f9c2810bc8a..1f67b0a7543 100644 --- a/docs/my-website/docs/moderation.md +++ b/docs/my-website/docs/moderation.md @@ -22,10 +22,19 @@ response = moderation( For `/moderations` endpoint, there is **no need to specify `model` in the request or on the litellm config.yaml** -Start litellm proxy server + +1. Setup config.yaml +```yaml +model_list: + - model_name: text-moderation-stable + litellm_params: + model: openai/omni-moderation-latest +``` + +2. Start litellm proxy server ``` -litellm +litellm --config /path/to/config.yaml ``` @@ -41,7 +50,7 @@ client = OpenAI(api_key="", base_url="http://0.0.0.0:4000") response = client.moderations.create( input="hello from litellm", - model="text-moderation-stable" # optional, defaults to `omni-moderation-latest` + model="text-moderation-stable" ) print(response) diff --git a/docs/my-website/docs/observability/datadog.md b/docs/my-website/docs/observability/datadog.md index 08ebf8b28ce..5cb5ab3af2d 100644 --- a/docs/my-website/docs/observability/datadog.md +++ b/docs/my-website/docs/observability/datadog.md @@ -56,12 +56,32 @@ litellm_settings: **Step 2**: Set Required env variables for datadog +#### Direct API + +Send logs directly to Datadog API: + ```shell DD_API_KEY="5f2d0f310***********" # your datadog API Key DD_SITE="us5.datadoghq.com" # your datadog base url DD_SOURCE="litellm_dev" # [OPTIONAL] your datadog source. use to differentiate dev vs. prod deployments ``` +#### Via DataDog Agent + +Send logs through a local DataDog agent (useful for containerized environments): + +```shell +DD_AGENT_HOST="localhost" # hostname or IP of DataDog agent +DD_AGENT_PORT="10518" # [OPTIONAL] port of DataDog agent (default: 10518) +DD_API_KEY="5f2d0f310***********" # [OPTIONAL] your datadog API Key (agent handles auth) +DD_SOURCE="litellm_dev" # [OPTIONAL] your datadog source +``` + +When `DD_AGENT_HOST` is set, logs are sent to the agent instead of directly to DataDog API. This is useful for: +- Centralized log shipping in containerized environments +- Reducing direct API calls from multiple services +- Leveraging agent-side processing and filtering + **Step 3**: Start the proxy, make a test request Start proxy @@ -169,8 +189,10 @@ LiteLLM supports customizing the following Datadog environment variables | Environment Variable | Description | Default Value | Required | |---------------------|-------------|---------------|----------| -| `DD_API_KEY` | Your Datadog API key for authentication | None | ✅ Yes | -| `DD_SITE` | Your Datadog site (e.g., "us5.datadoghq.com") | None | ✅ Yes | +| `DD_API_KEY` | Your Datadog API key for authentication (required for direct API, optional for agent) | None | Conditional* | +| `DD_SITE` | Your Datadog site (e.g., "us5.datadoghq.com") (required for direct API) | None | Conditional* | +| `DD_AGENT_HOST` | Hostname or IP of DataDog agent (e.g., "localhost"). When set, logs are sent to agent instead of direct API | None | ❌ No | +| `DD_AGENT_PORT` | Port of DataDog agent for log intake | "10518" | ❌ No | | `DD_ENV` | Environment tag for your logs (e.g., "production", "staging") | "unknown" | ❌ No | | `DD_SERVICE` | Service name for your logs | "litellm-server" | ❌ No | | `DD_SOURCE` | Source name for your logs | "litellm" | ❌ No | @@ -178,3 +200,6 @@ LiteLLM supports customizing the following Datadog environment variables | `HOSTNAME` | Hostname tag for your logs | "" | ❌ No | | `POD_NAME` | Pod name tag (useful for Kubernetes deployments) | "unknown" | ❌ No | +\* **Required when using Direct API** (default): `DD_API_KEY` and `DD_SITE` are required +\* **Optional when using DataDog Agent**: Set `DD_AGENT_HOST` to use agent mode; `DD_API_KEY` and `DD_SITE` are not required + diff --git a/docs/my-website/docs/projects/Softgen b/docs/my-website/docs/projects/Softgen new file mode 100644 index 00000000000..2e5024a0770 --- /dev/null +++ b/docs/my-website/docs/projects/Softgen @@ -0,0 +1,7 @@ +# Softgen + +`Softgen` is an AI-powered platform that builds full-stack web apps from your plain instructions. +LiteLLM helps `Softgen` users to choose and use different LLMs. + +- [Softgen](https://softgen.ai) +- [Academy](hhttps://academy.softgen.ai) diff --git a/docs/my-website/docs/providers/anthropic.md b/docs/my-website/docs/providers/anthropic.md index 1663d32ddfc..0ea042e5d98 100644 --- a/docs/my-website/docs/providers/anthropic.md +++ b/docs/my-website/docs/providers/anthropic.md @@ -953,7 +953,7 @@ except Exception as e: s/o @[Shekhar Patnaik](https://www.linkedin.com/in/patnaikshekhar) for requesting this! -### Anthropic Hosted Tools (Computer, Text Editor, Web Search) +### Anthropic Hosted Tools (Computer, Text Editor, Web Search, Memory) @@ -1183,6 +1183,72 @@ curl http://0.0.0.0:4000/v1/chat/completions \ + + + +:::info +The Anthropic Memory tool is currently in beta. +::: + + + + +```python +from litellm import completion + +tools = [{ + "type": "memory_20250818", + "name": "memory" +}] + +model = "claude-sonnet-4-5-20250929" +messages = [{"role": "user", "content": "Please remember that my favorite color is blue."}] + +response = completion( + model=model, + messages=messages, + tools=tools, +) + +print(response) +``` + + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: claude-memory-model + litellm_params: + model: anthropic/claude-sonnet-4-5-20250929 + api_key: os.environ/ANTHROPIC_API_KEY +``` + +2. Start proxy + +```bash +litellm --config /path/to/config.yaml +``` + +3. Test it! + +```bash +curl http://0.0.0.0:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_KEY" \ + -d '{ + "model": "claude-memory-model", + "messages": [{"role": "user", "content": "Please remember that my favorite color is blue."}], + "tools": [{"type": "memory_20250818", "name": "memory"}] + }' +``` + + + + + diff --git a/docs/my-website/docs/providers/azure/videos.md b/docs/my-website/docs/providers/azure/videos.md index 188713d6335..62f8d0df182 100644 --- a/docs/my-website/docs/providers/azure/videos.md +++ b/docs/my-website/docs/providers/azure/videos.md @@ -25,7 +25,6 @@ LiteLLM supports Azure OpenAI's video generation models including Sora with full import os os.environ["AZURE_OPENAI_API_KEY"] = "your-azure-api-key" os.environ["AZURE_OPENAI_API_BASE"] = "https://your-resource.openai.azure.com/" -os.environ["AZURE_OPENAI_API_VERSION"] = "2024-02-15-preview" ``` ### Basic Usage @@ -37,7 +36,6 @@ import time os.environ["AZURE_OPENAI_API_KEY"] = "your-azure-api-key" os.environ["AZURE_OPENAI_API_BASE"] = "https://your-resource.openai.azure.com/" -os.environ["AZURE_OPENAI_API_VERSION"] = "2024-02-15-preview" # Generate video response = video_generation( @@ -53,8 +51,7 @@ print(f"Initial Status: {response.status}") # Check status until video is ready while True: status_response = video_status( - video_id=response.id, - custom_llm_provider="azure" + video_id=response.id ) print(f"Current Status: {status_response.status}") @@ -69,8 +66,7 @@ while True: # Download video content when ready video_bytes = video_content( - video_id=response.id, - custom_llm_provider="azure" + video_id=response.id ) # Save to file @@ -87,7 +83,6 @@ Here's how to call Azure video generation models with the LiteLLM Proxy Server ```bash export AZURE_OPENAI_API_KEY="your-azure-api-key" export AZURE_OPENAI_API_BASE="https://your-resource.openai.azure.com/" -export AZURE_OPENAI_API_VERSION="2024-02-15-preview" ``` ### 2. Start the proxy @@ -102,7 +97,6 @@ model_list: model: azure/sora-2 api_key: os.environ/AZURE_OPENAI_API_KEY api_base: os.environ/AZURE_OPENAI_API_BASE - api_version: "2024-02-15-preview" ``` @@ -211,8 +205,7 @@ general_settings: ```python # Download video content video_bytes = video_content( - video_id="video_1234567890", - model="azure/sora-2" + video_id="video_1234567890" ) # Save to file @@ -243,8 +236,7 @@ def generate_and_download_video(prompt): # Step 3: Download video video_bytes = litellm.video_content( - video_id=video_id, - custom_llm_provider="azure" + video_id=video_id ) # Step 4: Save to file @@ -264,9 +256,9 @@ video_file = generate_and_download_video( ```python # Video editing with reference image response = litellm.video_remix( + video_id="video_456", prompt="Make the cat jump higher", input_reference=open("path/to/image.jpg", "rb"), # Reference image as file object - custom_llm_provider="azure" seconds="8" ) diff --git a/docs/my-website/docs/providers/fireworks_ai.md b/docs/my-website/docs/providers/fireworks_ai.md index 98d7c33ce7e..b1b10cd71b5 100644 --- a/docs/my-website/docs/providers/fireworks_ai.md +++ b/docs/my-website/docs/providers/fireworks_ai.md @@ -204,7 +204,7 @@ from litellm import completion import os os.environ["FIREWORKS_AI_API_KEY"] = "YOUR_API_KEY" -os.environ["FIREWORKS_AI_API_BASE"] = "https://audio-prod.us-virginia-1.direct.fireworks.ai/v1" +os.environ["FIREWORKS_AI_API_BASE"] = "https://audio-prod.api.fireworks.ai/v1" completion = litellm.completion( model="fireworks_ai/accounts/fireworks/models/llama-v3p3-70b-instruct", @@ -343,7 +343,7 @@ from litellm import transcription import os os.environ["FIREWORKS_AI_API_KEY"] = "YOUR_API_KEY" -os.environ["FIREWORKS_AI_API_BASE"] = "https://audio-prod.us-virginia-1.direct.fireworks.ai/v1" +os.environ["FIREWORKS_AI_API_BASE"] = "https://audio-prod.api.fireworks.ai/v1" response = transcription( model="fireworks_ai/whisper-v3", @@ -363,7 +363,7 @@ model_list: - model_name: whisper-v3 litellm_params: model: fireworks_ai/whisper-v3 - api_base: https://audio-prod.us-virginia-1.direct.fireworks.ai/v1 + api_base: https://audio-prod.api.fireworks.ai/v1 api_key: os.environ/FIREWORKS_API_KEY model_info: mode: audio_transcription diff --git a/docs/my-website/docs/providers/gemini.md b/docs/my-website/docs/providers/gemini.md index 40d64656528..31d3a491f40 100644 --- a/docs/my-website/docs/providers/gemini.md +++ b/docs/my-website/docs/providers/gemini.md @@ -10,7 +10,7 @@ import TabItem from '@theme/TabItem'; | Provider Route on LiteLLM | `gemini/` | | Provider Doc | [Google AI Studio ↗](https://aistudio.google.com/) | | API Endpoint for Provider | https://generativelanguage.googleapis.com | -| Supported OpenAI Endpoints | `/chat/completions`, [`/embeddings`](../embedding/supported_embedding#gemini-ai-embedding-models), `/completions` | +| Supported OpenAI Endpoints | `/chat/completions`, [`/embeddings`](../embedding/supported_embedding#gemini-ai-embedding-models), `/completions`, [`/videos`](./gemini/videos.md) | | Pass-through Endpoint | [Supported](../pass_through/google_ai_studio.md) |
diff --git a/docs/my-website/docs/providers/gemini/videos.md b/docs/my-website/docs/providers/gemini/videos.md new file mode 100644 index 00000000000..5b5d5a8a636 --- /dev/null +++ b/docs/my-website/docs/providers/gemini/videos.md @@ -0,0 +1,409 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Gemini Video Generation (Veo) + +LiteLLM supports Google's Veo video generation models through a unified API interface. + +| Property | Details | +|-------|-------| +| Description | Google's Veo AI video generation models | +| Provider Route on LiteLLM | `gemini/` | +| Supported Models | `veo-3.0-generate-preview`, `veo-3.1-generate-preview` | +| Cost Tracking | ✅ Duration-based pricing | +| Logging Support | ✅ Full request/response logging | +| Proxy Server Support | ✅ Full proxy integration with virtual keys | +| Spend Management | ✅ Budget tracking and rate limiting | +| Link to Provider Doc | [Google Veo Documentation ↗](https://ai.google.dev/gemini-api/docs/video) | + +## Quick Start + +### Required API Keys + +```python +import os +os.environ["GEMINI_API_KEY"] = "your-google-api-key" +# OR +os.environ["GOOGLE_API_KEY"] = "your-google-api-key" +``` + +### Basic Usage + +```python +from litellm import video_generation, video_status, video_content +import os +import time + +os.environ["GEMINI_API_KEY"] = "your-google-api-key" + +# Step 1: Generate video +response = video_generation( + model="gemini/veo-3.0-generate-preview", + prompt="A cat playing with a ball of yarn in a sunny garden" +) + +print(f"Video ID: {response.id}") +print(f"Initial Status: {response.status}") # "processing" + +# Step 2: Poll for completion +while True: + status_response = video_status( + video_id=response.id + ) + + print(f"Current Status: {status_response.status}") + + if status_response.status == "completed": + break + elif status_response.status == "failed": + print("Video generation failed") + break + + time.sleep(10) # Wait 10 seconds before checking again + +# Step 3: Download video content +video_bytes = video_content( + video_id=response.id +) + +# Save to file +with open("generated_video.mp4", "wb") as f: + f.write(video_bytes) + +print("Video downloaded successfully!") +``` + +## Supported Models + +| Model Name | Description | Max Duration | Status | +|------------|-------------|--------------|--------| +| veo-3.0-generate-preview | Veo 3.0 video generation | 8 seconds | Preview | +| veo-3.1-generate-preview | Veo 3.1 video generation | 8 seconds | Preview | + +## Video Generation Parameters + +LiteLLM automatically maps OpenAI-style parameters to Veo's format: + +| OpenAI Parameter | Veo Parameter | Description | Example | +|------------------|---------------|-------------|---------| +| `prompt` | `prompt` | Text description of the video | "A cat playing" | +| `size` | `aspectRatio` | Video dimensions → aspect ratio | "1280x720" → "16:9" | +| `seconds` | `durationSeconds` | Duration in seconds | "8" → 8 | +| `input_reference` | `image` | Reference image to animate | File object or path | +| `model` | `model` | Model to use | "gemini/veo-3.0-generate-preview" | + +### Size to Aspect Ratio Mapping + +LiteLLM automatically converts size dimensions to Veo's aspect ratio format: +- `"1280x720"`, `"1920x1080"` → `"16:9"` (landscape) +- `"720x1280"`, `"1080x1920"` → `"9:16"` (portrait) + +### Supported Veo Parameters + +Based on Veo's API: +- **prompt** (required): Text description with optional audio cues +- **aspectRatio**: `"16:9"` (default) or `"9:16"` +- **resolution**: `"720p"` (default) or `"1080p"` (Veo 3.1 only, 16:9 aspect ratio only) +- **durationSeconds**: Video length (max 8 seconds for most models) +- **image**: Reference image for animation +- **negativePrompt**: What to exclude from the video (Veo 3.1) +- **referenceImages**: Style and content references (Veo 3.1 only) + +## Complete Workflow Example + +```python +import litellm +import time + +def generate_and_download_veo_video( + prompt: str, + output_file: str = "video.mp4", + size: str = "1280x720", + seconds: str = "8" +): + """ + Complete workflow for Veo video generation. + + Args: + prompt: Text description of the video + output_file: Where to save the video + size: Video dimensions (e.g., "1280x720" for 16:9) + seconds: Duration in seconds + + Returns: + bool: True if successful + """ + print(f"🎬 Generating video: {prompt}") + + # Step 1: Initiate generation + response = litellm.video_generation( + model="gemini/veo-3.0-generate-preview", + prompt=prompt, + size=size, # Maps to aspectRatio + seconds=seconds # Maps to durationSeconds + ) + + video_id = response.id + print(f"✓ Video generation started (ID: {video_id})") + + # Step 2: Wait for completion + max_wait_time = 600 # 10 minutes + start_time = time.time() + + while time.time() - start_time < max_wait_time: + status_response = litellm.video_status(video_id=video_id) + + if status_response.status == "completed": + print("✓ Video generation completed!") + break + elif status_response.status == "failed": + print("✗ Video generation failed") + return False + + print(f"⏳ Status: {status_response.status}") + time.sleep(10) + else: + print("✗ Timeout waiting for video generation") + return False + + # Step 3: Download video + print("⬇️ Downloading video...") + video_bytes = litellm.video_content(video_id=video_id) + + with open(output_file, "wb") as f: + f.write(video_bytes) + + print(f"✓ Video saved to {output_file}") + return True + +# Use it +generate_and_download_veo_video( + prompt="A serene lake at sunset with mountains in the background", + output_file="sunset_lake.mp4" +) +``` + +## Async Usage + +```python +from litellm import avideo_generation, avideo_status, avideo_content +import asyncio + +async def async_video_workflow(): + # Generate video + response = await avideo_generation( + model="gemini/veo-3.0-generate-preview", + prompt="A cat playing with a ball of yarn" + ) + + # Poll for completion + while True: + status = await avideo_status(video_id=response.id) + if status.status == "completed": + break + await asyncio.sleep(10) + + # Download content + video_bytes = await avideo_content(video_id=response.id) + + with open("video.mp4", "wb") as f: + f.write(video_bytes) + +# Run it +asyncio.run(async_video_workflow()) +``` + +## LiteLLM Proxy Usage + +### Configuration + +Add Veo models to your `config.yaml`: + +```yaml +model_list: + - model_name: veo-3 + litellm_params: + model: gemini/veo-3.0-generate-preview + api_key: os.environ/GEMINI_API_KEY +``` + +Start the proxy: + +```bash +litellm --config config.yaml +# Server running on http://0.0.0.0:4000 +``` + +### Making Requests + + + + +```bash +# Step 1: Generate video +curl --location 'http://0.0.0.0:4000/v1/videos' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer sk-1234' \ +--data '{ + "model": "veo-3", + "prompt": "A cat playing with a ball of yarn in a sunny garden" +}' + +# Response: {"id": "gemini::operations/generate_12345::...", "status": "processing", ...} + +# Step 2: Check status +curl --location 'http://localhost:4000/v1/videos/{video_id}' \ +--header 'x-litellm-api-key: sk-1234' + +# Step 3: Download video (when status is "completed") +curl --location 'http://localhost:4000/v1/videos/{video_id}/content' \ +--header 'x-litellm-api-key: sk-1234' \ +--output video.mp4 +``` + + + + +```python +import litellm + +litellm.api_base = "http://0.0.0.0:4000" +litellm.api_key = "sk-1234" + +# Generate video +response = litellm.video_generation( + model="veo-3", + prompt="A cat playing with a ball of yarn in a sunny garden" +) + +# Check status +import time +while True: + status = litellm.video_status(video_id=response.id) + if status.status == "completed": + break + time.sleep(10) + +# Download video +video_bytes = litellm.video_content(video_id=response.id) +with open("video.mp4", "wb") as f: + f.write(video_bytes) +``` + + + + +## Cost Tracking + +LiteLLM automatically tracks costs for Veo video generation: + +```python +response = litellm.video_generation( + model="gemini/veo-3.0-generate-preview", + prompt="A beautiful sunset" +) + +# Cost is calculated based on video duration +# Veo pricing: ~$0.10 per second (estimated) +# Default video duration: ~5 seconds +# Estimated cost: ~$0.50 +``` + +## Differences from OpenAI Video API + +| Feature | OpenAI (Sora) | Gemini (Veo) | +|---------|---------------|--------------| +| Reference Images | ✅ Supported | ❌ Not supported | +| Size Control | ✅ Supported | ❌ Not supported | +| Duration Control | ✅ Supported | ❌ Not supported | +| Video Remix/Edit | ✅ Supported | ❌ Not supported | +| Video List | ✅ Supported | ❌ Not supported | +| Prompt-based Generation | ✅ Supported | ✅ Supported | +| Async Operations | ✅ Supported | ✅ Supported | + +## Error Handling + +```python +from litellm import video_generation, video_status, video_content +from litellm.exceptions import APIError, Timeout + +try: + response = video_generation( + model="gemini/veo-3.0-generate-preview", + prompt="A beautiful landscape" + ) + + # Poll with timeout + max_attempts = 60 # 10 minutes (60 * 10s) + for attempt in range(max_attempts): + status = video_status(video_id=response.id) + + if status.status == "completed": + video_bytes = video_content(video_id=response.id) + with open("video.mp4", "wb") as f: + f.write(video_bytes) + break + elif status.status == "failed": + raise APIError("Video generation failed") + + time.sleep(10) + else: + raise Timeout("Video generation timed out") + +except APIError as e: + print(f"API Error: {e}") +except Timeout as e: + print(f"Timeout: {e}") +except Exception as e: + print(f"Unexpected error: {e}") +``` + +## Best Practices + +1. **Always poll for completion**: Veo video generation is asynchronous and can take several minutes +2. **Set reasonable timeouts**: Allow at least 5-10 minutes for video generation +3. **Handle failures gracefully**: Check for `failed` status and implement retry logic +4. **Use descriptive prompts**: More detailed prompts generally produce better results +5. **Store video IDs**: Save the operation ID/video ID to resume polling if your application restarts + +## Troubleshooting + +### Video generation times out + +```python +# Increase polling timeout +max_wait_time = 900 # 15 minutes instead of 10 +``` + +### Video not found when downloading + +```python +# Make sure video is completed before downloading +status = video_status(video_id=video_id) +if status.status != "completed": + print("Video not ready yet!") +``` + +### API key errors + +```python +# Verify your API key is set +import os +print(os.environ.get("GEMINI_API_KEY")) + +# Or pass it explicitly +response = video_generation( + model="gemini/veo-3.0-generate-preview", + prompt="...", + api_key="your-api-key-here" +) +``` + +## See Also + +- [OpenAI Video Generation](../openai/videos.md) +- [Azure Video Generation](../azure/videos.md) +- [Vertex AI Video Generation](../vertex_ai/videos.md) +- [Video Generation API Reference](/docs/videos) +- [Veo Pass-through Endpoints](/docs/pass_through/google_ai_studio#example-4-video-generation-with-veo) + diff --git a/docs/my-website/docs/providers/openai/videos.md b/docs/my-website/docs/providers/openai/videos.md index 06d0934b180..202c79c2446 100644 --- a/docs/my-website/docs/providers/openai/videos.md +++ b/docs/my-website/docs/providers/openai/videos.md @@ -36,7 +36,6 @@ print(f"Status: {response.status}") # Download video content when ready video_bytes = video_content( video_id=response.id, - model="sora-2" ) # Save to file @@ -44,6 +43,113 @@ with open("generated_video.mp4", "wb") as f: f.write(video_bytes) ``` +## **LiteLLM Proxy Usage** + +LiteLLM provides OpenAI API compatible video endpoints for complete video generation workflow: + +- `/videos/generations` - Generate new videos +- `/videos/remix` - Edit existing videos with reference images +- `/videos/status` - Check video generation status +- `/videos/retrieval` - Download completed videos + +**Setup** + +Add this to your litellm proxy config.yaml + +```yaml +model_list: + - model_name: sora-2 + litellm_params: + model: openai/sora-2 + api_key: os.environ/OPENAI_API_KEY +``` + +Start litellm + +```bash +litellm --config /path/to/config.yaml + +# RUNNING on http://0.0.0.0:4000 +``` + +Test video generation request + +```bash +curl --location 'http://localhost:4000/v1/videos' \ +--header 'Content-Type: application/json' \ +--header 'x-litellm-api-key: sk-1234' \ +--data '{ + "model": "sora-2", + "prompt": "A beautiful sunset over the ocean" +}' +``` + +Test video status request + +```bash +# Using custom-llm-provider header +curl --location 'http://localhost:4000/v1/videos/video_id' \ +--header 'Accept: application/json' \ +--header 'x-litellm-api-key: sk-1234' \ +--header 'custom-llm-provider: openai' +``` + +Test video retrieval request + +```bash +# Using custom-llm-provider header +curl --location 'http://localhost:4000/v1/videos/video_id/content' \ +--header 'Accept: application/json' \ +--header 'x-litellm-api-key: sk-1234' \ +--header 'custom-llm-provider: openai' \ +--output video.mp4 + +# Or using query parameter +curl --location 'http://localhost:4000/v1/videos/video_id/content?custom_llm_provider=openai' \ +--header 'Accept: application/json' \ +--header 'x-litellm-api-key: sk-1234' \ +--output video.mp4 +``` + +Test video remix request + +```bash +# Using custom_llm_provider in request body +curl --location --request POST 'http://localhost:4000/v1/videos/video_id/remix' \ +--header 'Accept: application/json' \ +--header 'Content-Type: application/json' \ +--header 'x-litellm-api-key: sk-1234' \ +--data '{ + "prompt": "New remix instructions", + "custom_llm_provider": "openai" +}' + +# Or using custom-llm-provider header +curl --location --request POST 'http://localhost:4000/v1/videos/video_id/remix' \ +--header 'Accept: application/json' \ +--header 'Content-Type: application/json' \ +--header 'x-litellm-api-key: sk-1234' \ +--header 'custom-llm-provider: openai' \ +--data '{ + "prompt": "New remix instructions" +}' +``` + +Test OpenAI video generation request + +```bash +curl http://localhost:4000/v1/videos \ + -H "Authorization: Bearer sk-1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "sora-2", + "prompt": "A cat playing with a ball of yarn in a sunny garden", + "seconds": "8", + "size": "720x1280" + }' +``` + + ## Supported Models | Model Name | Description | Max Duration | Supported Sizes | @@ -64,8 +170,7 @@ with open("generated_video.mp4", "wb") as f: ```python # Download video content video_bytes = video_content( - video_id="video_1234567890", - custom_llm_provider="openai" # Or use model="sora-2" + video_id="video_1234567890" ) # Save to file @@ -96,8 +201,7 @@ def generate_and_download_video(prompt): # Step 3: Download video video_bytes = litellm.video_content( - video_id=video_id, - custom_llm_provider="openai" + video_id=video_id ) # Step 4: Save to file @@ -112,6 +216,7 @@ video_file = generate_and_download_video( ) ``` + ## Video Editing with Reference Images ```python @@ -133,8 +238,7 @@ from litellm.exceptions import BadRequestError, AuthenticationError try: response = video_generation( - prompt="A cat playing with a ball of yarn", - model="sora-2" + prompt="A cat playing with a ball of yarn" ) except AuthenticationError as e: print(f"Authentication failed: {e}") diff --git a/docs/my-website/docs/providers/vertex_ai/videos.md b/docs/my-website/docs/providers/vertex_ai/videos.md new file mode 100644 index 00000000000..4aaf74354b1 --- /dev/null +++ b/docs/my-website/docs/providers/vertex_ai/videos.md @@ -0,0 +1,268 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Vertex AI Video Generation (Veo) + +LiteLLM supports Vertex AI's Veo video generation models using the unified OpenAI video API surface. + +| Property | Details | +|-------|-------| +| Description | Google Cloud Vertex AI Veo video generation models | +| Provider Route on LiteLLM | `vertex_ai/` | +| Supported Models | `veo-2.0-generate-001`, `veo-3.0-generate-preview`, `veo-3.0-fast-generate-preview`, `veo-3.1-generate-preview`, `veo-3.1-fast-generate-preview` | +| Cost Tracking | ✅ Duration-based pricing | +| Logging Support | ✅ Full request/response logging | +| Proxy Server Support | ✅ Full proxy integration with virtual keys | +| Spend Management | ✅ Budget tracking and rate limiting | +| Link to Provider Doc | [Vertex AI Veo Documentation ↗](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo-video-generation) | + +## Quick Start + +### Required Environment Setup + +```python +import json +import os + +os.environ["VERTEXAI_PROJECT"] = "your-gcp-project-id" +os.environ["VERTEXAI_LOCATION"] = "us-central1" + +# Option 1: Point to a service account file +os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/service_account.json" + +# Option 2: Store the service account JSON directly +with open("/path/to/service_account.json", "r", encoding="utf-8") as f: + os.environ["VERTEXAI_CREDENTIALS"] = f.read() +``` + +### Basic Usage + +```python +from litellm import video_generation, video_status, video_content +import json +import os +import time + +with open("/path/to/service_account.json", "r", encoding="utf-8") as f: + vertex_credentials = f.read() + +response = video_generation( + model="vertex_ai/veo-3.0-generate-preview", + prompt="A cat playing with a ball of yarn in a sunny garden", + vertex_project="your-gcp-project-id", + vertex_location="us-central1", + vertex_credentials=vertex_credentials, + seconds="8", + size="1280x720", +) + +print(f"Video ID: {response.id}") +print(f"Initial Status: {response.status}") + +# Poll for completion +while True: + status = video_status( + video_id=response.id, + vertex_project="your-gcp-project-id", + vertex_location="us-central1", + vertex_credentials=vertex_credentials, + ) + + print(f"Current Status: {status.status}") + + if status.status == "completed": + break + if status.status == "failed": + raise RuntimeError("Video generation failed") + + time.sleep(10) + +# Download the rendered video +video_bytes = video_content( + video_id=response.id, + vertex_project="your-gcp-project-id", + vertex_location="us-central1", + vertex_credentials=vertex_credentials, +) + +with open("generated_video.mp4", "wb") as f: + f.write(video_bytes) +``` + +## Supported Models + +| Model Name | Description | Max Duration | Status | +|------------|-------------|--------------|--------| +| veo-2.0-generate-001 | Veo 2.0 video generation | 5 seconds | GA | +| veo-3.0-generate-preview | Veo 3.0 high quality | 8 seconds | Preview | +| veo-3.0-fast-generate-preview | Veo 3.0 fast generation | 8 seconds | Preview | +| veo-3.1-generate-preview | Veo 3.1 high quality | 10 seconds | Preview | +| veo-3.1-fast-generate-preview | Veo 3.1 fast | 10 seconds | Preview | + +## Video Generation Parameters + +LiteLLM converts OpenAI-style parameters to Veo's API shape automatically: + +| OpenAI Parameter | Vertex AI Parameter | Description | Example | +|------------------|---------------------|-------------|---------| +| `prompt` | `instances[].prompt` | Text description of the video | "A cat playing" | +| `size` | `parameters.aspectRatio` | Converted to `16:9` or `9:16` | "1280x720" → `16:9` | +| `seconds` | `parameters.durationSeconds` | Clip length in seconds | "8" → `8` | +| `input_reference` | `instances[].image` | Reference image for animation | `open("image.jpg", "rb")` | +| Provider-specific params | `extra_body` | Forwarded to Vertex API | `{"negativePrompt": "blurry"}` | + +### Size to Aspect Ratio Mapping + +- `1280x720`, `1920x1080` → `16:9` +- `720x1280`, `1080x1920` → `9:16` +- Unknown sizes default to `16:9` + +## Async Usage + +```python +from litellm import avideo_generation, avideo_status, avideo_content +import asyncio +import json + +with open("/path/to/service_account.json", "r", encoding="utf-8") as f: + vertex_credentials = f.read() + + +async def workflow(): + response = await avideo_generation( + model="vertex_ai/veo-3.1-generate-preview", + prompt="Slow motion water droplets splashing into a pool", + seconds="10", + vertex_project="your-gcp-project-id", + vertex_location="us-central1", + vertex_credentials=vertex_credentials, + ) + + while True: + status = await avideo_status( + video_id=response.id, + vertex_project="your-gcp-project-id", + vertex_location="us-central1", + vertex_credentials=vertex_credentials, + ) + + if status.status == "completed": + break + if status.status == "failed": + raise RuntimeError("Video generation failed") + + await asyncio.sleep(10) + + video_bytes = await avideo_content( + video_id=response.id, + vertex_project="your-gcp-project-id", + vertex_location="us-central1", + vertex_credentials=vertex_credentials, + ) + + with open("veo_water.mp4", "wb") as f: + f.write(video_bytes) + +asyncio.run(workflow()) +``` + +## LiteLLM Proxy Usage + +Add Veo models to your `config.yaml`: + +```yaml +model_list: + - model_name: veo-3 + litellm_params: + model: vertex_ai/veo-3.0-generate-preview + vertex_project: os.environ/VERTEXAI_PROJECT + vertex_location: os.environ/VERTEXAI_LOCATION + vertex_credentials: os.environ/VERTEXAI_CREDENTIALS +``` + +Start the proxy and make requests: + + + + +```bash +# Step 1: Generate video +curl --location 'http://0.0.0.0:4000/videos' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer sk-1234' \ +--data '{ + "model": "veo-3", + "prompt": "Aerial shot over a futuristic city at sunrise", + "seconds": "8" +}' + +# Step 2: Poll status +curl --location 'http://localhost:4000/v1/videos/{video_id}' \ +--header 'x-litellm-api-key: sk-1234' + +# Step 3: Download video +curl --location 'http://localhost:4000/v1/videos/{video_id}/content' \ +--header 'x-litellm-api-key: sk-1234' \ +--output video.mp4 +``` + + + + +```python +import litellm + +litellm.api_base = "http://0.0.0.0:4000" +litellm.api_key = "sk-1234" + +response = litellm.video_generation( + model="veo-3", + prompt="Aerial shot over a futuristic city at sunrise", +) + +status = litellm.video_status(video_id=response.id) +while status.status not in ["completed", "failed"]: + status = litellm.video_status(video_id=response.id) + +if status.status == "completed": + content = litellm.video_content(video_id=response.id) + with open("veo_city.mp4", "wb") as f: + f.write(content) +``` + + + + +## Cost Tracking + +LiteLLM records the duration returned by Veo so you can apply duration-based pricing. + +```python +with open("/path/to/service_account.json", "r", encoding="utf-8") as f: + vertex_credentials = f.read() + +response = video_generation( + model="vertex_ai/veo-2.0-generate-001", + prompt="Flowers blooming in fast forward", + seconds="5", + vertex_project="your-gcp-project-id", + vertex_location="us-central1", + vertex_credentials=vertex_credentials, +) + +print(response.usage) # {"duration_seconds": 5.0} +``` + +## Troubleshooting + +- **`vertex_project is required`**: set `VERTEXAI_PROJECT` env var or pass `vertex_project` in the request. +- **`Permission denied`**: ensure the service account has the `Vertex AI User` role and the correct region enabled. +- **Video stuck in `processing`**: Veo operations are long-running. Continue polling every 10–15 seconds up to ~10 minutes. + +## See Also + +- [OpenAI Video Generation](../openai/videos.md) +- [Azure Video Generation](../azure/videos.md) +- [Gemini Video Generation](../gemini/videos.md) +- [Video Generation API Reference](/docs/videos) + diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index b5a27770ed6..fbdbe6ea7f3 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -399,6 +399,8 @@ router_settings: | AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS | Input cost per 1K tokens for Azure Computer Use service | AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS | Output cost per 1K tokens for Azure Computer Use service | AZURE_DEFAULT_RESPONSES_API_VERSION | Version of the Azure Default Responses API being used. Default is "preview" +| AZURE_DOCUMENT_INTELLIGENCE_API_VERSION | API version for Azure Document Intelligence service +| AZURE_DOCUMENT_INTELLIGENCE_DEFAULT_DPI | Default DPI (dots per inch) setting for Azure Document Intelligence service | AZURE_TENANT_ID | Tenant ID for Azure Active Directory | AZURE_USERNAME | Username for Azure services, use in conjunction with AZURE_PASSWORD for azure ad token with basic username/password workflow | AZURE_PASSWORD | Password for Azure services, use in conjunction with AZURE_USERNAME for azure ad token with basic username/password workflow @@ -429,6 +431,12 @@ router_settings: | CLOUDZERO_MAX_FETCHED_DATA_RECORDS | Maximum number of data records to fetch from CloudZero | CLOUDZERO_TIMEZONE | Timezone for date handling (default: UTC) | CONFIG_FILE_PATH | File path for configuration file +| CYBERARK_ACCOUNT | CyberArk account name for secret management +| CYBERARK_API_BASE | Base URL for CyberArk API +| CYBERARK_API_KEY | API key for CyberArk secret management service +| CYBERARK_CLIENT_CERT | Path to client certificate for CyberArk authentication +| CYBERARK_CLIENT_KEY | Path to client key for CyberArk authentication +| CYBERARK_USERNAME | Username for CyberArk authentication | CONFIDENT_API_KEY | API key for DeepEval integration | CUSTOM_TIKTOKEN_CACHE_DIR | Custom directory for Tiktoken cache | CONFIDENT_API_KEY | API key for Confident AI (Deepeval) Logging service @@ -452,6 +460,8 @@ router_settings: | DD_BASE_URL | Base URL for Datadog integration | DATADOG_BASE_URL | (Alternative to DD_BASE_URL) Base URL for Datadog integration | _DATADOG_BASE_URL | (Alternative to DD_BASE_URL) Base URL for Datadog integration +| DD_AGENT_HOST | Hostname or IP of DataDog agent (e.g., "localhost"). When set, logs are sent to agent instead of direct API +| DD_AGENT_PORT | Port of DataDog agent for log intake. Default is 10518 | DD_API_KEY | API key for Datadog integration | DD_SITE | Site URL for Datadog (e.g., datadoghq.com) | DD_SOURCE | Source identifier for Datadog logs @@ -470,6 +480,7 @@ router_settings: | DEFAULT_FAILURE_THRESHOLD_PERCENT | Threshold percentage of failures to cool down a deployment. Default is 0.5 (50%) | DEFAULT_FLUSH_INTERVAL_SECONDS | Default interval in seconds for flushing operations. Default is 5 | DEFAULT_HEALTH_CHECK_INTERVAL | Default interval in seconds for health checks. Default is 300 (5 minutes) +| DEFAULT_HEALTH_CHECK_PROMPT | Default prompt used during health checks for non-image models. Default is "test from litellm" | DEFAULT_IMAGE_HEIGHT | Default height for images. Default is 300 | DEFAULT_IMAGE_TOKEN_COUNT | Default token count for images. Default is 250 | DEFAULT_IMAGE_WIDTH | Default width for images. Default is 300 @@ -496,6 +507,7 @@ router_settings: | DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_FLASH | Default minimal reasoning effort thinking budget for Gemini 2.5 Flash. Default is 512 | DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_FLASH_LITE | Default minimal reasoning effort thinking budget for Gemini 2.5 Flash Lite. Default is 512 | DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_PRO | Default minimal reasoning effort thinking budget for Gemini 2.5 Pro. Default is 512 +| DEFAULT_REDIS_MAJOR_VERSION | Default Redis major version to assume when version cannot be determined. Default is 7 | DEFAULT_REDIS_SYNC_INTERVAL | Default Redis synchronization interval in seconds. Default is 1 | DEFAULT_REPLICATE_GPU_PRICE_PER_SECOND | Default price per second for Replicate GPU. Default is 0.001400 | DEFAULT_REPLICATE_POLLING_DELAY_SECONDS | Default delay in seconds for Replicate polling. Default is 1 @@ -507,6 +519,7 @@ router_settings: | DEFAULT_SLACK_ALERTING_THRESHOLD | Default threshold for Slack alerting. Default is 300 | DEFAULT_SOFT_BUDGET | Default soft budget for LiteLLM proxy keys. Default is 50.0 | DEFAULT_TRIM_RATIO | Default ratio of tokens to trim from prompt end. Default is 0.75 +| DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS | Default duration for video generation in seconds in google. Default is 8 | DIRECT_URL | Direct URL for service endpoint | DISABLE_ADMIN_UI | Toggle to disable the admin UI | DISABLE_AIOHTTP_TRANSPORT | Flag to disable aiohttp transport. When this is set to True, litellm will use httpx instead of aiohttp. **Default is False** @@ -581,9 +594,14 @@ router_settings: | HEROKU_API_KEY | API key for Heroku services | HF_API_BASE | Base URL for Hugging Face API | HCP_VAULT_ADDR | Address for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) +| HCP_VAULT_APPROLE_MOUNT_PATH | Mount path for AppRole authentication in [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault). Default is "approle" +| HCP_VAULT_APPROLE_ROLE_ID | Role ID for AppRole authentication in [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) +| HCP_VAULT_APPROLE_SECRET_ID | Secret ID for AppRole authentication in [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) | HCP_VAULT_CLIENT_CERT | Path to client certificate for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) | HCP_VAULT_CLIENT_KEY | Path to client key for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) +| HCP_VAULT_MOUNT_NAME | Mount name for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) | HCP_VAULT_NAMESPACE | Namespace for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) +| HCP_VAULT_PATH_PREFIX | Path prefix for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) | HCP_VAULT_TOKEN | Token for [Hashicorp Vault Secret Manager](../secret.md#hashicorp-vault) | HCP_VAULT_CERT_ROLE | Role for [Hashicorp Vault Secret Manager Auth](../secret.md#hashicorp-vault) | HELICONE_API_KEY | API key for Helicone service @@ -650,6 +668,7 @@ router_settings: | LITELLM_OTEL_INTEGRATION_ENABLE_METRICS | Optionally enable emantic metrics for OTEL | LITELLM_MASTER_KEY | Master key for proxy authentication | LITELLM_MODE | Operating mode for LiteLLM (e.g., production, development) +| LITELLM_NON_ROOT | Flag to run LiteLLM in non-root mode for enhanced security in Docker containers | LITELLM_RATE_LIMIT_WINDOW_SIZE | Rate limit window size for LiteLLM. Default is 60 | LITELLM_SALT_KEY | Salt key for encryption in LiteLLM | LITELLM_SSL_CIPHERS | SSL/TLS cipher configuration for faster handshakes. Controls cipher suite preferences for OpenSSL connections. diff --git a/docs/my-website/docs/proxy/guardrails/litellm_content_filter.md b/docs/my-website/docs/proxy/guardrails/litellm_content_filter.md new file mode 100644 index 00000000000..29183c693a4 --- /dev/null +++ b/docs/my-website/docs/proxy/guardrails/litellm_content_filter.md @@ -0,0 +1,455 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import Image from '@theme/IdealImage'; + + +# LiteLLM Content Filter + +**Built-in guardrail** for detecting and filtering sensitive information using regex patterns and keyword matching. No external dependencies required. + +## Overview + +| Property | Details | +|----------|---------| +| Description | On-device guardrail for detecting and filtering sensitive information using regex patterns and keyword matching. Built into LiteLLM with no external dependencies. | +| Guardrail Name | `litellm_content_filter` | +| Detection Methods | Prebuilt regex patterns, custom regex, keyword matching | +| Actions | `BLOCK` (reject request), `MASK` (redact content) | +| Supported Modes | `pre_call`, `post_call`, `during_call` (streaming) | +| Performance | Fast - runs locally, no external API calls | + +## Quick Start + +## LiteLLM UI + +### Step 1: Select LiteLLM Content Filter + +Click "Add New Guardrail" and select "LiteLLM Content Filter" as your guardrail provider. + +Select LiteLLM Content Filter + +### Step 2: Configure Pattern Detection + +Select the prebuilt entities you want to block or mask. In this example, we select "Email" to detect and block email addresses. + +If you need to block a custom entity, you can add a custom regex pattern by clicking "Add custom regex". + +Select prebuilt entities or add custom regex + +### Step 3: Add Blocked Keywords + +Enter specific keywords you want to block. This is useful if you have policies to block certain words or phrases. + +Add blocked keywords + +### Step 4: Test Your Guardrail + +After creating the guardrail, navigate to "Test Playground" to test it. Select the guardrail you just created. + +Test examples: +- **Blocked keyword test**: Entering "hi blue" will trigger the block since we set "blue" as a blocked keyword +- **Pattern detection test**: Entering "Hi ishaan@berri.ai" will trigger the email pattern detector + +Test guardrail in playground + +## LiteLLM Config.yaml Setup + +### Step 1: Define Guardrails in config.yaml + +```yaml showLineNumbers title="config.yaml" +model_list: + - model_name: gpt-3.5-turbo + litellm_params: + model: openai/gpt-3.5-turbo + api_key: os.environ/OPENAI_API_KEY + +guardrails: + - guardrail_name: "content-filter-pre" + litellm_params: + guardrail: litellm_content_filter + mode: "pre_call" + + # Prebuilt patterns for common PII + patterns: + - pattern_type: "prebuilt" + pattern_name: "us_ssn" + action: "BLOCK" + + - pattern_type: "prebuilt" + pattern_name: "email" + action: "MASK" + + # Custom blocked keywords + blocked_words: + - keyword: "confidential" + action: "BLOCK" + description: "Sensitive internal information" +``` + +### Step 2: Start LiteLLM Gateway + +```shell +litellm --config config.yaml +``` + +### Step 3: Test Request + + + + +```shell +curl -i http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sk-1234" \ + -d '{ + "model": "gpt-3.5-turbo", + "messages": [ + {"role": "user", "content": "My SSN is 123-45-6789"} + ], + "guardrails": ["content-filter-pre"] + }' +``` + +**Response: HTTP 400 Error** +```json +{ + "error": { + "message": { + "error": "Content blocked: us_ssn pattern detected", + "pattern": "us_ssn" + }, + "code": "400" + } +} +``` + + + + + +```shell +curl -i http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sk-1234" \ + -d '{ + "model": "gpt-3.5-turbo", + "messages": [ + {"role": "user", "content": "Contact me at john@example.com"} + ], + "guardrails": ["content-filter-pre"] + }' +``` + +The request is sent to the LLM with the email masked: +``` +Contact me at [EMAIL_REDACTED] +``` + + + + +## Configuration + +### Supported Modes + +- **`pre_call`** - Run before LLM call, filters input messages +- **`post_call`** - Run after LLM call, filters output responses +- **`during_call`** - Run during streaming, filters each chunk in real-time + +### Actions + +- **`BLOCK`** - Reject the request with HTTP 400 error +- **`MASK`** - Replace sensitive content with redaction tags (e.g., `[EMAIL_REDACTED]`) + +## Prebuilt Patterns + +### Available Patterns + +| Pattern Name | Description | Example | +|-------------|-------------|---------| +| `us_ssn` | US Social Security Numbers | `123-45-6789` | +| `email` | Email addresses | `user@example.com` | +| `phone` | Phone numbers | `+1-555-123-4567` | +| `visa` | Visa credit cards | `4532-1234-5678-9010` | +| `mastercard` | Mastercard credit cards | `5425-2334-3010-9903` | +| `amex` | American Express cards | `3782-822463-10005` | +| `aws_access_key` | AWS access keys | `AKIAIOSFODNN7EXAMPLE` | +| `aws_secret_key` | AWS secret keys | `wJalrXUtnFEMI/K7MDENG/bPxRfi...` | +| `github_token` | GitHub tokens | `ghp_16C7e42F292c6912E7710c838347Ae178B4a` | + +### Using Prebuilt Patterns + +```yaml showLineNumbers title="config.yaml" +guardrails: + - guardrail_name: "pii-filter" + litellm_params: + guardrail: litellm_content_filter + mode: "pre_call" + patterns: + - pattern_type: "prebuilt" + pattern_name: "us_ssn" + action: "BLOCK" + + - pattern_type: "prebuilt" + pattern_name: "email" + action: "MASK" + + - pattern_type: "prebuilt" + pattern_name: "aws_access_key" + action: "BLOCK" +``` + +## Custom Regex Patterns + +Define your own regex patterns for domain-specific sensitive data: + +```yaml showLineNumbers title="config.yaml" +guardrails: + - guardrail_name: "custom-patterns" + litellm_params: + guardrail: litellm_content_filter + mode: "pre_call" + patterns: + # Custom employee ID format + - pattern_type: "regex" + pattern: '\b[A-Z]{3}-\d{4}\b' + name: "employee_id" + action: "MASK" + + # Custom project code format + - pattern_type: "regex" + pattern: 'PROJECT-\d{6}' + name: "project_code" + action: "BLOCK" +``` + +## Keyword Filtering + +Block or mask specific keywords: + +```yaml showLineNumbers title="config.yaml" +guardrails: + - guardrail_name: "keyword-filter" + litellm_params: + guardrail: litellm_content_filter + mode: "pre_call" + blocked_words: + - keyword: "confidential" + action: "BLOCK" + description: "Internal confidential information" + + - keyword: "proprietary" + action: "MASK" + description: "Proprietary company data" + + - keyword: "secret_project" + action: "BLOCK" +``` + +### Loading Keywords from File + +For large keyword lists, use a YAML file: + +```yaml showLineNumbers title="config.yaml" +guardrails: + - guardrail_name: "keyword-file-filter" + litellm_params: + guardrail: litellm_content_filter + mode: "pre_call" + blocked_words_file: "/path/to/sensitive_keywords.yaml" +``` + +```yaml showLineNumbers title="sensitive_keywords.yaml" +blocked_words: + - keyword: "project_apollo" + action: "BLOCK" + description: "Confidential project codename" + + - keyword: "internal_api" + action: "MASK" + description: "Internal API references" + + - keyword: "customer_database" + action: "BLOCK" + description: "Protected database name" +``` + +## Streaming Support + +Content filter works with streaming responses by checking each chunk: + +```yaml showLineNumbers title="config.yaml" +guardrails: + - guardrail_name: "streaming-filter" + litellm_params: + guardrail: litellm_content_filter + mode: "during_call" # Check each streaming chunk + patterns: + - pattern_type: "prebuilt" + pattern_name: "email" + action: "MASK" +``` + +```python +import openai + +client = openai.OpenAI( + api_key="sk-1234", + base_url="http://localhost:4000" +) + +response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Tell me about yourself"}], + stream=True, + extra_body={"guardrails": ["streaming-filter"]} +) + +for chunk in response: + print(chunk.choices[0].delta.content) + # Emails automatically masked in real-time +``` + +## Customizing Redaction Tags + +When using the `MASK` action, sensitive content is replaced with redaction tags. You can customize how these tags appear. + +### Default Behavior + +**Patterns:** Each pattern type gets its own tag based on the pattern name +``` +Input: "My email is john@example.com and SSN is 123-45-6789" +Output: "My email is [EMAIL_REDACTED] and SSN is [US_SSN_REDACTED]" +``` + +**Keywords:** All keywords use the same generic tag +``` +Input: "This is confidential and proprietary information" +Output: "This is [KEYWORD_REDACTED] and [KEYWORD_REDACTED] information" +``` + +### Customizing Tags + +Use `pattern_redaction_format` and `keyword_redaction_tag` to change the redaction format: + +```yaml showLineNumbers title="config.yaml" +guardrails: + - guardrail_name: "custom-redaction" + litellm_params: + guardrail: litellm_content_filter + mode: "pre_call" + pattern_redaction_format: "***{pattern_name}***" # Use {pattern_name} placeholder + keyword_redaction_tag: "***REDACTED***" + patterns: + - pattern_type: "prebuilt" + pattern_name: "email" + action: "MASK" + - pattern_type: "prebuilt" + pattern_name: "us_ssn" + action: "MASK" + blocked_words: + - keyword: "confidential" + action: "MASK" +``` + +**Output:** +``` +Input: "Email john@example.com, SSN 123-45-6789, confidential data" +Output: "Email ***EMAIL***, SSN ***US_SSN***, ***REDACTED*** data" +``` + +**Key Points:** +- `pattern_redaction_format` must include `{pattern_name}` placeholder +- Pattern names are automatically uppercased (e.g., `email` → `EMAIL`) +- `keyword_redaction_tag` is a fixed string (no placeholders) + +## Use Cases + +### 1. PII Protection +Block or mask personally identifiable information before sending to LLMs: + +```yaml +patterns: + - pattern_type: "prebuilt" + pattern_name: "us_ssn" + action: "BLOCK" + - pattern_type: "prebuilt" + pattern_name: "email" + action: "MASK" +``` + +### 2. Credential Detection +Prevent API keys and secrets from being exposed: + +```yaml +patterns: + - pattern_type: "prebuilt" + pattern_name: "aws_access_key" + action: "BLOCK" + - pattern_type: "prebuilt" + pattern_name: "github_token" + action: "BLOCK" +``` + +### 3. Sensitive Internal Data Protection +Block or mask references to confidential internal projects, codenames, or proprietary information: + +```yaml +blocked_words: + - keyword: "project_titan" + action: "BLOCK" + description: "Confidential project codename" + - keyword: "internal_api" + action: "MASK" + description: "Internal system references" +``` + +For large lists of sensitive terms, use a file: +```yaml +blocked_words_file: "/path/to/sensitive_terms.yaml" +``` + +### 4. Compliance +Ensure regulatory compliance by filtering sensitive data types: + +```yaml +patterns: + - pattern_type: "prebuilt" + pattern_name: "visa" + action: "BLOCK" + - pattern_type: "prebuilt" + pattern_name: "us_ssn" + action: "BLOCK" +``` + +## Troubleshooting + +### Pattern Not Matching + +**Issue:** Regex pattern isn't detecting expected content + +**Solution:** Test your regex pattern: +```python +import re +pattern = r'\b[A-Z]{3}-\d{4}\b' +test_text = "Employee ID: ABC-1234" +print(re.search(pattern, test_text)) # Should match +``` + +### Multiple Pattern Matches + +**Issue:** Text contains multiple sensitive patterns + +**Solution:** First matching pattern/keyword is processed. Order patterns by priority: +```yaml +patterns: + # Most critical first + - pattern_type: "prebuilt" + pattern_name: "us_ssn" + action: "BLOCK" + # Less critical + - pattern_type: "prebuilt" + pattern_name: "email" + action: "MASK" +``` + diff --git a/docs/my-website/docs/proxy/guardrails/panw_prisma_airs.md b/docs/my-website/docs/proxy/guardrails/panw_prisma_airs.md index e1d6ddf5928..edf2a05d24c 100644 --- a/docs/my-website/docs/proxy/guardrails/panw_prisma_airs.md +++ b/docs/my-website/docs/proxy/guardrails/panw_prisma_airs.md @@ -4,12 +4,12 @@ import TabItem from '@theme/TabItem'; # PANW Prisma AIRS -LiteLLM supports PANW Prisma AIRS (AI Runtime Security) guardrails via the [Prisma AIRS Scan API](https://pan.dev/prisma-airs/api/airuntimesecurity/scan-sync-request/). This integration provides **Security-as-Code** for AI applications using Palo Alto Networks' AI security platform. +LiteLLM supports PANW Prisma AIRS (AI Runtime Security) guardrails via the [Prisma AIRS Scan API](https://pan.dev/prisma-airs/api/airuntimesecurity/airuntimesecurityapi//). This integration provides **Security-as-Code** for AI applications using Palo Alto Networks' AI security platform. ## Features - ✅ **Real-time prompt injection detection** -- ✅ **Malicious content filtering** +- ✅ **Malicious URL detection** - ✅ **Data loss prevention (DLP)** - ✅ **Sensitive content masking** - Automatically mask PII, credit cards, SSNs instead of blocking - ✅ **Comprehensive threat detection** for AI models and datasets @@ -17,6 +17,7 @@ LiteLLM supports PANW Prisma AIRS (AI Runtime Security) guardrails via the [Pris - ✅ **Synchronous scanning** with immediate response - ✅ **Configurable security profiles** - ✅ **Streaming support** - Real-time masking for streaming responses +- ✅ **Multi-turn conversation tracking** - Automatic session grouping in Prisma AIRS SCM logs - ✅ **Fail-closed security** - Blocks requests if PANW API is unavailable (maximum security) ## Quick Start @@ -237,6 +238,74 @@ You can override guardrail settings on a per-request basis using the `metadata` - **Note:** If your API key is not linked to a profile, you must provide `profile_name` or `profile_id` ::: +## Multi-Turn Conversation Tracking + +PANW Prisma AIRS automatically tracks multi-turn conversations using LiteLLM's `litellm_trace_id`. This enables you to: + +- **Group related requests** - All requests in a conversation share the same AI Session ID in Prisma AIRS SCM logs +- **Track conversation context** - See the full history of prompts and responses for a user session +- **Analyze attack patterns** - Identify sophisticated multi-turn attacks across conversation history + +### How It Works + +LiteLLM automatically generates a unique `litellm_trace_id` for each conversation session. The PANW guardrail uses this as the PANW transaction ID (which maps to "AI Session ID" in Strata Cloud Manager): + +``` +Conversation Session: litellm_trace_id = "abc-123-def-456" + +Turn 1 (User): "What's the capital of France?" + → Scan ID: scan_001 | Prisma AIRS AI Session ID: abc-123-def-456 + +Turn 2 (Assistant): "Paris is the capital of France." + → Scan ID: scan_002 | Prisma AIRS AI Session ID: abc-123-def-456 + +Turn 3 (User): "What's the population?" + → Scan ID: scan_003 | Prisma AIRS AI Session ID: abc-123-def-456 + +Turn 4 (Assistant): "Paris has approximately 2.1 million residents." + → Scan ID: scan_004 | Prisma AIRS AI Session ID: abc-123-def-456 +``` + +All scans appear under the same AI Session ID in Prisma AIRS logs, making it easy to: +- Review complete conversation history (all 4 turns grouped together) +- Identify patterns across multiple turns +- Correlate security events within a session +- Track the flow of user prompts and AI responses + +### Session Tracking + +LiteLLM automatically generates a unique `litellm_trace_id` for each request, which the PANW guardrail uses as the AI Session ID in Strata Cloud Manager. All prompt and response scans for a request are automatically grouped under the same session. + +#### Custom Session IDs (Per-App Tracking) + +You can provide your own `litellm_trace_id` to track sessions on a per-app or per-conversation basis: + +```bash +curl -X POST http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sk-1234" \ + -d '{ + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "capital of France"}], + "litellm_trace_id": "my-app-session-123", # Custom AI Session ID + "metadata": { + "profile_name": "dev-allow-all-profile", # Override security profile + "user_ip": "192.168.1.1", # Track user IP + "app_name": "eng" # Custom app identifier + }, + "guardrails": ["panw-prisma-airs-pre-guard", "panw-prisma-airs-post-guard"] + }' +``` + +**Result in PANW SCM:** +- AI Session ID: `my-app-session-123` +- All prompt and response scans will be grouped under this custom session ID +- Perfect for tracking multi-turn conversations or per-application sessions + +:::tip Viewing Sessions in Prisma AIRS SCM Logs +In Strata Cloud Manager, navigate to **AI Runtime > Sessions** to view all AI Session IDs and their associated scans. Click on a session to see the complete conversation history with security analysis. +::: + ## Environment Variables ```bash diff --git a/docs/my-website/docs/proxy/guardrails/test_playground.md b/docs/my-website/docs/proxy/guardrails/test_playground.md new file mode 100644 index 00000000000..832a912e114 --- /dev/null +++ b/docs/my-website/docs/proxy/guardrails/test_playground.md @@ -0,0 +1,46 @@ +import Image from '@theme/IdealImage'; + +# Guardrail Testing Playground + +Test and compare multiple guardrails in real-time with an interactive playground interface. + +Guardrail Test Playground + +## How to Use the Guardrail Testing Playground + +The Guardrail Testing Playground allows you to quickly test and compare the behavior of different guardrails with sample inputs. + +### Steps to Test Guardrails + +1. **Navigate to the Guardrails Section** + - Open the LiteLLM Admin UI + - Go to the **Guardrails** section + +2. **Open Test Playground** + - Click on the **Test Playground** tab at the top of the page + +3. **Select Guardrails to Test** + - Check the guardrails you want to compare + - You can select multiple guardrails to see how they each respond to the same input + +4. **Enter Your Input** + - Type or paste your test input in the text area + - This could be a prompt, message, or any text you want to validate against the guardrails + +5. **Run the Test** + - Click the **Test guardrails** button (or press Enter) + +6. **View Results** + - See the output from each selected guardrail + - Compare how different guardrails handle the same input + - Results will show whether the input passed or was blocked by each guardrail + +## Use Cases + +This is ideal for **Security Teams** & **LiteLLM Admins** evaluating guardrail solutions. + +This brings the following benefits for LiteLLM users: + +- **Compare guardrail responses**: test the same prompt across multiple providers (Lakera, Noma AI, Bedrock Guardrails, etc.) simultaneously. + +- **Validate configurations**: verify your guardrails catch the threats you care about before production deployment. diff --git a/docs/my-website/docs/proxy/health.md b/docs/my-website/docs/proxy/health.md index 7df7685f335..6f98265e40a 100644 --- a/docs/my-website/docs/proxy/health.md +++ b/docs/my-website/docs/proxy/health.md @@ -106,6 +106,13 @@ model_list: mode: image_generation # 👈 ADD THIS ``` +#### Custom Health Check Prompt + +By default, health checks use the prompt `"test from litellm"`. You can customize this prompt globally by setting an environment variable, or per-model via config: + +```bash +DEFAULT_HEALTH_CHECK_PROMPT="this is a test prompt" +``` ### Text Completion Models diff --git a/docs/my-website/docs/secret_managers/hashicorp_vault.md b/docs/my-website/docs/secret_managers/hashicorp_vault.md index 4d0ef05a326..9e536270988 100644 --- a/docs/my-website/docs/secret_managers/hashicorp_vault.md +++ b/docs/my-website/docs/secret_managers/hashicorp_vault.md @@ -16,21 +16,28 @@ import Image from '@theme/IdealImage'; |---------|----------|-------------| | Reading Secrets | ✅ | Read secrets e.g `OPENAI_API_KEY` | | Writing Secrets | ✅ | Store secrets e.g `Virtual Keys` | +| Authentication Methods to Hashicorp Vault | ✅ | AppRole, TLS Certificate, Token | Read secrets from [Hashicorp Vault](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2) **Step 1.** Add Hashicorp Vault details in your environment -LiteLLM supports two methods of authentication: +LiteLLM supports three methods of authentication: -1. TLS cert authentication - `HCP_VAULT_CLIENT_CERT` and `HCP_VAULT_CLIENT_KEY` -2. Token authentication - `HCP_VAULT_TOKEN` +1. AppRole authentication (recommended) - `HCP_VAULT_APPROLE_ROLE_ID` and `HCP_VAULT_APPROLE_SECRET_ID` +2. TLS cert authentication - `HCP_VAULT_CLIENT_CERT` and `HCP_VAULT_CLIENT_KEY` +3. Token authentication - `HCP_VAULT_TOKEN` ```bash HCP_VAULT_ADDR="https://test-cluster-public-vault-0f98180c.e98296b2.z1.hashicorp.cloud:8200" HCP_VAULT_NAMESPACE="admin" -# Authentication via TLS cert +# Authentication via AppRole (recommended) +HCP_VAULT_APPROLE_ROLE_ID="your-role-id" +HCP_VAULT_APPROLE_SECRET_ID="your-secret-id" +HCP_VAULT_APPROLE_MOUNT_PATH="approle" # OPTIONAL. defaults to "approle" + +# OR - Authentication via TLS cert HCP_VAULT_CLIENT_CERT="path/to/client.pem" HCP_VAULT_CLIENT_KEY="path/to/client.key" @@ -64,6 +71,80 @@ $ litellm --config /path/to/config.yaml [Quick Test Proxy](../proxy/user_keys) +## Authentication Methods + +LiteLLM supports three authentication methods for Hashicorp Vault, with the following priority: + +1. **AppRole** - Recommended for production applications +2. **TLS Certificate** - For certificate-based authentication +3. **Token** - Direct token authentication + +### 1. AppRole Authentication + +To set up AppRole authentication: + +1. Enable AppRole auth in Vault: +```bash +vault auth enable approle +``` + +2. Create a policy and role for LiteLLM: +```bash +# Create a policy file (litellm-policy.hcl) +path "secret/data/*" { + capabilities = ["create", "read", "update", "delete", "list"] +} + +# Apply the policy +vault policy write litellm-policy litellm-policy.hcl + +# Create an AppRole +vault write auth/approle/role/litellm \ + token_policies="litellm-policy" \ + token_ttl=32d \ + token_max_ttl=32d +``` + +3. Get your Role ID and Secret ID: +```bash +# Get Role ID +vault read auth/approle/role/litellm/role-id + +# Generate Secret ID +vault write -f auth/approle/role/litellm/secret-id +``` + +4. Set the environment variables: +```bash +export HCP_VAULT_APPROLE_ROLE_ID="your-role-id" +export HCP_VAULT_APPROLE_SECRET_ID="your-secret-id" +``` + +### 2. TLS Certificate Authentication + +TLS Certificate authentication uses client certificates for mutual TLS authentication with Vault. + +**Environment Variables:** +```bash +export HCP_VAULT_CLIENT_CERT="path/to/client.pem" +export HCP_VAULT_CLIENT_KEY="path/to/client.key" +export HCP_VAULT_CERT_ROLE="your-cert-role" # Optional +``` + +**How it works:** +- LiteLLM uses the client certificate and key for mutual TLS authentication +- Vault validates the certificate and issues a temporary token +- The token is cached for the duration of its lease + +### 3. Token Authentication + +Direct token authentication uses a static Vault token. + +**Environment Variables:** +```bash +export HCP_VAULT_TOKEN="hvs.CAESIG52gL6ljBSdmq*****" +``` + ## How it works **Reading Secrets** diff --git a/docs/my-website/docs/videos.md b/docs/my-website/docs/videos.md index 96ff4c8190a..cc9f1bc9cea 100644 --- a/docs/my-website/docs/videos.md +++ b/docs/my-website/docs/videos.md @@ -9,7 +9,7 @@ Fallbacks | ✅ (Between supported models) | | Guardrails Support | ✅ Content moderation and safety checks | | Proxy Server Support | ✅ Full proxy integration with virtual keys | | Spend Management | ✅ Budget tracking and rate limiting | -| Supported Providers | `openai`, `azure` | +| Supported Providers | `openai`, `azure`, `gemini`, `vertex_ai` | :::tip @@ -41,8 +41,7 @@ print(f"Initial Status: {response.status}") # Check status until video is ready while True: status_response = video_status( - video_id=response.id, - custom_llm_provider="openai" + video_id=response.id ) print(f"Current Status: {status_response.status}") @@ -57,8 +56,7 @@ while True: # Download video content when ready video_bytes = video_content( - video_id=response.id, - custom_llm_provider="openai" + video_id=response.id ) # Save to file @@ -88,8 +86,7 @@ async def test_async_video(): # Check status until video is ready while True: status_response = await avideo_status( - video_id=response.id, - custom_llm_provider="openai" + video_id=response.id ) print(f"Current Status: {status_response.status}") @@ -104,8 +101,7 @@ async def test_async_video(): # Download video content when ready video_bytes = await avideo_content( - video_id=response.id, - custom_llm_provider="openai" + video_id=response.id ) # Save to file @@ -120,21 +116,27 @@ asyncio.run(test_async_video()) ```python from litellm import video_status -# Check the status of a video generation status_response = video_status( - video_id="video_1234567890", - custom_llm_provider="openai" + video_id="video_1234567890" ) print(f"Video Status: {status_response.status}") print(f"Created At: {status_response.created_at}") print(f"Model: {status_response.model}") +``` -# Possible status values: -# - "queued": Video is in the queue -# - "processing": Video is being generated -# - "completed": Video is ready for download -# - "failed": Video generation failed +### List Videos + +For listing videos, you need to specify the provider since there's no video_id to decode from: + +```python +from litellm import video_list + +# List videos from OpenAI +videos = video_list(custom_llm_provider="openai") + +for video in videos: + print(f"Video ID: {video['id']}") ``` ### Video Generation with Reference Image @@ -207,7 +209,7 @@ print(f"Video ID: {response.id}") LiteLLM provides OpenAI API compatible video endpoints for complete video generation workflow: -- `/videos/generations` - Generate new videos +- `/videos` - Generate new videos - `/videos/remix` - Edit existing videos with reference images - `/videos/status` - Check video generation status - `/videos/retrieval` - Download completed videos @@ -227,7 +229,6 @@ model_list: model: azure/sora-2 api_key: os.environ/AZURE_OPENAI_API_KEY api_base: os.environ/AZURE_OPENAI_API_BASE - api_version: "2024-02-15-preview" ``` Start litellm @@ -253,31 +254,14 @@ curl --location 'http://localhost:4000/v1/videos' \ Test video status request ```bash -# Using custom-llm-provider header -curl --location 'http://localhost:4000/v1/videos/video_id' \ ---header 'Accept: application/json' \ ---header 'x-litellm-api-key: sk-1234' \ ---header 'custom-llm-provider: azure' - -# Or using query parameter -curl --location 'http://localhost:4000/v1/videos/video_id?custom_llm_provider=azure' \ ---header 'Accept: application/json' \ +curl --location 'http://localhost:4000/v1/videos/{video_id}' \ --header 'x-litellm-api-key: sk-1234' ``` Test video retrieval request ```bash -# Using custom-llm-provider header -curl --location 'http://localhost:4000/v1/videos/video_id/content' \ ---header 'Accept: application/json' \ ---header 'x-litellm-api-key: sk-1234' \ ---header 'custom-llm-provider: openai' \ ---output video.mp4 - -# Or using query parameter -curl --location 'http://localhost:4000/v1/videos/video_id/content?custom_llm_provider=openai' \ ---header 'Accept: application/json' \ +curl --location 'http://localhost:4000/v1/videos/{video_id}/content' \ --header 'x-litellm-api-key: sk-1234' \ --output video.mp4 ``` @@ -285,27 +269,27 @@ curl --location 'http://localhost:4000/v1/videos/video_id/content?custom_llm_pro Test video remix request ```bash -# Using custom_llm_provider in request body -curl --location --request POST 'http://localhost:4000/v1/videos/video_id/remix' \ ---header 'Accept: application/json' \ +curl --location --request POST 'http://localhost:4000/v1/videos/{video_id}/remix' \ --header 'Content-Type: application/json' \ --header 'x-litellm-api-key: sk-1234' \ ---data '{ - "prompt": "New remix instructions", - "custom_llm_provider": "azure" -}' - -# Or using custom-llm-provider header -curl --location --request POST 'http://localhost:4000/v1/videos/video_id/remix' \ ---header 'Accept: application/json' \ ---header 'Content-Type: application/json' \ ---header 'x-litellm-api-key: sk-1234' \ ---header 'custom-llm-provider: azure' \ --data '{ "prompt": "New remix instructions" }' ``` +Test video list request (requires custom_llm_provider) + +```bash +# Note: video_list requires custom_llm_provider since there's no video_id to decode from +curl --location 'http://localhost:4000/v1/videos?custom_llm_provider=openai' \ +--header 'x-litellm-api-key: sk-1234' + +# Or using header +curl --location 'http://localhost:4000/v1/videos' \ +--header 'x-litellm-api-key: sk-1234' \ +--header 'custom-llm-provider: azure' +``` + Test Azure video generation request ```bash @@ -618,4 +602,6 @@ The response follows OpenAI's video generation format with the following structu | Provider | Link to Usage | |-------------|--------------------| | OpenAI | [Usage](providers/openai/videos) | -| Azure | [Usage](providers/azure/videos) | \ No newline at end of file +| Azure | [Usage](providers/azure/videos) | +| Gemini | [Usage](providers/gemini/videos) | +| Vertex AI | [Usage](providers/vertex_ai/videos) | diff --git a/docs/my-website/img/add_Guard2.gif b/docs/my-website/img/add_Guard2.gif new file mode 100644 index 00000000000..9df8f771815 Binary files /dev/null and b/docs/my-website/img/add_Guard2.gif differ diff --git a/docs/my-website/img/add_guard5.gif b/docs/my-website/img/add_guard5.gif new file mode 100644 index 00000000000..6574bc29102 Binary files /dev/null and b/docs/my-website/img/add_guard5.gif differ diff --git a/docs/my-website/img/create_guard.gif b/docs/my-website/img/create_guard.gif new file mode 100644 index 00000000000..6300f081a6e Binary files /dev/null and b/docs/my-website/img/create_guard.gif differ diff --git a/docs/my-website/img/create_guard3.gif b/docs/my-website/img/create_guard3.gif new file mode 100644 index 00000000000..287a1663df8 Binary files /dev/null and b/docs/my-website/img/create_guard3.gif differ diff --git a/docs/my-website/img/guardrail_playground.png b/docs/my-website/img/guardrail_playground.png new file mode 100644 index 00000000000..3b5efdffdea Binary files /dev/null and b/docs/my-website/img/guardrail_playground.png differ diff --git a/docs/my-website/img/release_notes/built_in_guard.png b/docs/my-website/img/release_notes/built_in_guard.png new file mode 100644 index 00000000000..32fcdc8dca9 Binary files /dev/null and b/docs/my-website/img/release_notes/built_in_guard.png differ diff --git a/docs/my-website/release_notes/v1.79.1-stable/index.md b/docs/my-website/release_notes/v1.79.1-stable/index.md index 986ea42c680..ea8cfeae740 100644 --- a/docs/my-website/release_notes/v1.79.1-stable/index.md +++ b/docs/my-website/release_notes/v1.79.1-stable/index.md @@ -1,5 +1,5 @@ --- -title: "[Preview] v1.79.1-stable - FAL AI Support" +title: "v1.79.1-stable - Guardrail Playground" slug: "v1-79-1" date: 2025-11-01T10:00:00 authors: @@ -27,7 +27,7 @@ import TabItem from '@theme/TabItem'; docker run \ -e STORE_MODEL_IN_DB=True \ -p 4000:4000 \ -ghcr.io/berriai/litellm:v1.80.0-stable +ghcr.io/berriai/litellm:v1.79.1-stable ``` @@ -151,7 +151,7 @@ pip install litellm==1.80.0 #### Features -- **[Container API](../../docs/container_api)** +- **[Container API](../../docs/containers)** - Add end-to-end OpenAI Container API support to LiteLLM SDK - [PR #16136](https://github.com/BerriAI/litellm/pull/16136) - Add proxy support for container APIs - [PR #16049](https://github.com/BerriAI/litellm/pull/16049) - Add logging support for Container API - [PR #16049](https://github.com/BerriAI/litellm/pull/16049) diff --git a/docs/my-website/release_notes/v1.79.3-stable/index.md b/docs/my-website/release_notes/v1.79.3-stable/index.md new file mode 100644 index 00000000000..f081fa614eb --- /dev/null +++ b/docs/my-website/release_notes/v1.79.3-stable/index.md @@ -0,0 +1,444 @@ +--- +title: "[Preview] v1.79.3-stable - Built-in Guardrails on AI Gateway" +slug: "v1-79-3" +date: 2025-11-08T10:00:00 +authors: + - name: Krrish Dholakia + title: CEO, LiteLLM + url: https://www.linkedin.com/in/krish-d/ + image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg + - name: Ishaan Jaff + title: CTO, LiteLLM + url: https://www.linkedin.com/in/reffajnaahsi/ + image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg +hide_table_of_contents: false +--- + +import Image from '@theme/IdealImage'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Deploy this version + + + + +``` showLineNumbers title="docker run litellm" +docker run \ +-e STORE_MODEL_IN_DB=True \ +-p 4000:4000 \ +ghcr.io/berriai/litellm:v1.79.3.rc.1 +``` + + + + + +``` showLineNumbers title="pip install litellm" +pip install litellm==1.79.3.rc.1 +``` + + + + +--- + +## Key Highlights + +- **LiteLLM Custom Guardrail** - Built-in guardrail with UI configuration support +- **Performance Improvements** - `/responses` API 19× Lower Median Latency +- **Veo3 Video Generation (Vertex AI + Google AI Studio)** - Use OpenAI Video API to generate videos with Vertex AI and Google AI Studio Veo3 models + +--- + +### Built-in Guardrails on AI Gateway + + + +
+ +This release introduces built-in guardrails for LiteLLM AI Gateway, allowing you to enforce protections without depending on an external guardrail API. + +- **Blocking Keywords** - Block known sensitive keywords like "litellm", "python", etc. +- **Pattern Detection** - Block known sensitive patterns like emails, Social Security Numbers, API keys, etc. +- **Custom Regex Patterns** - Define custom regex patterns for your specific use case. + + +Get started with the built-in guardrails on AI Gateway [here](https://docs.litellm.ai/docs/proxy/guardrails/litellm_content_filter). + +--- + +### Performance – `/responses` 19× Lower Median Latency + +This update significantly improves `/responses` latency by integrating our internal network management for connection handling, eliminating per-request setup overhead. + +#### Results + +| Metric | Before | After | Improvement | +|--------|--------|-------|-------------| +| Median latency | 3,600 ms | **190 ms** | **−95% (~19× faster)** | +| p95 latency | 4,300 ms | **280 ms** | −93% | +| p99 latency | 4,600 ms | **590 ms** | −87% | +| Average latency | 3,571 ms | **208 ms** | −94% | +| RPS | 231 | **1,059** | +358% | + +#### Test Setup + +| Category | Specification | +|----------|---------------| +| **Load Testing** | Locust: 1,000 concurrent users, 500 ramp-up | +| **System** | 4 vCPUs, 8 GB RAM, 4 workers, 4 instances | +| **Database** | PostgreSQL (Redis unused) | +| **Configuration** | [config.yaml](https://gist.github.com/AlexsanderHamir/550791675fd752befcac6a9e44024652) | +| **Load Script** | [no_cache_hits.py](https://gist.github.com/AlexsanderHamir/99d673bf74cdd81fd39f59fa9048f2e8) | + +--- + +## New Models / Updated Models + +#### New Model Support + +| Provider | Model | Context Window | Input ($/1M tokens) | Output ($/1M tokens) | Features | +| -------- | ----- | -------------- | ------------------- | -------------------- | -------- | +| Azure | `azure/gpt-5-pro` | 272K | $15.00 | $120.00 | Responses API, reasoning, vision, PDF input | +| Azure | `azure/gpt-image-1-mini` | - | - | - | Image generation - per pixel pricing | +| Azure | `azure/container` | - | - | - | Container API - $0.03/session | +| OpenAI | `openai/container` | - | - | - | Container API - $0.03/session | +| Cohere | `cohere/embed-v4.0` | 128K | $0.12 | - | Embeddings with image input support | +| Gemini | `gemini/gemini-live-2.5-flash-preview-native-audio-09-2025` | 1M | $0.30 | $2.00 | Native audio, vision, web search | +| Vertex AI | `vertex_ai/minimaxai/minimax-m2-maas` | 196K | $0.30 | $1.20 | Function calling, tool choice | +| NVIDIA | `nvidia/nemotron-nano-9b-v2` | - | - | - | Chat completions | + +#### OCR Models + +| Provider | Model | Cost Per Page | Features | +| -------- | ----- | ------------- | -------- | +| Azure AI | `azure_ai/doc-intelligence/prebuilt-read` | $0.0015 | Document reading | +| Azure AI | `azure_ai/doc-intelligence/prebuilt-layout` | $0.01 | Layout analysis | +| Azure AI | `azure_ai/doc-intelligence/prebuilt-document` | $0.01 | Document processing | +| Vertex AI | `vertex_ai/mistral-ocr-2505` | $0.0005 | OCR processing | + +#### Search Models + +| Provider | Model | Pricing | Features | +| -------- | ----- | ------- | -------- | +| Firecrawl | `firecrawl/search` | Tiered: $0.00166-$0.0166/query | 10-100 results per query | +| SearXNG | `searxng/search` | Free | Open-source metasearch | + +#### Features + +- **[Azure](../../docs/providers/azure)** + - Add Azure GPT-5-Pro Responses API support with reasoning capabilities - [PR #16235](https://github.com/BerriAI/litellm/pull/16235) + - Add gpt-image-1-mini pricing for Azure with quality tiers (low/medium/high) - [PR #16182](https://github.com/BerriAI/litellm/pull/16182) + - Add support for returning Azure Content Policy error information when exceptions from Azure OpenAI occur - [PR #16231](https://github.com/BerriAI/litellm/pull/16231) + - Fix Azure GPT-5 incorrectly routed to O-series config (temperature parameter unsupported) - [PR #16246](https://github.com/BerriAI/litellm/pull/16246) + - Fix Azure doesn't accept extra body param - [PR #16116](https://github.com/BerriAI/litellm/pull/16116) + - Fix Azure DALL-E-3 health check content policy violation by using safe default prompt - [PR #16329](https://github.com/BerriAI/litellm/pull/16329) + +- **[Bedrock](../../docs/providers/bedrock)** + - Fix empty assistant message handling in AWS Bedrock Converse API to prevent 400 Bad Request errors - [PR #15850](https://github.com/BerriAI/litellm/pull/15850) + - Fix: Filter AWS authentication params from Bedrock InvokeModel request body - [PR #16315](https://github.com/BerriAI/litellm/pull/16315) + - Fix Bedrock proxy adding name to file content, breaks when cache_control in use - [PR #16275](https://github.com/BerriAI/litellm/pull/16275) + - Fix global.anthropic.claude-haiku-4-5-20251001-v1:0 supports_reasoning flag and update pricing - [PR #16263](https://github.com/BerriAI/litellm/pull/16263) + +- **[Gemini (Google AI Studio + Vertex AI)](../../docs/providers/gemini)** + - Add gemini live audio model cost in model map - [PR #16183](https://github.com/BerriAI/litellm/pull/16183) + - Fix translation problem with Gemini parallel tool calls - [PR #16194](https://github.com/BerriAI/litellm/pull/16194) + - Fix: Send Gemini API key via x-goog-api-key header with custom api_base - [PR #16085](https://github.com/BerriAI/litellm/pull/16085) + - Fix image_config.aspect_ratio not working for gemini-2.5-flash-image - [PR #15999](https://github.com/BerriAI/litellm/pull/15999) + - Fix Gemini minimal reasoning env overrides disabling thoughts - [PR #16347](https://github.com/BerriAI/litellm/pull/16347) + - Fix cache_read_input_token_cost for gemini-2.5-flash - [PR #16354](https://github.com/BerriAI/litellm/pull/16354) + +- **[Anthropic](../../docs/providers/anthropic)** + - Fix Anthropic token counting for VertexAI - [PR #16171](https://github.com/BerriAI/litellm/pull/16171) + - Fix anthropic-adapter: properly translate Anthropic image format to OpenAI - [PR #16202](https://github.com/BerriAI/litellm/pull/16202) + - Enable automated prompt caching message format for Claude on Databricks - [PR #16200](https://github.com/BerriAI/litellm/pull/16200) + - Add support for Anthropic Memory Tool - [PR #16115](https://github.com/BerriAI/litellm/pull/16115) + - Propagate cache creation/read token costs for model info to fix Anthropic long context cost calculations - [PR #16376](https://github.com/BerriAI/litellm/pull/16376) + +- **[Vertex AI](../../docs/providers/vertex_ai)** + - Add Vertex MiniMAX m2 model support - [PR #16373](https://github.com/BerriAI/litellm/pull/16373) + - Correctly map 429 Resource Exhausted to RateLimitError - [PR #16363](https://github.com/BerriAI/litellm/pull/16363) + - Add `vertex_credentials` support to `litellm.rerank()` for Vertex AI - [PR #16266](https://github.com/BerriAI/litellm/pull/16266) + +- **[Databricks](../../docs/providers/databricks)** + - Fix databricks streaming - [PR #16368](https://github.com/BerriAI/litellm/pull/16368) + +- **[Deepgram](../../docs/providers/deepgram)** + - Return the diarized transcript when it's required in the request - [PR #16133](https://github.com/BerriAI/litellm/pull/16133) + +- **[Fireworks](../../docs/providers/fireworks_ai)** + - Update Fireworks audio endpoints to new `api.fireworks.ai` domains - [PR #16346](https://github.com/BerriAI/litellm/pull/16346) + +- **[Cohere](../../docs/providers/cohere)** + - Add cohere embed-v4.0 model support - [PR #16358](https://github.com/BerriAI/litellm/pull/16358) + +- **[Watsonx](../../docs/providers/watsonx)** + - Support `reasoning_effort` for watsonx chat models - [PR #16261](https://github.com/BerriAI/litellm/pull/16261) + +- **[OpenAI](../../docs/providers/openai)** + - Remove automatic summary from reasoning_effort transformation - [PR #16210](https://github.com/BerriAI/litellm/pull/16210) + +- **[XAI](../../docs/providers/xai)** + - Remove Grok 4 Models Reasoning Effort Parameter - [PR #16265](https://github.com/BerriAI/litellm/pull/16265) + +- **[Hosted VLLM](../../docs/providers/vllm)** + - Fix HostedVLLMRerankConfig will not be used - [PR #16352](https://github.com/BerriAI/litellm/pull/16352) + +#### New Provider Support + +- **[Bedrock Agentcore](../../docs/providers/bedrock)** + - Add Bedrock Agentcore as a provider on LiteLLM Python SDK and LiteLLM AI Gateway - [PR #16252](https://github.com/BerriAI/litellm/pull/16252) + +--- + +## LLM API Endpoints + +#### Features + +- **[OCR API](../../docs/ocr)** + - Add VertexAI OCR provider support + cost tracking - [PR #16216](https://github.com/BerriAI/litellm/pull/16216) + - Add Azure AI Doc Intelligence OCR support - [PR #16219](https://github.com/BerriAI/litellm/pull/16219) + +- **[Search API](../../docs/search)** + - Add firecrawl search API support with tiered pricing - [PR #16257](https://github.com/BerriAI/litellm/pull/16257) + - Add searxng search API provider - [PR #16259](https://github.com/BerriAI/litellm/pull/16259) + +- **[Responses API](../../docs/response_api)** + - Support responses API streaming in langfuse otel - [PR #16153](https://github.com/BerriAI/litellm/pull/16153) + - Pass extra_body parameters to provider in Responses API requests - [PR #16320](https://github.com/BerriAI/litellm/pull/16320) + +- **[Container API](../../docs/container_api)** + - Add E2E Container API Support - [PR #16136](https://github.com/BerriAI/litellm/pull/16136) + - Update container documentation to be similar to others - [PR #16327](https://github.com/BerriAI/litellm/pull/16327) + +- **[Video Generation API](../../docs/video_generation)** + - Add Vertex and Gemini Videos API with Cost Tracking + UI support - [PR #16323](https://github.com/BerriAI/litellm/pull/16323) + - Add `custom_llm_provider` support for video endpoints (non-generation) - [PR #16121](https://github.com/BerriAI/litellm/pull/16121) + +- **[Audio API](../../docs/audio)** + - Add gpt-4o-transcribe cost tracking - [PR #16412](https://github.com/BerriAI/litellm/pull/16412) + +- **[Vector Stores](../../docs/vector_stores)** + - Milvus - search vector store support + support multi-part form data on passthrough - [PR #16035](https://github.com/BerriAI/litellm/pull/16035) + - Azure AI Vector Stores - support "virtual" indexes + create vector store on passthrough API - [PR #16160](https://github.com/BerriAI/litellm/pull/16160) + - Milvus - Passthrough API support - adds create + read vector store support via passthrough API's - [PR #16170](https://github.com/BerriAI/litellm/pull/16170) + +- **[Embeddings API](../../docs/embedding/supported_embedding)** + - Use valid CallTypes enum value in embeddings endpoint - [PR #16328](https://github.com/BerriAI/litellm/pull/16328) + +- **[Rerank API](../../docs/rerank)** + - Generalize tiered pricing in generic cost calculator - [PR #16150](https://github.com/BerriAI/litellm/pull/16150) + +#### Bugs + +- **General** + - Fix index field not populated in streaming mode with n>1 and tool calls - [PR #15962](https://github.com/BerriAI/litellm/pull/15962) + - Pass aws_region_name in litellm_params - [PR #16321](https://github.com/BerriAI/litellm/pull/16321) + - Add `retry-after` header support for errors `502`, `503`, `504` - [PR #16288](https://github.com/BerriAI/litellm/pull/16288) + +--- + +## Management Endpoints / UI + +#### Features + +- **Virtual Keys** + - UI - Delete Team Member with friction - [PR #16167](https://github.com/BerriAI/litellm/pull/16167) + - UI - Litellm test key audio support - [PR #16251](https://github.com/BerriAI/litellm/pull/16251) + - UI - Test Key Page Revert Model To Single Select - [PR #16390](https://github.com/BerriAI/litellm/pull/16390) + +- **Models + Endpoints** + - UI - Add Model Existing Credentials Improvement - [PR #16166](https://github.com/BerriAI/litellm/pull/16166) + - UI - Add Azure AD Token field and Azure API Key optional - [PR #16331](https://github.com/BerriAI/litellm/pull/16331) + - UI - Fixed Label for vLLM in Model Create Flow - [PR #16285](https://github.com/BerriAI/litellm/pull/16285) + - UI - Include Model Access Group Models on Team Models Table - [PR #16298](https://github.com/BerriAI/litellm/pull/16298) + - Fix /model_group/info Returning Entire Model List for SSO Users - [PR #16296](https://github.com/BerriAI/litellm/pull/16296) + - Litellm non root docker Model Hub Table fix - [PR #16282](https://github.com/BerriAI/litellm/pull/16282) + +- **Guardrails** + - UI - Fix regression where Guardrail Entity Could not be selected and entity was not displayed - [PR #16165](https://github.com/BerriAI/litellm/pull/16165) + - UI - Guardrail Info Page Show PII Config - [PR #16164](https://github.com/BerriAI/litellm/pull/16164) + - Change guardrail_information to list type - [PR #16127](https://github.com/BerriAI/litellm/pull/16127) + - UI - LiteLLM Guardrail - ensure you can see UI Friendly name for PII Patterns - [PR #16382](https://github.com/BerriAI/litellm/pull/16382) + - UI - Guardrails - LiteLLM Content Filter, Allow Viewing/Editing Content Filter Settings - [PR #16383](https://github.com/BerriAI/litellm/pull/16383) + - UI - Guardrails - allow updating guardrails through UI. Ensure litellm_params actually get updated in memory - [PR #16384](https://github.com/BerriAI/litellm/pull/16384) + +- **SSO Settings** + - Support dot notation on ui sso - [PR #16135](https://github.com/BerriAI/litellm/pull/16135) + - UI - Prevent trailing slash in sso proxy base url input - [PR #16244](https://github.com/BerriAI/litellm/pull/16244) + - UI - SSO Proxy Base URL input validation and remove normalizing / - [PR #16332](https://github.com/BerriAI/litellm/pull/16332) + - UI - Surface SSO Create errors on create flow - [PR #16369](https://github.com/BerriAI/litellm/pull/16369) + +- **Usage & Analytics** + - UI - Tag Usage Top Model Table View and Label Fix - [PR #16249](https://github.com/BerriAI/litellm/pull/16249) + - UI - Litellm usage date picker - [PR #16264](https://github.com/BerriAI/litellm/pull/16264) + +- **Cache Settings** + - UI - Cache Settings Redis Add Semantic Cache Settings - [PR #16398](https://github.com/BerriAI/litellm/pull/16398) + +#### Bugs + +- **General** + - UI - Remove encoding_format in request for embedding models - [PR #16367](https://github.com/BerriAI/litellm/pull/16367) + - UI - Revert Changes for Test Key Multiple Model Select - [PR #16372](https://github.com/BerriAI/litellm/pull/16372) + - UI - Various Small Issues - [PR #16406](https://github.com/BerriAI/litellm/pull/16406) + +--- + +## AI Integrations + +### Logging + +- **[Langfuse](../../docs/proxy/logging#langfuse)** + - Fix langfuse input tokens logic for cached tokens - [PR #16203](https://github.com/BerriAI/litellm/pull/16203) + +- **[Opik](../../docs/proxy/logging#opik)** + - Fix the bug with not incorrect attachment to existing trace & refactor - [PR #15529](https://github.com/BerriAI/litellm/pull/15529) + +- **[S3](../../docs/proxy/logging#s3)** + - S3 logger, add support for ssl_verify when using minio logger - [PR #16211](https://github.com/BerriAI/litellm/pull/16211) + - Strip base64 in s3 - [PR #16157](https://github.com/BerriAI/litellm/pull/16157) + - Add allowing Key based prefix to s3 path - [PR #16237](https://github.com/BerriAI/litellm/pull/16237) + - Add Prometheus metric to track callback logging failures in S3 - [PR #16209](https://github.com/BerriAI/litellm/pull/16209) + +- **[OpenTelemetry](../../docs/proxy/logging#opentelemetry)** + - OTEL - Log Cost Breakdown on OTEL Logger - [PR #16334](https://github.com/BerriAI/litellm/pull/16334) + +- **[DataDog](../../docs/proxy/logging#datadog)** + - Add DD Agent Host support for `datadog` callback - [PR #16379](https://github.com/BerriAI/litellm/pull/16379) + +### Guardrails + +- **[Noma](../../docs/proxy/guardrails)** + - Revert Noma Apply Guardrail implementation - [PR #16214](https://github.com/BerriAI/litellm/pull/16214) + - Litellm noma guardrail support images - [PR #16199](https://github.com/BerriAI/litellm/pull/16199) + +- **[PANW Prisma AIRS](../../docs/proxy/guardrails)** + - PANW prisma airs guardrail deduplication and enhanced session tracking - [PR #16273](https://github.com/BerriAI/litellm/pull/16273) + +- **[LiteLLM Custom Guardrail](../../docs/proxy/guardrails)** + - Add LiteLLM Gateway built in guardrail - [PR #16338](https://github.com/BerriAI/litellm/pull/16338) + - UI - Allow configuring LiteLLM Custom Guardrail - [PR #16339](https://github.com/BerriAI/litellm/pull/16339) + - Bug Fix: Content Filter Guard - [PR #16414](https://github.com/BerriAI/litellm/pull/16414) + +### Secret Managers + +- **[CyberArk](../../docs/secret_managers)** + - Add CyberArk Secrets Manager Integration - [PR #16278](https://github.com/BerriAI/litellm/pull/16278) + - Cyber Ark - Add Key Rotations support - [PR #16289](https://github.com/BerriAI/litellm/pull/16289) + +- **[HashiCorp Vault](../../docs/secret_managers)** + - Add configurable mount name and path prefix for HashiCorp Vault - [PR #16253](https://github.com/BerriAI/litellm/pull/16253) + - Secret Manager - Hashicorp, add auth via approle - [PR #16374](https://github.com/BerriAI/litellm/pull/16374) + +- **[AWS Secrets Manager](../../docs/secret_managers)** + - Add tags and descriptions support to aws secrets manager - [PR #16224](https://github.com/BerriAI/litellm/pull/16224) + +- **[Custom Secret Manager](../../docs/secret_managers)** + - Add Custom Secret Manager - Allow users to define and write a custom secret manager - [PR #16297](https://github.com/BerriAI/litellm/pull/16297) + +- **General** + - Email Notifications - Ensure Users get Key Rotated Email - [PR #16292](https://github.com/BerriAI/litellm/pull/16292) + - Fix verify ssl on sts boto3 - [PR #16313](https://github.com/BerriAI/litellm/pull/16313) + +--- + +## Spend Tracking, Budgets and Rate Limiting + +- **Cost Tracking** + - Fix OpenAI Responses API streaming tests usage field names and cost calculation - [PR #16236](https://github.com/BerriAI/litellm/pull/16236) + +--- + +## MCP Gateway + +- **Configuration** + - Configure static mcp header - [PR #16179](https://github.com/BerriAI/litellm/pull/16179) + - Persist mcp credentials in db - [PR #16308](https://github.com/BerriAI/litellm/pull/16308) + + +## Performance / Loadbalancing / Reliability improvements + +- **Memory Leak Fixes** + - Resolve memory accumulation caused by Pydantic 2.11+ deprecation warnings - [PR #16110](https://github.com/BerriAI/litellm/pull/16110) + +- **Session Management** + - Add shared_session support to responses API - [PR #16260](https://github.com/BerriAI/litellm/pull/16260) + +- **Error Handling** + - Gracefully handle connection closed errors during streaming - [PR #16294](https://github.com/BerriAI/litellm/pull/16294) + - Handle None values in daily spend sort key - [PR #16245](https://github.com/BerriAI/litellm/pull/16245) + +- **Configuration** + - Remove minimum validation for cache control injection index - [PR #16149](https://github.com/BerriAI/litellm/pull/16149) + - Improve clearing logic - only remove unvisited endpoints - [PR #16400](https://github.com/BerriAI/litellm/pull/16400) + +- **Redis** + - Handle float redis_version from AWS ElastiCache Valkey - [PR #16207](https://github.com/BerriAI/litellm/pull/16207) + +- **Hooks** + - Add parallel execution handling in during_call_hook - [PR #16279](https://github.com/BerriAI/litellm/pull/16279) + +- **Infrastructure** + - Install runtime node for prisma - [PR #16410](https://github.com/BerriAI/litellm/pull/16410) + + + +--- + +## Documentation Updates + +- **Provider Documentation** + - Docs - v1.79.1 - [PR #16163](https://github.com/BerriAI/litellm/pull/16163) + - Fix broken link on model_management.md - [PR #16217](https://github.com/BerriAI/litellm/pull/16217) + - Fix image generation response format - use 'images' array instead of 'image' object - [PR #16378](https://github.com/BerriAI/litellm/pull/16378) + +- **General Documentation** + - Add minimum resource requirement for production - [PR #16146](https://github.com/BerriAI/litellm/pull/16146) + - Add benchmark comparison with other AI gateways - [PR #16248](https://github.com/BerriAI/litellm/pull/16248) + - LiteLLM content filter guard documentation - [PR #16413](https://github.com/BerriAI/litellm/pull/16413) + - Fix typo of the word orginal - [PR #16255](https://github.com/BerriAI/litellm/pull/16255) + +- **Security** + - Remove tornado test files (including test.key), fixes Python 3.13 security issues - [PR #16342](https://github.com/BerriAI/litellm/pull/16342) + +--- + +## New Contributors + +* @steve-gore-snapdocs made their first contribution in [PR #16149](https://github.com/BerriAI/litellm/pull/16149) +* @timbmg made their first contribution in [PR #16120](https://github.com/BerriAI/litellm/pull/16120) +* @Nivg made their first contribution in [PR #16202](https://github.com/BerriAI/litellm/pull/16202) +* @pablobgar made their first contribution in [PR #16194](https://github.com/BerriAI/litellm/pull/16194) +* @AlanPonnachan made their first contribution in [PR #16150](https://github.com/BerriAI/litellm/pull/16150) +* @Chesars made their first contribution in [PR #16236](https://github.com/BerriAI/litellm/pull/16236) +* @bowenliang123 made their first contribution in [PR #16255](https://github.com/BerriAI/litellm/pull/16255) +* @dean-zavad made their first contribution in [PR #16199](https://github.com/BerriAI/litellm/pull/16199) +* @alexkuzmik made their first contribution in [PR #15529](https://github.com/BerriAI/litellm/pull/15529) +* @Granine made their first contribution in [PR #16281](https://github.com/BerriAI/litellm/pull/16281) +* @Oodapow made their first contribution in [PR #16279](https://github.com/BerriAI/litellm/pull/16279) +* @jgoodyear made their first contribution in [PR #16275](https://github.com/BerriAI/litellm/pull/16275) +* @Qanpi made their first contribution in [PR #16321](https://github.com/BerriAI/litellm/pull/16321) +* @ShimonMimoun made their first contribution in [PR #16313](https://github.com/BerriAI/litellm/pull/16313) +* @andriykislitsyn made their first contribution in [PR #16288](https://github.com/BerriAI/litellm/pull/16288) +* @reckless-huang made their first contribution in [PR #16263](https://github.com/BerriAI/litellm/pull/16263) +* @chenmoneygithub made their first contribution in [PR #16368](https://github.com/BerriAI/litellm/pull/16368) +* @stembe-digitalex made their first contribution in [PR #16354](https://github.com/BerriAI/litellm/pull/16354) +* @jfcherng made their first contribution in [PR #16352](https://github.com/BerriAI/litellm/pull/16352) +* @xingyaoww made their first contribution in [PR #16246](https://github.com/BerriAI/litellm/pull/16246) +* @emerzon made their first contribution in [PR #16373](https://github.com/BerriAI/litellm/pull/16373) +* @wwwillchen made their first contribution in [PR #16376](https://github.com/BerriAI/litellm/pull/16376) +* @fabriciojoc made their first contribution in [PR #16203](https://github.com/BerriAI/litellm/pull/16203) +* @jroberts2600 made their first contribution in [PR #16273](https://github.com/BerriAI/litellm/pull/16273) + +--- + +## Full Changelog + +**[View complete changelog on GitHub](https://github.com/BerriAI/litellm/compare/v1.79.1-nightly...v1.79.2.rc.1)** + + diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 0b303b8c6af..f009abd766a 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -31,6 +31,7 @@ const sidebars = { label: "Guardrails", items: [ "proxy/guardrails/quick_start", + "proxy/guardrails/test_playground", ...[ "adding_provider/adding_guardrail_support", "proxy/guardrails/aim_security", @@ -41,6 +42,7 @@ const sidebars = { "proxy/guardrails/ibm_guardrails", "proxy/guardrails/grayswan", "proxy/guardrails/lasso_security", + "proxy/guardrails/litellm_content_filter", "proxy/guardrails/guardrails_ai", "proxy/guardrails/lakera_ai", "proxy/guardrails/model_armor", @@ -477,6 +479,7 @@ const sidebars = { label: "Vertex AI", items: [ "providers/vertex", + "providers/vertex_ai/videos", "providers/vertex_partner", "providers/vertex_self_deployed", "providers/vertex_image", @@ -489,6 +492,7 @@ const sidebars = { label: "Google AI Studio", items: [ "providers/gemini", + "providers/gemini/videos", "providers/google_ai_studio/files", "providers/google_ai_studio/image_gen", "providers/google_ai_studio/realtime", @@ -780,6 +784,7 @@ const sidebars = { "projects/GPTLocalhost", "projects/HolmesGPT", "projects/Railtracks", + "projects/Softgen", ], }, "extras/code_quality", diff --git a/enterprise/enterprise_hooks/aporia_ai.py b/enterprise/enterprise_hooks/aporia_ai.py index 55ba6071820..28b49bfce21 100644 --- a/enterprise/enterprise_hooks/aporia_ai.py +++ b/enterprise/enterprise_hooks/aporia_ai.py @@ -8,6 +8,8 @@ import os import sys +from litellm.types.utils import CallTypesLiteral + sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path @@ -166,16 +168,7 @@ class AporiaGuardrail(CustomGuardrail): self, data: dict, user_api_key_dict: UserAPIKeyAuth, - call_type: Literal[ - "completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "responses", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ): from litellm.proxy.common_utils.callback_utils import ( add_guardrail_to_applied_guardrails_header, diff --git a/enterprise/enterprise_hooks/google_text_moderation.py b/enterprise/enterprise_hooks/google_text_moderation.py index c1c932dcb04..1f26d52adf8 100644 --- a/enterprise/enterprise_hooks/google_text_moderation.py +++ b/enterprise/enterprise_hooks/google_text_moderation.py @@ -6,14 +6,13 @@ # +-----------------------------------------------+ # Thank you users! We ❤️ you! - Krrish & Ishaan -from typing import Literal - from fastapi import HTTPException import litellm from litellm._logging import verbose_proxy_logger from litellm.integrations.custom_logger import CustomLogger from litellm.proxy._types import UserAPIKeyAuth +from litellm.types.utils import CallTypesLiteral class _ENTERPRISE_GoogleTextModeration(CustomLogger): @@ -89,16 +88,7 @@ class _ENTERPRISE_GoogleTextModeration(CustomLogger): self, data: dict, user_api_key_dict: UserAPIKeyAuth, - call_type: Literal[ - "completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "responses", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ): """ - Calls Google's Text Moderation API diff --git a/enterprise/enterprise_hooks/openai_moderation.py b/enterprise/enterprise_hooks/openai_moderation.py index 4464fff25c9..a1db9818e5e 100644 --- a/enterprise/enterprise_hooks/openai_moderation.py +++ b/enterprise/enterprise_hooks/openai_moderation.py @@ -12,7 +12,6 @@ sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path import sys -from typing import Literal from fastapi import HTTPException @@ -20,6 +19,7 @@ import litellm from litellm._logging import verbose_proxy_logger from litellm.integrations.custom_logger import CustomLogger from litellm.proxy._types import UserAPIKeyAuth +from litellm.types.utils import CallTypesLiteral class _ENTERPRISE_OpenAI_Moderation(CustomLogger): @@ -35,16 +35,7 @@ class _ENTERPRISE_OpenAI_Moderation(CustomLogger): self, data: dict, user_api_key_dict: UserAPIKeyAuth, - call_type: Literal[ - "completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "responses", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ): text = "" if "messages" in data and isinstance(data["messages"], list): diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/llama_guard.py b/enterprise/litellm_enterprise/enterprise_callbacks/llama_guard.py index 80de7a396a4..5e1aebdbdfb 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/llama_guard.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/llama_guard.py @@ -23,7 +23,7 @@ import litellm from litellm._logging import verbose_proxy_logger from litellm.integrations.custom_logger import CustomLogger from litellm.proxy._types import UserAPIKeyAuth -from litellm.types.utils import Choices, ModelResponse +from litellm.types.utils import CallTypesLiteral, Choices, ModelResponse class _ENTERPRISE_LlamaGuard(CustomLogger): @@ -98,16 +98,7 @@ class _ENTERPRISE_LlamaGuard(CustomLogger): self, data: dict, user_api_key_dict: UserAPIKeyAuth, - call_type: Literal[ - "completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "responses", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ): """ - Calls the Llama Guard Endpoint diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/llm_guard.py b/enterprise/litellm_enterprise/enterprise_callbacks/llm_guard.py index 6f07250a61a..ad8aabf77b6 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/llm_guard.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/llm_guard.py @@ -17,6 +17,7 @@ from litellm._logging import verbose_proxy_logger from litellm.integrations.custom_logger import CustomLogger from litellm.proxy._types import UserAPIKeyAuth from litellm.secret_managers.main import get_secret_str +from litellm.types.utils import CallTypesLiteral from litellm.utils import get_formatted_prompt @@ -120,16 +121,7 @@ class _ENTERPRISE_LLMGuard(CustomLogger): self, data: dict, user_api_key_dict: UserAPIKeyAuth, - call_type: Literal[ - "completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "responses", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ): """ - Calls the LLM Guard Endpoint diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/pagerduty/pagerduty.py b/enterprise/litellm_enterprise/enterprise_callbacks/pagerduty/pagerduty.py index 3162c2f12f8..e481cdc995c 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/pagerduty/pagerduty.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/pagerduty/pagerduty.py @@ -31,6 +31,7 @@ from litellm.types.integrations.pagerduty import ( PagerDutyRequestBody, ) from litellm.types.utils import ( + CallTypesLiteral, StandardLoggingPayload, StandardLoggingPayloadErrorInformation, ) @@ -142,18 +143,7 @@ class PagerDutyAlerting(SlackAlerting): user_api_key_dict: UserAPIKeyAuth, cache: DualCache, data: dict, - call_type: Literal[ - "completion", - "text_completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "pass_through_endpoint", - "rerank", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ) -> Optional[Union[Exception, str, dict]]: """ Example of detecting hanging requests by waiting a given threshold. diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index c55a4f03898..80cc77883fe 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -36,6 +36,7 @@ from litellm.types.llms.openai import ( OpenAIFilesPurpose, ) from litellm.types.utils import ( + CallTypesLiteral, LiteLLMBatch, LiteLLMFineTuningJob, LLMResponseTypes, @@ -272,28 +273,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): user_api_key_dict: UserAPIKeyAuth, cache: DualCache, data: Dict, - call_type: Literal[ - "completion", - "text_completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "pass_through_endpoint", - "rerank", - "acreate_batch", - "aretrieve_batch", - "acreate_file", - "afile_list", - "afile_delete", - "afile_content", - "acreate_fine_tuning_job", - "aretrieve_fine_tuning_job", - "alist_fine_tuning_jobs", - "acancel_fine_tuning_job", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ) -> Union[Exception, str, Dict, None]: """ - Detect litellm_proxy/ file_id diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.2-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.2-py3-none-any.whl new file mode 100644 index 00000000000..f65cee904be Binary files /dev/null and b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.2-py3-none-any.whl differ diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.2.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.2.tar.gz new file mode 100644 index 00000000000..389a114c7da Binary files /dev/null and b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.2.tar.gz differ diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.3-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.3-py3-none-any.whl new file mode 100644 index 00000000000..aaca139a071 Binary files /dev/null and b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.3-py3-none-any.whl differ diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.3.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.3.tar.gz new file mode 100644 index 00000000000..9b41e61f6dc Binary files /dev/null and b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.3.tar.gz differ diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20251104220043_add_credentials_to_mcp_servers/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251104220043_add_credentials_to_mcp_servers/migration.sql new file mode 100644 index 00000000000..800c96f18b7 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251104220043_add_credentials_to_mcp_servers/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "LiteLLM_MCPServerTable" ADD COLUMN "credentials" JSONB DEFAULT '{}'; diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 9702c390288..1ab193bba7c 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -174,6 +174,7 @@ model LiteLLM_MCPServerTable { url String? transport String @default("sse") auth_type String? + credentials Json? @default("{}") created_at DateTime? @default(now()) @map("created_at") created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index 07077759397..6c782eace2f 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.4.1" +version = "0.4.3" 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.1" +version = "0.4.3" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/litellm/__init__.py b/litellm/__init__.py index 20fe4a2aeac..5f3b1156c92 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -4,7 +4,9 @@ import warnings warnings.filterwarnings("ignore", message=".*conflict with protected namespace.*") # Suppress Pydantic 2.11+ deprecation warning about accessing model_fields on instances # This warning can accumulate during streaming and cause memory leaks -warnings.filterwarnings("ignore", message=".*Accessing the.*attribute on the instance is deprecated.*") +warnings.filterwarnings( + "ignore", message=".*Accessing the.*attribute on the instance is deprecated.*" +) ### INIT VARIABLES ####################### import threading import os @@ -32,7 +34,7 @@ from litellm.types.utils import ( all_litellm_params as _litellm_completion_params, CredentialItem, PriorityReservationDict, -) # maintain backwards compatibility for root param +) # maintain backwards compatibility for root param. from litellm._logging import ( set_verbose, _turn_on_debug, @@ -179,22 +181,22 @@ prometheus_initialize_budget_metrics: Optional[bool] = False require_auth_for_metrics_endpoint: Optional[bool] = False argilla_batch_size: Optional[int] = None datadog_use_v1: Optional[bool] = False # if you want to use v1 datadog logged payload. -gcs_pub_sub_use_v1: Optional[bool] = ( - False # if you want to use v1 gcs pubsub logged payload -) -generic_api_use_v1: Optional[bool] = ( - False # if you want to use v1 generic api logged payload -) +gcs_pub_sub_use_v1: Optional[ + bool +] = False # if you want to use v1 gcs pubsub logged payload +generic_api_use_v1: Optional[ + bool +] = False # if you want to use v1 generic api logged payload argilla_transformation_object: Optional[Dict[str, Any]] = None -_async_input_callback: List[Union[str, Callable, CustomLogger]] = ( - [] -) # internal variable - async custom callbacks are routed here. -_async_success_callback: List[Union[str, Callable, CustomLogger]] = ( - [] -) # internal variable - async custom callbacks are routed here. -_async_failure_callback: List[Union[str, Callable, CustomLogger]] = ( - [] -) # internal variable - async custom callbacks are routed here. +_async_input_callback: List[ + Union[str, Callable, CustomLogger] +] = [] # internal variable - async custom callbacks are routed here. +_async_success_callback: List[ + Union[str, Callable, CustomLogger] +] = [] # internal variable - async custom callbacks are routed here. +_async_failure_callback: List[ + Union[str, Callable, CustomLogger] +] = [] # internal variable - async custom callbacks are routed here. pre_call_rules: List[Callable] = [] post_call_rules: List[Callable] = [] turn_off_message_logging: Optional[bool] = False @@ -202,18 +204,18 @@ log_raw_request_response: bool = False redact_messages_in_exceptions: Optional[bool] = False redact_user_api_key_info: Optional[bool] = False filter_invalid_headers: Optional[bool] = False -add_user_information_to_llm_headers: Optional[bool] = ( - None # adds user_id, team_id, token hash (params from StandardLoggingMetadata) to request headers -) +add_user_information_to_llm_headers: Optional[ + bool +] = None # adds user_id, team_id, token hash (params from StandardLoggingMetadata) to request headers store_audit_logs = False # Enterprise feature, allow users to see audit logs ### end of callbacks ############# -email: Optional[str] = ( - None # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648 -) -token: Optional[str] = ( - None # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648 -) +email: Optional[ + str +] = None # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648 +token: Optional[ + str +] = None # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648 telemetry = True max_tokens: int = DEFAULT_MAX_TOKENS # OpenAI Defaults drop_params = bool(os.getenv("LITELLM_DROP_PARAMS", False)) @@ -269,9 +271,9 @@ use_client: bool = False ssl_verify: Union[str, bool] = True ssl_security_level: Optional[str] = None ssl_certificate: Optional[str] = None -ssl_ecdh_curve: Optional[str] = ( - None # Set to 'X25519' to disable PQC and improve performance -) +ssl_ecdh_curve: Optional[ + str +] = None # Set to 'X25519' to disable PQC and improve performance disable_streaming_logging: bool = False disable_token_counter: bool = False disable_add_transform_inline_image_block: bool = False @@ -317,24 +319,20 @@ enable_loadbalancing_on_batch_endpoints: Optional[bool] = None enable_caching_on_provider_specific_optional_params: bool = ( False # feature-flag for caching on optional params - e.g. 'top_k' ) -caching: bool = ( - False # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648 -) -caching_with_models: bool = ( - False # # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648 -) -cache: Optional[Cache] = ( - None # cache object <- use this - https://docs.litellm.ai/docs/caching -) +caching: bool = False # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648 +caching_with_models: bool = False # # Not used anymore, will be removed in next MAJOR release - https://github.com/BerriAI/litellm/discussions/648 +cache: Optional[ + Cache +] = None # cache object <- use this - https://docs.litellm.ai/docs/caching default_in_memory_ttl: Optional[float] = None default_redis_ttl: Optional[float] = None default_redis_batch_cache_expiry: Optional[float] = None model_alias_map: Dict[str, str] = {} model_group_settings: Optional["ModelGroupSettings"] = None max_budget: float = 0.0 # set the max budget across all providers -budget_duration: Optional[str] = ( - None # proxy only - resets budget after fixed duration. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). -) +budget_duration: Optional[ + str +] = None # proxy only - resets budget after fixed duration. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). default_soft_budget: float = ( DEFAULT_SOFT_BUDGET # by default all litellm proxy keys have a soft budget of 50.0 ) @@ -343,15 +341,11 @@ forward_traceparent_to_llm_provider: bool = False _current_cost = 0.0 # private variable, used if max budget is set error_logs: Dict = {} -add_function_to_prompt: bool = ( - False # if function calling not supported by api, append function call details to system prompt -) +add_function_to_prompt: bool = False # if function calling not supported by api, append function call details to system prompt client_session: Optional[httpx.Client] = None aclient_session: Optional[httpx.AsyncClient] = None model_fallbacks: Optional[List] = None # Deprecated for 'litellm.fallbacks' -model_cost_map_url: str = ( - "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" -) +model_cost_map_url: str = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" suppress_debug_info = False dynamodb_table_name: Optional[str] = None s3_callback_params: Optional[Dict] = None @@ -381,9 +375,7 @@ prometheus_metrics_config: Optional[List] = None disable_add_prefix_to_prompt: bool = ( False # used by anthropic, to disable adding prefix to prompt ) -disable_copilot_system_to_assistant: bool = ( - False # If false (default), converts all 'system' role messages to 'assistant' for GitHub Copilot compatibility. Set to true to disable this behavior. -) +disable_copilot_system_to_assistant: bool = False # If false (default), converts all 'system' role messages to 'assistant' for GitHub Copilot compatibility. Set to true to disable this behavior. public_model_groups: Optional[List[str]] = None public_model_groups_links: Dict[str, str] = {} #### REQUEST PRIORITIZATION ####### @@ -394,17 +386,13 @@ priority_reservation_settings: "PriorityReservationSettings" = ( ######## Networking Settings ######## -use_aiohttp_transport: bool = ( - True # Older variable, aiohttp is now the default. use disable_aiohttp_transport instead. -) +use_aiohttp_transport: bool = True # Older variable, aiohttp is now the default. use disable_aiohttp_transport instead. aiohttp_trust_env: bool = False # set to true to use HTTP_ Proxy settings disable_aiohttp_transport: bool = False # Set this to true to use httpx instead disable_aiohttp_trust_env: bool = ( False # When False, aiohttp will respect HTTP(S)_PROXY env vars ) -force_ipv4: bool = ( - False # when True, litellm will force ipv4 for all LLM requests. Some users have seen httpx ConnectionError when using ipv6. -) +force_ipv4: bool = False # when True, litellm will force ipv4 for all LLM requests. Some users have seen httpx ConnectionError when using ipv6. module_level_aclient = AsyncHTTPHandler( timeout=request_timeout, client_alias="module level aclient" ) @@ -418,13 +406,13 @@ fallbacks: Optional[List] = None context_window_fallbacks: Optional[List] = None content_policy_fallbacks: Optional[List] = None allowed_fails: int = 3 -num_retries_per_request: Optional[int] = ( - None # for the request overall (incl. fallbacks + model retries) -) +num_retries_per_request: Optional[ + int +] = None # for the request overall (incl. fallbacks + model retries) ####### SECRET MANAGERS ##################### -secret_manager_client: Optional[Any] = ( - None # list of instantiated key management clients - e.g. azure kv, infisical, etc. -) +secret_manager_client: Optional[ + Any +] = None # list of instantiated key management clients - e.g. azure kv, infisical, etc. _google_kms_resource_name: Optional[str] = None _key_management_system: Optional[KeyManagementSystem] = None _key_management_settings: KeyManagementSettings = KeyManagementSettings() @@ -434,9 +422,9 @@ output_parse_pii: bool = False from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map model_cost = get_model_cost_map(url=model_cost_map_url) -cost_discount_config: Dict[str, float] = ( - {} -) # Provider-specific cost discounts {"vertex_ai": 0.05} = 5% discount +cost_discount_config: Dict[ + str, float +] = {} # Provider-specific cost discounts {"vertex_ai": 0.05} = 5% discount custom_prompt_dict: Dict[str, dict] = {} check_provider_endpoint = False @@ -492,6 +480,7 @@ vertex_deepseek_models: Set = set() vertex_ai_ai21_models: Set = set() vertex_mistral_models: Set = set() vertex_openai_models: Set = set() +vertex_minimax_models: Set = set() ai21_models: Set = set() ai21_chat_models: Set = set() nlp_cloud_models: Set = set() @@ -652,6 +641,9 @@ def add_known_models(): elif value.get("litellm_provider") == "vertex_ai-openai_models": key = key.replace("vertex_ai/", "") vertex_openai_models.add(key) + elif value.get("litellm_provider") == "vertex_ai-minimax_models": + key = key.replace("vertex_ai/", "") + vertex_minimax_models.add(key) elif value.get("litellm_provider") == "ai21": if value.get("mode") == "chat": ai21_chat_models.add(key) @@ -907,7 +899,8 @@ models_by_provider: dict = { | vertex_anthropic_models | vertex_vision_models | vertex_language_models - | vertex_deepseek_models, + | vertex_deepseek_models + | vertex_minimax_models, "ai21": ai21_models, "bedrock": bedrock_models | bedrock_converse_models, "petals": petals_models, @@ -1105,6 +1098,7 @@ from .llms.azure_ai.rerank.transformation import AzureAIRerankConfig from .llms.infinity.rerank.transformation import InfinityRerankConfig from .llms.jina_ai.rerank.transformation import JinaAIRerankConfig from .llms.deepinfra.rerank.transformation import DeepinfraRerankConfig +from .llms.hosted_vllm.rerank.transformation import HostedVLLMRerankConfig from .llms.nvidia_nim.rerank.transformation import NvidiaNimRerankConfig from .llms.vertex_ai.rerank.transformation import VertexAIRerankConfig from .llms.clarifai.chat.transformation import ClarifaiConfig @@ -1345,6 +1339,7 @@ from .exceptions import ( NotFoundError, RateLimitError, ServiceUnavailableError, + BadGatewayError, OpenAIError, ContextWindowExceededError, ContentPolicyViolationError, @@ -1400,12 +1395,12 @@ from .types.llms.custom_llm import CustomLLMItem from .types.utils import GenericStreamingChunk custom_provider_map: List[CustomLLMItem] = [] -_custom_providers: List[str] = ( - [] -) # internal helper util, used to track names of custom providers -disable_hf_tokenizer_download: Optional[bool] = ( - None # disable huggingface tokenizer download. Defaults to openai clk100 -) +_custom_providers: List[ + str +] = [] # internal helper util, used to track names of custom providers +disable_hf_tokenizer_download: Optional[ + bool +] = None # disable huggingface tokenizer download. Defaults to openai clk100 global_disable_no_log_param: bool = False ### CLI UTILITIES ### diff --git a/litellm/constants.py b/litellm/constants.py index 7565aa7d3ae..43fc37ad1c7 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1,6 +1,7 @@ import os from typing import List, Literal +DEFAULT_HEALTH_CHECK_PROMPT = str(os.getenv("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm")) AZURE_DEFAULT_RESPONSES_API_VERSION = str( os.getenv("AZURE_DEFAULT_RESPONSES_API_VERSION", "preview") ) @@ -219,7 +220,7 @@ REDIS_SOCKET_TIMEOUT = float(os.getenv("REDIS_SOCKET_TIMEOUT", 0.1)) REDIS_CONNECTION_POOL_TIMEOUT = int(os.getenv("REDIS_CONNECTION_POOL_TIMEOUT", 5)) # Default Redis major version to assume when version cannot be determined # Using 7 as it's the modern version that supports LPOP with count parameter -DEFAULT_REDIS_MAJOR_VERSION = 7 +DEFAULT_REDIS_MAJOR_VERSION = int(os.getenv("DEFAULT_REDIS_MAJOR_VERSION", 7)) NON_LLM_CONNECTION_TIMEOUT = int( os.getenv("NON_LLM_CONNECTION_TIMEOUT", 15) ) # timeout for adjacent services (e.g. jwt auth) @@ -279,6 +280,8 @@ ANTHROPIC_WEB_SEARCH_TOOL_MAX_USES = { DEFAULT_IMAGE_ENDPOINT_MODEL = "dall-e-2" DEFAULT_VIDEO_ENDPOINT_MODEL = "sora-2" +DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS = int(os.getenv("DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS", 8)) + ### DATAFORSEO CONSTANTS ### DEFAULT_DATAFORSEO_LOCATION_CODE = int( os.getenv("DEFAULT_DATAFORSEO_LOCATION_CODE", 2250) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 42fcabcf680..d4a4c441eb7 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -17,6 +17,9 @@ from litellm.constants import ( from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( StandardBuiltInToolCostTracking, ) +from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import ( + TranscriptionUsageObjectTransformation, +) from litellm.litellm_core_utils.llm_cost_calc.utils import ( CostCalculatorUtils, _generic_cost_per_character, @@ -81,6 +84,8 @@ from litellm.types.utils import ( LlmProvidersSet, ModelInfo, StandardBuiltInToolsParams, + TranscriptionUsageDurationObject, + TranscriptionUsageTokensObject, Usage, VectorStoreSearchResponse, ) @@ -319,20 +324,32 @@ def cost_per_token( # noqa: PLR0915 usage=usage_block, model=model, custom_llm_provider=custom_llm_provider ) elif call_type == "atranscription" or call_type == "transcription": - return openai_cost_per_second( - model=model, - custom_llm_provider=custom_llm_provider, - duration=audio_transcription_file_duration, - ) + + if model == "gpt-4o-mini-transcribe": + return openai_cost_per_token( + model=model, + usage=usage_block, + service_tier=service_tier, + ) + else: + return openai_cost_per_second( + model=model, + custom_llm_provider=custom_llm_provider, + duration=audio_transcription_file_duration, + ) elif call_type == "search" or call_type == "asearch": # Search providers use per-query pricing from litellm.search import search_provider_cost_per_query - + return search_provider_cost_per_query( model=model, custom_llm_provider=custom_llm_provider, number_of_queries=number_of_queries or 1, - optional_params=response._hidden_params if response and hasattr(response, "_hidden_params") else None + optional_params=( + response._hidden_params + if response and hasattr(response, "_hidden_params") + else None + ), ) elif custom_llm_provider == "vertex_ai": cost_router = google_cost_router( @@ -509,16 +526,18 @@ def _select_model_name_for_cost_calc( else: return_model = model - if base_model is not None: + elif base_model is not None: return_model = base_model - if completion_response_model is None and hidden_params is not None: + elif completion_response_model is None and hidden_params is not None: if ( hidden_params.get("model", None) is not None and len(hidden_params["model"]) > 0 ): return_model = hidden_params.get("model", model) - if hidden_params is not None and hidden_params.get("region_name", None) is not None: + elif ( + hidden_params is not None and hidden_params.get("region_name", None) is not None + ): region_name = hidden_params.get("region_name", None) if return_model is None and completion_response_model is not None: @@ -573,6 +592,19 @@ def _get_usage_object( return ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( usage_obj ) + elif TranscriptionUsageObjectTransformation.is_transcription_usage_object( + usage_obj + ): + return ( + TranscriptionUsageObjectTransformation.transform_transcription_usage_object( + cast( + Union[ + TranscriptionUsageDurationObject, TranscriptionUsageTokensObject + ], + usage_obj, + ) + ) + ) elif isinstance(usage_obj, dict): return Usage(**usage_obj) elif isinstance(usage_obj, BaseModel): @@ -586,8 +618,12 @@ def _get_usage_object( def _is_known_usage_objects(usage_obj): """Returns True if the usage obj is a known Usage type""" - return isinstance(usage_obj, litellm.Usage) or isinstance( - usage_obj, ResponseAPIUsage + return ( + isinstance(usage_obj, litellm.Usage) + or isinstance(usage_obj, ResponseAPIUsage) + or TranscriptionUsageObjectTransformation.is_transcription_usage_object( + usage_obj + ) ) @@ -827,6 +863,22 @@ def completion_cost( # noqa: PLR0915 _usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( _usage ).model_dump() + elif TranscriptionUsageObjectTransformation.is_transcription_usage_object( + _usage + ): + tr_usage = TranscriptionUsageObjectTransformation.transform_transcription_usage_object( + cast( + Union[ + TranscriptionUsageDurationObject, + TranscriptionUsageTokensObject, + ], + _usage, + ) + ) + if tr_usage is not None: + _usage = tr_usage.model_dump() + else: + _usage = _usage # get input/output tokens from completion_response prompt_tokens = _usage.get("prompt_tokens", 0) @@ -853,15 +905,6 @@ def completion_cost( # noqa: PLR0915 "custom_llm_provider", custom_llm_provider or None ) region_name = hidden_params.get("region_name", region_name) - size = hidden_params.get("optional_params", {}).get( - "size", "1024-x-1024" - ) # openai default - quality = hidden_params.get("optional_params", {}).get( - "quality", "standard" - ) # openai default - n = hidden_params.get("optional_params", {}).get( - "n", 1 - ) # openai default else: if model is None: raise ValueError( @@ -888,7 +931,9 @@ def completion_cost( # noqa: PLR0915 str(e) ) ) - if CostCalculatorUtils._call_type_has_image_response(call_type): + if CostCalculatorUtils._call_type_has_image_response( + call_type + ) and isinstance(completion_response, ImageResponse): ### IMAGE GENERATION COST CALCULATION ### return CostCalculatorUtils.route_image_generation_cost_calculator( model=model, @@ -906,27 +951,32 @@ def completion_cost( # noqa: PLR0915 or call_type == CallTypes.avideo_remix.value ): ### VIDEO GENERATION COST CALCULATION ### - if completion_response is not None and hasattr(completion_response, 'usage'): - usage_obj = completion_response.usage + usage_obj = getattr(completion_response, "usage", None) + if completion_response is not None and usage_obj: # Handle both dict and Pydantic Usage object if isinstance(usage_obj, dict): - duration_seconds = usage_obj.get('duration_seconds', None) + duration_seconds = usage_obj.get("duration_seconds", None) else: - duration_seconds = getattr(usage_obj, 'duration_seconds', None) + duration_seconds = getattr( + usage_obj, "duration_seconds", None + ) if duration_seconds is not None: # Calculate cost based on video duration using video-specific cost calculation - from litellm.llms.openai.cost_calculation import video_generation_cost + from litellm.llms.openai.cost_calculation import ( + video_generation_cost, + ) + return video_generation_cost( model=model, duration_seconds=duration_seconds, - custom_llm_provider=custom_llm_provider + custom_llm_provider=custom_llm_provider, ) # Fallback to default video cost calculation if no duration available return default_video_cost_calculator( model=model, duration_seconds=0.0, # Default to 0 if no duration available - custom_llm_provider=custom_llm_provider + custom_llm_provider=custom_llm_provider, ) elif ( call_type == CallTypes.speech.value @@ -1460,13 +1510,13 @@ def default_video_cost_calculator( model_name_without_custom_llm_provider = model.replace( f"{custom_llm_provider}/", "" ) - base_model_name = f"{custom_llm_provider}/{model_name_without_custom_llm_provider}" + base_model_name = ( + f"{custom_llm_provider}/{model_name_without_custom_llm_provider}" + ) - verbose_logger.debug( - f"Looking up cost for video model: {base_model_name}" - ) + verbose_logger.debug(f"Looking up cost for video model: {base_model_name}") - model_without_provider = model.split('/')[-1] + model_without_provider = model.split("/")[-1] # Try model with provider first, fall back to base model name cost_info: Optional[dict] = None @@ -1480,7 +1530,7 @@ def default_video_cost_calculator( if _model is not None and _model in litellm.model_cost: cost_info = litellm.model_cost[_model] break - + # If still not found, try with custom_llm_provider prefix if cost_info is None and custom_llm_provider: prefixed_model = f"{custom_llm_provider}/{model}" @@ -1495,12 +1545,12 @@ def default_video_cost_calculator( video_cost_per_second = cost_info.get("output_cost_per_video_per_second") if video_cost_per_second is not None: return video_cost_per_second * duration_seconds - + # Fallback to general output cost per second output_cost_per_second = cost_info.get("output_cost_per_second") if output_cost_per_second is not None: return output_cost_per_second * duration_seconds - + # If no cost information found, return 0 verbose_logger.info( f"No cost information found for video model {model}. Please add pricing to model_prices_and_context_window.json" diff --git a/litellm/exceptions.py b/litellm/exceptions.py index ccb3ce90e9c..d963cac754c 100644 --- a/litellm/exceptions.py +++ b/litellm/exceptions.py @@ -450,6 +450,7 @@ class ContentPolicyViolationError(BadRequestError): # type: ignore llm_provider, response: Optional[httpx.Response] = None, litellm_debug_info: Optional[str] = None, + provider_specific_fields: Optional[dict] = None, ): self.status_code = 400 self.message = "litellm.ContentPolicyViolationError: {}".format(message) @@ -458,6 +459,8 @@ class ContentPolicyViolationError(BadRequestError): # type: ignore self.litellm_debug_info = litellm_debug_info request = httpx.Request(method="POST", url="https://api.openai.com/v1") self.response = httpx.Response(status_code=400, request=request) + self.provider_specific_fields = provider_specific_fields + super().__init__( message=self.message, model=self.model, # type: ignore @@ -465,16 +468,18 @@ class ContentPolicyViolationError(BadRequestError): # type: ignore response=self.response, litellm_debug_info=self.litellm_debug_info, ) # Call the base class constructor with the parameters it needs + def __str__(self): - _message = self.message - if self.num_retries: - _message += f" LiteLLM Retried: {self.num_retries} times" - if self.max_retries: - _message += f", LiteLLM Max Retries: {self.max_retries}" - return _message + return self._transform_error_to_string() def __repr__(self): + return self._transform_error_to_string() + + def _transform_error_to_string(self) -> str: + """ + Transform the error to a string + """ _message = self.message if self.num_retries: _message += f" LiteLLM Retried: {self.num_retries} times" @@ -501,8 +506,62 @@ class ServiceUnavailableError(openai.APIStatusError): # type: ignore self.litellm_debug_info = litellm_debug_info self.max_retries = max_retries self.num_retries = num_retries + _response_headers = ( + getattr(response, "headers", None) if response is not None else None + ) self.response = httpx.Response( status_code=self.status_code, + headers=_response_headers, + request=httpx.Request( + method="POST", + url=" https://cloud.google.com/vertex-ai/", + ), + ) + super().__init__( + self.message, response=self.response, body=None + ) # Call the base class constructor with the parameters it needs + + def __str__(self): + _message = self.message + if self.num_retries: + _message += f" LiteLLM Retried: {self.num_retries} times" + if self.max_retries: + _message += f", LiteLLM Max Retries: {self.max_retries}" + return _message + + def __repr__(self): + _message = self.message + if self.num_retries: + _message += f" LiteLLM Retried: {self.num_retries} times" + if self.max_retries: + _message += f", LiteLLM Max Retries: {self.max_retries}" + return _message + + +class BadGatewayError(openai.APIStatusError): # type: ignore + def __init__( + self, + message, + llm_provider, + model, + response: Optional[httpx.Response] = None, + litellm_debug_info: Optional[str] = None, + max_retries: Optional[int] = None, + num_retries: Optional[int] = None, + ): + self.status_code = 502 + self.message = "litellm.BadGatewayError: {}".format(message) + self.llm_provider = llm_provider + self.model = model + self.litellm_debug_info = litellm_debug_info + self.max_retries = max_retries + self.num_retries = num_retries + _response_headers = ( + getattr(response, "headers", None) if response is not None else None + ) + self.response = httpx.Response( + status_code=self.status_code, + headers=_response_headers, request=httpx.Request( method="POST", url=" https://cloud.google.com/vertex-ai/", @@ -547,8 +606,12 @@ class InternalServerError(openai.InternalServerError): # type: ignore self.litellm_debug_info = litellm_debug_info self.max_retries = max_retries self.num_retries = num_retries + _response_headers = ( + getattr(response, "headers", None) if response is not None else None + ) self.response = httpx.Response( status_code=self.status_code, + headers=_response_headers, request=httpx.Request( method="POST", url=" https://cloud.google.com/vertex-ai/", @@ -754,6 +817,7 @@ LITELLM_EXCEPTION_TYPES = [ ContentPolicyViolationError, InternalServerError, ServiceUnavailableError, + BadGatewayError, APIError, APIConnectionError, APIResponseValidationError, diff --git a/litellm/integrations/custom_logger.py b/litellm/integrations/custom_logger.py index 2a08408f7de..a3e67d8a73f 100644 --- a/litellm/integrations/custom_logger.py +++ b/litellm/integrations/custom_logger.py @@ -8,7 +8,6 @@ from typing import ( AsyncGenerator, Dict, List, - Literal, Optional, Tuple, Union, @@ -24,6 +23,7 @@ from litellm.types.llms.openai import AllMessageValues, ChatCompletionRequest from litellm.types.utils import ( AdapterCompletionStreamWrapper, CallTypes, + CallTypesLiteral, LLMResponseTypes, ModelResponse, ModelResponseStream, @@ -65,12 +65,11 @@ _BASE64_INLINE_PATTERN = re.compile( class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callback#callback-class # Class variables or attributes def __init__( - self, + self, turn_off_message_logging: bool = False, - # deprecated param, use `turn_off_message_logging` instead message_logging: bool = True, - **kwargs + **kwargs, ) -> None: """ Args: @@ -259,7 +258,7 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac ) -> Optional[Any]: """ Allow modifying streaming chunks just before they're returned to the user. - + This is called for each streaming chunk in the response. """ pass @@ -330,18 +329,7 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac user_api_key_dict: UserAPIKeyAuth, cache: DualCache, data: dict, - call_type: Literal[ - "completion", - "text_completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "pass_through_endpoint", - "rerank", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ) -> Optional[ Union[Exception, str, dict] ]: # raise exception if invalid, return a str for the user to receive - if rejected, or return a modified dictionary for passing into litellm @@ -380,16 +368,7 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac self, data: dict, user_api_key_dict: UserAPIKeyAuth, - call_type: Literal[ - "completion", - "embeddings", - "image_generation", - "moderation", - "audio_transcription", - "responses", - "mcp_call", - "anthropic_messages", - ], + call_type: CallTypesLiteral, ) -> Any: pass @@ -473,7 +452,6 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac # MCP TOOL CALL HOOKS ######################################################### - async def async_post_mcp_tool_call_hook( self, kwargs, response_obj: MCPPostCallResponseObject, start_time, end_time ) -> Optional[MCPPostCallResponseObject]: @@ -557,33 +535,32 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac if LITELLM_METADATA_FIELD in request_kwargs: return LITELLM_METADATA_FIELD return OLD_LITELLM_METADATA_FIELD - + def redact_standard_logging_payload_from_model_call_details( self, model_call_details: Dict ) -> Dict: """ Only redacts messages and responses when self.turn_off_message_logging is True - + By default, self.turn_off_message_logging is False and this does nothing. - + Return a redacted deepcopy of the provided logging payload. - + This is useful for logging payloads that contain sensitive information. """ from copy import copy from litellm import Choices, Message, ModelResponse - from litellm.types.utils import LiteLLMCommonStrings turn_off_message_logging: bool = getattr(self, "turn_off_message_logging", False) if turn_off_message_logging is False: return model_call_details - + # Only make a shallow copy of the top-level dict to avoid deepcopy issues # with complex objects like AuthenticationError that may be present model_call_details_copy = copy(model_call_details) - redacted_str = LiteLLMCommonStrings.redacted_by_litellm.value + redacted_str = "redacted-by-litellm" standard_logging_object = model_call_details.get("standard_logging_object") if standard_logging_object is None: return model_call_details_copy @@ -592,20 +569,40 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac standard_logging_object_copy = copy(standard_logging_object) if standard_logging_object_copy.get("messages") is not None: - standard_logging_object_copy["messages"] = [Message(content=redacted_str).model_dump()] + standard_logging_object_copy["messages"] = [ + Message(content=redacted_str).model_dump() + ] if standard_logging_object_copy.get("response") is not None: - model_response = ModelResponse( - choices=[Choices(message=Message(content=redacted_str))] - ) - model_response_dict = model_response.model_dump() - standard_logging_object_copy["response"] = model_response_dict + response = standard_logging_object_copy["response"] + # Check if this is a ResponsesAPIResponse (has "output" field) + if isinstance(response, dict) and "output" in response: + # Make a copy to avoid modifying the original + from copy import deepcopy + response_copy = deepcopy(response) + # Redact content in output array + if isinstance(response_copy.get("output"), list): + for output_item in response_copy["output"]: + if isinstance(output_item, dict) and "content" in output_item: + if isinstance(output_item["content"], list): + # Redact text in content items + for content_item in output_item["content"]: + if isinstance(content_item, dict) and "text" in content_item: + content_item["text"] = redacted_str + standard_logging_object_copy["response"] = response_copy + else: + # Standard ModelResponse format + model_response = ModelResponse( + choices=[Choices(message=Message(content=redacted_str))] + ) + model_response_dict = model_response.model_dump() + standard_logging_object_copy["response"] = model_response_dict - model_call_details_copy["standard_logging_object"] = standard_logging_object_copy + model_call_details_copy["standard_logging_object"] = ( + standard_logging_object_copy + ) return model_call_details_copy - - async def get_proxy_server_request_from_cold_storage_with_object_key( self, object_key: str, @@ -643,7 +640,9 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac verbose_logger.debug(f"Error in handle_callback_failure for {callback_name}: {str(e)}") async def _strip_base64_from_messages( - self, payload: "StandardLoggingPayload", max_depth: int = DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER + self, + payload: "StandardLoggingPayload", + max_depth: int = DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER, ) -> "StandardLoggingPayload": """ Removes or redacts base64-encoded file data (e.g., PDFs, images, audio) @@ -692,7 +691,9 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac verbose_logger.debug(f"[CustomLogger] Stripping base64 from {len(messages)} messages") if messages: - payload["messages"] = self._process_messages(messages=messages, max_depth=max_depth) + payload["messages"] = self._process_messages( + messages=messages, max_depth=max_depth + ) total_items = 0 for m in payload.get("messages", []) or []: @@ -705,9 +706,13 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac f"[CustomLogger] Completed base64 strip; retained {total_items} content items" ) return payload - - def _redact_base64(self, value: Any, depth: int = 0, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER) -> Any: + def _redact_base64( + self, + value: Any, + depth: int = 0, + max_depth: int = DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER, + ) -> Any: """Recursively redact inline base64 from any nested structure with a max recursion depth limit.""" if depth > max_depth: verbose_logger.warning( @@ -724,10 +729,16 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac return value if isinstance(value, list): - return [self._redact_base64(value=v, depth=depth + 1, max_depth=max_depth) for v in value] + return [ + self._redact_base64(value=v, depth=depth + 1, max_depth=max_depth) + for v in value + ] if isinstance(value, dict): - return {k: self._redact_base64(value=v, depth=depth + 1, max_depth=max_depth) for k, v in value.items()} + return { + k: self._redact_base64(value=v, depth=depth + 1, max_depth=max_depth) + for k, v in value.items() + } return value @@ -750,10 +761,14 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac cleaned: List[Any] = [] for c in contents: if self._should_keep_content(content=c): - cleaned.append(self._redact_base64(value=c, max_depth=max_depth)) + cleaned.append( + self._redact_base64(value=c, max_depth=max_depth) + ) msg["content"] = cleaned else: - msg["content"] = self._redact_base64(value=contents, max_depth=max_depth) + msg["content"] = self._redact_base64( + value=contents, max_depth=max_depth + ) for key, val in list(msg.items()): if key != "content": diff --git a/litellm/integrations/datadog/datadog.py b/litellm/integrations/datadog/datadog.py index 0c62667f749..46e1a2c201f 100644 --- a/litellm/integrations/datadog/datadog.py +++ b/litellm/integrations/datadog/datadog.py @@ -17,7 +17,6 @@ import asyncio import datetime import os import traceback -from litellm._uuid import uuid from datetime import datetime as datetimeObj from typing import Any, Dict, List, Optional, Union @@ -26,6 +25,7 @@ from httpx import Response import litellm from litellm._logging import verbose_logger +from litellm._uuid import uuid from litellm.integrations.custom_batch_logger import CustomBatchLogger from litellm.llms.custom_httpx.http_handler import ( _get_httpx_client, @@ -60,17 +60,19 @@ class DataDogLogger( """ Initializes the datadog logger, checks if the correct env variables are set - Required environment variables: + Required environment variables (Direct API): `DD_API_KEY` - your datadog api key `DD_SITE` - your datadog site, example = `"us5.datadoghq.com"` + + Optional environment variables (DataDog Agent): + `DD_AGENT_HOST` - hostname or IP of DataDog agent, example = `"localhost"` + `DD_AGENT_PORT` - port of DataDog agent (default: 10518 for logs) + + Note: If DD_AGENT_HOST is set, logs will be sent to the agent instead of directly to DataDog API. + In this case, DD_API_KEY and DD_SITE are not required (agent handles authentication). """ try: verbose_logger.debug("Datadog: in init datadog logger") - # check if the correct env variables are set - if os.getenv("DD_API_KEY", None) is None: - raise Exception("DD_API_KEY is not set, set 'DD_API_KEY=<>") - if os.getenv("DD_SITE", None) is None: - raise Exception("DD_SITE is not set in .env, set 'DD_SITE=<>") ######################################################### # Handle datadog_params set as litellm.datadog_params @@ -81,21 +83,16 @@ class DataDogLogger( self.async_client = get_async_httpx_client( llm_provider=httpxSpecialProvider.LoggingCallback ) - self.DD_API_KEY = os.getenv("DD_API_KEY") - self.intake_url = ( - f"https://http-intake.logs.{os.getenv('DD_SITE')}/api/v2/logs" - ) - - ################################### - # OPTIONAL -only used for testing - dd_base_url: Optional[str] = ( - os.getenv("_DATADOG_BASE_URL") - or os.getenv("DATADOG_BASE_URL") - or os.getenv("DD_BASE_URL") - ) - if dd_base_url is not None: - self.intake_url = f"{dd_base_url}/api/v2/logs" - ################################### + + # Configure DataDog endpoint (Agent or Direct API) + dd_agent_host = os.getenv("DD_AGENT_HOST") + if dd_agent_host: + self._configure_dd_agent(dd_agent_host=dd_agent_host) + else: + self._configure_dd_direct_api() + + # Optional override for testing + self._apply_dd_base_url_override() self.sync_client = _get_httpx_client() asyncio.create_task(self.periodic_flush()) self.flush_lock = asyncio.Lock() @@ -123,6 +120,47 @@ class DataDogLogger( dict_datadog_params = DatadogInitParams(**litellm.datadog_params).model_dump() return dict_datadog_params + def _configure_dd_agent(self, dd_agent_host: str) -> None: + """ + Configure DataDog Agent for log forwarding + + Args: + dd_agent_host: Hostname or IP of DataDog agent + """ + dd_agent_port = os.getenv("DD_AGENT_PORT", "10518") # default port for logs + self.intake_url = f"http://{dd_agent_host}:{dd_agent_port}/api/v2/logs" + self.DD_API_KEY = os.getenv("DD_API_KEY") # Optional when using agent + verbose_logger.debug(f"Datadog: Using DD Agent at {self.intake_url}") + + def _configure_dd_direct_api(self) -> None: + """ + Configure direct DataDog API connection + + Raises: + Exception: If required environment variables are not set + """ + if os.getenv("DD_API_KEY", None) is None: + raise Exception("DD_API_KEY is not set, set 'DD_API_KEY=<>") + if os.getenv("DD_SITE", None) is None: + raise Exception("DD_SITE is not set in .env, set 'DD_SITE=<>") + + self.DD_API_KEY = os.getenv("DD_API_KEY") + self.intake_url = ( + f"https://http-intake.logs.{os.getenv('DD_SITE')}/api/v2/logs" + ) + + def _apply_dd_base_url_override(self) -> None: + """ + Apply base URL override for testing purposes + """ + dd_base_url: Optional[str] = ( + os.getenv("_DATADOG_BASE_URL") + or os.getenv("DATADOG_BASE_URL") + or os.getenv("DD_BASE_URL") + ) + if dd_base_url is not None: + self.intake_url = f"{dd_base_url}/api/v2/logs" + async def async_log_success_event(self, kwargs, response_obj, start_time, end_time): """ Async Log success events to Datadog @@ -226,12 +264,16 @@ class DataDogLogger( end_time=end_time, ) + # Build headers + headers = {} + # Add API key if available (required for direct API, optional for agent) + if self.DD_API_KEY: + headers["DD-API-KEY"] = self.DD_API_KEY + response = self.sync_client.post( url=self.intake_url, json=dd_payload, # type: ignore - headers={ - "DD-API-KEY": self.DD_API_KEY, - }, + headers=headers, ) response.raise_for_status() @@ -342,14 +384,21 @@ class DataDogLogger( from litellm.litellm_core_utils.safe_json_dumps import safe_dumps compressed_data = gzip.compress(safe_dumps(data).encode("utf-8")) + + # Build headers + headers = { + "Content-Encoding": "gzip", + "Content-Type": "application/json", + } + + # Add API key if available (required for direct API, optional for agent) + if self.DD_API_KEY: + headers["DD-API-KEY"] = self.DD_API_KEY + response = await self.async_client.post( url=self.intake_url, data=compressed_data, # type: ignore - headers={ - "DD-API-KEY": self.DD_API_KEY, - "Content-Encoding": "gzip", - "Content-Type": "application/json", - }, + headers=headers, ) return response diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 9a17244a06d..53b7825b3d3 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -10,6 +10,7 @@ from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.types.services import ServiceLoggerPayload from litellm.types.utils import ( ChatCompletionMessageToolCall, + CostBreakdown, Function, StandardCallbackDynamicParams, StandardLoggingPayload, @@ -1076,6 +1077,16 @@ class OpenTelemetry(CustomLogger): self.safe_set_attribute( span=span, key="hidden_params", value=safe_dumps(hidden_params) ) + # Cost breakdown tracking + cost_breakdown: Optional[CostBreakdown] = standard_logging_payload.get("cost_breakdown") + if cost_breakdown: + for key, value in cost_breakdown.items(): + if value is not None: + self.safe_set_attribute( + span=span, + key=f"gen_ai.cost.{key}", + value=value, + ) ############################################# ########## LLM Request Attributes ########### ############################################# diff --git a/litellm/integrations/opik/utils.py b/litellm/integrations/opik/utils.py index 8989a861cdc..b0ab5991c91 100644 --- a/litellm/integrations/opik/utils.py +++ b/litellm/integrations/opik/utils.py @@ -1,7 +1,7 @@ import configparser import os import time -from typing import Any, Dict, Final, List, Optional +from typing import Any, Dict, Final, List, Optional, Tuple CONFIG_FILE_PATH_DEFAULT: Final[str] = "~/.opik.config" @@ -106,7 +106,7 @@ def _remove_nulls(x: Dict[str, Any]) -> Dict[str, Any]: def get_traces_and_spans_from_payload( payload: List[Dict[str, Any]] -) -> tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: +) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: """ Separate traces and spans from payload. diff --git a/litellm/litellm_core_utils/audio_utils/utils.py b/litellm/litellm_core_utils/audio_utils/utils.py index fc0c8aca842..2f0db4978ff 100644 --- a/litellm/litellm_core_utils/audio_utils/utils.py +++ b/litellm/litellm_core_utils/audio_utils/utils.py @@ -4,6 +4,7 @@ Utils used for litellm.transcription() and litellm.atranscription() import os from dataclasses import dataclass +from typing import Optional from litellm.types.files import get_file_mime_type_from_extension from litellm.types.utils import FileTypes @@ -13,12 +14,13 @@ from litellm.types.utils import FileTypes class ProcessedAudioFile: """ Processed audio file data. - + Attributes: file_content: The binary content of the audio file filename: The filename (extracted or generated) content_type: The MIME type of the audio file """ + file_content: bytes filename: str content_type: str @@ -27,61 +29,63 @@ class ProcessedAudioFile: def process_audio_file(audio_file: FileTypes) -> ProcessedAudioFile: """ Common utility function to process audio files for audio transcription APIs. - + Handles various input types: - File paths (str, os.PathLike) - Raw bytes/bytearray - Tuples (filename, content, optional content_type) - File-like objects with read() method - + Args: audio_file: The audio file input in various formats - + Returns: ProcessedAudioFile: Structured data with file content, filename, and content type - + Raises: ValueError: If audio_file type is unsupported or content cannot be extracted """ file_content = None filename = None - + if isinstance(audio_file, (bytes, bytearray)): # Raw bytes - filename = 'audio.wav' + filename = "audio.wav" file_content = bytes(audio_file) elif isinstance(audio_file, (str, os.PathLike)): # File path or PathLike file_path = str(audio_file) - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: file_content = f.read() - filename = file_path.split('/')[-1] + filename = file_path.split("/")[-1] elif isinstance(audio_file, tuple): # Tuple format: (filename, content, content_type) or (filename, content) if len(audio_file) >= 2: - filename = audio_file[0] or 'audio.wav' + filename = audio_file[0] or "audio.wav" content = audio_file[1] if isinstance(content, (bytes, bytearray)): file_content = bytes(content) elif isinstance(content, (str, os.PathLike)): # File path or PathLike - with open(str(content), 'rb') as f: + with open(str(content), "rb") as f: file_content = f.read() - elif hasattr(content, 'read'): + elif hasattr(content, "read"): # File-like object file_content = content.read() - if hasattr(content, 'seek'): + if hasattr(content, "seek"): content.seek(0) else: raise ValueError(f"Unsupported content type in tuple: {type(content)}") else: raise ValueError("Tuple must have at least 2 elements: (filename, content)") - elif hasattr(audio_file, 'read') and not isinstance(audio_file, (str, bytes, bytearray, tuple, os.PathLike)): + elif hasattr(audio_file, "read") and not isinstance( + audio_file, (str, bytes, bytearray, tuple, os.PathLike) + ): # File-like object (IO) - check this after all other types - filename = getattr(audio_file, 'name', 'audio.wav') + filename = getattr(audio_file, "name", "audio.wav") file_content = audio_file.read() # type: ignore # Reset file pointer if possible - if hasattr(audio_file, 'seek'): + if hasattr(audio_file, "seek"): audio_file.seek(0) # type: ignore else: raise ValueError(f"Unsupported audio_file type: {type(audio_file)}") @@ -90,20 +94,18 @@ def process_audio_file(audio_file: FileTypes) -> ProcessedAudioFile: raise ValueError("Could not extract file content from audio_file") # Determine content type using LiteLLM's file type utilities - content_type = 'audio/wav' # Default fallback + content_type = "audio/wav" # Default fallback if filename: try: # Extract extension from filename - extension = filename.split('.')[-1].lower() if '.' in filename else 'wav' + extension = filename.split(".")[-1].lower() if "." in filename else "wav" content_type = get_file_mime_type_from_extension(extension) except ValueError: # If extension is not recognized, fallback to audio/wav - content_type = 'audio/wav' - + content_type = "audio/wav" + return ProcessedAudioFile( - file_content=file_content, - filename=filename, - content_type=content_type + file_content=file_content, filename=filename, content_type=content_type ) @@ -134,3 +136,74 @@ def get_audio_file_for_health_check() -> FileTypes: pwd = os.path.dirname(os.path.realpath(__file__)) file_path = os.path.join(pwd, "audio_health_check.wav") return open(file_path, "rb") + + +def calculate_request_duration(file: FileTypes) -> Optional[float]: + """ + Calculate audio duration from file content. + + Args: + file: The audio file (can be file path, bytes, or file-like object) + + Returns: + Duration in seconds, or None if extraction fails or soundfile is not available + """ + try: + import soundfile as sf + except ImportError: + # soundfile not available, cannot extract duration + return None + + try: + import io + + # Handle different file input types + file_content: Optional[bytes] = None + + if isinstance(file, (bytes, bytearray)): + # Raw bytes + file_content = bytes(file) + elif isinstance(file, (str, os.PathLike)): + # File path + with open(str(file), "rb") as f: + file_content = f.read() + elif isinstance(file, tuple): + # Tuple format: (filename, content, optional content_type) + if len(file) >= 2: + content = file[1] + if isinstance(content, bytes): + file_content = content + elif hasattr(content, "read") and not isinstance( + content, (str, os.PathLike) + ): + # File-like object in tuple + current_pos = getattr(content, "tell", lambda: None)() + # Seek to start to ensure we read the entire content + if hasattr(content, "seek"): + content.seek(0) + file_content = content.read() + if current_pos is not None and hasattr(content, "seek"): + content.seek(current_pos) + elif hasattr(file, "read") and not isinstance(file, tuple): + # File-like object (including BytesIO) + current_position = file.tell() if hasattr(file, "tell") else None + # Seek to start to ensure we read the entire content + if hasattr(file, "seek"): + file.seek(0) + file_content = file.read() + # Reset file position if possible + if current_position is not None and hasattr(file, "seek"): + file.seek(current_position) + + if file_content is None or not isinstance(file_content, bytes): + return None + + # Extract duration using soundfile + file_object = io.BytesIO(file_content) + with sf.SoundFile(file_object) as audio: + duration = len(audio) / audio.samplerate + return duration + + except Exception: + # Silently fail if duration extraction fails + return None diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index 61551b04236..1a43ff2e176 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -12,6 +12,7 @@ from ..exceptions import ( APIConnectionError, APIError, AuthenticationError, + BadGatewayError, BadRequestError, ContentPolicyViolationError, ContextWindowExceededError, @@ -43,16 +44,16 @@ class ExceptionCheckers: """ if not isinstance(error_str, str): return False - + if "429" in error_str or "rate limit" in error_str.lower(): return True - + ####################################### # Mistral API returns this error string ######################################### if "service tier capacity exceeded" in error_str.lower(): return True - + return False @staticmethod @@ -73,6 +74,24 @@ class ExceptionCheckers: if substring in _error_str_lowercase: return True return False + + @staticmethod + def is_azure_content_policy_violation_error(error_str: str) -> bool: + """ + Check if an error string indicates a content policy violation error. + """ + known_exception_substrings = [ + "invalid_request_error", + "content_policy_violation", + "the response was filtered due to the prompt triggering azure openai's content management", + "your task failed as a result of our safety system", + "the model produced invalid content", + "content_filter_policy", + ] + for substring in known_exception_substrings: + if substring in error_str.lower(): + return True + return False def get_error_message(error_obj) -> Optional[str]: @@ -507,6 +526,15 @@ def exception_type( # type: ignore # noqa: PLR0915 response=getattr(original_exception, "response", None), litellm_debug_info=extra_information, ) + elif original_exception.status_code == 502: + exception_mapping_worked = True + raise BadGatewayError( + message=f"BadGatewayError: {exception_provider} - {message}", + model=model, + llm_provider=custom_llm_provider, + response=getattr(original_exception, "response", None), + litellm_debug_info=extra_information, + ) elif original_exception.status_code == 503: exception_mapping_worked = True raise ServiceUnavailableError( @@ -637,6 +665,15 @@ def exception_type( # type: ignore # noqa: PLR0915 message=f"AnthropicException - {error_str}. Handle with `litellm.InternalServerError`.", llm_provider="anthropic", model=model, + response=getattr(original_exception, "response", None), + ) + elif original_exception.status_code == 502: + exception_mapping_worked = True + raise BadGatewayError( + message=f"AnthropicException BadGatewayError - {error_str}", + llm_provider="anthropic", + model=model, + response=getattr(original_exception, "response", None), ) elif original_exception.status_code == 503: exception_mapping_worked = True @@ -644,6 +681,15 @@ def exception_type( # type: ignore # noqa: PLR0915 message=f"AnthropicException - {error_str}. Handle with `litellm.ServiceUnavailableError`.", llm_provider="anthropic", model=model, + response=getattr(original_exception, "response", None), + ) + elif original_exception.status_code == 504: # gateway timeout error + exception_mapping_worked = True + raise Timeout( + message=f"AnthropicException Timeout - {error_str}", + model=model, + llm_provider="anthropic", + exception_status_code=original_exception.status_code, ) elif custom_llm_provider == "replicate": if "Incorrect authentication token" in error_str: @@ -1260,6 +1306,7 @@ def exception_type( # type: ignore # noqa: PLR0915 elif ( "429 Quota exceeded" in error_str or "Quota exceeded for" in error_str + or "Resource exhausted" in error_str or "IndexError: list index out of range" in error_str or "429 Unable to submit request because the service is temporarily out of capacity." in error_str @@ -1992,26 +2039,19 @@ def exception_type( # type: ignore # noqa: PLR0915 response=getattr(original_exception, "response", None), ) elif ( - ( - "invalid_request_error" in error_str - and "content_policy_violation" in error_str - ) - or ( - "The response was filtered due to the prompt triggering Azure OpenAI's content management" - in error_str - ) - or "Your task failed as a result of our safety system" in error_str - or "The model produced invalid content" in error_str - or "content_filter_policy" in error_str + ExceptionCheckers.is_azure_content_policy_violation_error(error_str) ): exception_mapping_worked = True - raise ContentPolicyViolationError( - message=f"litellm.ContentPolicyViolationError: AzureException - {message}", - llm_provider="azure", - model=model, - litellm_debug_info=extra_information, - response=getattr(original_exception, "response", None), + from litellm.llms.azure.exception_mapping import ( + AzureOpenAIExceptionMapping, ) + raise AzureOpenAIExceptionMapping.create_content_policy_violation_error( + message=message, + model=model, + extra_information=extra_information, + original_exception=original_exception, + ) + elif "invalid_request_error" in error_str: exception_mapping_worked = True raise BadRequestError( @@ -2089,6 +2129,15 @@ def exception_type( # type: ignore # noqa: PLR0915 litellm_debug_info=extra_information, response=getattr(original_exception, "response", None), ) + elif original_exception.status_code == 502: + exception_mapping_worked = True + raise BadGatewayError( + message=f"AzureException BadGatewayError - {message}", + model=model, + llm_provider="azure", + litellm_debug_info=extra_information, + response=getattr(original_exception, "response", None), + ) elif original_exception.status_code == 503: exception_mapping_worked = True raise ServiceUnavailableError( diff --git a/litellm/litellm_core_utils/get_litellm_params.py b/litellm/litellm_core_utils/get_litellm_params.py index c167c202e5d..d5675a2ac51 100644 --- a/litellm/litellm_core_utils/get_litellm_params.py +++ b/litellm/litellm_core_utils/get_litellm_params.py @@ -120,5 +120,6 @@ def get_litellm_params( "vertex_project": kwargs.get("vertex_project"), "use_litellm_proxy": use_litellm_proxy, "litellm_request_debug": litellm_request_debug, + "aws_region_name": kwargs.get("aws_region_name"), } return litellm_params diff --git a/litellm/litellm_core_utils/health_check_helpers.py b/litellm/litellm_core_utils/health_check_helpers.py index 9cbee7fc70d..cc3916af069 100644 --- a/litellm/litellm_core_utils/health_check_helpers.py +++ b/litellm/litellm_core_utils/health_check_helpers.py @@ -97,6 +97,7 @@ class HealthCheckHelpers: "audio_speech", "audio_transcription", "image_generation", + "video_generation", "rerank", "realtime", "batch", @@ -159,6 +160,10 @@ class HealthCheckHelpers: **_filter_model_params(model_params=model_params), prompt=prompt, ), + "video_generation": lambda: litellm.avideo_generation( + **_filter_model_params(model_params=model_params), + prompt=prompt or "test video generation", + ), "rerank": lambda: litellm.arerank( **_filter_model_params(model_params=model_params), query=prompt or "", diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index d7be4d296b3..41a5eed55d8 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -308,9 +308,9 @@ class Logging(LiteLLMLoggingBaseClass): self.litellm_trace_id: str = litellm_trace_id or str(uuid.uuid4()) self.function_id = function_id self.streaming_chunks: List[Any] = [] # for generating complete stream response - self.sync_streaming_chunks: List[ - Any - ] = [] # for generating complete stream response + self.sync_streaming_chunks: List[Any] = ( + [] + ) # for generating complete stream response self.log_raw_request_response = log_raw_request_response # Initialize dynamic callbacks @@ -686,9 +686,9 @@ class Logging(LiteLLMLoggingBaseClass): if anthropic_cache_control_logger := AnthropicCacheControlHook.get_custom_logger_for_anthropic_cache_control_hook( non_default_params ): - self.model_call_details[ - "prompt_integration" - ] = anthropic_cache_control_logger.__class__.__name__ + self.model_call_details["prompt_integration"] = ( + anthropic_cache_control_logger.__class__.__name__ + ) return anthropic_cache_control_logger ######################################################### @@ -700,9 +700,9 @@ class Logging(LiteLLMLoggingBaseClass): internal_usage_cache=None, llm_router=None, ) - self.model_call_details[ - "prompt_integration" - ] = vector_store_custom_logger.__class__.__name__ + self.model_call_details["prompt_integration"] = ( + vector_store_custom_logger.__class__.__name__ + ) # Add to global callbacks so post-call hooks are invoked if ( vector_store_custom_logger @@ -762,9 +762,9 @@ class Logging(LiteLLMLoggingBaseClass): model ): # if model name was changes pre-call, overwrite the initial model call name with the new one self.model_call_details["model"] = model - self.model_call_details["litellm_params"][ - "api_base" - ] = self._get_masked_api_base(additional_args.get("api_base", "")) + self.model_call_details["litellm_params"]["api_base"] = ( + self._get_masked_api_base(additional_args.get("api_base", "")) + ) def pre_call(self, input, api_key, model=None, additional_args={}): # noqa: PLR0915 # Log the exact input to the LLM API @@ -793,10 +793,10 @@ class Logging(LiteLLMLoggingBaseClass): try: # [Non-blocking Extra Debug Information in metadata] if turn_off_message_logging is True: - _metadata[ - "raw_request" - ] = "redacted by litellm. \ + _metadata["raw_request"] = ( + "redacted by litellm. \ 'litellm.turn_off_message_logging=True'" + ) else: curl_command = self._get_request_curl_command( api_base=additional_args.get("api_base", ""), @@ -807,32 +807,32 @@ class Logging(LiteLLMLoggingBaseClass): _metadata["raw_request"] = str(curl_command) # split up, so it's easier to parse in the UI - self.model_call_details[ - "raw_request_typed_dict" - ] = RawRequestTypedDict( - raw_request_api_base=str( - additional_args.get("api_base") or "" - ), - raw_request_body=self._get_raw_request_body( - additional_args.get("complete_input_dict", {}) - ), - raw_request_headers=self._get_masked_headers( - additional_args.get("headers", {}) or {}, - ignore_sensitive_headers=True, - ), - error=None, + self.model_call_details["raw_request_typed_dict"] = ( + RawRequestTypedDict( + raw_request_api_base=str( + additional_args.get("api_base") or "" + ), + raw_request_body=self._get_raw_request_body( + additional_args.get("complete_input_dict", {}) + ), + raw_request_headers=self._get_masked_headers( + additional_args.get("headers", {}) or {}, + ignore_sensitive_headers=True, + ), + error=None, + ) ) except Exception as e: - self.model_call_details[ - "raw_request_typed_dict" - ] = RawRequestTypedDict( - error=str(e), + self.model_call_details["raw_request_typed_dict"] = ( + RawRequestTypedDict( + error=str(e), + ) ) - _metadata[ - "raw_request" - ] = "Unable to Log \ + _metadata["raw_request"] = ( + "Unable to Log \ raw request: {}".format( - str(e) + str(e) + ) ) if getattr(self, "logger_fn", None) and callable(self.logger_fn): try: @@ -1133,13 +1133,13 @@ class Logging(LiteLLMLoggingBaseClass): for callback in callbacks: try: if isinstance(callback, CustomLogger): - response: Optional[ - MCPPostCallResponseObject - ] = await callback.async_post_mcp_tool_call_hook( - kwargs=kwargs, - response_obj=post_mcp_tool_call_response_obj, - start_time=start_time, - end_time=end_time, + response: Optional[MCPPostCallResponseObject] = ( + await callback.async_post_mcp_tool_call_hook( + kwargs=kwargs, + response_obj=post_mcp_tool_call_response_obj, + start_time=start_time, + end_time=end_time, + ) ) ###################################################################### # if any of the callbacks modify the response, use the modified response @@ -1243,6 +1243,7 @@ class Logging(LiteLLMLoggingBaseClass): used for consistent cost calculation across response headers + logging integrations. """ + if isinstance(result, BaseModel) and hasattr(result, "_hidden_params"): hidden_params = getattr(result, "_hidden_params", {}) if ( @@ -1302,9 +1303,9 @@ class Logging(LiteLLMLoggingBaseClass): verbose_logger.debug( f"response_cost_failure_debug_information: {debug_info}" ) - self.model_call_details[ - "response_cost_failure_debug_information" - ] = debug_info + self.model_call_details["response_cost_failure_debug_information"] = ( + debug_info + ) return None try: @@ -1330,9 +1331,9 @@ class Logging(LiteLLMLoggingBaseClass): verbose_logger.debug( f"response_cost_failure_debug_information: {debug_info}" ) - self.model_call_details[ - "response_cost_failure_debug_information" - ] = debug_info + self.model_call_details["response_cost_failure_debug_information"] = ( + debug_info + ) return None @@ -1461,6 +1462,51 @@ class Logging(LiteLLMLoggingBaseClass): ) return logging_result + def _process_hidden_params_and_response_cost( + self, + logging_result, + start_time, + end_time, + ): + hidden_params = getattr(logging_result, "_hidden_params", {}) + if hidden_params: + if self.model_call_details.get("litellm_params") is not None: + self.model_call_details["litellm_params"].setdefault("metadata", {}) + if self.model_call_details["litellm_params"]["metadata"] is None: + self.model_call_details["litellm_params"]["metadata"] = {} + self.model_call_details["litellm_params"]["metadata"]["hidden_params"] = getattr(logging_result, "_hidden_params", {}) # type: ignore + + if "response_cost" in hidden_params: + self.model_call_details["response_cost"] = hidden_params["response_cost"] + else: + self.model_call_details["response_cost"] = self._response_cost_calculator(result=logging_result) + + self.model_call_details["standard_logging_object"] = get_standard_logging_object_payload( + kwargs=self.model_call_details, + init_response_obj=logging_result, + start_time=start_time, + end_time=end_time, + logging_obj=self, + status="success", + standard_built_in_tools_params=self.standard_built_in_tools_params, + ) + + def _transform_usage_objects(self, result): + if isinstance(result, ResponsesAPIResponse): + result = result.model_copy() + transformed_usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(result.usage) + setattr(result, "usage", transformed_usage.model_dump() if hasattr(transformed_usage, "model_dump") else dict(transformed_usage)) + if (standard_logging_payload := self.model_call_details.get("standard_logging_object")) is not None: + standard_logging_payload["response"] = result.model_dump() if hasattr(result, "model_dump") else dict(result) + elif isinstance(result, TranscriptionResponse): + from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import ( + TranscriptionUsageObjectTransformation, + ) + result = result.model_copy() + transformed_usage = TranscriptionUsageObjectTransformation.transform_transcription_usage_object(result.usage) # type: ignore + setattr(result, "usage", transformed_usage) + return result + def _success_handler_helper_fn( self, result=None, @@ -1476,82 +1522,24 @@ class Logging(LiteLLMLoggingBaseClass): end_time = datetime.datetime.now() if self.completion_start_time is None: self.completion_start_time = end_time - self.model_call_details[ - "completion_start_time" - ] = self.completion_start_time + self.model_call_details["completion_start_time"] = self.completion_start_time + self.model_call_details["log_event_type"] = "successful_api_call" self.model_call_details["end_time"] = end_time self.model_call_details["cache_hit"] = cache_hit + if self.call_type == CallTypes.anthropic_messages.value: result = self._handle_anthropic_messages_response_logging(result=result) - elif ( - self.call_type == CallTypes.generate_content.value - or self.call_type == CallTypes.agenerate_content.value - ): - result = self._handle_non_streaming_google_genai_generate_content_response_logging( - result=result - ) - ## if model in model cost map - log the response cost - ## else set cost to None - + elif self.call_type == CallTypes.generate_content.value or self.call_type == CallTypes.agenerate_content.value: + result = self._handle_non_streaming_google_genai_generate_content_response_logging(result=result) + logging_result = self.normalize_logging_result(result=result) - if ( - standard_logging_object is None - and result is not None - and self.stream is not True - ): - if self._is_recognized_call_type_for_logging( - logging_result=logging_result - ): - ## HIDDEN PARAMS ## - hidden_params = getattr(logging_result, "_hidden_params", {}) - if hidden_params: - # add to metadata for logging - if self.model_call_details.get("litellm_params") is not None: - self.model_call_details["litellm_params"].setdefault( - "metadata", {} - ) - if ( - self.model_call_details["litellm_params"]["metadata"] - is None - ): - self.model_call_details["litellm_params"][ - "metadata" - ] = {} - - self.model_call_details["litellm_params"]["metadata"][ # type: ignore - "hidden_params" - ] = getattr( - logging_result, "_hidden_params", {} - ) - ## RESPONSE COST - Only calculate if not in hidden_params ## - if "response_cost" in hidden_params: - self.model_call_details["response_cost"] = hidden_params[ - "response_cost" - ] - else: - self.model_call_details[ - "response_cost" - ] = self._response_cost_calculator(result=logging_result) - ## STANDARDIZED LOGGING PAYLOAD - - self.model_call_details[ - "standard_logging_object" - ] = get_standard_logging_object_payload( - kwargs=self.model_call_details, - init_response_obj=logging_result, - start_time=start_time, - end_time=end_time, - logging_obj=self, - status="success", - standard_built_in_tools_params=self.standard_built_in_tools_params, - ) + if standard_logging_object is None and result is not None and self.stream is not True: + if self._is_recognized_call_type_for_logging(logging_result=logging_result): + self._process_hidden_params_and_response_cost(logging_result=logging_result, start_time=start_time, end_time=end_time) elif isinstance(result, dict) or isinstance(result, list): - ## STANDARDIZED LOGGING PAYLOAD - self.model_call_details[ - "standard_logging_object" - ] = get_standard_logging_object_payload( + self.model_call_details["standard_logging_object"] = get_standard_logging_object_payload( kwargs=self.model_call_details, init_response_obj=result, start_time=start_time, @@ -1561,31 +1549,13 @@ class Logging(LiteLLMLoggingBaseClass): standard_built_in_tools_params=self.standard_built_in_tools_params, ) elif standard_logging_object is not None: - self.model_call_details[ - "standard_logging_object" - ] = standard_logging_object - else: # streaming chunks + image gen. + self.model_call_details["standard_logging_object"] = standard_logging_object + else: self.model_call_details["response_cost"] = None - ## RESPONSES API USAGE OBJECT TRANSFORMATION ## - # MAP RESPONSES API USAGE OBJECT TO LITELLM USAGE OBJECT - if isinstance(result, ResponsesAPIResponse): - result = result.model_copy() - setattr( - result, - "usage", - ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( - result.usage - ), - ) - - if ( - litellm.max_budget - and self.stream is False - and result is not None - and isinstance(result, dict) - and "content" in result - ): + result = self._transform_usage_objects(result=result) + + if litellm.max_budget and self.stream is False and result is not None and isinstance(result, dict) and "content" in result: time_diff = (end_time - start_time).total_seconds() float_diff = float(time_diff) litellm._current_cost += litellm.completion_cost( @@ -1625,7 +1595,7 @@ class Logging(LiteLLMLoggingBaseClass): or isinstance(logging_result, OCRResponse) # OCR or isinstance(logging_result, dict) and logging_result.get("object") == "vector_store.search_results.page" - or isinstance(logging_result, VideoObject) + or isinstance(logging_result, VideoObject) or isinstance(logging_result, ContainerObject) or (self.call_type == CallTypes.call_mcp_tool.value) ): @@ -1719,23 +1689,23 @@ class Logging(LiteLLMLoggingBaseClass): verbose_logger.debug( "Logging Details LiteLLM-Success Call streaming complete" ) - self.model_call_details[ - "complete_streaming_response" - ] = complete_streaming_response - self.model_call_details[ - "response_cost" - ] = self._response_cost_calculator(result=complete_streaming_response) + self.model_call_details["complete_streaming_response"] = ( + complete_streaming_response + ) + self.model_call_details["response_cost"] = ( + self._response_cost_calculator(result=complete_streaming_response) + ) ## STANDARDIZED LOGGING PAYLOAD - self.model_call_details[ - "standard_logging_object" - ] = get_standard_logging_object_payload( - kwargs=self.model_call_details, - init_response_obj=complete_streaming_response, - start_time=start_time, - end_time=end_time, - logging_obj=self, - status="success", - standard_built_in_tools_params=self.standard_built_in_tools_params, + self.model_call_details["standard_logging_object"] = ( + get_standard_logging_object_payload( + kwargs=self.model_call_details, + init_response_obj=complete_streaming_response, + start_time=start_time, + end_time=end_time, + logging_obj=self, + status="success", + standard_built_in_tools_params=self.standard_built_in_tools_params, + ) ) callbacks = self.get_combined_callback_list( dynamic_success_callbacks=self.dynamic_success_callbacks, @@ -2063,10 +2033,10 @@ class Logging(LiteLLMLoggingBaseClass): ) else: if self.stream and complete_streaming_response: - self.model_call_details[ - "complete_response" - ] = self.model_call_details.get( - "complete_streaming_response", {} + self.model_call_details["complete_response"] = ( + self.model_call_details.get( + "complete_streaming_response", {} + ) ) result = self.model_call_details["complete_response"] openMeterLogger.log_success_event( @@ -2105,10 +2075,10 @@ class Logging(LiteLLMLoggingBaseClass): ) else: if self.stream and complete_streaming_response: - self.model_call_details[ - "complete_response" - ] = self.model_call_details.get( - "complete_streaming_response", {} + self.model_call_details["complete_response"] = ( + self.model_call_details.get( + "complete_streaming_response", {} + ) ) result = self.model_call_details["complete_response"] @@ -2251,9 +2221,9 @@ class Logging(LiteLLMLoggingBaseClass): if complete_streaming_response is not None: print_verbose("Async success callbacks: Got a complete streaming response") - self.model_call_details[ - "async_complete_streaming_response" - ] = complete_streaming_response + self.model_call_details["async_complete_streaming_response"] = ( + complete_streaming_response + ) try: if self.model_call_details.get("cache_hit", False) is True: @@ -2264,10 +2234,10 @@ class Logging(LiteLLMLoggingBaseClass): model_call_details=self.model_call_details ) # base_model defaults to None if not set on model_info - self.model_call_details[ - "response_cost" - ] = self._response_cost_calculator( - result=complete_streaming_response + self.model_call_details["response_cost"] = ( + self._response_cost_calculator( + result=complete_streaming_response + ) ) verbose_logger.debug( @@ -2280,16 +2250,16 @@ class Logging(LiteLLMLoggingBaseClass): self.model_call_details["response_cost"] = None ## STANDARDIZED LOGGING PAYLOAD - self.model_call_details[ - "standard_logging_object" - ] = get_standard_logging_object_payload( - kwargs=self.model_call_details, - init_response_obj=complete_streaming_response, - start_time=start_time, - end_time=end_time, - logging_obj=self, - status="success", - standard_built_in_tools_params=self.standard_built_in_tools_params, + self.model_call_details["standard_logging_object"] = ( + get_standard_logging_object_payload( + kwargs=self.model_call_details, + init_response_obj=complete_streaming_response, + start_time=start_time, + end_time=end_time, + logging_obj=self, + status="success", + standard_built_in_tools_params=self.standard_built_in_tools_params, + ) ) callbacks = self.get_combined_callback_list( dynamic_success_callbacks=self.dynamic_async_success_callbacks, @@ -2478,26 +2448,24 @@ class Logging(LiteLLMLoggingBaseClass): def _handle_callback_failure(self, callback: Any): """ Handle callback logging failures by incrementing Prometheus metrics. - + Works for both sync and async contexts since Prometheus counter increment is synchronous. - + Args: callback: The callback that failed """ try: callback_name = self._get_callback_name(callback) - + all_callbacks = litellm.logging_callback_manager._get_all_callbacks() - + for callback_obj in all_callbacks: - if hasattr(callback_obj, 'increment_callback_logging_failure'): + if hasattr(callback_obj, "increment_callback_logging_failure"): callback_obj.increment_callback_logging_failure(callback_name=callback_name) # type: ignore break # Only increment once - + except Exception as e: - verbose_logger.debug( - f"Error in _handle_callback_failure: {str(e)}" - ) + verbose_logger.debug(f"Error in _handle_callback_failure: {str(e)}") def _failure_handler_helper_fn( self, exception, traceback_exception, start_time=None, end_time=None @@ -2527,18 +2495,18 @@ class Logging(LiteLLMLoggingBaseClass): ## STANDARDIZED LOGGING PAYLOAD - self.model_call_details[ - "standard_logging_object" - ] = get_standard_logging_object_payload( - kwargs=self.model_call_details, - init_response_obj={}, - start_time=start_time, - end_time=end_time, - logging_obj=self, - status="failure", - error_str=str(exception), - original_exception=exception, - standard_built_in_tools_params=self.standard_built_in_tools_params, + self.model_call_details["standard_logging_object"] = ( + get_standard_logging_object_payload( + kwargs=self.model_call_details, + init_response_obj={}, + start_time=start_time, + end_time=end_time, + logging_obj=self, + status="failure", + error_str=str(exception), + original_exception=exception, + standard_built_in_tools_params=self.standard_built_in_tools_params, + ) ) return start_time, end_time @@ -2975,12 +2943,12 @@ class Logging(LiteLLMLoggingBaseClass): """ if isinstance(cb, str): return cb - if hasattr(cb, "__class__"): - return cb.__class__.__name__ if hasattr(cb, "__name__"): return cb.__name__ if hasattr(cb, "__func__"): return cb.__func__.__name__ + if hasattr(cb, "__class__"): + return cb.__class__.__name__ return str(cb) def _is_internal_litellm_proxy_callback(self, cb) -> bool: @@ -3036,13 +3004,19 @@ class Logging(LiteLLMLoggingBaseClass): elif isinstance(result, ResponseCompletedEvent): ## return unified Usage object if isinstance(result.response.usage, ResponseAPIUsage): + transformed_usage = ( + ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( + result.response.usage + ) + ) + # Set as dict instead of Usage object so model_dump() serializes it correctly setattr( result.response, "usage", ( - ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage( - result.response.usage - ) + transformed_usage.model_dump() + if hasattr(transformed_usage, "model_dump") + else dict(transformed_usage) ), ) return result.response @@ -3443,9 +3417,9 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 endpoint=arize_config.endpoint, ) - os.environ[ - "OTEL_EXPORTER_OTLP_TRACES_HEADERS" - ] = f"space_id={arize_config.space_key},api_key={arize_config.api_key}" + os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = ( + f"space_id={arize_config.space_key},api_key={arize_config.api_key}" + ) for callback in _in_memory_loggers: if ( isinstance(callback, ArizeLogger) @@ -3469,9 +3443,9 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 # auth can be disabled on local deployments of arize phoenix if arize_phoenix_config.otlp_auth_headers is not None: - os.environ[ - "OTEL_EXPORTER_OTLP_TRACES_HEADERS" - ] = arize_phoenix_config.otlp_auth_headers + os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = ( + arize_phoenix_config.otlp_auth_headers + ) for callback in _in_memory_loggers: if ( @@ -3603,9 +3577,9 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 exporter="otlp_http", endpoint="https://langtrace.ai/api/trace", ) - os.environ[ - "OTEL_EXPORTER_OTLP_TRACES_HEADERS" - ] = f"api_key={os.getenv('LANGTRACE_API_KEY')}" + os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = ( + f"api_key={os.getenv('LANGTRACE_API_KEY')}" + ) for callback in _in_memory_loggers: if ( isinstance(callback, OpenTelemetry) @@ -4305,10 +4279,10 @@ class StandardLoggingPayloadSetup: for key in StandardLoggingHiddenParams.__annotations__.keys(): if key in hidden_params: if key == "additional_headers": - clean_hidden_params[ - "additional_headers" - ] = StandardLoggingPayloadSetup.get_additional_headers( - hidden_params[key] + clean_hidden_params["additional_headers"] = ( + StandardLoggingPayloadSetup.get_additional_headers( + hidden_params[key] + ) ) else: clean_hidden_params[key] = hidden_params[key] # type: ignore @@ -4369,7 +4343,7 @@ class StandardLoggingPayloadSetup: s3_object_key = get_s3_object_key( s3_path=s3_path, # Use actual s3_path from logger configuration - team_alias_prefix="", # Don't split by team alias for cold storage + prefix="", # Don't split by team alias for cold storage start_time=start_time, s3_file_name=s3_file_name, ) @@ -4871,9 +4845,9 @@ def scrub_sensitive_keys_in_metadata(litellm_params: Optional[dict]): ): for k, v in metadata["user_api_key_metadata"].items(): if k == "logging": # prevent logging user logging keys - cleaned_user_api_key_metadata[ - k - ] = "scrubbed_by_litellm_for_sensitive_keys" + cleaned_user_api_key_metadata[k] = ( + "scrubbed_by_litellm_for_sensitive_keys" + ) else: cleaned_user_api_key_metadata[k] = v diff --git a/litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py b/litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py new file mode 100644 index 00000000000..1432e912fd8 --- /dev/null +++ b/litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py @@ -0,0 +1,38 @@ +from typing import Any, Optional, Union + +from litellm.types.utils import ( + PromptTokensDetailsWrapper, + TranscriptionUsageDurationObject, + TranscriptionUsageTokensObject, + Usage, +) + + +class TranscriptionUsageObjectTransformation: + @staticmethod + def is_transcription_usage_object( + usage_object: Any, + ) -> bool: + return isinstance(usage_object, TranscriptionUsageDurationObject) or isinstance( + usage_object, TranscriptionUsageTokensObject + ) + + @staticmethod + def transform_transcription_usage_object( + usage_object: Union[ + TranscriptionUsageDurationObject, TranscriptionUsageTokensObject + ], + ) -> Optional[Usage]: + if isinstance(usage_object, TranscriptionUsageDurationObject): + return None + elif isinstance(usage_object, TranscriptionUsageTokensObject): + return Usage( + prompt_tokens=usage_object.input_tokens, + completion_tokens=usage_object.output_tokens, + total_tokens=usage_object.total_tokens, + prompt_tokens_details=PromptTokensDetailsWrapper( + text_tokens=usage_object.input_token_details.text_tokens, + audio_tokens=usage_object.input_token_details.audio_tokens, + ), + ) + return None diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index d9dbc703d3a..99f3853d21a 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -1,7 +1,7 @@ # What is this? ## Helper utilities for cost_per_token() -from typing import Any, Literal, Optional, Tuple, TypedDict, cast +from typing import Literal, Optional, Tuple, TypedDict, cast import litellm from litellm._logging import verbose_logger @@ -118,21 +118,21 @@ def _generic_cost_per_character( def _get_service_tier_cost_key(base_key: str, service_tier: Optional[str]) -> str: """ Get the appropriate cost key based on service tier. - + Args: base_key: The base cost key (e.g., "input_cost_per_token") service_tier: The service tier ("flex", "priority", or None for standard) - + Returns: str: The cost key to use (e.g., "input_cost_per_token_flex" or "input_cost_per_token") """ if service_tier is None: return base_key - + # Only use service tier specific keys for "flex" and "priority" if service_tier.lower() in [ServiceTier.FLEX.value, ServiceTier.PRIORITY.value]: return f"{base_key}_{service_tier.lower()}" - + # For any other service tier, use standard pricing return base_key @@ -152,15 +152,15 @@ def _get_token_base_cost( # Get service tier aware cost keys input_cost_key = _get_service_tier_cost_key("input_cost_per_token", service_tier) output_cost_key = _get_service_tier_cost_key("output_cost_per_token", service_tier) - cache_creation_cost_key = _get_service_tier_cost_key("cache_creation_input_token_cost", service_tier) - cache_read_cost_key = _get_service_tier_cost_key("cache_read_input_token_cost", service_tier) - - prompt_base_cost = cast( - float, _get_cost_per_unit(model_info, input_cost_key) + cache_creation_cost_key = _get_service_tier_cost_key( + "cache_creation_input_token_cost", service_tier ) - completion_base_cost = cast( - float, _get_cost_per_unit(model_info, output_cost_key) + cache_read_cost_key = _get_service_tier_cost_key( + "cache_read_input_token_cost", service_tier ) + + prompt_base_cost = cast(float, _get_cost_per_unit(model_info, input_cost_key)) + completion_base_cost = cast(float, _get_cost_per_unit(model_info, output_cost_key)) cache_creation_cost = cast( float, _get_cost_per_unit(model_info, cache_creation_cost_key) ) @@ -168,9 +168,7 @@ def _get_token_base_cost( float, _get_cost_per_unit(model_info, "cache_creation_input_token_cost_above_1hr"), ) - cache_read_cost = cast( - float, _get_cost_per_unit(model_info, cache_read_cost_key) - ) + cache_read_cost = cast(float, _get_cost_per_unit(model_info, cache_read_cost_key)) ## CHECK IF ABOVE THRESHOLD threshold: Optional[float] = None @@ -278,7 +276,7 @@ def _get_cost_per_unit( verbose_logger.exception( f"litellm.litellm_core_utils.llm_cost_calc.utils.py::calculate_cost_per_component(): Exception occured - {cost_per_unit}\nDefaulting to 0.0" ) - + # If the service tier key doesn't exist or is None, try to fall back to the standard key if cost_per_unit is None: # Check if any service tier suffix exists in the cost key using ServiceTier enum @@ -286,7 +284,7 @@ def _get_cost_per_unit( suffix = f"_{service_tier.value}" if suffix in cost_key: # Extract the base key by removing the matched suffix - base_key = cost_key.replace(suffix, '') + base_key = cost_key.replace(suffix, "") fallback_cost = model_info.get(base_key) if isinstance(fallback_cost, float): return fallback_cost @@ -300,7 +298,7 @@ def _get_cost_per_unit( f"litellm.litellm_core_utils.llm_cost_calc.utils.py::_get_cost_per_unit(): Exception occured - {fallback_cost}\nDefaulting to 0.0" ) break # Only try the first matching suffix - + return default_value @@ -495,7 +493,10 @@ def _calculate_input_cost( def generic_cost_per_token( - model: str, usage: Usage, custom_llm_provider: str, service_tier: Optional[str] = None + model: str, + usage: Usage, + custom_llm_provider: str, + service_tier: Optional[str] = None, ) -> Tuple[float, float]: """ Calculates the cost per token for a given model, prompt tokens, and completion tokens. @@ -547,7 +548,9 @@ def generic_cost_per_token( cache_creation_cost, cache_creation_cost_above_1hr, cache_read_cost, - ) = _get_token_base_cost(model_info=model_info, usage=usage, service_tier=service_tier) + ) = _get_token_base_cost( + model_info=model_info, usage=usage, service_tier=service_tier + ) prompt_cost = _calculate_input_cost( prompt_tokens_details=prompt_tokens_details, @@ -631,7 +634,7 @@ class CostCalculatorUtils: @staticmethod def route_image_generation_cost_calculator( model: str, - completion_response: Any, + completion_response: ImageResponse, custom_llm_provider: Optional[str] = None, quality: Optional[str] = None, n: Optional[int] = None, @@ -658,6 +661,13 @@ class CostCalculatorUtils: cost_calculator as vertex_ai_image_cost_calculator, ) + if size is None: + size = completion_response.size or "1024-x-1024" + if quality is None: + quality = completion_response.quality or "standard" + if n is None: + n = len(completion_response.data) if completion_response.data else 0 + if custom_llm_provider == litellm.LlmProviders.VERTEX_AI.value: if isinstance(completion_response, ImageResponse): return vertex_ai_image_cost_calculator( diff --git a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py index 6ed9d5725e9..5a50806218f 100644 --- a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py +++ b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py @@ -37,6 +37,8 @@ from litellm.types.utils import ( TextChoices, TextCompletionResponse, TranscriptionResponse, + TranscriptionUsageDurationObject, + TranscriptionUsageTokensObject, Usage, ) @@ -684,6 +686,24 @@ def convert_to_model_response_object( # noqa: PLR0915 if key in response_object: setattr(model_response_object, key, response_object[key]) + if "usage" in response_object and response_object["usage"] is not None: + tr_usage_object: Optional[ + Union[ + TranscriptionUsageDurationObject, TranscriptionUsageTokensObject + ] + ] = None + + if response_object["usage"].get("type", None) == "duration": + tr_usage_object = TranscriptionUsageDurationObject( + **response_object["usage"] + ) + elif response_object["usage"].get("type", None) == "tokens": + tr_usage_object = TranscriptionUsageTokensObject( + **response_object["usage"] + ) + if tr_usage_object is not None: + setattr(model_response_object, "usage", tr_usage_object) + if hidden_params is not None: model_response_object._hidden_params = hidden_params diff --git a/litellm/litellm_core_utils/redact_messages.py b/litellm/litellm_core_utils/redact_messages.py index 849cb20cdc5..0effed3db70 100644 --- a/litellm/litellm_core_utils/redact_messages.py +++ b/litellm/litellm_core_utils/redact_messages.py @@ -7,17 +7,17 @@ # # Thank you users! We ❤️ you! - Krrish & Ishaan +import asyncio import copy from typing import TYPE_CHECKING, Any, Optional import litellm from litellm.integrations.custom_logger import CustomLogger -from litellm.secret_managers.main import str_to_bool -from litellm.types.utils import StandardCallbackDynamicParams from litellm.litellm_core_utils.core_helpers import ( get_metadata_variable_name_from_kwargs, ) -import asyncio +from litellm.secret_managers.main import str_to_bool +from litellm.types.utils import StandardCallbackDynamicParams if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import ( diff --git a/litellm/litellm_core_utils/safe_json_dumps.py b/litellm/litellm_core_utils/safe_json_dumps.py index c714e36b5f9..8b50e41a795 100644 --- a/litellm/litellm_core_utils/safe_json_dumps.py +++ b/litellm/litellm_core_utils/safe_json_dumps.py @@ -49,4 +49,4 @@ def safe_dumps(data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH) -> str: return "Unserializable Object" safe_data = _serialize(data, set(), 0) - return json.dumps(safe_data, default=str) + return json.dumps(safe_data, default=str) \ No newline at end of file diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 691b46af8da..ced5a089fcc 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -82,9 +82,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): to pass metadata to anthropic, it's {"user_id": "any-relevant-information"} """ - max_tokens: Optional[int] = ( - DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS # anthropic requires a default value (Opus, Sonnet, and Haiku have the same default) - ) + max_tokens: Optional[ + int + ] = DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS # anthropic requires a default value (Opus, Sonnet, and Haiku have the same default) stop_sequences: Optional[list] = None temperature: Optional[int] = None top_p: Optional[int] = None @@ -118,7 +118,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): return super().get_config() def get_supported_openai_params(self, model: str): - params = [ "stream", "stop", @@ -465,11 +464,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if mcp_servers: optional_params["mcp_servers"] = mcp_servers if param == "tool_choice" or param == "parallel_tool_calls": - _tool_choice: Optional[AnthropicMessagesToolChoice] = ( - self._map_tool_choice( - tool_choice=non_default_params.get("tool_choice"), - parallel_tool_use=non_default_params.get("parallel_tool_calls"), - ) + _tool_choice: Optional[ + AnthropicMessagesToolChoice + ] = self._map_tool_choice( + tool_choice=non_default_params.get("tool_choice"), + parallel_tool_use=non_default_params.get("parallel_tool_calls"), ) if _tool_choice is not None: @@ -517,6 +516,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): self._add_tools_to_optional_params( optional_params=optional_params, tools=[hosted_web_search_tool] ) + elif param == "extra_headers": + optional_params["extra_headers"] = value ## handle thinking tokens self.update_optional_params_with_thinking_tokens( @@ -575,9 +576,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): text=system_message_block["content"], ) if "cache_control" in system_message_block: - anthropic_system_message_content["cache_control"] = ( - system_message_block["cache_control"] - ) + anthropic_system_message_content[ + "cache_control" + ] = system_message_block["cache_control"] anthropic_system_message_list.append( anthropic_system_message_content ) @@ -591,9 +592,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) ) if "cache_control" in _content: - anthropic_system_message_content["cache_control"] = ( - _content["cache_control"] - ) + anthropic_system_message_content[ + "cache_control" + ] = _content["cache_control"] anthropic_system_message_list.append( anthropic_system_message_content @@ -641,13 +642,25 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) ) return tools - - def update_headers_with_optional_anthropic_beta(self, headers: dict, optional_params: dict) -> dict: + + def update_headers_with_optional_anthropic_beta( + self, headers: dict, optional_params: dict + ) -> dict: """Update headers with optional anthropic beta.""" _tools = optional_params.get("tools", []) for tool in _tools: - if tool.get("type", None) and tool.get("type").startswith(ANTHROPIC_HOSTED_TOOLS.WEB_FETCH.value): - headers["anthropic-beta"] = ANTHROPIC_BETA_HEADER_VALUES.WEB_FETCH_2025_09_10.value + if tool.get("type", None) and tool.get("type").startswith( + ANTHROPIC_HOSTED_TOOLS.WEB_FETCH.value + ): + headers[ + "anthropic-beta" + ] = ANTHROPIC_BETA_HEADER_VALUES.WEB_FETCH_2025_09_10.value + elif tool.get("type", None) and tool.get("type").startswith( + ANTHROPIC_HOSTED_TOOLS.MEMORY.value + ): + headers[ + "anthropic-beta" + ] = ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value return headers def transform_request( @@ -685,7 +698,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): llm_provider="anthropic", ) - headers = self.update_headers_with_optional_anthropic_beta(headers=headers, optional_params=optional_params) + headers = self.update_headers_with_optional_anthropic_beta( + headers=headers, optional_params=optional_params + ) # Separate system prompt from rest of message anthropic_system_message_list = self.translate_system_message(messages=messages) @@ -764,7 +779,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) return _message - def extract_response_content(self, completion_response: dict) -> Tuple[ + def extract_response_content( + self, completion_response: dict + ) -> Tuple[ str, Optional[List[Any]], Optional[ diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index 488aed4e031..e7aa93ac882 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -36,6 +36,7 @@ from .common_utils import ( process_azure_headers, select_azure_base_url_or_endpoint, ) +from .image_generation import get_azure_image_generation_config class AzureOpenAIAssistantsAPIConfig: @@ -1011,7 +1012,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): async def aimage_generation( self, data: dict, - model_response: ModelResponse, + model_response: Optional[ImageResponse], azure_client_params: dict, api_key: str, input: list, @@ -1020,6 +1021,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): client=None, timeout=None, ) -> litellm.ImageResponse: + response: Optional[dict] = None try: # response = await azure_client.images.generate(**data, timeout=timeout) @@ -1052,21 +1054,38 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): data=data, headers=headers, ) - response = httpx_response.json() - stringified_response = response - ## LOGGING - logging_obj.post_call( - input=input, - api_key=api_key, - additional_args={"complete_input_dict": data}, - original_response=stringified_response, - ) - return convert_to_model_response_object( # type: ignore - response_object=stringified_response, - model_response_object=model_response, - response_type="image_generation", + provider_config = get_azure_image_generation_config( + data.get("model", "dall-e-2") ) + if provider_config is not None: + return provider_config.transform_image_generation_response( + model=data.get("model", "dall-e-2"), + raw_response=httpx_response, + model_response=model_response or ImageResponse(), + logging_obj=logging_obj, + request_data=data, + optional_params=data, + litellm_params=data, + encoding=litellm.encoding, + ) + + else: + response = httpx_response.json() + + stringified_response = response + ## LOGGING + logging_obj.post_call( + input=input, + api_key=api_key, + additional_args={"complete_input_dict": data}, + original_response=stringified_response, + ) + return convert_to_model_response_object( # type: ignore + response_object=stringified_response, + model_response_object=model_response, + response_type="image_generation", + ) except Exception as e: ## LOGGING logging_obj.post_call( @@ -1124,9 +1143,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): if api_key is None and azure_ad_token_provider is not None: azure_ad_token = azure_ad_token_provider() if azure_ad_token: - headers.pop( - "api-key", None - ) + headers.pop("api-key", None) headers["Authorization"] = f"Bearer {azure_ad_token}" # init AzureOpenAI Client diff --git a/litellm/llms/azure/exception_mapping.py b/litellm/llms/azure/exception_mapping.py new file mode 100644 index 00000000000..70c2609c6b4 --- /dev/null +++ b/litellm/llms/azure/exception_mapping.py @@ -0,0 +1,42 @@ +from typing import Optional + +from litellm.exceptions import ContentPolicyViolationError + + +class AzureOpenAIExceptionMapping: + """ + Class for creating Azure OpenAI specific exceptions + """ + @staticmethod + def create_content_policy_violation_error( + message: str, + model: str, + extra_information: str, + original_exception: Exception, + ) -> ContentPolicyViolationError: + """ + Create a content policy violation error + """ + raise ContentPolicyViolationError( + message=f"litellm.ContentPolicyViolationError: AzureException - {message}", + llm_provider="azure", + model=model, + litellm_debug_info=extra_information, + response=getattr(original_exception, "response", None), + provider_specific_fields={ + "innererror": AzureOpenAIExceptionMapping._get_innererror_from_exception(original_exception) + }, + ) + + @staticmethod + def _get_innererror_from_exception(original_exception: Exception) -> Optional[dict]: + """ + Azure OpenAI returns the innererror in the body of the exception + This method extracts the innererror from the exception + """ + innererror = None + body_dict = getattr(original_exception, "body", None) or {} + if isinstance(body_dict, dict): + innererror = body_dict.get("innererror") + return innererror + \ No newline at end of file diff --git a/litellm/llms/azure_ai/rerank/transformation.py b/litellm/llms/azure_ai/rerank/transformation.py index 4465e0d70a2..a47b6082c37 100644 --- a/litellm/llms/azure_ai/rerank/transformation.py +++ b/litellm/llms/azure_ai/rerank/transformation.py @@ -18,7 +18,12 @@ class AzureAIRerankConfig(CohereRerankConfig): Azure AI Rerank - Follows the same Spec as Cohere Rerank """ - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: if api_base is None: raise ValueError( "Azure AI API Base is required. api_base=None. Set in call or via `AZURE_AI_API_BASE` env var." @@ -32,6 +37,7 @@ class AzureAIRerankConfig(CohereRerankConfig): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[dict] = None, ) -> dict: if api_key is None: api_key = get_secret_str("AZURE_AI_API_KEY") or litellm.azure_key diff --git a/litellm/llms/base_llm/rerank/transformation.py b/litellm/llms/base_llm/rerank/transformation.py index 6e9c03dee89..b22d85e82be 100644 --- a/litellm/llms/base_llm/rerank/transformation.py +++ b/litellm/llms/base_llm/rerank/transformation.py @@ -23,6 +23,7 @@ class BaseRerankConfig(ABC): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[dict] = None, ) -> dict: pass @@ -50,7 +51,12 @@ class BaseRerankConfig(ABC): return model_response @abstractmethod - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: """ OPTIONAL diff --git a/litellm/llms/base_llm/videos/transformation.py b/litellm/llms/base_llm/videos/transformation.py index 7234093778c..16341932fe8 100644 --- a/litellm/llms/base_llm/videos/transformation.py +++ b/litellm/llms/base_llm/videos/transformation.py @@ -92,10 +92,11 @@ class BaseVideoConfig(ABC): self, model: str, prompt: str, + api_base: str, video_create_optional_request_params: Dict, litellm_params: GenericLiteLLMParams, headers: dict, - ) -> Tuple[Dict, RequestFiles]: + ) -> Tuple[Dict, RequestFiles, str]: pass @abstractmethod @@ -104,6 +105,8 @@ class BaseVideoConfig(ABC): model: str, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + request_data: Optional[Dict] = None, ) -> VideoObject: pass @@ -154,6 +157,7 @@ class BaseVideoConfig(ABC): self, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, ) -> VideoObject: pass @@ -181,6 +185,7 @@ class BaseVideoConfig(ABC): self, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, ) -> Dict[str,str]: pass @@ -229,6 +234,7 @@ class BaseVideoConfig(ABC): self, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, ) -> VideoObject: pass diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index d73860e1bc9..baaec996535 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -237,6 +237,7 @@ def init_bedrock_client( "sts", aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key, + verify=ssl_verify ) sts_response = sts_client.assume_role( diff --git a/litellm/llms/cohere/rerank/transformation.py b/litellm/llms/cohere/rerank/transformation.py index f9c979712da..d085cb13c44 100644 --- a/litellm/llms/cohere/rerank/transformation.py +++ b/litellm/llms/cohere/rerank/transformation.py @@ -20,7 +20,12 @@ class CohereRerankConfig(BaseRerankConfig): def __init__(self) -> None: pass - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: if api_base: # Remove trailing slashes and ensure clean base URL api_base = api_base.rstrip("/") @@ -72,6 +77,7 @@ class CohereRerankConfig(BaseRerankConfig): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[dict] = None, ) -> dict: if api_key is None: api_key = ( diff --git a/litellm/llms/cohere/rerank_v2/transformation.py b/litellm/llms/cohere/rerank_v2/transformation.py index eb551a8a949..01309d937f9 100644 --- a/litellm/llms/cohere/rerank_v2/transformation.py +++ b/litellm/llms/cohere/rerank_v2/transformation.py @@ -12,7 +12,12 @@ class CohereRerankV2Config(CohereRerankConfig): def __init__(self) -> None: pass - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: if api_base: # Remove trailing slashes and ensure clean base URL api_base = api_base.rstrip("/") diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index dd39c29203a..883f2de44df 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -921,11 +921,13 @@ class BaseLLMHTTPHandler: api_key=api_key, headers=headers or {}, model=model, + optional_params=optional_rerank_params, ) api_base = provider_config.get_complete_url( api_base=api_base, model=model, + optional_params=optional_rerank_params, ) data = provider_config.transform_rerank_request( @@ -2009,6 +2011,9 @@ class BaseLLMHTTPHandler: headers=headers, ) + if extra_body: + data.update(extra_body) + ## LOGGING logging_obj.pre_call( input=input, @@ -2135,6 +2140,9 @@ class BaseLLMHTTPHandler: headers=headers, ) + if extra_body: + data.update(extra_body) + ## LOGGING logging_obj.pre_call( input=input, @@ -4091,7 +4099,7 @@ class BaseLLMHTTPHandler: or {}, model=model, ) - + if extra_headers: headers.update(extra_headers) @@ -4101,12 +4109,13 @@ class BaseLLMHTTPHandler: litellm_params=dict(litellm_params), ) - data, files = video_generation_provider_config.transform_video_create_request( + data, files, api_base = video_generation_provider_config.transform_video_create_request( model=model, prompt=prompt, video_create_optional_request_params=video_generation_optional_request_params, litellm_params=litellm_params, headers=headers, + api_base=api_base, ) ## LOGGING @@ -4132,8 +4141,8 @@ class BaseLLMHTTPHandler: timeout=timeout, ) - # --- END MOCK VIDEO RESPONSE --- else: + # Use JSON content type for POST requests without files response = sync_httpx_client.post( url=api_base, headers=headers, @@ -4151,6 +4160,8 @@ class BaseLLMHTTPHandler: model=model, raw_response=response, logging_obj=logging_obj, + custom_llm_provider=custom_llm_provider, + request_data=data, ) async def async_video_generation_handler( @@ -4198,9 +4209,10 @@ class BaseLLMHTTPHandler: litellm_params=dict(litellm_params), ) - data, files = video_generation_provider_config.transform_video_create_request( + data, files, api_base = video_generation_provider_config.transform_video_create_request( model=model, prompt=prompt, + api_base=api_base, video_create_optional_request_params=video_generation_optional_request_params, litellm_params=litellm_params, headers=headers, @@ -4218,7 +4230,7 @@ class BaseLLMHTTPHandler: ) try: - # Use JSON when no files, otherwise use form data with files + #Use JSON when no files, otherwise use form data with files if files is None or len(files) == 0: response = await async_httpx_client.post( url=api_base, @@ -4245,6 +4257,8 @@ class BaseLLMHTTPHandler: model=model, raw_response=response, logging_obj=logging_obj, + custom_llm_provider=custom_llm_provider, + request_data=data, ) ###### VIDEO CONTENT HANDLER ###### @@ -4300,7 +4314,7 @@ class BaseLLMHTTPHandler: ) # Transform the request using the provider config - url, params = video_content_provider_config.transform_video_content_request( + url, data = video_content_provider_config.transform_video_content_request( video_id=video_id, api_base=api_base, litellm_params=litellm_params, @@ -4308,12 +4322,21 @@ class BaseLLMHTTPHandler: ) try: - # Make the GET request to download content - response = sync_httpx_client.get( - url=url, - headers=headers, - params=params, - ) + # Use POST if params contains data (e.g., Vertex AI fetchPredictOperation) + # Otherwise use GET (e.g., OpenAI video content download) + if data: + response = sync_httpx_client.post( + url=url, + headers=headers, + json=data, + ) + else: + # Otherwise it's a GET request with query params + response = sync_httpx_client.get( + url=url, + headers=headers, + params=data, + ) # Transform the response using the provider config return video_content_provider_config.transform_video_content_response( @@ -4366,7 +4389,7 @@ class BaseLLMHTTPHandler: ) # Transform the request using the provider config - url, params = video_content_provider_config.transform_video_content_request( + url, data = video_content_provider_config.transform_video_content_request( video_id=video_id, api_base=api_base, litellm_params=litellm_params, @@ -4374,12 +4397,21 @@ class BaseLLMHTTPHandler: ) try: - # Make the GET request to download content - response = await async_httpx_client.get( - url=url, - headers=headers, - params=params, - ) + # Use POST if params contains data (e.g., Vertex AI fetchPredictOperation) + # Otherwise use GET (e.g., OpenAI video content download) + if data: + response = await async_httpx_client.post( + url=url, + headers=headers, + json=data, + ) + else: + # Otherwise it's a GET request with query params + response = await async_httpx_client.get( + url=url, + headers=headers, + params=data, + ) # Transform the response using the provider config return video_content_provider_config.transform_video_content_response( @@ -4484,6 +4516,7 @@ class BaseLLMHTTPHandler: return video_remix_provider_config.transform_video_remix_response( raw_response=response, logging_obj=logging_obj, + custom_llm_provider=custom_llm_provider, ) except Exception as e: @@ -4565,6 +4598,7 @@ class BaseLLMHTTPHandler: return video_remix_provider_config.transform_video_remix_response( raw_response=response, logging_obj=logging_obj, + custom_llm_provider=custom_llm_provider, ) except Exception as e: @@ -4700,6 +4734,7 @@ class BaseLLMHTTPHandler: return video_list_provider_config.transform_video_list_response( raw_response=response, logging_obj=logging_obj, + custom_llm_provider=custom_llm_provider, ) except Exception as e: @@ -4855,17 +4890,29 @@ class BaseLLMHTTPHandler: "api_base": url, "headers": headers, "video_id": video_id, + "data": data, }, ) try: - response = sync_httpx_client.get( - url=url, - headers=headers, - ) + # Use POST if data is provided (e.g., Vertex AI fetchPredictOperation) + # Otherwise use GET (e.g., OpenAI video status) + if data: + response = sync_httpx_client.post( + url=url, + headers=headers, + json=data, + ) + else: + response = sync_httpx_client.get( + url=url, + headers=headers, + ) + return video_status_provider_config.transform_video_status_retrieve_response( raw_response=response, logging_obj=logging_obj, + custom_llm_provider=custom_llm_provider, ) except Exception as e: @@ -4929,17 +4976,28 @@ class BaseLLMHTTPHandler: "api_base": url, "headers": headers, "video_id": video_id, + "data": data, }, ) try: - response = await async_httpx_client.get( - url=url, - headers=headers, - ) + # Use POST if data is provided (e.g., Vertex AI fetchPredictOperation) + # Otherwise use GET (e.g., OpenAI video status) + if data: + response = await async_httpx_client.post( + url=url, + headers=headers, + json=data, + ) + else: + response = await async_httpx_client.get( + url=url, + headers=headers, + ) return video_status_provider_config.transform_video_status_retrieve_response( raw_response=response, logging_obj=logging_obj, + custom_llm_provider=custom_llm_provider, ) except Exception as e: diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index 9ee6cd25d20..ac3be0c3518 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -640,7 +640,7 @@ class DatabricksChatResponseIterator(BaseModelResponseIterator): for _tc in tool_calls: if _tc.get("function", {}).get("arguments") == "{}": _tc["function"]["arguments"] = "" # avoid invalid json - if isinstance(choice["delta"]["content"], list) and ( + if isinstance(choice["delta"].get("content"), list) and ( content := choice["delta"]["content"] ): if citations := content[0].get("citations"): diff --git a/litellm/llms/deepinfra/rerank/transformation.py b/litellm/llms/deepinfra/rerank/transformation.py index 69c7dabebd8..47f47418cb2 100644 --- a/litellm/llms/deepinfra/rerank/transformation.py +++ b/litellm/llms/deepinfra/rerank/transformation.py @@ -28,7 +28,12 @@ class DeepinfraRerankConfig(BaseRerankConfig): Deepinfra Rerank - Follows the same Spec as Cohere Rerank """ - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: """ Constructs the complete DeepInfra inference endpoint URL for rerank. @@ -63,6 +68,7 @@ class DeepinfraRerankConfig(BaseRerankConfig): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[dict] = None, ) -> dict: if api_key is None: api_key = get_secret_str("DEEPINFRA_API_KEY") diff --git a/litellm/llms/gemini/videos/__init__.py b/litellm/llms/gemini/videos/__init__.py new file mode 100644 index 00000000000..c5aed2db2d0 --- /dev/null +++ b/litellm/llms/gemini/videos/__init__.py @@ -0,0 +1,5 @@ +# Gemini Video Generation Support +from .transformation import GeminiVideoConfig + +__all__ = ["GeminiVideoConfig"] + diff --git a/litellm/llms/gemini/videos/transformation.py b/litellm/llms/gemini/videos/transformation.py new file mode 100644 index 00000000000..d1ae47af269 --- /dev/null +++ b/litellm/llms/gemini/videos/transformation.py @@ -0,0 +1,523 @@ +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union +import base64 + +import httpx +from httpx._types import RequestFiles + +from litellm.types.videos.main import VideoCreateOptionalRequestParams, VideoObject +from litellm.types.router import GenericLiteLLMParams +from litellm.secret_managers.main import get_secret_str +from litellm.types.videos.utils import ( + encode_video_id_with_provider, + extract_original_video_id, +) +from litellm.images.utils import ImageEditRequestUtils +import litellm +from litellm.types.llms.gemini import GeminiLongRunningOperationResponse, GeminiVideoGenerationInstance, GeminiVideoGenerationParameters, GeminiVideoGenerationRequest +from litellm.constants import DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + from ...base_llm.videos.transformation import BaseVideoConfig as _BaseVideoConfig + from ...base_llm.chat.transformation import BaseLLMException as _BaseLLMException + + LiteLLMLoggingObj = _LiteLLMLoggingObj + BaseVideoConfig = _BaseVideoConfig + BaseLLMException = _BaseLLMException +else: + LiteLLMLoggingObj = Any + BaseVideoConfig = Any + BaseLLMException = Any + + +def _convert_image_to_gemini_format(image_file) -> Dict[str, str]: + """ + Convert image file to Gemini format with base64 encoding and MIME type. + + Args: + image_file: File-like object opened in binary mode (e.g., open("path", "rb")) + + Returns: + Dict with bytesBase64Encoded and mimeType + """ + mime_type = ImageEditRequestUtils.get_image_content_type(image_file) + + if hasattr(image_file, 'seek'): + image_file.seek(0) + image_bytes = image_file.read() + base64_encoded = base64.b64encode(image_bytes).decode("utf-8") + + return { + "bytesBase64Encoded": base64_encoded, + "mimeType": mime_type + } + + +class GeminiVideoConfig(BaseVideoConfig): + """ + Configuration class for Gemini (Veo) video generation. + + Veo uses a long-running operation model: + 1. POST to :predictLongRunning returns operation name + 2. Poll operation until done=true + 3. Extract video URI from response + 4. Download video using file API + """ + + def __init__(self): + super().__init__() + + def get_supported_openai_params(self, model: str) -> list: + """ + Get the list of supported OpenAI parameters for Veo video generation. + Veo supports minimal parameters compared to OpenAI. + """ + return [ + "model", + "prompt", + "input_reference", + "seconds", + "size" + ] + + def map_openai_params( + self, + video_create_optional_params: VideoCreateOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict[str, Any]: + """ + Map OpenAI-style parameters to Veo format. + + Mappings: + - prompt → prompt + - input_reference → image + - size → aspectRatio (e.g., "1280x720" → "16:9") + - seconds → durationSeconds (defaults to 4 seconds if not provided) + + All other params are passed through as-is to support Gemini-specific parameters. + """ + mapped_params: Dict[str, Any] = {} + + # Get supported OpenAI params (exclude "model" and "prompt" which are handled separately) + supported_openai_params = self.get_supported_openai_params(model) + openai_params_to_map = { + param for param in supported_openai_params + if param not in {"model", "prompt"} + } + + # Map input_reference to image + if "input_reference" in video_create_optional_params: + mapped_params["image"] = video_create_optional_params["input_reference"] + + # Map size to aspectRatio + if "size" in video_create_optional_params: + size = video_create_optional_params["size"] + if size is not None: + aspect_ratio = self._convert_size_to_aspect_ratio(size) + if aspect_ratio: + mapped_params["aspectRatio"] = aspect_ratio + + # Map seconds to durationSeconds, default to 4 seconds (matching OpenAI) + if "seconds" in video_create_optional_params: + seconds = video_create_optional_params["seconds"] + try: + duration = int(seconds) if isinstance(seconds, str) else seconds + if duration is not None: + mapped_params["durationSeconds"] = duration + except (ValueError, TypeError): + # If conversion fails, use default + pass + + # Pass through any other params that weren't mapped (Gemini-specific params) + for key, value in video_create_optional_params.items(): + if key not in openai_params_to_map and key not in mapped_params: + mapped_params[key] = value + + return mapped_params + + def _convert_size_to_aspect_ratio(self, size: str) -> Optional[str]: + """ + Convert OpenAI size format to Veo aspectRatio format. + + https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-videos + + Supported aspect ratios: 9:16 (portrait), 16:9 (landscape) + """ + if not size: + return None + + aspect_ratio_map = { + "1280x720": "16:9", + "1920x1080": "16:9", + "720x1280": "9:16", + "1080x1920": "9:16", + } + + return aspect_ratio_map.get(size, "16:9") + + + def validate_environment( + self, + headers: dict, + model: str, + api_key: Optional[str] = None, + ) -> dict: + """ + Validate environment and add Gemini API key to headers. + Gemini uses x-goog-api-key header for authentication. + """ + api_key = ( + api_key + or litellm.api_key + or get_secret_str("GOOGLE_API_KEY") + or get_secret_str("GEMINI_API_KEY") + ) + + if not api_key: + raise ValueError( + "GEMINI_API_KEY or GOOGLE_API_KEY is required for Veo video generation. " + "Set it via environment variable or pass it as api_key parameter." + ) + + headers.update({ + "x-goog-api-key": api_key, + "Content-Type": "application/json", + }) + return headers + + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for Veo video generation. + For video creation: returns full URL with :predictLongRunning + For status/delete: returns base URL only + """ + if api_base is None: + api_base = get_secret_str("GEMINI_API_BASE") or "https://generativelanguage.googleapis.com" + + if not model or model == "": + return api_base.rstrip('/') + + model_name = model.replace("gemini/", "") + url = f"{api_base.rstrip('/')}/v1beta/models/{model_name}:predictLongRunning" + + return url + + def transform_video_create_request( + self, + model: str, + prompt: str, + api_base: str, + video_create_optional_request_params: Dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[Dict, RequestFiles, str]: + """ + Transform the video creation request for Veo API. + + Veo expects: + { + "instances": [ + { + "prompt": "A cat playing with a ball of yarn" + } + ], + "parameters": { + "aspectRatio": "16:9", + "durationSeconds": 8, + "resolution": "720p" + } + } + """ + instance = GeminiVideoGenerationInstance(prompt=prompt) + + params_copy = video_create_optional_request_params.copy() + + if "image" in params_copy and params_copy["image"] is not None: + image_data = _convert_image_to_gemini_format(params_copy["image"]) + params_copy["image"] = image_data + + parameters = GeminiVideoGenerationParameters(**params_copy) + + request_body_obj = GeminiVideoGenerationRequest( + instances=[instance], + parameters=parameters + ) + + request_data = request_body_obj.model_dump(exclude_none=True) + + return request_data, [], api_base + + def transform_video_create_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + request_data: Optional[Dict] = None, + ) -> VideoObject: + """ + Transform the Veo video creation response. + + Veo returns: + { + "name": "operations/generate_1234567890", + "metadata": {...}, + "done": false, + "error": {...} + } + + We return this as a VideoObject with: + - id: operation name (used for polling) + - status: "processing" + - usage: includes duration_seconds for cost calculation + """ + response_data = raw_response.json() + + # Parse response using Pydantic model for type safety + try: + operation_response = GeminiLongRunningOperationResponse(**response_data) + except Exception as e: + raise ValueError(f"Failed to parse operation response: {e}") + + operation_name = operation_response.name + if not operation_name: + raise ValueError(f"No operation name in Veo response: {response_data}") + + if custom_llm_provider: + video_id = encode_video_id_with_provider(operation_name, custom_llm_provider, model) + else: + video_id = operation_name + + video_obj = VideoObject( + id=video_id, + object="video", + status="processing", + model=model, + ) + + usage_data = {} + if request_data: + parameters = request_data.get("parameters", {}) + duration = parameters.get("durationSeconds") or DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS + if duration is not None: + try: + usage_data["duration_seconds"] = float(duration) + except (ValueError, TypeError): + pass + + video_obj.usage = usage_data + return video_obj + + def transform_video_status_retrieve_request( + self, + video_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """ + Transform the video status retrieve request for Veo API. + + Veo polls operations at: + GET https://generativelanguage.googleapis.com/v1beta/{operation_name} + """ + operation_name = extract_original_video_id(video_id) + url = f"{api_base.rstrip('/')}/v1beta/{operation_name}" + params: Dict[str, Any] = {} + + return url, params + + def transform_video_status_retrieve_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + ) -> VideoObject: + """ + Transform the Veo operation status response. + + Veo returns: + { + "name": "operations/generate_1234567890", + "done": false # or true when complete + } + + When done=true: + { + "name": "operations/generate_1234567890", + "done": true, + "response": { + "generateVideoResponse": { + "generatedSamples": [ + { + "video": { + "uri": "files/abc123..." + } + } + ] + } + } + } + """ + response_data = raw_response.json() + # Parse response using Pydantic model for type safety + operation_response = GeminiLongRunningOperationResponse(**response_data) + + operation_name = operation_response.name + is_done = operation_response.done + + if custom_llm_provider: + video_id = encode_video_id_with_provider(operation_name, custom_llm_provider, None) + else: + video_id = operation_name + + video_obj = VideoObject( + id=video_id, + object="video", + status="processing" if not is_done else "completed" + ) + return video_obj + + def transform_video_content_request( + self, + video_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """ + Transform the video content request for Veo API. + + For Veo, we need to: + 1. Get operation status to extract video URI + 2. Return download URL for the video + """ + operation_name = extract_original_video_id(video_id) + + status_url = f"{api_base.rstrip('/')}/v1beta/{operation_name}" + client = litellm.module_level_client + status_response = client.get(url=status_url, headers=headers) + status_response.raise_for_status() + response_data = status_response.json() + + operation_response = GeminiLongRunningOperationResponse(**response_data) + + if not operation_response.done: + raise ValueError( + "Video generation is not complete yet. " + "Please check status with video_status() before downloading." + ) + + if not operation_response.response: + raise ValueError("No response data in completed operation") + + generated_samples = operation_response.response.generateVideoResponse.generatedSamples + download_url = generated_samples[0].video.uri + + params: Dict[str, Any] = {} + + return download_url, params + + def transform_video_content_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> bytes: + """ + Transform the Veo video content download response. + Returns the video bytes directly. + """ + return raw_response.content + + def transform_video_remix_request( + self, + video_id: str, + prompt: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + extra_body: Optional[Dict[str, Any]] = None, + ) -> Tuple[str, Dict]: + """ + Video remix is not supported by Veo API. + """ + raise NotImplementedError( + "Video remix is not supported by Google Veo. " + "Please use video_generation() to create new videos." + ) + + def transform_video_remix_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + ) -> VideoObject: + """Video remix is not supported.""" + raise NotImplementedError("Video remix is not supported by Google Veo.") + + def transform_video_list_request( + self, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + after: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + extra_query: Optional[Dict[str, Any]] = None, + ) -> Tuple[str, Dict]: + """ + Video list is not supported by Veo API. + """ + raise NotImplementedError( + "Video list is not supported by Google Veo. " + "Use the operations endpoint directly if you need to list operations." + ) + + def transform_video_list_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + ) -> Dict[str, str]: + """Video list is not supported.""" + raise NotImplementedError("Video list is not supported by Google Veo.") + + def transform_video_delete_request( + self, + video_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """ + Video delete is not supported by Veo API. + """ + raise NotImplementedError( + "Video delete is not supported by Google Veo. " + "Videos are automatically cleaned up by Google." + ) + + def transform_video_delete_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoObject: + """Video delete is not supported.""" + raise NotImplementedError("Video delete is not supported by Google Veo.") + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + from ..common_utils import GeminiError + + return GeminiError( + status_code=status_code, + message=error_message, + headers=headers, + ) + diff --git a/litellm/llms/hosted_vllm/rerank/transformation.py b/litellm/llms/hosted_vllm/rerank/transformation.py index 2faef2c4c73..8316e923df3 100644 --- a/litellm/llms/hosted_vllm/rerank/transformation.py +++ b/litellm/llms/hosted_vllm/rerank/transformation.py @@ -37,7 +37,12 @@ class HostedVLLMRerankConfig(BaseRerankConfig): def __init__(self) -> None: pass - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: if api_base: # Remove trailing slashes and ensure clean base URL api_base = api_base.rstrip("/") @@ -91,6 +96,7 @@ class HostedVLLMRerankConfig(BaseRerankConfig): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[dict] = None, ) -> dict: if api_key is None: api_key = get_secret_str("HOSTED_VLLM_API_KEY") or "fake-api-key" @@ -150,7 +156,7 @@ class HostedVLLMRerankConfig(BaseRerankConfig): f"Error parsing response: {raw_response.text}, status_code={raw_response.status_code}" ) - return RerankResponse(**raw_response_json) + return self._transform_response(raw_response_json) def get_error_class( self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] diff --git a/litellm/llms/huggingface/rerank/transformation.py b/litellm/llms/huggingface/rerank/transformation.py index 1454328cc13..b386daf1c83 100644 --- a/litellm/llms/huggingface/rerank/transformation.py +++ b/litellm/llms/huggingface/rerank/transformation.py @@ -60,7 +60,12 @@ class HuggingFaceRerankConfig(BaseRerankConfig): else: return "https://api-inference.huggingface.co" - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: """ Get the complete URL for the API call, including the /rerank suffix if necessary. """ @@ -117,6 +122,7 @@ class HuggingFaceRerankConfig(BaseRerankConfig): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[dict] = None, api_base: Optional[str] = None, ) -> dict: # Get API credentials diff --git a/litellm/llms/infinity/rerank/transformation.py b/litellm/llms/infinity/rerank/transformation.py index 55aac6033d5..1c15de714b6 100644 --- a/litellm/llms/infinity/rerank/transformation.py +++ b/litellm/llms/infinity/rerank/transformation.py @@ -26,7 +26,12 @@ from ..common_utils import InfinityError class InfinityRerankConfig(CohereRerankConfig): - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: if api_base is None: raise ValueError("api_base is required for Infinity rerank") # Remove trailing slashes and ensure clean base URL @@ -40,6 +45,7 @@ class InfinityRerankConfig(CohereRerankConfig): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[dict] = None, ) -> dict: if api_key is None: api_key = ( diff --git a/litellm/llms/jina_ai/rerank/transformation.py b/litellm/llms/jina_ai/rerank/transformation.py index 3ba24680fd4..0fddd754a9c 100644 --- a/litellm/llms/jina_ai/rerank/transformation.py +++ b/litellm/llms/jina_ai/rerank/transformation.py @@ -55,7 +55,12 @@ class JinaAIRerankConfig(BaseRerankConfig): **optional_params, )) - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: base_path = "/v1/rerank" if api_base is None: @@ -122,7 +127,11 @@ class JinaAIRerankConfig(BaseRerankConfig): ) # Return response def validate_environment( - self, headers: Dict, model: str, api_key: Optional[str] = None + self, + headers: Dict, + model: str, + api_key: Optional[str] = None, + optional_params: Optional[dict] = None, ) -> Dict: if api_key is None: raise ValueError( diff --git a/litellm/llms/nvidia_nim/rerank/transformation.py b/litellm/llms/nvidia_nim/rerank/transformation.py index cb9fd4bebaa..5bbe16e5381 100644 --- a/litellm/llms/nvidia_nim/rerank/transformation.py +++ b/litellm/llms/nvidia_nim/rerank/transformation.py @@ -55,7 +55,12 @@ class NvidiaNimRerankConfig(BaseRerankConfig): def __init__(self) -> None: pass - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[dict] = None, + ) -> str: """ Construct the Nvidia NIM rerank URL. @@ -131,6 +136,7 @@ class NvidiaNimRerankConfig(BaseRerankConfig): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[dict] = None, ) -> dict: """ Validate that the Nvidia NIM API key is present. diff --git a/litellm/llms/openai/cost_calculation.py b/litellm/llms/openai/cost_calculation.py index 65a50224bb8..e5349db3af7 100644 --- a/litellm/llms/openai/cost_calculation.py +++ b/litellm/llms/openai/cost_calculation.py @@ -18,7 +18,9 @@ def cost_router(call_type: CallTypes) -> Literal["cost_per_token", "cost_per_sec return "cost_per_token" -def cost_per_token(model: str, usage: Usage, service_tier: Optional[str] = None) -> Tuple[float, float]: +def cost_per_token( + model: str, usage: Usage, service_tier: Optional[str] = None +) -> Tuple[float, float]: """ Calculates the cost per token for a given model, prompt tokens, and completion tokens. @@ -31,7 +33,10 @@ def cost_per_token(model: str, usage: Usage, service_tier: Optional[str] = None) """ ## CALCULATE INPUT COST return generic_cost_per_token( - model=model, usage=usage, custom_llm_provider="openai", service_tier=service_tier + model=model, + usage=usage, + custom_llm_provider="openai", + service_tier=service_tier, ) # ### Non-cached text tokens # non_cached_text_tokens = usage.prompt_tokens @@ -92,6 +97,7 @@ def cost_per_second( Returns: Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd """ + ## GET MODEL INFO model_info = get_model_info( model=model, custom_llm_provider=custom_llm_provider or "openai" @@ -123,18 +129,16 @@ def cost_per_second( def video_generation_cost( - model: str, - duration_seconds: float, - custom_llm_provider: Optional[str] = None + model: str, duration_seconds: float, custom_llm_provider: Optional[str] = None ) -> float: """ Calculates the cost for video generation based on duration in seconds. - + Input: - model: str, the model name without provider prefix - duration_seconds: float, the duration of the generated video in seconds - custom_llm_provider: str, the custom llm provider - + Returns: float - total_cost_in_usd """ @@ -142,7 +146,7 @@ def video_generation_cost( model_info = get_model_info( model=model, custom_llm_provider=custom_llm_provider or "openai" ) - + # Check for video-specific cost per second video_cost_per_second = model_info.get("output_cost_per_video_per_second") if video_cost_per_second is not None: @@ -150,7 +154,7 @@ def video_generation_cost( f"For model={model} - output_cost_per_video_per_second: {video_cost_per_second}; duration: {duration_seconds}" ) return video_cost_per_second * duration_seconds - + # Fallback to general output cost per second output_cost_per_second = model_info.get("output_cost_per_second") if output_cost_per_second is not None: @@ -158,7 +162,7 @@ def video_generation_cost( f"For model={model} - output_cost_per_second: {output_cost_per_second}; duration: {duration_seconds}" ) return output_cost_per_second * duration_seconds - + # If no cost information found, return 0 verbose_logger.warning( f"No cost information found for video model {model}. Please add pricing to model_prices_and_context_window.json" diff --git a/litellm/llms/openai/image_generation/dall_e_2_transformation.py b/litellm/llms/openai/image_generation/dall_e_2_transformation.py index 8e306a83375..22c2349a837 100644 --- a/litellm/llms/openai/image_generation/dall_e_2_transformation.py +++ b/litellm/llms/openai/image_generation/dall_e_2_transformation.py @@ -1,9 +1,16 @@ -from typing import List +from typing import TYPE_CHECKING, Any, List, Optional + +import httpx from litellm.llms.base_llm.image_generation.transformation import ( BaseImageGenerationConfig, ) from litellm.types.llms.openai import OpenAIImageGenerationOptionalParams +from litellm.types.utils import ImageResponse +from litellm.utils import convert_to_model_response_object + +if TYPE_CHECKING: + from litellm.litellm_core_utils.logging import Logging as LiteLLMLoggingObj class DallE2ImageGenerationConfig(BaseImageGenerationConfig): @@ -36,3 +43,45 @@ class DallE2ImageGenerationConfig(BaseImageGenerationConfig): ) return optional_params + + def transform_image_generation_response( + self, + model: str, + raw_response: httpx.Response, + model_response: ImageResponse, + logging_obj: "LiteLLMLoggingObj", + request_data: dict, + optional_params: dict, + litellm_params: dict, + encoding: Any, + api_key: Optional[str] = None, + json_mode: Optional[bool] = None, + ) -> ImageResponse: + response = raw_response.json() + + stringified_response = response + ## LOGGING + logging_obj.post_call( + input=request_data.get("prompt", ""), + api_key=api_key, + additional_args={"complete_input_dict": request_data}, + original_response=stringified_response, + ) + image_response: ImageResponse = convert_to_model_response_object( # type: ignore + response_object=stringified_response, + model_response_object=model_response, + response_type="image_generation", + ) + + # set optional params + image_response.size = optional_params.get( + "size", "1024x1024" + ) # default is always 1024x1024 + image_response.quality = optional_params.get( + "quality", "standard" + ) # always standard for dall-e-2 + image_response.output_format = optional_params.get( + "output_format", "png" + ) # always png for dall-e-2 + + return image_response diff --git a/litellm/llms/openai/image_generation/dall_e_3_transformation.py b/litellm/llms/openai/image_generation/dall_e_3_transformation.py index c4b0b66e112..9e2bdabc3a1 100644 --- a/litellm/llms/openai/image_generation/dall_e_3_transformation.py +++ b/litellm/llms/openai/image_generation/dall_e_3_transformation.py @@ -1,9 +1,16 @@ -from typing import List +from typing import TYPE_CHECKING, Any, List, Optional + +import httpx from litellm.llms.base_llm.image_generation.transformation import ( BaseImageGenerationConfig, ) from litellm.types.llms.openai import OpenAIImageGenerationOptionalParams +from litellm.types.utils import ImageResponse +from litellm.utils import convert_to_model_response_object + +if TYPE_CHECKING: + from litellm.litellm_core_utils.logging import Logging as LiteLLMLoggingObj class DallE3ImageGenerationConfig(BaseImageGenerationConfig): @@ -36,3 +43,45 @@ class DallE3ImageGenerationConfig(BaseImageGenerationConfig): ) return optional_params + + def transform_image_generation_response( + self, + model: str, + raw_response: httpx.Response, + model_response: ImageResponse, + logging_obj: "LiteLLMLoggingObj", + request_data: dict, + optional_params: dict, + litellm_params: dict, + encoding: Any, + api_key: Optional[str] = None, + json_mode: Optional[bool] = None, + ) -> ImageResponse: + response = raw_response.json() + + stringified_response = response + ## LOGGING + logging_obj.post_call( + input=request_data.get("prompt", ""), + api_key=api_key, + additional_args={"complete_input_dict": request_data}, + original_response=stringified_response, + ) + image_response: ImageResponse = convert_to_model_response_object( # type: ignore + response_object=stringified_response, + model_response_object=model_response, + response_type="image_generation", + ) + + # set optional params + image_response.size = optional_params.get( + "size", "1024x1024" + ) # default is always 1024x1024 + image_response.quality = optional_params.get( + "quality", "hd" + ) # always hd for dall-e-3 + image_response.output_format = optional_params.get( + "output_format", "png" + ) # always png for dall-e-3 + + return image_response diff --git a/litellm/llms/openai/image_generation/gpt_transformation.py b/litellm/llms/openai/image_generation/gpt_transformation.py index 1cee13784e7..c106d7f17b6 100644 --- a/litellm/llms/openai/image_generation/gpt_transformation.py +++ b/litellm/llms/openai/image_generation/gpt_transformation.py @@ -1,9 +1,16 @@ -from typing import List +from typing import TYPE_CHECKING, Any, List, Optional + +import httpx from litellm.llms.base_llm.image_generation.transformation import ( BaseImageGenerationConfig, ) from litellm.types.llms.openai import OpenAIImageGenerationOptionalParams +from litellm.types.utils import ImageResponse +from litellm.utils import convert_to_model_response_object + +if TYPE_CHECKING: + from litellm.litellm_core_utils.logging import Logging as LiteLLMLoggingObj class GPTImageGenerationConfig(BaseImageGenerationConfig): @@ -45,3 +52,45 @@ class GPTImageGenerationConfig(BaseImageGenerationConfig): ) return optional_params + + def transform_image_generation_response( + self, + model: str, + raw_response: httpx.Response, + model_response: ImageResponse, + logging_obj: "LiteLLMLoggingObj", + request_data: dict, + optional_params: dict, + litellm_params: dict, + encoding: Any, + api_key: Optional[str] = None, + json_mode: Optional[bool] = None, + ) -> ImageResponse: + response = raw_response.json() + + stringified_response = response + ## LOGGING + logging_obj.post_call( + input=request_data.get("prompt", ""), + api_key=api_key, + additional_args={"complete_input_dict": request_data}, + original_response=stringified_response, + ) + image_response: ImageResponse = convert_to_model_response_object( # type: ignore + response_object=stringified_response, + model_response_object=model_response, + response_type="image_generation", + ) + + # set optional params + image_response.size = optional_params.get( + "size", "1024x1024" + ) # default is always 1024x1024 + image_response.quality = optional_params.get( + "quality", "high" + ) # always hd for dall-e-3 + image_response.output_format = optional_params.get( + "response_format", "png" + ) # always png for dall-e-3 + + return image_response diff --git a/litellm/llms/openai/transcriptions/handler.py b/litellm/llms/openai/transcriptions/handler.py index 19b303bb968..4d60b8a8310 100644 --- a/litellm/llms/openai/transcriptions/handler.py +++ b/litellm/llms/openai/transcriptions/handler.py @@ -213,6 +213,7 @@ class OpenAIAudioTranscription(OpenAIChatCompletion): # Extract the actual model from data instead of hardcoding "whisper-1" actual_model = data.get("model", "whisper-1") hidden_params = {"model": actual_model, "custom_llm_provider": "openai"} + return convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, hidden_params=hidden_params, response_type="audio_transcription") # type: ignore except Exception as e: ## LOGGING diff --git a/litellm/llms/openai/videos/transformation.py b/litellm/llms/openai/videos/transformation.py index c573f3b59b0..9848477f32d 100644 --- a/litellm/llms/openai/videos/transformation.py +++ b/litellm/llms/openai/videos/transformation.py @@ -9,6 +9,7 @@ from litellm.types.llms.openai import CreateVideoRequest from litellm.types.router import GenericLiteLLMParams from litellm.secret_managers.main import get_secret_str from litellm.types.videos.main import VideoObject +from litellm.types.videos.utils import encode_video_id_with_provider, extract_original_video_id import litellm from litellm.llms.openai.image_edit.transformation import ImageEditRequestUtils if TYPE_CHECKING: @@ -94,17 +95,18 @@ class OpenAIVideoConfig(BaseVideoConfig): self, model: str, prompt: str, + api_base: str, video_create_optional_request_params: Dict, litellm_params: GenericLiteLLMParams, headers: dict, - ) -> Tuple[Dict, RequestFiles]: + ) -> Tuple[Dict, RequestFiles, str]: """ Transform the video creation request for OpenAI API. """ # Remove model and extra_headers from optional params as they're handled separately video_create_optional_request_params = { k: v for k, v in video_create_optional_request_params.items() - if k not in ["model", "extra_headers"] + if k not in ["model", "extra_headers", "prompt"] } # Create the request data @@ -129,26 +131,24 @@ class OpenAIVideoConfig(BaseVideoConfig): image=_input_reference, field_name="input_reference", ) - # Convert to dict for JSON serialization - return data_without_files, files_list + return data_without_files, files_list, api_base def transform_video_create_response( self, model: str, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + request_data: Optional[Dict] = None, ) -> VideoObject: - """ - Transform the OpenAI video creation response. - """ + """Transform the OpenAI video creation response.""" response_data = raw_response.json() - - # Transform the response data video_obj = VideoObject(**response_data) # type: ignore[arg-type] - # Create usage object with duration information for cost calculation - # Video generation API doesn't provide usage, so we create one with duration + if custom_llm_provider and video_obj.id: + video_obj.id = encode_video_id_with_provider(video_obj.id, custom_llm_provider, model) + usage_data = {} if video_obj: if hasattr(video_obj, 'seconds') and video_obj.seconds: @@ -156,9 +156,7 @@ class OpenAIVideoConfig(BaseVideoConfig): usage_data["duration_seconds"] = float(video_obj.seconds) except (ValueError, TypeError): pass - # Create the response video_obj.usage = usage_data - return video_obj @@ -175,11 +173,13 @@ class OpenAIVideoConfig(BaseVideoConfig): OpenAI API expects the following request: - GET /v1/videos/{video_id}/content """ + original_video_id = extract_original_video_id(video_id) + # Construct the URL for video content download - url = f"{api_base.rstrip('/')}/{video_id}/content" + url = f"{api_base.rstrip('/')}/{original_video_id}/content" # Add video_id as query parameter - params = {"video_id": video_id} + params = {"video_id": original_video_id} return url, params @@ -198,8 +198,10 @@ class OpenAIVideoConfig(BaseVideoConfig): OpenAI API expects the following request: - POST /v1/videos/{video_id}/remix """ + original_video_id = extract_original_video_id(video_id) + # Construct the URL for video remix - url = f"{api_base.rstrip('/')}/{video_id}/remix" + url = f"{api_base.rstrip('/')}/{original_video_id}/remix" # Prepare the request data data = {"prompt": prompt} @@ -215,17 +217,14 @@ class OpenAIVideoConfig(BaseVideoConfig): raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, ) -> bytes: - """ - Transform the OpenAI video content download response. - Returns raw video content as bytes. - """ - # For video content download, return the raw content as bytes + """Transform the OpenAI video content download response.""" return raw_response.content def transform_video_remix_response( self, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, ) -> VideoObject: """ Transform the OpenAI video remix response. @@ -235,6 +234,9 @@ class OpenAIVideoConfig(BaseVideoConfig): # Transform the response data video_obj = VideoObject(**response_data) # type: ignore[arg-type] + if custom_llm_provider and video_obj.id: + video_obj.id = encode_video_id_with_provider(video_obj.id, custom_llm_provider, None) + # Create usage object with duration information for cost calculation # Video remix API doesn't provide usage, so we create one with duration usage_data = {} @@ -287,8 +289,20 @@ class OpenAIVideoConfig(BaseVideoConfig): self, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, ) -> Dict[str,str]: - return raw_response.json() + response_data = raw_response.json() + + if custom_llm_provider and "data" in response_data: + for video_obj in response_data.get("data", []): + if isinstance(video_obj, dict) and "id" in video_obj: + video_obj["id"] = encode_video_id_with_provider( + video_obj["id"], + custom_llm_provider, + video_obj.get("model") + ) + + return response_data def transform_video_delete_request( self, @@ -303,8 +317,10 @@ class OpenAIVideoConfig(BaseVideoConfig): OpenAI API expects the following request: - DELETE /v1/videos/{video_id} """ + original_video_id = extract_original_video_id(video_id) + # Construct the URL for video delete - url = f"{api_base.rstrip('/')}/{video_id}" + url = f"{api_base.rstrip('/')}/{original_video_id}" # No data needed for DELETE request data: Dict[str, Any] = {} @@ -336,8 +352,11 @@ class OpenAIVideoConfig(BaseVideoConfig): """ Transform the OpenAI video retrieve request. """ + # Extract the original video_id (remove provider encoding if present) + original_video_id = extract_original_video_id(video_id) + # For video retrieve, we just need to construct the URL - url = f"{api_base.rstrip('/')}/{video_id}" + url = f"{api_base.rstrip('/')}/{original_video_id}" # No additional data needed for GET request data: Dict[str, Any] = {} @@ -348,6 +367,7 @@ class OpenAIVideoConfig(BaseVideoConfig): self, raw_response: httpx.Response, logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, ) -> VideoObject: """ Transform the OpenAI video retrieve response. @@ -355,6 +375,9 @@ class OpenAIVideoConfig(BaseVideoConfig): response_data = raw_response.json() # Transform the response data video_obj = VideoObject(**response_data) # type: ignore[arg-type] + + if custom_llm_provider and video_obj.id: + video_obj.id = encode_video_id_with_provider(video_obj.id, custom_llm_provider, None) return video_obj diff --git a/litellm/llms/perplexity/chat/__init__.py b/litellm/llms/perplexity/chat/__init__.py deleted file mode 100644 index f4f9edf38e5..00000000000 --- a/litellm/llms/perplexity/chat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Perplexity chat completion transformations.""" diff --git a/litellm/llms/perplexity/chat/transformation.py b/litellm/llms/perplexity/chat/transformation.py index 831b009de7a..27e6415ff8b 100644 --- a/litellm/llms/perplexity/chat/transformation.py +++ b/litellm/llms/perplexity/chat/transformation.py @@ -1,32 +1,25 @@ -"""Translate from OpenAI's `/v1/chat/completions` to Perplexity's `/v1/chat/completions`.""" +""" +Translate from OpenAI's `/v1/chat/completions` to Perplexity's `/v1/chat/completions` +""" -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, List, Optional, Tuple +from typing import Any, List, Optional, Tuple +import httpx import litellm from litellm._logging import verbose_logger -from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig from litellm.secret_managers.main import get_secret_str -from litellm.types.utils import ModelResponse, PromptTokensDetailsWrapper, Usage - -if TYPE_CHECKING: - import httpx - - from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj - from litellm.types.llms.openai import ( - AllMessageValues, - ChatCompletionAnnotation, - ChatCompletionAnnotationURLCitation, - ) +from litellm.types.llms.openai import AllMessageValues +from litellm.types.utils import Usage, PromptTokensDetailsWrapper +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig +from litellm.types.utils import ModelResponse +from litellm.types.llms.openai import ChatCompletionAnnotation +from litellm.types.llms.openai import ChatCompletionAnnotationURLCitation class PerplexityChatConfig(OpenAIGPTConfig): - """Configuration for Perplexity chat completions.""" - @property - def custom_llm_provider(self) -> str | None: - """Return the custom LLM provider name.""" + def custom_llm_provider(self) -> Optional[str]: return "perplexity" def _get_openai_compatible_provider_info( @@ -40,38 +33,6 @@ class PerplexityChatConfig(OpenAIGPTConfig): ) return api_base, dynamic_api_key - def validate_environment( - self, - headers: dict, - model: str, - messages: list, - optional_params: dict, - litellm_params: dict, - api_key: Optional[str] = None, - api_base: Optional[str] = None, - ) -> dict: - """Validate Perplexity environment and set headers.""" - # Get API key from environment if not provided - if api_key is None: - _, api_key = self._get_openai_compatible_provider_info( - api_base=api_base, api_key=api_key - ) - - # Validate API key is present - if api_key is None: - raise ValueError( - "The api_key client option must be set either by passing api_key to the client or by setting the PERPLEXITY_API_KEY environment variable" - ) - - # Set authorization header - headers["Authorization"] = f"Bearer {api_key}" - - # Ensure Content-Type is set to application/json - if "content-type" not in headers and "Content-Type" not in headers: - headers["Content-Type"] = "application/json" - - return headers - def get_supported_openai_params(self, model: str) -> list: """ Perplexity supports a subset of OpenAI params @@ -111,8 +72,7 @@ class PerplexityChatConfig(OpenAIGPTConfig): return base_openai_params - - def transform_response( # noqa: PLR0913 + def transform_response( self, model: str, raw_response: httpx.Response, @@ -122,11 +82,10 @@ class PerplexityChatConfig(OpenAIGPTConfig): messages: List[AllMessageValues], optional_params: dict, litellm_params: dict, - encoding: Any, + encoding: Any, api_key: Optional[str] = None, - json_mode: Optional[bool] = None, + json_mode: Optional[bool] = None, ) -> ModelResponse: - """Transform Perplexity response to standard format.""" # Call the parent transform_response first to handle the standard transformation model_response = super().transform_response( model=model, @@ -145,29 +104,28 @@ class PerplexityChatConfig(OpenAIGPTConfig): # Extract and enhance usage with Perplexity-specific fields try: raw_response_json = raw_response.json() - self.add_cost_to_usage(model_response, raw_response_json) self._enhance_usage_with_perplexity_fields( - model_response, raw_response_json, + model_response, raw_response_json ) self._add_citations_as_annotations(model_response, raw_response_json) - except (ValueError, TypeError, KeyError) as e: + except Exception as e: verbose_logger.debug(f"Error extracting Perplexity-specific usage fields: {e}") return model_response - def _enhance_usage_with_perplexity_fields( - self, model_response: ModelResponse, raw_response_json: dict, + def _enhance_usage_with_perplexity_fields( + self, model_response: ModelResponse, raw_response_json: dict ) -> None: - """Extract citation tokens and search queries from Perplexity API response. - - Add them to the usage object using standard LiteLLM fields. + """ + Extract citation tokens and search queries from Perplexity API response + and add them to the usage object using standard LiteLLM fields. """ if not hasattr(model_response, "usage") or model_response.usage is None: # Create a usage object if it doesn't exist (when usage was None) model_response.usage = Usage( # type: ignore[attr-defined] prompt_tokens=0, completion_tokens=0, - total_tokens=0, + total_tokens=0 ) usage = model_response.usage # type: ignore[attr-defined] @@ -188,7 +146,7 @@ class PerplexityChatConfig(OpenAIGPTConfig): # Extract search queries count from usage or response metadata # Perplexity might include this in the usage object or as separate metadata perplexity_usage = raw_response_json.get("usage", {}) - + # Try to extract search queries from usage field first, then root level num_search_queries = perplexity_usage.get("num_search_queries") if num_search_queries is None: @@ -197,18 +155,18 @@ class PerplexityChatConfig(OpenAIGPTConfig): num_search_queries = perplexity_usage.get("search_queries") if num_search_queries is None: num_search_queries = raw_response_json.get("search_queries") - + # Create or update prompt_tokens_details to include web search requests and citation tokens if citation_tokens > 0 or ( num_search_queries is not None and num_search_queries > 0 ): if usage.prompt_tokens_details is None: usage.prompt_tokens_details = PromptTokensDetailsWrapper() - + # Store citation tokens count for cost calculation if citation_tokens > 0: - usage.citation_tokens = citation_tokens - + setattr(usage, "citation_tokens", citation_tokens) + # Store search queries count in the standard web_search_requests field if num_search_queries is not None and num_search_queries > 0: usage.prompt_tokens_details.web_search_requests = num_search_queries @@ -290,35 +248,4 @@ class PerplexityChatConfig(OpenAIGPTConfig): if citations: setattr(model_response, "citations", citations) if search_results: - setattr(model_response, "search_results", search_results) - - def add_cost_to_usage(self, model_response: ModelResponse, raw_response_json: dict) -> None: - """Add the cost to the usage object.""" - try: - usage_data = raw_response_json.get("usage") - if usage_data: - # Try different possible cost field locations - response_cost = None - - # Check if cost is directly in usage (flat structure) - if "total_cost" in usage_data: - response_cost = usage_data["total_cost"] - # Check if cost is nested (cost.total_cost structure) - elif "cost" in usage_data and isinstance(usage_data["cost"], dict): - response_cost = usage_data["cost"].get("total_cost") - # Check if cost is a simple value - elif "cost" in usage_data: - response_cost = usage_data["cost"] - - if response_cost is not None: - # Store cost in hidden params for the cost calculator to use - if not hasattr(model_response, "_hidden_params"): - model_response._hidden_params = {} - if "additional_headers" not in model_response._hidden_params: - model_response._hidden_params["additional_headers"] = {} - model_response._hidden_params["additional_headers"][ - "llm_provider-x-litellm-response-cost" - ] = float(response_cost) - except (ValueError, TypeError, KeyError) as e: - verbose_logger.debug(f"Error adding cost to usage: {e}") - # If we can't extract cost, continue without it - don't fail the response + setattr(model_response, "search_results", search_results) \ No newline at end of file diff --git a/litellm/llms/vertex_ai/rerank/transformation.py b/litellm/llms/vertex_ai/rerank/transformation.py index 966368bc3cc..c3cdd2b0fb6 100644 --- a/litellm/llms/vertex_ai/rerank/transformation.py +++ b/litellm/llms/vertex_ai/rerank/transformation.py @@ -27,19 +27,40 @@ class VertexAIRerankConfig(BaseRerankConfig, VertexBase): def __init__(self) -> None: super().__init__() - def get_complete_url(self, api_base: Optional[str], model: str) -> str: + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: Optional[Dict] = None, + ) -> str: """ Get the complete URL for the Vertex AI Discovery Engine ranking API """ - # Get project ID from environment or litellm config + # Try to get project ID from optional_params first (e.g., vertex_project parameter) + params = optional_params or {} + + # Get credentials to extract project ID if needed + vertex_credentials = self.get_vertex_ai_credentials(params.copy()) + vertex_project = self.get_vertex_ai_project(params.copy()) + + # Use _ensure_access_token to extract project_id from credentials + # This is the same method used in vertex embeddings + _, vertex_project = self._ensure_access_token( + credentials=vertex_credentials, + project_id=vertex_project, + custom_llm_provider="vertex_ai", + ) + + # Fallback to environment or litellm config project_id = ( - get_secret_str("VERTEXAI_PROJECT") + vertex_project + or get_secret_str("VERTEXAI_PROJECT") or litellm.vertex_project ) if not project_id: raise ValueError( - "Vertex AI project ID is required. Please set 'VERTEXAI_PROJECT' or 'litellm.vertex_project'" + "Vertex AI project ID is required. Please set 'VERTEXAI_PROJECT', 'litellm.vertex_project', or pass 'vertex_project' parameter" ) return f"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/global/rankingConfigs/default_ranking_config:rank" @@ -49,13 +70,15 @@ class VertexAIRerankConfig(BaseRerankConfig, VertexBase): headers: dict, model: str, api_key: Optional[str] = None, + optional_params: Optional[Dict] = None, ) -> dict: """ Validate and set up authentication for Vertex AI Discovery Engine API """ - # Get credentials and project info - vertex_credentials = self.get_vertex_ai_credentials({}) - vertex_project = self.get_vertex_ai_project({}) + # Get credentials and project info from optional_params (which contains vertex_credentials, etc.) + litellm_params = optional_params or {} + vertex_credentials = self.get_vertex_ai_credentials(litellm_params) + vertex_project = self.get_vertex_ai_project(litellm_params) # Get access token using the base class method access_token, project_id = self._ensure_access_token( @@ -218,10 +241,12 @@ class VertexAIRerankConfig(BaseRerankConfig, VertexBase): """ Map Cohere rerank params to Vertex AI format """ - return { + result = { "query": query, "documents": documents, "top_n": top_n, "return_documents": return_documents, } + result.update(non_default_params) + return result diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/main.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/main.py index 85b1a6bc0db..624e682ec59 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/main.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/main.py @@ -38,6 +38,7 @@ class PartnerModelPrefixes(str, Enum): CLAUDE_PREFIX = "claude" QWEN_PREFIX = "qwen" GPT_OSS_PREFIX = "openai/gpt-oss-" + MINIMAX_PREFIX = "minimaxai/" class VertexAIPartnerModels(VertexBase): @@ -62,6 +63,7 @@ class VertexAIPartnerModels(VertexBase): or model.startswith(PartnerModelPrefixes.CLAUDE_PREFIX) or model.startswith(PartnerModelPrefixes.QWEN_PREFIX) or model.startswith(PartnerModelPrefixes.GPT_OSS_PREFIX) + or model.startswith(PartnerModelPrefixes.MINIMAX_PREFIX) ): return True return False @@ -73,6 +75,7 @@ class VertexAIPartnerModels(VertexBase): PartnerModelPrefixes.DEEPSEEK_PREFIX, PartnerModelPrefixes.QWEN_PREFIX, PartnerModelPrefixes.GPT_OSS_PREFIX, + PartnerModelPrefixes.MINIMAX_PREFIX, ] if any(provider in model for provider in OPENAI_LIKE_VERTEX_PROVIDERS): return True diff --git a/litellm/llms/vertex_ai/videos/__init__.py b/litellm/llms/vertex_ai/videos/__init__.py new file mode 100644 index 00000000000..1dcdbdf4ded --- /dev/null +++ b/litellm/llms/vertex_ai/videos/__init__.py @@ -0,0 +1,10 @@ +""" +Vertex AI Video Generation Module + +This module provides support for Vertex AI's Veo video generation API. +""" + +from .transformation import VertexAIVideoConfig + +__all__ = ["VertexAIVideoConfig"] + diff --git a/litellm/llms/vertex_ai/videos/transformation.py b/litellm/llms/vertex_ai/videos/transformation.py new file mode 100644 index 00000000000..2b6d43dd708 --- /dev/null +++ b/litellm/llms/vertex_ai/videos/transformation.py @@ -0,0 +1,597 @@ +""" +Vertex AI Video Generation Transformation + +Handles transformation of requests/responses for Vertex AI's Veo video generation API. +Based on: https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo-video-generation +""" + +import base64 +import time +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union + +import httpx +from httpx._types import RequestFiles + +from litellm.llms.base_llm.videos.transformation import BaseVideoConfig +from litellm.llms.vertex_ai.common_utils import ( + _convert_vertex_datetime_to_openai_datetime, +) +from litellm.llms.vertex_ai.vertex_llm_base import VertexBase +from litellm.types.router import GenericLiteLLMParams +from litellm.types.videos.main import VideoCreateOptionalRequestParams, VideoObject +from litellm.types.videos.utils import ( + encode_video_id_with_provider, + extract_original_video_id, +) +from litellm.images.utils import ImageEditRequestUtils +from litellm.constants import DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + from litellm.llms.base_llm.chat.transformation import ( + BaseLLMException as _BaseLLMException, + ) + + LiteLLMLoggingObj = _LiteLLMLoggingObj + BaseLLMException = _BaseLLMException +else: + LiteLLMLoggingObj = Any + BaseLLMException = Any + + +def _convert_image_to_vertex_format(image_file) -> Dict[str, str]: + """ + Convert image file to Vertex AI format with base64 encoding and MIME type. + + Args: + image_file: File-like object opened in binary mode (e.g., open("path", "rb")) + + Returns: + Dict with bytesBase64Encoded and mimeType + """ + mime_type = ImageEditRequestUtils.get_image_content_type(image_file) + + if hasattr(image_file, "seek"): + image_file.seek(0) + image_bytes = image_file.read() + base64_encoded = base64.b64encode(image_bytes).decode("utf-8") + + return {"bytesBase64Encoded": base64_encoded, "mimeType": mime_type} + + +class VertexAIVideoConfig(BaseVideoConfig, VertexBase): + """ + Configuration class for Vertex AI (Veo) video generation. + + Veo uses a long-running operation model: + 1. POST to :predictLongRunning returns operation name + 2. Poll operation using :fetchPredictOperation until done=true + 3. Extract video data (base64) from response + """ + + def __init__(self): + BaseVideoConfig.__init__(self) + VertexBase.__init__(self) + + @staticmethod + def extract_model_from_operation_name(operation_name: str) -> Optional[str]: + """ + Extract the model name from a Vertex AI operation name. + + Args: + operation_name: Operation name in format: + projects/PROJECT/locations/LOCATION/publishers/google/models/MODEL/operations/OPERATION_ID + + Returns: + Model name (e.g., "veo-2.0-generate-001") or None if extraction fails + """ + parts = operation_name.split("/") + # Model is at index 7 in the operation name format + if len(parts) >= 8: + return parts[7] + return None + + def get_supported_openai_params(self, model: str) -> list: + """ + Get the list of supported OpenAI parameters for Veo video generation. + Veo supports minimal parameters compared to OpenAI. + """ + return ["model", "prompt", "input_reference", "seconds", "size"] + + def map_openai_params( + self, + video_create_optional_params: VideoCreateOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict[str, Any]: + """ + Map OpenAI-style parameters to Veo format. + + Mappings: + - prompt → prompt (in instances) + - input_reference → image (in instances) + - size → aspectRatio (e.g., "1280x720" → "16:9") + - seconds → durationSeconds (defaults to 4 seconds if not provided) + """ + mapped_params: Dict[str, Any] = {} + + # Map input_reference to image (will be processed in transform_video_create_request) + if "input_reference" in video_create_optional_params: + mapped_params["image"] = video_create_optional_params["input_reference"] + + # Map size to aspectRatio + if "size" in video_create_optional_params: + size = video_create_optional_params["size"] + if size is not None: + aspect_ratio = self._convert_size_to_aspect_ratio(size) + if aspect_ratio: + mapped_params["aspectRatio"] = aspect_ratio + + # Map seconds to durationSeconds, default to 4 seconds (matching OpenAI) + if "seconds" in video_create_optional_params: + seconds = video_create_optional_params["seconds"] + try: + duration = int(seconds) if isinstance(seconds, str) else seconds + if duration is not None: + mapped_params["durationSeconds"] = duration + except (ValueError, TypeError): + # If conversion fails, use default + pass + + return mapped_params + + def _convert_size_to_aspect_ratio(self, size: str) -> Optional[str]: + """ + Convert OpenAI size format to Veo aspectRatio format. + + Supported aspect ratios: 9:16 (portrait), 16:9 (landscape) + """ + if not size: + return None + + aspect_ratio_map = { + "1280x720": "16:9", + "1920x1080": "16:9", + "720x1280": "9:16", + "1080x1920": "9:16", + } + + return aspect_ratio_map.get(size, "16:9") + + def validate_environment( + self, + headers: Dict, + model: str, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + litellm_params: Optional[dict] = None, + **kwargs, + ) -> Dict: + """ + Validate environment and return headers for Vertex AI OCR. + + Vertex AI uses Bearer token authentication with access token from credentials. + """ + # Extract Vertex AI parameters using safe helpers from VertexBase + # Use safe_get_* methods that don't mutate litellm_params dict + litellm_params = litellm_params or {} + + vertex_project = VertexBase.safe_get_vertex_ai_project(litellm_params=litellm_params) + vertex_credentials = VertexBase.safe_get_vertex_ai_credentials(litellm_params=litellm_params) + + # Get access token from Vertex credentials + access_token, project_id = self.get_access_token( + credentials=vertex_credentials, + project_id=vertex_project, + ) + + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + **headers, + } + + return headers + + def get_complete_url( + self, + model: str, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for Veo video generation. + + Returns URL for :predictLongRunning endpoint: + https://LOCATION-aiplatform.googleapis.com/v1/projects/PROJECT/locations/LOCATION/publishers/google/models/MODEL:predictLongRunning + """ + vertex_project = VertexBase.safe_get_vertex_ai_project(litellm_params) + vertex_location = VertexBase.safe_get_vertex_ai_location(litellm_params) + + if not vertex_project: + raise ValueError( + "vertex_project is required for Vertex AI video generation. " + "Set it via environment variable VERTEXAI_PROJECT or pass as parameter." + ) + + # Default to us-central1 if no location specified + vertex_location = vertex_location or "us-central1" + + # Extract model name (remove vertex_ai/ prefix if present) + model_name = model.replace("vertex_ai/", "") + + # Construct the URL + if api_base: + base_url = api_base.rstrip("/") + else: + base_url = f"https://{vertex_location}-aiplatform.googleapis.com" + + url = f"{base_url}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/google/models/{model_name}" + + return url + + def transform_video_create_request( + self, + model: str, + prompt: str, + api_base: str, + video_create_optional_request_params: Dict, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[Dict, RequestFiles, str]: + """ + Transform the video creation request for Veo API. + + Veo expects: + { + "instances": [ + { + "prompt": "A cat playing with a ball of yarn", + "image": { + "bytesBase64Encoded": "...", + "mimeType": "image/jpeg" + } + } + ], + "parameters": { + "aspectRatio": "16:9", + "durationSeconds": 8 + } + } + """ + # Build instance with prompt + instance_dict: Dict[str, Any] = {"prompt": prompt} + params_copy = video_create_optional_request_params.copy() + + + # Check if user wants to provide full instance dict + if "instances" in params_copy and isinstance(params_copy["instances"], dict): + # Replace/merge with user-provided instance + instance_dict.update(params_copy["instances"]) + params_copy.pop("instances") + elif "image" in params_copy and params_copy["image"] is not None: + image_data = _convert_image_to_vertex_format(params_copy["image"]) + instance_dict["image"] = image_data + params_copy.pop("image") + + # Build request data directly (TypedDict doesn't have model_dump) + request_data: Dict[str, Any] = {"instances": [instance_dict]} + + # Only add parameters if there are any + if params_copy: + request_data["parameters"] = params_copy + + # Append :predictLongRunning endpoint to api_base + url = f"{api_base}:predictLongRunning" + + # No files needed - everything is in JSON + return request_data, [], url + + def transform_video_create_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + request_data: Optional[Dict] = None, + ) -> VideoObject: + """ + Transform the Veo video creation response. + + Veo returns: + { + "name": "projects/PROJECT_ID/locations/LOCATION/publishers/google/models/MODEL/operations/OPERATION_ID" + } + + We return this as a VideoObject with: + - id: operation name (used for polling) + - status: "processing" + - usage: includes duration_seconds for cost calculation + """ + response_data = raw_response.json() + + operation_name = response_data.get("name") + if not operation_name: + raise ValueError(f"No operation name in Veo response: {response_data}") + + if custom_llm_provider: + video_id = encode_video_id_with_provider( + operation_name, custom_llm_provider, model + ) + else: + video_id = operation_name + + + video_obj = VideoObject( + id=video_id, + object="video", + status="processing", + model=model + ) + + usage_data = {} + if request_data: + parameters = request_data.get("parameters", {}) + duration = parameters.get("durationSeconds") or DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS + if duration is not None: + try: + usage_data["duration_seconds"] = float(duration) + except (ValueError, TypeError): + pass + + video_obj.usage = usage_data + return video_obj + + def transform_video_status_retrieve_request( + self, + video_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """ + Transform the video status retrieve request for Veo API. + + Veo polls operations using :fetchPredictOperation endpoint with POST request. + """ + operation_name = extract_original_video_id(video_id) + model = self.extract_model_from_operation_name(operation_name) + + if not model: + raise ValueError( + f"Invalid operation name format: {operation_name}. " + "Expected format: projects/PROJECT/locations/LOCATION/publishers/google/models/MODEL/operations/OPERATION_ID" + ) + + # Construct the full URL including model ID + # URL format: https://LOCATION-aiplatform.googleapis.com/v1/projects/PROJECT/locations/LOCATION/publishers/google/models/MODEL:fetchPredictOperation + # Strip trailing slashes from api_base and append model + url = f"{api_base.rstrip('/')}/{model}:fetchPredictOperation" + + # Request body contains the operation name + params = {"operationName": operation_name} + + return url, params + + def transform_video_status_retrieve_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + ) -> VideoObject: + """ + Transform the Veo operation status response. + + Veo returns: + { + "name": "projects/.../operations/OPERATION_ID", + "done": false # or true when complete + } + + When done=true: + { + "name": "projects/.../operations/OPERATION_ID", + "done": true, + "response": { + "@type": "type.googleapis.com/cloud.ai.large_models.vision.GenerateVideoResponse", + "raiMediaFilteredCount": 0, + "videos": [ + { + "bytesBase64Encoded": "...", + "mimeType": "video/mp4" + } + ] + } + } + """ + response_data = raw_response.json() + + operation_name = response_data.get("name", "") + is_done = response_data.get("done", False) + error_data = response_data.get("error") + + # Extract model from operation name + model = self.extract_model_from_operation_name(operation_name) + + if custom_llm_provider: + video_id = encode_video_id_with_provider( + operation_name, custom_llm_provider, model + ) + else: + video_id = operation_name + + # Convert createTime to Unix timestamp + create_time_str = response_data.get("metadata", {}).get("createTime") + if create_time_str: + try: + created_at = _convert_vertex_datetime_to_openai_datetime( + create_time_str + ) + except Exception: + created_at = int(time.time()) + else: + created_at = int(time.time()) + + if error_data: + status = "failed" + elif is_done: + status = "completed" + else: + status = "processing" + + video_obj = VideoObject( + id=video_id, + object="video", + status=status, + model=model, + created_at=created_at, + error=error_data, + ) + return video_obj + + def transform_video_content_request( + self, + video_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """ + Transform the video content request for Veo API. + + For Veo, we need to: + 1. Poll the operation status to ensure it's complete + 2. Extract the base64 video data from the response + 3. Return it for decoding + + Since we need to make an HTTP call here, we'll use the same fetchPredictOperation + approach as status retrieval. + """ + return self.transform_video_status_retrieve_request(video_id, api_base, litellm_params, headers) + + def transform_video_content_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> bytes: + """ + Transform the Veo video content download response. + + Extracts the base64 encoded video from the response and decodes it to bytes. + """ + response_data = raw_response.json() + + if not response_data.get("done", False): + raise ValueError( + "Video generation is not complete yet. " + "Please check status with video_status() before downloading." + ) + + try: + video_response = response_data.get("response", {}) + videos = video_response.get("videos", []) + + if not videos or len(videos) == 0: + raise ValueError("No video data found in completed operation") + + # Get the first video + video_data = videos[0] + base64_encoded = video_data.get("bytesBase64Encoded") + + if not base64_encoded: + raise ValueError("No base64 encoded video data found") + + # Decode base64 to bytes + video_bytes = base64.b64decode(base64_encoded) + return video_bytes + + except (KeyError, IndexError) as e: + raise ValueError(f"Failed to extract video data: {e}") + + def transform_video_remix_request( + self, + video_id: str, + prompt: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + extra_body: Optional[Dict[str, Any]] = None, + ) -> Tuple[str, Dict]: + """ + Video remix is not supported by Veo API. + """ + raise NotImplementedError( + "Video remix is not supported by Vertex AI Veo. " + "Please use video_generation() to create new videos." + ) + + def transform_video_remix_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + ) -> VideoObject: + """Video remix is not supported.""" + raise NotImplementedError("Video remix is not supported by Vertex AI Veo.") + + def transform_video_list_request( + self, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + after: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + extra_query: Optional[Dict[str, Any]] = None, + ) -> Tuple[str, Dict]: + """ + Video list is not supported by Veo API. + """ + raise NotImplementedError( + "Video list is not supported by Vertex AI Veo. " + "Use the operations endpoint directly if you need to list operations." + ) + + def transform_video_list_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + custom_llm_provider: Optional[str] = None, + ) -> Dict[str, str]: + """Video list is not supported.""" + raise NotImplementedError("Video list is not supported by Vertex AI Veo.") + + def transform_video_delete_request( + self, + video_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + """ + Video delete is not supported by Veo API. + """ + raise NotImplementedError( + "Video delete is not supported by Vertex AI Veo. " + "Videos are automatically cleaned up by Google." + ) + + def transform_video_delete_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> VideoObject: + """Video delete is not supported.""" + raise NotImplementedError("Video delete is not supported by Vertex AI Veo.") + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + from litellm.llms.vertex_ai.common_utils import VertexAIError + + return VertexAIError( + status_code=status_code, + message=error_message, + headers=headers, + ) + diff --git a/litellm/llms/xai/responses/__init__.py b/litellm/llms/xai/responses/__init__.py deleted file mode 100644 index 9610a119787..00000000000 --- a/litellm/llms/xai/responses/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# XAI Responses API -from .transformation import XAIResponsesAPIConfig - -__all__ = ["XAIResponsesAPIConfig"] - diff --git a/litellm/llms/xai/responses/transformation.py b/litellm/llms/xai/responses/transformation.py index 5767177b7a6..bd422c8d81e 100644 --- a/litellm/llms/xai/responses/transformation.py +++ b/litellm/llms/xai/responses/transformation.py @@ -85,7 +85,7 @@ class XAIResponsesAPIConfig(OpenAIResponsesAPIConfig): # XAI supports code_interpreter but doesn't use the container field # Keep only the type field verbose_logger.debug( - f"XAI: Transforming code_interpreter tool, removing container field" + "XAI: Transforming code_interpreter tool, removing container field" ) transformed_tools.append({"type": "code_interpreter"}) else: diff --git a/litellm/main.py b/litellm/main.py index 9cae34d1678..2ad444a9a20 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -65,7 +65,10 @@ from litellm.constants import ( ) from litellm.exceptions import LiteLLMUnknownProvider from litellm.integrations.custom_logger import CustomLogger -from litellm.litellm_core_utils.audio_utils.utils import get_audio_file_for_health_check +from litellm.litellm_core_utils.audio_utils.utils import ( + calculate_request_duration, + get_audio_file_for_health_check, +) from litellm.litellm_core_utils.dd_tracing import tracer from litellm.litellm_core_utils.get_provider_specific_headers import ( ProviderSpecificHeaderUtils, @@ -2033,36 +2036,11 @@ def completion( # type: ignore # noqa: PLR0915 logging.post_call( input=messages, api_key=api_key, original_response=response ) - elif custom_llm_provider == "perplexity": - response = base_llm_http_handler.completion( - model=model, - messages=messages, - headers=headers, - model_response=model_response, - api_key=api_key, - api_base=api_base, - acompletion=acompletion, - logging_obj=logging, - optional_params=optional_params, - litellm_params=litellm_params, - shared_session=shared_session, - timeout=timeout, - client=client, - custom_llm_provider=custom_llm_provider, - encoding=encoding, - stream=stream, - provider_config=provider_config, - ) - - ## LOGGING - Call after response has been processed by transform_response - logging.post_call( - input=messages, api_key=api_key, original_response=response - ) - elif ( model in litellm.open_ai_chat_completion_models or custom_llm_provider == "custom_openai" or custom_llm_provider == "deepinfra" + or custom_llm_provider == "perplexity" or custom_llm_provider == "nvidia_nim" or custom_llm_provider == "cerebras" or custom_llm_provider == "baseten" @@ -5431,6 +5409,7 @@ async def atranscription(*args, **kwargs) -> TranscriptionResponse: model = args[0] if len(args) > 0 else kwargs["model"] ### PASS ARGS TO Image Generation ### kwargs["atranscription"] = True + file = kwargs.get("file", None) custom_llm_provider = None try: # Use a partial function to pass your keyword arguments @@ -5459,6 +5438,20 @@ async def atranscription(*args, **kwargs) -> TranscriptionResponse: raise ValueError( f"Invalid response from transcription provider, expected TranscriptionResponse, but got {type(response)}" ) + + # Calculate and add duration if response is missing it + if ( + response is not None + and not isinstance(response, Coroutine) + and file is not None + ): + # Check if response is missing duration + existing_duration = getattr(response, "duration", None) + if existing_duration is None: + calculated_duration = calculate_request_duration(file) + if calculated_duration is not None: + setattr(response, "duration", calculated_duration) + return response except Exception as e: custom_llm_provider = custom_llm_provider or "openai" @@ -5669,6 +5662,16 @@ def transcription( headers={}, provider_config=provider_config, ) + + # Calculate and add duration if response is missing it + if response is not None and not isinstance(response, Coroutine): + # Check if response is missing duration + existing_duration = getattr(response, "duration", None) + if existing_duration is None: + calculated_duration = calculate_request_duration(file) + if calculated_duration is not None: + setattr(response, "duration", calculated_duration) + if response is None: raise ValueError("Unmapped provider passed in. Unable to get the response.") return response @@ -6026,6 +6029,7 @@ async def ahealth_check( "audio_speech", "audio_transcription", "image_generation", + "video_generation", "batch", "rerank", "realtime", diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 49236741fff..f571dfb5243 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -3704,7 +3704,6 @@ "output_cost_per_token": 2.75e-05, "source": "https://azure.microsoft.com/en-us/blog/grok-4-is-now-available-in-azure-ai-foundry-unlock-frontier-intelligence-and-business-ready-capabilities/", "supports_function_calling": true, - "supports_reasoning": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_web_search": true @@ -3732,7 +3731,6 @@ "mode": "chat", "source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/announcing-the-grok-4-fast-models-from-xai-now-available-in-azure-ai-foundry/4456701", "supports_function_calling": true, - "supports_reasoning": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_web_search": true @@ -5755,6 +5753,16 @@ "output_vector_size": 1536, "supports_embedding_image_input": true }, + "cohere/embed-v4.0": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "cohere", + "max_input_tokens": 128000, + "max_tokens": 128000, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1536, + "supports_embedding_image_input": true + }, "cohere.rerank-v3-5:0": { "input_cost_per_query": 0.002, "input_cost_per_token": 0.0, @@ -10342,6 +10350,7 @@ "mode": "chat", "output_cost_per_audio_token": 1.2e-05, "output_cost_per_token": 2e-06, + "rpm": 100000, "source": "https://ai.google.dev/gemini-api/docs/pricing", "supported_endpoints": [ "/v1/chat/completions", @@ -10368,7 +10377,8 @@ "supports_tool_choice": true, "supports_url_context": true, "supports_vision": true, - "supports_web_search": true + "supports_web_search": true, + "tpm": 8000000 }, "gemini-2.5-flash-lite-preview-06-17": { "cache_read_input_token_cost": 2.5e-08, @@ -11607,7 +11617,7 @@ "tpm": 1000000 }, "gemini/gemini-2.5-flash": { - "cache_read_input_token_cost": 7.5e-08, + "cache_read_input_token_cost": 3e-08, "input_cost_per_audio_token": 1e-06, "input_cost_per_token": 3e-07, "litellm_provider": "gemini", @@ -12658,6 +12668,34 @@ "video" ] }, + "gemini/veo-3.1-fast-generate-preview": { + "litellm_provider": "gemini", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.15, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "gemini/veo-3.1-generate-preview": { + "litellm_provider": "gemini", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.40, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, "google_pse/search": { "input_cost_per_query": 0.005, "litellm_provider": "google_pse", @@ -12724,19 +12762,21 @@ "tool_use_system_prompt_tokens": 159 }, "global.anthropic.claude-haiku-4-5-20251001-v1:0": { - "cache_creation_input_token_cost": 1.25e-06, - "cache_read_input_token_cost": 1e-07, - "input_cost_per_token": 1e-06, + "cache_creation_input_token_cost": 1.375e-06, + "cache_read_input_token_cost": 1.1e-07, + "input_cost_per_token": 1.1e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 200000, "max_output_tokens": 8192, "max_tokens": 8192, "mode": "chat", - "output_cost_per_token": 5e-06, + "output_cost_per_token": 5.5e-06, + "source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock", "supports_assistant_prefill": true, "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, + "supports_reasoning": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, @@ -23096,6 +23136,18 @@ "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", "supports_tool_choice": true }, + "vertex_ai/minimaxai/minimax-m2-maas": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-minimax_models", + "max_input_tokens": 196608, + "max_output_tokens": 196608, + "max_tokens": 196608, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supports_function_calling": true, + "supports_tool_choice": true + }, "vertex_ai/mistral-medium-3": { "input_cost_per_token": 4e-07, "litellm_provider": "vertex_ai-mistral_models", @@ -23350,6 +23402,34 @@ "video" ] }, + "vertex_ai/veo-3.1-generate-preview": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.4, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "vertex_ai/veo-3.1-fast-generate-preview": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.15, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, "voyage/rerank-2": { "input_cost_per_query": 5e-08, "input_cost_per_token": 5e-08, diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index dca9af62c72..a9734233a61 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Iterable, List, Optional, Set, Union +from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast from litellm._logging import verbose_proxy_logger from litellm._uuid import uuid @@ -11,7 +11,12 @@ from litellm.proxy._types import ( UpdateMCPServerRequest, UserAPIKeyAuth, ) +from litellm.proxy.common_utils.encrypt_decrypt_utils import ( + _get_salt_key, + encrypt_value_helper, +) from litellm.proxy.utils import PrismaClient +from litellm.types.mcp import MCPCredentials def _prepare_mcp_server_data( @@ -35,6 +40,14 @@ def _prepare_mcp_server_data( if "alias" not in data_dict: data_dict["alias"] = getattr(data, "alias", None) + # Handle credentials serialization + credentials = data_dict.get("credentials") + if credentials is not None: + data_dict["credentials"] = encrypt_credentials( + credentials=credentials, encryption_key=_get_salt_key() + ) + data_dict["credentials"] = safe_dumps(data_dict["credentials"]) + # Handle static_headers serialization if data.static_headers is not None: data_dict["static_headers"] = safe_dumps(data.static_headers) @@ -52,6 +65,30 @@ def _prepare_mcp_server_data( return data_dict +def encrypt_credentials( + credentials: MCPCredentials, encryption_key: Optional[str] +) -> MCPCredentials: + auth_value = credentials.get("auth_value") + if auth_value is not None: + credentials["auth_value"] = encrypt_value_helper( + value=auth_value, + new_encryption_key=encryption_key, + ) + client_id = credentials.get("client_id") + if client_id is not None: + credentials["client_id"] = encrypt_value_helper( + value=client_id, + new_encryption_key=encryption_key, + ) + client_secret = credentials.get("client_secret") + if client_secret is not None: + credentials["client_secret"] = encrypt_value_helper( + value=client_secret, + new_encryption_key=encryption_key, + ) + return credentials + + async def get_all_mcp_servers( prisma_client: PrismaClient, ) -> List[LiteLLM_MCPServerTable]: @@ -303,3 +340,32 @@ async def update_mcp_server( ) return updated_mcp_server + + +async def rotate_mcp_server_credentials_master_key( + prisma_client: PrismaClient, touched_by: str, new_master_key: str +): + mcp_servers = await prisma_client.db.litellm_mcpservertable.find_many() + + for mcp_server in mcp_servers: + credentials = mcp_server.credentials + if not credentials: + continue + + credentials_copy = dict(credentials) + encrypted_credentials = encrypt_credentials( + credentials=cast(MCPCredentials, credentials_copy), + encryption_key=new_master_key, + ) + + from litellm.litellm_core_utils.safe_json_dumps import safe_dumps + + serialized_credentials = safe_dumps(encrypted_credentials) + + await prisma_client.db.litellm_mcpservertable.update( + where={"server_id": mcp_server.server_id}, + data={ + "credentials": serialized_credentials, + "updated_by": touched_by, + }, + ) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 49ff1effd19..5b1dc5933c3 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -38,6 +38,9 @@ from litellm.proxy._types import ( MCPTransportType, UserAPIKeyAuth, ) +from litellm.proxy.common_utils.encrypt_decrypt_utils import ( + decrypt_value_helper, +) from litellm.proxy.utils import ProxyLogging from litellm.types.mcp import MCPAuth, MCPStdioConfig from litellm.types.mcp_server.mcp_server_manager import MCPInfo, MCPServer @@ -400,6 +403,20 @@ class MCPServerManager: static_headers_dict = _deserialize_json_dict( getattr(mcp_server, "static_headers", None) ) + credentials_dict = _deserialize_json_dict( + getattr(mcp_server, "credentials", None) + ) + + encrypted_auth_value: Optional[str] = None + if credentials_dict: + encrypted_auth_value = credentials_dict.get("auth_value") + + auth_value: Optional[str] = None + if encrypted_auth_value: + auth_value = decrypt_value_helper( + value=encrypted_auth_value, + key="auth_value", + ) # Use alias for name if present, else server_name name_for_prefix = ( mcp_server.alias or mcp_server.server_name or mcp_server.server_id @@ -422,6 +439,7 @@ class MCPServerManager: url=mcp_server.url, transport=cast(MCPTransportType, mcp_server.transport), auth_type=cast(MCPAuthType, mcp_server.auth_type), + authentication_token=auth_value, mcp_info=mcp_info, extra_headers=getattr(mcp_server, "extra_headers", None), static_headers=static_headers_dict, diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1162-278deed893787c5d.js b/litellm/proxy/_experimental/out/_next/static/chunks/1162-278deed893787c5d.js deleted file mode 100644 index 42e084fc3b7..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/1162-278deed893787c5d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1162,4851],{88009:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"}}]},name:"appstore",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},37527:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M894 462c30.9 0 43.8-39.7 18.7-58L530.8 126.2a31.81 31.81 0 00-37.6 0L111.3 404c-25.1 18.2-12.2 58 18.8 58H192v374h-72c-4.4 0-8 3.6-8 8v52c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-52c0-4.4-3.6-8-8-8h-72V462h62zM512 196.7l271.1 197.2H240.9L512 196.7zM264 462h117v374H264V462zm189 0h117v374H453V462zm307 374H642V462h118v374z"}}]},name:"bank",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},9775:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm-600-80h56c4.4 0 8-3.6 8-8V560c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v144c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V384c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v320c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V462c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v242c0 4.4 3.6 8 8 8zm152 0h56c4.4 0 8-3.6 8-8V304c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v400c0 4.4 3.6 8 8 8z"}}]},name:"bar-chart",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},11429:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M766.4 744.3c43.7 0 79.4-36.2 79.4-80.5 0-53.5-79.4-140.8-79.4-140.8S687 610.3 687 663.8c0 44.3 35.7 80.5 79.4 80.5zm-377.1-44.1c7.1 7.1 18.6 7.1 25.6 0l256.1-256c7.1-7.1 7.1-18.6 0-25.6l-256-256c-.6-.6-1.3-1.2-2-1.7l-78.2-78.2a9.11 9.11 0 00-12.8 0l-48 48a9.11 9.11 0 000 12.8l67.2 67.2-207.8 207.9c-7.1 7.1-7.1 18.6 0 25.6l255.9 256zm12.9-448.6l178.9 178.9H223.4l178.8-178.9zM904 816H120c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8z"}}]},name:"bg-colors",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},68208:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M856 376H648V168c0-8.8-7.2-16-16-16H168c-8.8 0-16 7.2-16 16v464c0 8.8 7.2 16 16 16h208v208c0 8.8 7.2 16 16 16h464c8.8 0 16-7.2 16-16V392c0-8.8-7.2-16-16-16zm-480 16v188H220V220h360v156H392c-8.8 0-16 7.2-16 16zm204 52v136H444V444h136zm224 360H444V648h188c8.8 0 16-7.2 16-16V444h156v360z"}}]},name:"block",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},83669:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"check-circle",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},62670:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm47.7-395.2l-25.4-5.9V348.6c38 5.2 61.5 29 65.5 58.2.5 4 3.9 6.9 7.9 6.9h44.9c4.7 0 8.4-4.1 8-8.8-6.1-62.3-57.4-102.3-125.9-109.2V263c0-4.4-3.6-8-8-8h-28.1c-4.4 0-8 3.6-8 8v33c-70.8 6.9-126.2 46-126.2 119 0 67.6 49.8 100.2 102.1 112.7l24.7 6.3v142.7c-44.2-5.9-69-29.5-74.1-61.3-.6-3.8-4-6.6-7.9-6.6H363c-4.7 0-8.4 4-8 8.7 4.5 55 46.2 105.6 135.2 112.1V761c0 4.4 3.6 8 8 8h28.4c4.4 0 8-3.6 8-8.1l-.2-31.7c78.3-6.9 134.3-48.8 134.3-124-.1-69.4-44.2-100.4-109-116.4zm-68.6-16.2c-5.6-1.6-10.3-3.1-15-5-33.8-12.2-49.5-31.9-49.5-57.3 0-36.3 27.5-57 64.5-61.7v124zM534.3 677V543.3c3.1.9 5.9 1.6 8.8 2.2 47.3 14.4 63.2 34.4 63.2 65.1 0 39.1-29.4 62.6-72 66.4z"}}]},name:"dollar",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},29271:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}},{tag:"path",attrs:{d:"M464 688a48 48 0 1096 0 48 48 0 10-96 0zm24-112h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8z"}}]},name:"exclamation-circle",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},41169:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 472a40 40 0 1080 0 40 40 0 10-80 0zm367 352.9L696.3 352V178H768v-68H256v68h71.7v174L145 824.9c-2.8 7.4-4.3 15.2-4.3 23.1 0 35.3 28.7 64 64 64h614.6c7.9 0 15.7-1.5 23.1-4.3 33-12.7 49.4-49.8 36.6-82.8zM395.7 364.7V180h232.6v184.7L719.2 600c-20.7-5.3-42.1-8-63.9-8-61.2 0-119.2 21.5-165.3 60a188.78 188.78 0 01-121.3 43.9c-32.7 0-64.1-8.3-91.8-23.7l118.8-307.5zM210.5 844l41.7-107.8c35.7 18.1 75.4 27.8 116.6 27.8 61.2 0 119.2-21.5 165.3-60 33.9-28.2 76.3-43.9 121.3-43.9 35 0 68.4 9.5 97.6 27.1L813.5 844h-603z"}}]},name:"experiment",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},10798:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M880.1 154H143.9c-24.5 0-39.8 26.7-27.5 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48zM603.4 798H420.6V642h182.9v156zm9.6-236.6l-9.5 16.6h-183l-9.5-16.6L212.7 226h598.6L613 561.4z"}}]},name:"filter",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},48231:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M888 792H200V168c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v688c0 4.4 3.6 8 8 8h752c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM305.8 637.7c3.1 3.1 8.1 3.1 11.3 0l138.3-137.6L583 628.5c3.1 3.1 8.2 3.1 11.3 0l275.4-275.3c3.1-3.1 3.1-8.2 0-11.3l-39.6-39.6a8.03 8.03 0 00-11.3 0l-230 229.9L461.4 404a8.03 8.03 0 00-11.3 0L266.3 586.7a8.03 8.03 0 000 11.3l39.5 39.7z"}}]},name:"line-chart",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},62272:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M574 665.4a8.03 8.03 0 00-11.3 0L446.5 781.6c-53.8 53.8-144.6 59.5-204 0-59.5-59.5-53.8-150.2 0-204l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3l-39.8-39.8a8.03 8.03 0 00-11.3 0L191.4 526.5c-84.6 84.6-84.6 221.5 0 306s221.5 84.6 306 0l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3L574 665.4zm258.6-474c-84.6-84.6-221.5-84.6-306 0L410.3 307.6a8.03 8.03 0 000 11.3l39.7 39.7c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c53.8-53.8 144.6-59.5 204 0 59.5 59.5 53.8 150.2 0 204L665.3 562.6a8.03 8.03 0 000 11.3l39.8 39.8c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c84.5-84.6 84.5-221.5 0-306.1zM610.1 372.3a8.03 8.03 0 00-11.3 0L372.3 598.7a8.03 8.03 0 000 11.3l39.6 39.6c3.1 3.1 8.2 3.1 11.3 0l226.4-226.4c3.1-3.1 3.1-8.2 0-11.3l-39.5-39.6z"}}]},name:"link",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},28595:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}},{tag:"path",attrs:{d:"M719.4 499.1l-296.1-215A15.9 15.9 0 00398 297v430c0 13.1 14.8 20.5 25.3 12.9l296.1-215a15.9 15.9 0 000-25.8zm-257.6 134V390.9L628.5 512 461.8 633.1z"}}]},name:"play-circle",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},34419:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M696 480H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"}},{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}}]},name:"plus-circle",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},23907:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"defs",attrs:{},children:[{tag:"style",attrs:{}}]},{tag:"path",attrs:{d:"M931.4 498.9L94.9 79.5c-3.4-1.7-7.3-2.1-11-1.2a15.99 15.99 0 00-11.7 19.3l86.2 352.2c1.3 5.3 5.2 9.6 10.4 11.3l147.7 50.7-147.6 50.7c-5.2 1.8-9.1 6-10.3 11.3L72.2 926.5c-.9 3.7-.5 7.6 1.2 10.9 3.9 7.9 13.5 11.1 21.5 7.2l836.5-417c3.1-1.5 5.6-4.1 7.2-7.1 3.9-8 .7-17.6-7.2-21.6zM170.8 826.3l50.3-205.6 295.2-101.3c2.3-.8 4.2-2.6 5-5 1.4-4.2-.8-8.7-5-10.2L221.1 403 171 198.2l628 314.9-628.2 313.2z"}}]},name:"send",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},55322:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"}}]},name:"setting",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},41361:function(e,t,n){n.d(t,{Z:function(){return l}});var r=n(1119),o=n(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M824.2 699.9a301.55 301.55 0 00-86.4-60.4C783.1 602.8 812 546.8 812 484c0-110.8-92.4-201.7-203.2-200-109.1 1.7-197 90.6-197 200 0 62.8 29 118.8 74.2 155.5a300.95 300.95 0 00-86.4 60.4C345 754.6 314 826.8 312 903.8a8 8 0 008 8.2h56c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5A226.62 226.62 0 01612 684c60.9 0 118.2 23.7 161.3 66.8C814.5 792 838 846.3 840 904.3c.1 4.3 3.7 7.7 8 7.7h56a8 8 0 008-8.2c-2-77-33-149.2-87.8-203.9zM612 612c-34.2 0-66.4-13.3-90.5-37.5a126.86 126.86 0 01-37.5-91.8c.3-32.8 13.4-64.5 36.3-88 24-24.6 56.1-38.3 90.4-38.7 33.9-.3 66.8 12.9 91 36.6 24.8 24.3 38.4 56.8 38.4 91.4 0 34.2-13.3 66.3-37.5 90.5A127.3 127.3 0 01612 612zM361.5 510.4c-.9-8.7-1.4-17.5-1.4-26.4 0-15.9 1.5-31.4 4.3-46.5.7-3.6-1.2-7.3-4.5-8.8-13.6-6.1-26.1-14.5-36.9-25.1a127.54 127.54 0 01-38.7-95.4c.9-32.1 13.8-62.6 36.3-85.6 24.7-25.3 57.9-39.1 93.2-38.7 31.9.3 62.7 12.6 86 34.4 7.9 7.4 14.7 15.6 20.4 24.4 2 3.1 5.9 4.4 9.3 3.2 17.6-6.1 36.2-10.4 55.3-12.4 5.6-.6 8.8-6.6 6.3-11.6-32.5-64.3-98.9-108.7-175.7-109.9-110.9-1.7-203.3 89.2-203.3 199.9 0 62.8 28.9 118.8 74.2 155.5-31.8 14.7-61.1 35-86.5 60.4-54.8 54.7-85.8 126.9-87.8 204a8 8 0 008 8.2h56.1c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5 29.4-29.4 65.4-49.8 104.7-59.7 3.9-1 6.5-4.7 6-8.7z"}}]},name:"team",theme:"outlined"},i=n(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,r.Z)({},e,{ref:t,icon:a}))})},92414:function(e,t,n){n.d(t,{Z:function(){return b}});var r=n(5853),o=n(2265);n(42698),n(64016),n(8710);var a=n(33232),i=n(44140),l=n(58747);let c=e=>{var t=(0,r._T)(e,[]);return o.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},t),o.createElement("path",{d:"M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"}))};var s=n(4537),d=n(9528),u=n(33044);let m=e=>{var t=(0,r._T)(e,[]);return o.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",width:"100%",height:"100%",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"},t),o.createElement("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),o.createElement("line",{x1:"6",y1:"6",x2:"18",y2:"18"}))};var h=n(97324),f=n(1153),p=n(96398);let g=(0,f.fn)("MultiSelect"),b=o.forwardRef((e,t)=>{let{defaultValue:n,value:f,onValueChange:b,placeholder:v="Select...",placeholderSearch:k="Search",disabled:x=!1,icon:y,children:w,className:C}=e,E=(0,r._T)(e,["defaultValue","value","onValueChange","placeholder","placeholderSearch","disabled","icon","children","className"]),[S,M]=(0,i.Z)(n,f),{reactElementChildren:z,optionsAvailable:Z}=(0,o.useMemo)(()=>{let e=o.Children.toArray(w).filter(o.isValidElement);return{reactElementChildren:e,optionsAvailable:(0,p.n0)("",e)}},[w]),[O,j]=(0,o.useState)(""),N=(null!=S?S:[]).length>0,_=(0,o.useMemo)(()=>O?(0,p.n0)(O,z):Z,[O,z,Z]),I=()=>{j("")};return o.createElement(d.R,Object.assign({as:"div",ref:t,defaultValue:S,value:S,onChange:e=>{null==b||b(e),M(e)},disabled:x,className:(0,h.q)("w-full min-w-[10rem] relative text-tremor-default",C)},E,{multiple:!0}),e=>{let{value:t}=e;return o.createElement(o.Fragment,null,o.createElement(d.R.Button,{className:(0,h.q)("w-full outline-none text-left whitespace-nowrap truncate rounded-tremor-default focus:ring-2 transition duration-100 border pr-8 py-1.5","border-tremor-border shadow-tremor-input focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted","dark:border-dark-tremor-border dark:shadow-dark-tremor-input dark:focus:border-dark-tremor-brand-subtle dark:focus:ring-dark-tremor-brand-muted",y?"pl-11 -ml-0.5":"pl-3",(0,p.um)(t.length>0,x))},y&&o.createElement("span",{className:(0,h.q)("absolute inset-y-0 left-0 flex items-center ml-px pl-2.5")},o.createElement(y,{className:(0,h.q)(g("Icon"),"flex-none h-5 w-5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})),o.createElement("div",{className:"h-6 flex items-center"},t.length>0?o.createElement("div",{className:"flex flex-nowrap overflow-x-scroll [&::-webkit-scrollbar]:hidden [scrollbar-width:none] gap-x-1 mr-5 -ml-1.5 relative"},Z.filter(e=>t.includes(e.props.value)).map((e,n)=>{var r;return o.createElement("div",{key:n,className:(0,h.q)("max-w-[100px] lg:max-w-[200px] flex justify-center items-center pl-2 pr-1.5 py-1 font-medium","rounded-tremor-small","bg-tremor-background-muted dark:bg-dark-tremor-background-muted","bg-tremor-background-subtle dark:bg-dark-tremor-background-subtle","text-tremor-content-default dark:text-dark-tremor-content-default","text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis")},o.createElement("div",{className:"text-xs truncate "},null!==(r=e.props.children)&&void 0!==r?r:e.props.value),o.createElement("div",{onClick:n=>{n.preventDefault();let r=t.filter(t=>t!==e.props.value);null==b||b(r),M(r)}},o.createElement(m,{className:(0,h.q)(g("clearIconItem"),"cursor-pointer rounded-tremor-full w-3.5 h-3.5 ml-2","text-tremor-content-subtle hover:text-tremor-content","dark:text-dark-tremor-content-subtle dark:hover:text-tremor-content")})))})):o.createElement("span",null,v)),o.createElement("span",{className:(0,h.q)("absolute inset-y-0 right-0 flex items-center mr-2.5")},o.createElement(l.Z,{className:(0,h.q)(g("arrowDownIcon"),"flex-none h-5 w-5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")}))),N&&!x?o.createElement("button",{type:"button",className:(0,h.q)("absolute inset-y-0 right-0 flex items-center mr-8"),onClick:e=>{e.preventDefault(),M([]),null==b||b([])}},o.createElement(s.Z,{className:(0,h.q)(g("clearIconAllItems"),"flex-none h-4 w-4","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})):null,o.createElement(u.u,{className:"absolute z-10 w-full",enter:"transition ease duration-100 transform",enterFrom:"opacity-0 -translate-y-4",enterTo:"opacity-100 translate-y-0",leave:"transition ease duration-100 transform",leaveFrom:"opacity-100 translate-y-0",leaveTo:"opacity-0 -translate-y-4"},o.createElement(d.R.Options,{className:(0,h.q)("divide-y overflow-y-auto outline-none rounded-tremor-default max-h-[228px] left-0 border my-1","bg-tremor-background border-tremor-border divide-tremor-border shadow-tremor-dropdown","dark:bg-dark-tremor-background dark:border-dark-tremor-border dark:divide-dark-tremor-border dark:shadow-dark-tremor-dropdown")},o.createElement("div",{className:(0,h.q)("flex items-center w-full px-2.5","bg-tremor-background-muted","dark:bg-dark-tremor-background-muted")},o.createElement("span",null,o.createElement(c,{className:(0,h.q)("flex-none w-4 h-4 mr-2","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})),o.createElement("input",{name:"search",type:"input",autoComplete:"off",placeholder:k,className:(0,h.q)("w-full focus:outline-none focus:ring-none bg-transparent text-tremor-default py-2","text-tremor-content-emphasis","dark:text-dark-tremor-content-emphasis"),onKeyDown:e=>{"Space"===e.code&&""!==e.target.value&&e.stopPropagation()},onChange:e=>j(e.target.value),value:O})),o.createElement(a.Z.Provider,Object.assign({},{onBlur:{handleResetSearch:I}},{value:{selectedValue:t}}),_))))})});b.displayName="MultiSelect"},46030:function(e,t,n){n.d(t,{Z:function(){return d}});var r=n(5853);n(42698),n(64016),n(8710);var o=n(33232),a=n(2265),i=n(97324),l=n(1153),c=n(9528);let s=(0,l.fn)("MultiSelectItem"),d=a.forwardRef((e,t)=>{let{value:n,className:d,children:u}=e,m=(0,r._T)(e,["value","className","children"]),{selectedValue:h}=(0,a.useContext)(o.Z),f=(0,l.NZ)(n,h);return a.createElement(c.R.Option,Object.assign({className:(0,i.q)(s("root"),"flex justify-start items-center cursor-default text-tremor-default p-2.5","ui-active:bg-tremor-background-muted ui-active:text-tremor-content-strong ui-selected:text-tremor-content-strong text-tremor-content-emphasis","dark:ui-active:bg-dark-tremor-background-muted dark:ui-active:text-dark-tremor-content-strong dark:ui-selected:text-dark-tremor-content-strong dark:ui-selected:bg-dark-tremor-background-muted dark:text-dark-tremor-content-emphasis",d),ref:t,key:n,value:n},m),a.createElement("input",{type:"checkbox",className:(0,i.q)(s("checkbox"),"flex-none focus:ring-none focus:outline-none cursor-pointer mr-2.5","accent-tremor-brand","dark:accent-dark-tremor-brand"),checked:f,readOnly:!0}),a.createElement("span",{className:"whitespace-nowrap truncate"},null!=u?u:n))});d.displayName="MultiSelectItem"},16853:function(e,t,n){n.d(t,{Z:function(){return d}});var r=n(5853),o=n(96398),a=n(44140),i=n(2265),l=n(97324),c=n(1153);let s=(0,c.fn)("Textarea"),d=i.forwardRef((e,t)=>{let{value:n,defaultValue:d="",placeholder:u="Type...",error:m=!1,errorMessage:h,disabled:f=!1,className:p,onChange:g,onValueChange:b}=e,v=(0,r._T)(e,["value","defaultValue","placeholder","error","errorMessage","disabled","className","onChange","onValueChange"]),[k,x]=(0,a.Z)(d,n),y=(0,i.useRef)(null),w=(0,o.Uh)(k);return i.createElement(i.Fragment,null,i.createElement("textarea",Object.assign({ref:(0,c.lq)([y,t]),value:k,placeholder:u,disabled:f,className:(0,l.q)(s("Textarea"),"w-full flex items-center outline-none rounded-tremor-default px-3 py-2 text-tremor-default focus:ring-2 transition duration-100 border","shadow-tremor-input focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted","dark:shadow-dark-tremor-input focus:dark:border-dark-tremor-brand-subtle focus:dark:ring-dark-tremor-brand-muted",(0,o.um)(w,f,m),f?"placeholder:text-tremor-content-subtle dark:placeholder:text-dark-tremor-content-subtle":"placeholder:text-tremor-content dark:placeholder:text-dark-tremor-content",p),"data-testid":"text-area",onChange:e=>{null==g||g(e),x(e.target.value),null==b||b(e.target.value)}},v)),m&&h?i.createElement("p",{className:(0,l.q)(s("errorMessage"),"text-sm text-red-500 mt-1")},h):null)});d.displayName="Textarea"},67982:function(e,t,n){n.d(t,{Z:function(){return c}});var r=n(5853),o=n(97324),a=n(1153),i=n(2265);let l=(0,a.fn)("Divider"),c=i.forwardRef((e,t)=>{let{className:n,children:a}=e,c=(0,r._T)(e,["className","children"]);return i.createElement("div",Object.assign({ref:t,className:(0,o.q)(l("root"),"w-full mx-auto my-6 flex justify-between gap-3 items-center text-tremor-default","text-tremor-content","dark:text-dark-tremor-content",n)},c),a?i.createElement(i.Fragment,null,i.createElement("div",{className:(0,o.q)("w-full h-[1px] bg-tremor-border dark:bg-dark-tremor-border")}),i.createElement("div",{className:(0,o.q)("text-inherit whitespace-nowrap")},a),i.createElement("div",{className:(0,o.q)("w-full h-[1px] bg-tremor-border dark:bg-dark-tremor-border")})):i.createElement("div",{className:(0,o.q)("w-full h-[1px] bg-tremor-border dark:bg-dark-tremor-border")}))});c.displayName="Divider"},96889:function(e,t,n){n.d(t,{Z:function(){return s}});var r=n(5853),o=n(2265),a=n(26898),i=n(97324),l=n(1153);let c=(0,l.fn)("BarList"),s=o.forwardRef((e,t)=>{var n;let s;let{data:d=[],color:u,valueFormatter:m=l.Cj,showAnimation:h=!1,className:f}=e,p=(0,r._T)(e,["data","color","valueFormatter","showAnimation","className"]),g=(n=d.map(e=>e.value),s=-1/0,n.forEach(e=>{s=Math.max(s,e)}),n.map(e=>0===e?0:Math.max(e/s*100,1)));return o.createElement("div",Object.assign({ref:t,className:(0,i.q)(c("root"),"flex justify-between space-x-6",f)},p),o.createElement("div",{className:(0,i.q)(c("bars"),"relative w-full")},d.map((e,t)=>{var n,r,s;let m=e.icon;return o.createElement("div",{key:null!==(n=e.key)&&void 0!==n?n:e.name,className:(0,i.q)(c("bar"),"flex items-center rounded-tremor-small bg-opacity-30","h-9",e.color||u?(0,l.bM)(null!==(r=e.color)&&void 0!==r?r:u,a.K.background).bgColor:"bg-tremor-brand-subtle dark:bg-dark-tremor-brand-subtle dark:bg-opacity-30",t===d.length-1?"mb-0":"mb-2"),style:{width:"".concat(g[t],"%"),transition:h?"all 1s":""}},o.createElement("div",{className:(0,i.q)("absolute max-w-full flex left-2")},m?o.createElement(m,{className:(0,i.q)(c("barIcon"),"flex-none h-5 w-5 mr-2","text-tremor-content","dark:text-dark-tremor-content")}):null,e.href?o.createElement("a",{href:e.href,target:null!==(s=e.target)&&void 0!==s?s:"_blank",rel:"noreferrer",className:(0,i.q)(c("barLink"),"whitespace-nowrap hover:underline truncate text-tremor-default","text-tremor-content-emphasis","dark:text-dark-tremor-content-emphasis")},e.name):o.createElement("p",{className:(0,i.q)(c("barText"),"whitespace-nowrap truncate text-tremor-default","text-tremor-content-emphasis","dark:text-dark-tremor-content-emphasis")},e.name)))})),o.createElement("div",{className:"text-right min-w-min"},d.map((e,t)=>{var n;return o.createElement("div",{key:null!==(n=e.key)&&void 0!==n?n:e.name,className:(0,i.q)(c("labelWrapper"),"flex justify-end items-center","h-9",t===d.length-1?"mb-0":"mb-2")},o.createElement("p",{className:(0,i.q)(c("labelText"),"whitespace-nowrap truncate text-tremor-default","text-tremor-content-emphasis","dark:text-dark-tremor-content-emphasis")},m(e.value)))})))});s.displayName="BarList"},44851:function(e,t,n){n.d(t,{default:function(){return q}});var r=n(2265),o=n(77565),a=n(36760),i=n.n(a),l=n(83145),c=n(26365),s=n(41154),d=n(50506),u=n(32559),m=n(1119),h=n(6989),f=n(45287),p=n(11993),g=n(47970),b=n(95814),v=r.forwardRef(function(e,t){var n,o=e.prefixCls,a=e.forceRender,l=e.className,s=e.style,d=e.children,u=e.isActive,m=e.role,h=r.useState(u||a),f=(0,c.Z)(h,2),g=f[0],b=f[1];return(r.useEffect(function(){(a||u)&&b(!0)},[a,u]),g)?r.createElement("div",{ref:t,className:i()("".concat(o,"-content"),(n={},(0,p.Z)(n,"".concat(o,"-content-active"),u),(0,p.Z)(n,"".concat(o,"-content-inactive"),!u),n),l),style:s,role:m},r.createElement("div",{className:"".concat(o,"-content-box")},d)):null});v.displayName="PanelContent";var k=["showArrow","headerClass","isActive","onItemClick","forceRender","className","prefixCls","collapsible","accordion","panelKey","extra","header","expandIcon","openMotion","destroyInactivePanel","children"],x=r.forwardRef(function(e,t){var n,o,a=e.showArrow,l=e.headerClass,c=e.isActive,s=e.onItemClick,d=e.forceRender,u=e.className,f=e.prefixCls,x=e.collapsible,y=e.accordion,w=e.panelKey,C=e.extra,E=e.header,S=e.expandIcon,M=e.openMotion,z=e.destroyInactivePanel,Z=e.children,O=(0,h.Z)(e,k),j="disabled"===x,N="header"===x,_="icon"===x,I=function(){null==s||s(w)},R="function"==typeof S?S(e):r.createElement("i",{className:"arrow"});R&&(R=r.createElement("div",{className:"".concat(f,"-expand-icon"),onClick:["header","icon"].includes(x)?I:void 0},R));var P=i()((n={},(0,p.Z)(n,"".concat(f,"-item"),!0),(0,p.Z)(n,"".concat(f,"-item-active"),c),(0,p.Z)(n,"".concat(f,"-item-disabled"),j),n),u),L={className:i()(l,(o={},(0,p.Z)(o,"".concat(f,"-header"),!0),(0,p.Z)(o,"".concat(f,"-header-collapsible-only"),N),(0,p.Z)(o,"".concat(f,"-icon-collapsible-only"),_),o)),"aria-expanded":c,"aria-disabled":j,onKeyDown:function(e){("Enter"===e.key||e.keyCode===b.Z.ENTER||e.which===b.Z.ENTER)&&I()}};return N||_||(L.onClick=I,L.role=y?"tab":"button",L.tabIndex=j?-1:0),r.createElement("div",(0,m.Z)({},O,{ref:t,className:P}),r.createElement("div",L,(void 0===a||a)&&R,r.createElement("span",{className:"".concat(f,"-header-text"),onClick:"header"===x?I:void 0},E),null!=C&&"boolean"!=typeof C&&r.createElement("div",{className:"".concat(f,"-extra")},C)),r.createElement(g.ZP,(0,m.Z)({visible:c,leavedClassName:"".concat(f,"-content-hidden")},M,{forceRender:d,removeOnLeave:z}),function(e,t){var n=e.className,o=e.style;return r.createElement(v,{ref:t,prefixCls:f,className:n,style:o,isActive:c,forceRender:d,role:y?"tabpanel":void 0},Z)}))}),y=["children","label","key","collapsible","onItemClick","destroyInactivePanel"],w=function(e,t){var n=t.prefixCls,o=t.accordion,a=t.collapsible,i=t.destroyInactivePanel,l=t.onItemClick,c=t.activeKey,s=t.openMotion,d=t.expandIcon;return e.map(function(e,t){var u=e.children,f=e.label,p=e.key,g=e.collapsible,b=e.onItemClick,v=e.destroyInactivePanel,k=(0,h.Z)(e,y),w=String(null!=p?p:t),C=null!=g?g:a,E=!1;return E=o?c[0]===w:c.indexOf(w)>-1,r.createElement(x,(0,m.Z)({},k,{prefixCls:n,key:w,panelKey:w,isActive:E,accordion:o,openMotion:s,expandIcon:d,header:f,collapsible:C,onItemClick:function(e){"disabled"!==C&&(l(e),null==b||b(e))},destroyInactivePanel:null!=v?v:i}),u)})},C=function(e,t,n){if(!e)return null;var o=n.prefixCls,a=n.accordion,i=n.collapsible,l=n.destroyInactivePanel,c=n.onItemClick,s=n.activeKey,d=n.openMotion,u=n.expandIcon,m=e.key||String(t),h=e.props,f=h.header,p=h.headerClass,g=h.destroyInactivePanel,b=h.collapsible,v=h.onItemClick,k=!1;k=a?s[0]===m:s.indexOf(m)>-1;var x=null!=b?b:i,y={key:m,panelKey:m,header:f,headerClass:p,isActive:k,prefixCls:o,destroyInactivePanel:null!=g?g:l,openMotion:d,accordion:a,children:e.props.children,onItemClick:function(e){"disabled"!==x&&(c(e),null==v||v(e))},expandIcon:u,collapsible:x};return"string"==typeof e.type?e:(Object.keys(y).forEach(function(e){void 0===y[e]&&delete y[e]}),r.cloneElement(e,y))};function E(e){var t=e;if(!Array.isArray(t)){var n=(0,s.Z)(t);t="number"===n||"string"===n?[t]:[]}return t.map(function(e){return String(e)})}var S=Object.assign(r.forwardRef(function(e,t){var n,o=e.prefixCls,a=void 0===o?"rc-collapse":o,s=e.destroyInactivePanel,m=e.style,h=e.accordion,p=e.className,g=e.children,b=e.collapsible,v=e.openMotion,k=e.expandIcon,x=e.activeKey,y=e.defaultActiveKey,S=e.onChange,M=e.items,z=i()(a,p),Z=(0,d.Z)([],{value:x,onChange:function(e){return null==S?void 0:S(e)},defaultValue:y,postState:E}),O=(0,c.Z)(Z,2),j=O[0],N=O[1];(0,u.ZP)(!g,"[rc-collapse] `children` will be removed in next major version. Please use `items` instead.");var _=(n={prefixCls:a,accordion:h,openMotion:v,expandIcon:k,collapsible:b,destroyInactivePanel:void 0!==s&&s,onItemClick:function(e){return N(function(){return h?j[0]===e?[]:[e]:j.indexOf(e)>-1?j.filter(function(t){return t!==e}):[].concat((0,l.Z)(j),[e])})},activeKey:j},Array.isArray(M)?w(M,n):(0,f.Z)(g).map(function(e,t){return C(e,t,n)}));return r.createElement("div",{ref:t,className:z,style:m,role:h?"tablist":void 0},_)}),{Panel:x});S.Panel;var M=n(18694),z=n(68710),Z=n(19722),O=n(71744),j=n(33759);let N=r.forwardRef((e,t)=>{let{getPrefixCls:n}=r.useContext(O.E_),{prefixCls:o,className:a,showArrow:l=!0}=e,c=n("collapse",o),s=i()({["".concat(c,"-no-arrow")]:!l},a);return r.createElement(S.Panel,Object.assign({ref:t},e,{prefixCls:c,className:s}))});var _=n(352),I=n(12918),R=n(63074),P=n(80669),L=n(3104);let B=e=>{let{componentCls:t,contentBg:n,padding:r,headerBg:o,headerPadding:a,collapseHeaderPaddingSM:i,collapseHeaderPaddingLG:l,collapsePanelBorderRadius:c,lineWidth:s,lineType:d,colorBorder:u,colorText:m,colorTextHeading:h,colorTextDisabled:f,fontSizeLG:p,lineHeight:g,lineHeightLG:b,marginSM:v,paddingSM:k,paddingLG:x,paddingXS:y,motionDurationSlow:w,fontSizeIcon:C,contentPadding:E,fontHeight:S,fontHeightLG:M}=e,z="".concat((0,_.bf)(s)," ").concat(d," ").concat(u);return{[t]:Object.assign(Object.assign({},(0,I.Wf)(e)),{backgroundColor:o,border:z,borderBottom:0,borderRadius:c,"&-rtl":{direction:"rtl"},["& > ".concat(t,"-item")]:{borderBottom:z,"&:last-child":{["\n &,\n & > ".concat(t,"-header")]:{borderRadius:"0 0 ".concat((0,_.bf)(c)," ").concat((0,_.bf)(c))}},["> ".concat(t,"-header")]:{position:"relative",display:"flex",flexWrap:"nowrap",alignItems:"flex-start",padding:a,color:h,lineHeight:g,cursor:"pointer",transition:"all ".concat(w,", visibility 0s"),["> ".concat(t,"-header-text")]:{flex:"auto"},"&:focus":{outline:"none"},["".concat(t,"-expand-icon")]:{height:S,display:"flex",alignItems:"center",paddingInlineEnd:v},["".concat(t,"-arrow")]:Object.assign(Object.assign({},(0,I.Ro)()),{fontSize:C,svg:{transition:"transform ".concat(w)}}),["".concat(t,"-header-text")]:{marginInlineEnd:"auto"}},["".concat(t,"-icon-collapsible-only")]:{cursor:"unset",["".concat(t,"-expand-icon")]:{cursor:"pointer"}}},["".concat(t,"-content")]:{color:m,backgroundColor:n,borderTop:z,["& > ".concat(t,"-content-box")]:{padding:E},"&-hidden":{display:"none"}},"&-small":{["> ".concat(t,"-item")]:{["> ".concat(t,"-header")]:{padding:i,paddingInlineStart:y,["> ".concat(t,"-expand-icon")]:{marginInlineStart:e.calc(k).sub(y).equal()}},["> ".concat(t,"-content > ").concat(t,"-content-box")]:{padding:k}}},"&-large":{["> ".concat(t,"-item")]:{fontSize:p,lineHeight:b,["> ".concat(t,"-header")]:{padding:l,paddingInlineStart:r,["> ".concat(t,"-expand-icon")]:{height:M,marginInlineStart:e.calc(x).sub(r).equal()}},["> ".concat(t,"-content > ").concat(t,"-content-box")]:{padding:x}}},["".concat(t,"-item:last-child")]:{["> ".concat(t,"-content")]:{borderRadius:"0 0 ".concat((0,_.bf)(c)," ").concat((0,_.bf)(c))}},["& ".concat(t,"-item-disabled > ").concat(t,"-header")]:{"\n &,\n & > .arrow\n ":{color:f,cursor:"not-allowed"}},["&".concat(t,"-icon-position-end")]:{["& > ".concat(t,"-item")]:{["> ".concat(t,"-header")]:{["".concat(t,"-expand-icon")]:{order:1,paddingInlineEnd:0,paddingInlineStart:v}}}}})}},H=e=>{let{componentCls:t}=e,n="> ".concat(t,"-item > ").concat(t,"-header ").concat(t,"-arrow svg");return{["".concat(t,"-rtl")]:{[n]:{transform:"rotate(180deg)"}}}},V=e=>{let{componentCls:t,headerBg:n,paddingXXS:r,colorBorder:o}=e;return{["".concat(t,"-borderless")]:{backgroundColor:n,border:0,["> ".concat(t,"-item")]:{borderBottom:"1px solid ".concat(o)},["\n > ".concat(t,"-item:last-child,\n > ").concat(t,"-item:last-child ").concat(t,"-header\n ")]:{borderRadius:0},["> ".concat(t,"-item:last-child")]:{borderBottom:0},["> ".concat(t,"-item > ").concat(t,"-content")]:{backgroundColor:"transparent",borderTop:0},["> ".concat(t,"-item > ").concat(t,"-content > ").concat(t,"-content-box")]:{paddingTop:r}}}},A=e=>{let{componentCls:t,paddingSM:n}=e;return{["".concat(t,"-ghost")]:{backgroundColor:"transparent",border:0,["> ".concat(t,"-item")]:{borderBottom:0,["> ".concat(t,"-content")]:{backgroundColor:"transparent",border:0,["> ".concat(t,"-content-box")]:{paddingBlock:n}}}}}};var T=(0,P.I$)("Collapse",e=>{let t=(0,L.TS)(e,{collapseHeaderPaddingSM:"".concat((0,_.bf)(e.paddingXS)," ").concat((0,_.bf)(e.paddingSM)),collapseHeaderPaddingLG:"".concat((0,_.bf)(e.padding)," ").concat((0,_.bf)(e.paddingLG)),collapsePanelBorderRadius:e.borderRadiusLG});return[B(t),V(t),A(t),H(t),(0,R.Z)(t)]},e=>({headerPadding:"".concat(e.paddingSM,"px ").concat(e.padding,"px"),headerBg:e.colorFillAlter,contentPadding:"".concat(e.padding,"px 16px"),contentBg:e.colorBgContainer})),q=Object.assign(r.forwardRef((e,t)=>{let{getPrefixCls:n,direction:a,collapse:l}=r.useContext(O.E_),{prefixCls:c,className:s,rootClassName:d,style:u,bordered:m=!0,ghost:h,size:p,expandIconPosition:g="start",children:b,expandIcon:v}=e,k=(0,j.Z)(e=>{var t;return null!==(t=null!=p?p:e)&&void 0!==t?t:"middle"}),x=n("collapse",c),y=n(),[w,C,E]=T(x),N=r.useMemo(()=>"left"===g?"start":"right"===g?"end":g,[g]),_=i()("".concat(x,"-icon-position-").concat(N),{["".concat(x,"-borderless")]:!m,["".concat(x,"-rtl")]:"rtl"===a,["".concat(x,"-ghost")]:!!h,["".concat(x,"-").concat(k)]:"middle"!==k},null==l?void 0:l.className,s,d,C,E),I=Object.assign(Object.assign({},(0,z.Z)(y)),{motionAppear:!1,leavedClassName:"".concat(x,"-content-hidden")}),R=r.useMemo(()=>b?(0,f.Z)(b).map((e,t)=>{var n,r;if(null===(n=e.props)||void 0===n?void 0:n.disabled){let n=null!==(r=e.key)&&void 0!==r?r:String(t),{disabled:o,collapsible:a}=e.props,i=Object.assign(Object.assign({},(0,M.Z)(e.props,["disabled"])),{key:n,collapsible:null!=a?a:o?"disabled":void 0});return(0,Z.Tm)(e,i)}return e}):null,[b]);return w(r.createElement(S,Object.assign({ref:t,openMotion:I},(0,M.Z)(e,["rootClassName"]),{expandIcon:function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=v?v(e):r.createElement(o.Z,{rotate:e.isActive?90:void 0});return(0,Z.Tm)(t,()=>({className:i()(t.props.className,"".concat(x,"-arrow"))}))},prefixCls:x,className:_,style:Object.assign(Object.assign({},null==l?void 0:l.style),u)}),R))}),{Panel:N})},19226:function(e,t,n){n.d(t,{default:function(){return S}});var r=n(83145),o=n(2265),a=n(36760),i=n.n(a),l=n(18694),c=n(71744),s=n(80856),d=n(45287),u=n(92239),m=n(352),h=n(80669),f=e=>{let{componentCls:t,bodyBg:n,lightSiderBg:r,lightTriggerBg:o,lightTriggerColor:a}=e;return{["".concat(t,"-sider-light")]:{background:r,["".concat(t,"-sider-trigger")]:{color:a,background:o},["".concat(t,"-sider-zero-width-trigger")]:{color:a,background:o,border:"1px solid ".concat(n),borderInlineStart:0}}}};let p=e=>{let{antCls:t,componentCls:n,colorText:r,triggerColor:o,footerBg:a,triggerBg:i,headerHeight:l,headerPadding:c,headerColor:s,footerPadding:d,triggerHeight:u,zeroTriggerHeight:h,zeroTriggerWidth:p,motionDurationMid:g,motionDurationSlow:b,fontSize:v,borderRadius:k,bodyBg:x,headerBg:y,siderBg:w}=e;return{[n]:Object.assign(Object.assign({display:"flex",flex:"auto",flexDirection:"column",minHeight:0,background:x,"&, *":{boxSizing:"border-box"},["&".concat(n,"-has-sider")]:{flexDirection:"row",["> ".concat(n,", > ").concat(n,"-content")]:{width:0}},["".concat(n,"-header, &").concat(n,"-footer")]:{flex:"0 0 auto"},["".concat(n,"-sider")]:{position:"relative",minWidth:0,background:w,transition:"all ".concat(g,", background 0s"),"&-children":{height:"100%",marginTop:-.1,paddingTop:.1,["".concat(t,"-menu").concat(t,"-menu-inline-collapsed")]:{width:"auto"}},"&-has-trigger":{paddingBottom:u},"&-right":{order:1},"&-trigger":{position:"fixed",bottom:0,zIndex:1,height:u,color:o,lineHeight:(0,m.bf)(u),textAlign:"center",background:i,cursor:"pointer",transition:"all ".concat(g)},"&-zero-width":{"> *":{overflow:"hidden"},"&-trigger":{position:"absolute",top:l,insetInlineEnd:e.calc(p).mul(-1).equal(),zIndex:1,width:p,height:h,color:o,fontSize:e.fontSizeXL,display:"flex",alignItems:"center",justifyContent:"center",background:w,borderStartStartRadius:0,borderStartEndRadius:k,borderEndEndRadius:k,borderEndStartRadius:0,cursor:"pointer",transition:"background ".concat(b," ease"),"&::after":{position:"absolute",inset:0,background:"transparent",transition:"all ".concat(b),content:'""'},"&:hover::after":{background:"rgba(255, 255, 255, 0.2)"},"&-right":{insetInlineStart:e.calc(p).mul(-1).equal(),borderStartStartRadius:k,borderStartEndRadius:0,borderEndEndRadius:0,borderEndStartRadius:k}}}}},f(e)),{"&-rtl":{direction:"rtl"}}),["".concat(n,"-header")]:{height:l,padding:c,color:s,lineHeight:(0,m.bf)(l),background:y,["".concat(t,"-menu")]:{lineHeight:"inherit"}},["".concat(n,"-footer")]:{padding:d,color:r,fontSize:v,background:a},["".concat(n,"-content")]:{flex:"auto",minHeight:0}}};var g=(0,h.I$)("Layout",e=>[p(e)],e=>{let{colorBgLayout:t,controlHeight:n,controlHeightLG:r,colorText:o,controlHeightSM:a,marginXXS:i,colorTextLightSolid:l,colorBgContainer:c}=e,s=1.25*r;return{colorBgHeader:"#001529",colorBgBody:t,colorBgTrigger:"#002140",bodyBg:t,headerBg:"#001529",headerHeight:2*n,headerPadding:"0 ".concat(s,"px"),headerColor:o,footerPadding:"".concat(a,"px ").concat(s,"px"),footerBg:t,siderBg:"#001529",triggerHeight:r+2*i,triggerBg:"#002140",triggerColor:l,zeroTriggerWidth:r,zeroTriggerHeight:r,lightSiderBg:c,lightTriggerBg:c,lightTriggerColor:o}},{deprecatedTokens:[["colorBgBody","bodyBg"],["colorBgHeader","headerBg"],["colorBgTrigger","triggerBg"]]}),b=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&0>t.indexOf(r)&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var o=0,r=Object.getOwnPropertySymbols(e);ot.indexOf(r[o])&&Object.prototype.propertyIsEnumerable.call(e,r[o])&&(n[r[o]]=e[r[o]]);return n};function v(e){let{suffixCls:t,tagName:n,displayName:r}=e;return e=>o.forwardRef((r,a)=>o.createElement(e,Object.assign({ref:a,suffixCls:t,tagName:n},r)))}let k=o.forwardRef((e,t)=>{let{prefixCls:n,suffixCls:r,className:a,tagName:l}=e,s=b(e,["prefixCls","suffixCls","className","tagName"]),{getPrefixCls:d}=o.useContext(c.E_),u=d("layout",n),[m,h,f]=g(u),p=r?"".concat(u,"-").concat(r):u;return m(o.createElement(l,Object.assign({className:i()(n||p,a,h,f),ref:t},s)))}),x=o.forwardRef((e,t)=>{let{direction:n}=o.useContext(c.E_),[a,m]=o.useState([]),{prefixCls:h,className:f,rootClassName:p,children:v,hasSider:k,tagName:x,style:y}=e,w=b(e,["prefixCls","className","rootClassName","children","hasSider","tagName","style"]),C=(0,l.Z)(w,["suffixCls"]),{getPrefixCls:E,layout:S}=o.useContext(c.E_),M=E("layout",h),z="boolean"==typeof k?k:!!a.length||(0,d.Z)(v).some(e=>e.type===u.Z),[Z,O,j]=g(M),N=i()(M,{["".concat(M,"-has-sider")]:z,["".concat(M,"-rtl")]:"rtl"===n},null==S?void 0:S.className,f,p,O,j),_=o.useMemo(()=>({siderHook:{addSider:e=>{m(t=>[].concat((0,r.Z)(t),[e]))},removeSider:e=>{m(t=>t.filter(t=>t!==e))}}}),[]);return Z(o.createElement(s.V.Provider,{value:_},o.createElement(x,Object.assign({ref:t,className:N,style:Object.assign(Object.assign({},null==S?void 0:S.style),y)},C),v)))}),y=v({tagName:"div",displayName:"Layout"})(x),w=v({suffixCls:"header",tagName:"header",displayName:"Header"})(k),C=v({suffixCls:"footer",tagName:"footer",displayName:"Footer"})(k),E=v({suffixCls:"content",tagName:"main",displayName:"Content"})(k);y.Header=w,y.Footer=C,y.Content=E,y.Sider=u.Z,y._InternalSiderContext=u.D;var S=y},35631:function(e,t,n){n.d(t,{Z:function(){return N}});var r=n(83145),o=n(36760),a=n.n(o),i=n(2265),l=n(53253),c=n(6543),s=n(71744),d=n(91086),u=n(10295),m=n(28617),h=n(19097),f=n(87908),p=n(19722),g=n(54998);let b=i.createContext({});b.Consumer;var v=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&0>t.indexOf(r)&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var o=0,r=Object.getOwnPropertySymbols(e);ot.indexOf(r[o])&&Object.prototype.propertyIsEnumerable.call(e,r[o])&&(n[r[o]]=e[r[o]]);return n};let k=(0,i.forwardRef)((e,t)=>{let n;var{prefixCls:r,children:o,actions:l,extra:c,className:d,colStyle:u}=e,m=v(e,["prefixCls","children","actions","extra","className","colStyle"]);let{grid:h,itemLayout:f}=(0,i.useContext)(b),{getPrefixCls:k}=(0,i.useContext)(s.E_),x=k("list",r),y=l&&l.length>0&&i.createElement("ul",{className:"".concat(x,"-item-action"),key:"actions"},l.map((e,t)=>i.createElement("li",{key:"".concat(x,"-item-action-").concat(t)},e,t!==l.length-1&&i.createElement("em",{className:"".concat(x,"-item-action-split")})))),w=i.createElement(h?"div":"li",Object.assign({},m,h?{}:{ref:t},{className:a()("".concat(x,"-item"),{["".concat(x,"-item-no-flex")]:!("vertical"===f?!!c:(i.Children.forEach(o,e=>{"string"==typeof e&&(n=!0)}),!(n&&i.Children.count(o)>1)))},d)}),"vertical"===f&&c?[i.createElement("div",{className:"".concat(x,"-item-main"),key:"content"},o,y),i.createElement("div",{className:"".concat(x,"-item-extra"),key:"extra"},c)]:[o,y,(0,p.Tm)(c,{key:"extra"})]);return h?i.createElement(g.Z,{ref:t,flex:1,style:u},w):w});k.Meta=e=>{var{prefixCls:t,className:n,avatar:r,title:o,description:l}=e,c=v(e,["prefixCls","className","avatar","title","description"]);let{getPrefixCls:d}=(0,i.useContext)(s.E_),u=d("list",t),m=a()("".concat(u,"-item-meta"),n),h=i.createElement("div",{className:"".concat(u,"-item-meta-content")},o&&i.createElement("h4",{className:"".concat(u,"-item-meta-title")},o),l&&i.createElement("div",{className:"".concat(u,"-item-meta-description")},l));return i.createElement("div",Object.assign({},c,{className:m}),r&&i.createElement("div",{className:"".concat(u,"-item-meta-avatar")},r),(o||l)&&h)};var x=n(352),y=n(12918),w=n(80669),C=n(3104);let E=e=>{let{listBorderedCls:t,componentCls:n,paddingLG:r,margin:o,itemPaddingSM:a,itemPaddingLG:i,marginLG:l,borderRadiusLG:c}=e;return{["".concat(t)]:{border:"".concat((0,x.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorBorder),borderRadius:c,["".concat(n,"-header,").concat(n,"-footer,").concat(n,"-item")]:{paddingInline:r},["".concat(n,"-pagination")]:{margin:"".concat((0,x.bf)(o)," ").concat((0,x.bf)(l))}},["".concat(t).concat(n,"-sm")]:{["".concat(n,"-item,").concat(n,"-header,").concat(n,"-footer")]:{padding:a}},["".concat(t).concat(n,"-lg")]:{["".concat(n,"-item,").concat(n,"-header,").concat(n,"-footer")]:{padding:i}}}},S=e=>{let{componentCls:t,screenSM:n,screenMD:r,marginLG:o,marginSM:a,margin:i}=e;return{["@media screen and (max-width:".concat(r,"px)")]:{["".concat(t)]:{["".concat(t,"-item")]:{["".concat(t,"-item-action")]:{marginInlineStart:o}}},["".concat(t,"-vertical")]:{["".concat(t,"-item")]:{["".concat(t,"-item-extra")]:{marginInlineStart:o}}}},["@media screen and (max-width: ".concat(n,"px)")]:{["".concat(t)]:{["".concat(t,"-item")]:{flexWrap:"wrap",["".concat(t,"-action")]:{marginInlineStart:a}}},["".concat(t,"-vertical")]:{["".concat(t,"-item")]:{flexWrap:"wrap-reverse",["".concat(t,"-item-main")]:{minWidth:e.contentWidth},["".concat(t,"-item-extra")]:{margin:"auto auto ".concat((0,x.bf)(i))}}}}}},M=e=>{let{componentCls:t,antCls:n,controlHeight:r,minHeight:o,paddingSM:a,marginLG:i,padding:l,itemPadding:c,colorPrimary:s,itemPaddingSM:d,itemPaddingLG:u,paddingXS:m,margin:h,colorText:f,colorTextDescription:p,motionDurationSlow:g,lineWidth:b,headerBg:v,footerBg:k,emptyTextPadding:w,metaMarginBottom:C,avatarMarginRight:E,titleMarginBottom:S,descriptionFontSize:M}=e,z={};return["start","center","end"].forEach(e=>{z["&-align-".concat(e)]={textAlign:e}}),{["".concat(t)]:Object.assign(Object.assign({},(0,y.Wf)(e)),{position:"relative","*":{outline:"none"},["".concat(t,"-header")]:{background:v},["".concat(t,"-footer")]:{background:k},["".concat(t,"-header, ").concat(t,"-footer")]:{paddingBlock:a},["".concat(t,"-pagination")]:Object.assign(Object.assign({marginBlockStart:i},z),{["".concat(n,"-pagination-options")]:{textAlign:"start"}}),["".concat(t,"-spin")]:{minHeight:o,textAlign:"center"},["".concat(t,"-items")]:{margin:0,padding:0,listStyle:"none"},["".concat(t,"-item")]:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:c,color:f,["".concat(t,"-item-meta")]:{display:"flex",flex:1,alignItems:"flex-start",maxWidth:"100%",["".concat(t,"-item-meta-avatar")]:{marginInlineEnd:E},["".concat(t,"-item-meta-content")]:{flex:"1 0",width:0,color:f},["".concat(t,"-item-meta-title")]:{margin:"0 0 ".concat((0,x.bf)(e.marginXXS)," 0"),color:f,fontSize:e.fontSize,lineHeight:e.lineHeight,"> a":{color:f,transition:"all ".concat(g),"&:hover":{color:s}}},["".concat(t,"-item-meta-description")]:{color:p,fontSize:M,lineHeight:e.lineHeight}},["".concat(t,"-item-action")]:{flex:"0 0 auto",marginInlineStart:e.marginXXL,padding:0,fontSize:0,listStyle:"none","& > li":{position:"relative",display:"inline-block",padding:"0 ".concat((0,x.bf)(m)),color:p,fontSize:e.fontSize,lineHeight:e.lineHeight,textAlign:"center","&:first-child":{paddingInlineStart:0}},["".concat(t,"-item-action-split")]:{position:"absolute",insetBlockStart:"50%",insetInlineEnd:0,width:b,height:e.calc(e.fontHeight).sub(e.calc(e.marginXXS).mul(2)).equal(),transform:"translateY(-50%)",backgroundColor:e.colorSplit}}},["".concat(t,"-empty")]:{padding:"".concat((0,x.bf)(l)," 0"),color:p,fontSize:e.fontSizeSM,textAlign:"center"},["".concat(t,"-empty-text")]:{padding:w,color:e.colorTextDisabled,fontSize:e.fontSize,textAlign:"center"},["".concat(t,"-item-no-flex")]:{display:"block"}}),["".concat(t,"-grid ").concat(n,"-col > ").concat(t,"-item")]:{display:"block",maxWidth:"100%",marginBlockEnd:h,paddingBlock:0,borderBlockEnd:"none"},["".concat(t,"-vertical ").concat(t,"-item")]:{alignItems:"initial",["".concat(t,"-item-main")]:{display:"block",flex:1},["".concat(t,"-item-extra")]:{marginInlineStart:i},["".concat(t,"-item-meta")]:{marginBlockEnd:C,["".concat(t,"-item-meta-title")]:{marginBlockStart:0,marginBlockEnd:S,color:f,fontSize:e.fontSizeLG,lineHeight:e.lineHeightLG}},["".concat(t,"-item-action")]:{marginBlockStart:l,marginInlineStart:"auto","> li":{padding:"0 ".concat((0,x.bf)(l)),"&:first-child":{paddingInlineStart:0}}}},["".concat(t,"-split ").concat(t,"-item")]:{borderBlockEnd:"".concat((0,x.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit),"&:last-child":{borderBlockEnd:"none"}},["".concat(t,"-split ").concat(t,"-header")]:{borderBlockEnd:"".concat((0,x.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit)},["".concat(t,"-split").concat(t,"-empty ").concat(t,"-footer")]:{borderTop:"".concat((0,x.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit)},["".concat(t,"-loading ").concat(t,"-spin-nested-loading")]:{minHeight:r},["".concat(t,"-split").concat(t,"-something-after-last-item ").concat(n,"-spin-container > ").concat(t,"-items > ").concat(t,"-item:last-child")]:{borderBlockEnd:"".concat((0,x.bf)(e.lineWidth)," ").concat(e.lineType," ").concat(e.colorSplit)},["".concat(t,"-lg ").concat(t,"-item")]:{padding:u},["".concat(t,"-sm ").concat(t,"-item")]:{padding:d},["".concat(t,":not(").concat(t,"-vertical)")]:{["".concat(t,"-item-no-flex")]:{["".concat(t,"-item-action")]:{float:"right"}}}}};var z=(0,w.I$)("List",e=>{let t=(0,C.TS)(e,{listBorderedCls:"".concat(e.componentCls,"-bordered"),minHeight:e.controlHeightLG});return[M(t),E(t),S(t)]},e=>({contentWidth:220,itemPadding:"".concat((0,x.bf)(e.paddingContentVertical)," 0"),itemPaddingSM:"".concat((0,x.bf)(e.paddingContentVerticalSM)," ").concat((0,x.bf)(e.paddingContentHorizontal)),itemPaddingLG:"".concat((0,x.bf)(e.paddingContentVerticalLG)," ").concat((0,x.bf)(e.paddingContentHorizontalLG)),headerBg:"transparent",footerBg:"transparent",emptyTextPadding:e.padding,metaMarginBottom:e.padding,avatarMarginRight:e.padding,titleMarginBottom:e.paddingSM,descriptionFontSize:e.fontSize})),Z=n(33759),O=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&0>t.indexOf(r)&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols)for(var o=0,r=Object.getOwnPropertySymbols(e);ot.indexOf(r[o])&&Object.prototype.propertyIsEnumerable.call(e,r[o])&&(n[r[o]]=e[r[o]]);return n};function j(e){var t,{pagination:n=!1,prefixCls:o,bordered:p=!1,split:g=!0,className:v,rootClassName:k,style:x,children:y,itemLayout:w,loadMore:C,grid:E,dataSource:S=[],size:M,header:j,footer:N,loading:_=!1,rowKey:I,renderItem:R,locale:P}=e,L=O(e,["pagination","prefixCls","bordered","split","className","rootClassName","style","children","itemLayout","loadMore","grid","dataSource","size","header","footer","loading","rowKey","renderItem","locale"]);let B=n&&"object"==typeof n?n:{},[H,V]=i.useState(B.defaultCurrent||1),[A,T]=i.useState(B.defaultPageSize||10),{getPrefixCls:q,renderEmpty:W,direction:F,list:D}=i.useContext(s.E_),G=e=>(t,r)=>{var o;V(t),T(r),n&&n[e]&&(null===(o=null==n?void 0:n[e])||void 0===o||o.call(n,t,r))},K=G("onChange"),U=G("onShowSizeChange"),X=(e,t)=>{let n;return R?((n="function"==typeof I?I(e):I?e[I]:e.key)||(n="list-item-".concat(t)),i.createElement(i.Fragment,{key:n},R(e,t))):null},$=q("list",o),[Y,J,Q]=z($),ee=_;"boolean"==typeof ee&&(ee={spinning:ee});let et=ee&&ee.spinning,en=(0,Z.Z)(M),er="";switch(en){case"large":er="lg";break;case"small":er="sm"}let eo=a()($,{["".concat($,"-vertical")]:"vertical"===w,["".concat($,"-").concat(er)]:er,["".concat($,"-split")]:g,["".concat($,"-bordered")]:p,["".concat($,"-loading")]:et,["".concat($,"-grid")]:!!E,["".concat($,"-something-after-last-item")]:!!(C||n||N),["".concat($,"-rtl")]:"rtl"===F},null==D?void 0:D.className,v,k,J,Q),ea=(0,l.Z)({current:1,total:0},{total:S.length,current:H,pageSize:A},n||{}),ei=Math.ceil(ea.total/ea.pageSize);ea.current>ei&&(ea.current=ei);let el=n?i.createElement("div",{className:a()("".concat($,"-pagination"),"".concat($,"-pagination-align-").concat(null!==(t=null==ea?void 0:ea.align)&&void 0!==t?t:"end"))},i.createElement(h.Z,Object.assign({},ea,{onChange:K,onShowSizeChange:U}))):null,ec=(0,r.Z)(S);n&&S.length>(ea.current-1)*ea.pageSize&&(ec=(0,r.Z)(S).splice((ea.current-1)*ea.pageSize,ea.pageSize));let es=Object.keys(E||{}).some(e=>["xs","sm","md","lg","xl","xxl"].includes(e)),ed=(0,m.Z)(es),eu=i.useMemo(()=>{for(let e=0;e{if(!E)return;let e=eu&&E[eu]?E[eu]:E.column;if(e)return{width:"".concat(100/e,"%"),maxWidth:"".concat(100/e,"%")}},[null==E?void 0:E.column,eu]),eh=et&&i.createElement("div",{style:{minHeight:53}});if(ec.length>0){let e=ec.map((e,t)=>X(e,t));eh=E?i.createElement(u.Z,{gutter:E.gutter},i.Children.map(e,e=>i.createElement("div",{key:null==e?void 0:e.key,style:em},e))):i.createElement("ul",{className:"".concat($,"-items")},e)}else y||et||(eh=i.createElement("div",{className:"".concat($,"-empty-text")},P&&P.emptyText||(null==W?void 0:W("List"))||i.createElement(d.Z,{componentName:"List"})));let ef=ea.position||"bottom",ep=i.useMemo(()=>({grid:E,itemLayout:w}),[JSON.stringify(E),w]);return Y(i.createElement(b.Provider,{value:ep},i.createElement("div",Object.assign({style:Object.assign(Object.assign({},null==D?void 0:D.style),x),className:eo},L),("top"===ef||"both"===ef)&&el,j&&i.createElement("div",{className:"".concat($,"-header")},j),i.createElement(f.Z,Object.assign({},ee),eh,y),N&&i.createElement("div",{className:"".concat($,"-footer")},N),C||("bottom"===ef||"both"===ef)&&el)))}j.Item=k;var N=j},40875:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("chevron-down",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]])},22135:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("chevron-up",[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]])},5136:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("clipboard",[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1",key:"tgr4d6"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2",key:"116196"}]])},64935:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("code",[["path",{d:"m16 18 6-6-6-6",key:"eg8j8"}],["path",{d:"m8 6-6 6 6 6",key:"ppft3o"}]])},96362:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("external-link",[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]])},29202:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]])},33245:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]])},54001:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("key",[["path",{d:"m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4",key:"g0fldk"}],["path",{d:"m21 2-9.6 9.6",key:"1j0ho8"}],["circle",{cx:"7.5",cy:"15.5",r:"5.5",key:"yqb3hr"}]])},51817:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("loader-circle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]])},21047:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("minus",[["path",{d:"M5 12h14",key:"1ays0h"}]])},96137:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("server",[["rect",{width:"20",height:"8",x:"2",y:"2",rx:"2",ry:"2",key:"ngkwjq"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2",ry:"2",key:"iecqi9"}],["line",{x1:"6",x2:"6.01",y1:"6",y2:"6",key:"16zg32"}],["line",{x1:"6",x2:"6.01",y1:"18",y2:"18",key:"nzw8ys"}]])},80221:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("terminal",[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]])},70525:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("trending-up",[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]])},76865:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("triangle-alert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]])},49663:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("user-check",[["path",{d:"m16 11 2 2 4-4",key:"9rsbq5"}],["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}]])},95805:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744",key:"16gr8j"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}]])},11239:function(e,t,n){n.d(t,{Z:function(){return r}});let r=(0,n(79205).Z)("zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]])},33145:function(e,t,n){n.d(t,{default:function(){return o.a}});var r=n(48461),o=n.n(r)},65878:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"Image",{enumerable:!0,get:function(){return k}});let r=n(47043),o=n(53099),a=n(57437),i=o._(n(2265)),l=r._(n(54887)),c=r._(n(38293)),s=n(55346),d=n(90128),u=n(62589);n(31765);let m=n(25523),h=r._(n(5084)),f={deviceSizes:[640,750,828,1080,1200,1920,2048,3840],imageSizes:[16,32,48,64,96,128,256,384],path:"/_next/image",loader:"default",dangerouslyAllowSVG:!1,unoptimized:!1};function p(e,t,n,r,o,a,i){let l=null==e?void 0:e.src;e&&e["data-loaded-src"]!==l&&(e["data-loaded-src"]=l,("decode"in e?e.decode():Promise.resolve()).catch(()=>{}).then(()=>{if(e.parentElement&&e.isConnected){if("empty"!==t&&o(!0),null==n?void 0:n.current){let t=new Event("load");Object.defineProperty(t,"target",{writable:!1,value:e});let r=!1,o=!1;n.current({...t,nativeEvent:t,currentTarget:e,target:e,isDefaultPrevented:()=>r,isPropagationStopped:()=>o,persist:()=>{},preventDefault:()=>{r=!0,t.preventDefault()},stopPropagation:()=>{o=!0,t.stopPropagation()}})}(null==r?void 0:r.current)&&r.current(e)}}))}function g(e){return i.use?{fetchPriority:e}:{fetchpriority:e}}"undefined"==typeof window&&(globalThis.__NEXT_IMAGE_IMPORTED=!0);let b=(0,i.forwardRef)((e,t)=>{let{src:n,srcSet:r,sizes:o,height:l,width:c,decoding:s,className:d,style:u,fetchPriority:m,placeholder:h,loading:f,unoptimized:b,fill:v,onLoadRef:k,onLoadingCompleteRef:x,setBlurComplete:y,setShowAltText:w,sizesInput:C,onLoad:E,onError:S,...M}=e;return(0,a.jsx)("img",{...M,...g(m),loading:f,width:c,height:l,decoding:s,"data-nimg":v?"fill":"1",className:d,style:u,sizes:o,srcSet:r,src:n,ref:(0,i.useCallback)(e=>{t&&("function"==typeof t?t(e):"object"==typeof t&&(t.current=e)),e&&(S&&(e.src=e.src),e.complete&&p(e,h,k,x,y,b,C))},[n,h,k,x,y,S,b,C,t]),onLoad:e=>{p(e.currentTarget,h,k,x,y,b,C)},onError:e=>{w(!0),"empty"!==h&&y(!0),S&&S(e)}})});function v(e){let{isAppRouter:t,imgAttributes:n}=e,r={as:"image",imageSrcSet:n.srcSet,imageSizes:n.sizes,crossOrigin:n.crossOrigin,referrerPolicy:n.referrerPolicy,...g(n.fetchPriority)};return t&&l.default.preload?(l.default.preload(n.src,r),null):(0,a.jsx)(c.default,{children:(0,a.jsx)("link",{rel:"preload",href:n.srcSet?void 0:n.src,...r},"__nimg-"+n.src+n.srcSet+n.sizes)})}let k=(0,i.forwardRef)((e,t)=>{let n=(0,i.useContext)(m.RouterContext),r=(0,i.useContext)(u.ImageConfigContext),o=(0,i.useMemo)(()=>{var e;let t=f||r||d.imageConfigDefault,n=[...t.deviceSizes,...t.imageSizes].sort((e,t)=>e-t),o=t.deviceSizes.sort((e,t)=>e-t),a=null==(e=t.qualities)?void 0:e.sort((e,t)=>e-t);return{...t,allSizes:n,deviceSizes:o,qualities:a}},[r]),{onLoad:l,onLoadingComplete:c}=e,p=(0,i.useRef)(l);(0,i.useEffect)(()=>{p.current=l},[l]);let g=(0,i.useRef)(c);(0,i.useEffect)(()=>{g.current=c},[c]);let[k,x]=(0,i.useState)(!1),[y,w]=(0,i.useState)(!1),{props:C,meta:E}=(0,s.getImgProps)(e,{defaultLoader:h.default,imgConf:o,blurComplete:k,showAltText:y});return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(b,{...C,unoptimized:E.unoptimized,placeholder:E.placeholder,fill:E.fill,onLoadRef:p,onLoadingCompleteRef:g,setBlurComplete:x,setShowAltText:w,sizesInput:e.sizes,ref:t}),E.priority?(0,a.jsx)(v,{isAppRouter:!n,imgAttributes:C}):null]})});("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},91436:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"AmpStateContext",{enumerable:!0,get:function(){return r}});let r=n(47043)._(n(2265)).default.createContext({})},23964:function(e,t){function n(e){let{ampFirst:t=!1,hybrid:n=!1,hasQuery:r=!1}=void 0===e?{}:e;return t||n&&r}Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"isInAmpMode",{enumerable:!0,get:function(){return n}})},55346:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"getImgProps",{enumerable:!0,get:function(){return l}}),n(31765);let r=n(96496),o=n(90128);function a(e){return void 0!==e.default}function i(e){return void 0===e?e:"number"==typeof e?Number.isFinite(e)?e:NaN:"string"==typeof e&&/^[0-9]+$/.test(e)?parseInt(e,10):NaN}function l(e,t){var n,l;let c,s,d,{src:u,sizes:m,unoptimized:h=!1,priority:f=!1,loading:p,className:g,quality:b,width:v,height:k,fill:x=!1,style:y,overrideSrc:w,onLoad:C,onLoadingComplete:E,placeholder:S="empty",blurDataURL:M,fetchPriority:z,decoding:Z="async",layout:O,objectFit:j,objectPosition:N,lazyBoundary:_,lazyRoot:I,...R}=e,{imgConf:P,showAltText:L,blurComplete:B,defaultLoader:H}=t,V=P||o.imageConfigDefault;if("allSizes"in V)c=V;else{let e=[...V.deviceSizes,...V.imageSizes].sort((e,t)=>e-t),t=V.deviceSizes.sort((e,t)=>e-t),r=null==(n=V.qualities)?void 0:n.sort((e,t)=>e-t);c={...V,allSizes:e,deviceSizes:t,qualities:r}}if(void 0===H)throw Error("images.loaderFile detected but the file is missing default export.\nRead more: https://nextjs.org/docs/messages/invalid-images-config");let A=R.loader||H;delete R.loader,delete R.srcSet;let T="__next_img_default"in A;if(T){if("custom"===c.loader)throw Error('Image with src "'+u+'" is missing "loader" prop.\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader')}else{let e=A;A=t=>{let{config:n,...r}=t;return e(r)}}if(O){"fill"===O&&(x=!0);let e={intrinsic:{maxWidth:"100%",height:"auto"},responsive:{width:"100%",height:"auto"}}[O];e&&(y={...y,...e});let t={responsive:"100vw",fill:"100vw"}[O];t&&!m&&(m=t)}let q="",W=i(v),F=i(k);if("object"==typeof(l=u)&&(a(l)||void 0!==l.src)){let e=a(u)?u.default:u;if(!e.src)throw Error("An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received "+JSON.stringify(e));if(!e.height||!e.width)throw Error("An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received "+JSON.stringify(e));if(s=e.blurWidth,d=e.blurHeight,M=M||e.blurDataURL,q=e.src,!x){if(W||F){if(W&&!F){let t=W/e.width;F=Math.round(e.height*t)}else if(!W&&F){let t=F/e.height;W=Math.round(e.width*t)}}else W=e.width,F=e.height}}let D=!f&&("lazy"===p||void 0===p);(!(u="string"==typeof u?u:q)||u.startsWith("data:")||u.startsWith("blob:"))&&(h=!0,D=!1),c.unoptimized&&(h=!0),T&&u.endsWith(".svg")&&!c.dangerouslyAllowSVG&&(h=!0),f&&(z="high");let G=i(b),K=Object.assign(x?{position:"absolute",height:"100%",width:"100%",left:0,top:0,right:0,bottom:0,objectFit:j,objectPosition:N}:{},L?{}:{color:"transparent"},y),U=B||"empty"===S?null:"blur"===S?'url("data:image/svg+xml;charset=utf-8,'+(0,r.getImageBlurSvg)({widthInt:W,heightInt:F,blurWidth:s,blurHeight:d,blurDataURL:M||"",objectFit:K.objectFit})+'")':'url("'+S+'")',X=U?{backgroundSize:K.objectFit||"cover",backgroundPosition:K.objectPosition||"50% 50%",backgroundRepeat:"no-repeat",backgroundImage:U}:{},$=function(e){let{config:t,src:n,unoptimized:r,width:o,quality:a,sizes:i,loader:l}=e;if(r)return{src:n,srcSet:void 0,sizes:void 0};let{widths:c,kind:s}=function(e,t,n){let{deviceSizes:r,allSizes:o}=e;if(n){let e=/(^|\s)(1?\d?\d)vw/g,t=[];for(let r;r=e.exec(n);r)t.push(parseInt(r[2]));if(t.length){let e=.01*Math.min(...t);return{widths:o.filter(t=>t>=r[0]*e),kind:"w"}}return{widths:o,kind:"w"}}return"number"!=typeof t?{widths:r,kind:"w"}:{widths:[...new Set([t,2*t].map(e=>o.find(t=>t>=e)||o[o.length-1]))],kind:"x"}}(t,o,i),d=c.length-1;return{sizes:i||"w"!==s?i:"100vw",srcSet:c.map((e,r)=>l({config:t,src:n,quality:a,width:e})+" "+("w"===s?e:r+1)+s).join(", "),src:l({config:t,src:n,quality:a,width:c[d]})}}({config:c,src:u,unoptimized:h,width:W,quality:G,sizes:m,loader:A});return{props:{...R,loading:D?"lazy":p,fetchPriority:z,width:W,height:F,decoding:Z,className:g,style:{...K,...X},sizes:$.sizes,srcSet:$.srcSet,src:w||$.src},meta:{unoptimized:h,priority:f,placeholder:S,fill:x}}}},38293:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),function(e,t){for(var n in t)Object.defineProperty(e,n,{enumerable:!0,get:t[n]})}(t,{default:function(){return p},defaultHead:function(){return u}});let r=n(47043),o=n(53099),a=n(57437),i=o._(n(2265)),l=r._(n(17421)),c=n(91436),s=n(48701),d=n(23964);function u(e){void 0===e&&(e=!1);let t=[(0,a.jsx)("meta",{charSet:"utf-8"})];return e||t.push((0,a.jsx)("meta",{name:"viewport",content:"width=device-width"})),t}function m(e,t){return"string"==typeof t||"number"==typeof t?e:t.type===i.default.Fragment?e.concat(i.default.Children.toArray(t.props.children).reduce((e,t)=>"string"==typeof t||"number"==typeof t?e:e.concat(t),[])):e.concat(t)}n(31765);let h=["name","httpEquiv","charSet","itemProp"];function f(e,t){let{inAmpMode:n}=t;return e.reduce(m,[]).reverse().concat(u(n).reverse()).filter(function(){let e=new Set,t=new Set,n=new Set,r={};return o=>{let a=!0,i=!1;if(o.key&&"number"!=typeof o.key&&o.key.indexOf("$")>0){i=!0;let t=o.key.slice(o.key.indexOf("$")+1);e.has(t)?a=!1:e.add(t)}switch(o.type){case"title":case"base":t.has(o.type)?a=!1:t.add(o.type);break;case"meta":for(let e=0,t=h.length;e{let r=e.key||t;if(!n&&"link"===e.type&&e.props.href&&["https://fonts.googleapis.com/css","https://use.typekit.net/"].some(t=>e.props.href.startsWith(t))){let t={...e.props||{}};return t["data-href"]=t.href,t.href=void 0,t["data-optimized-fonts"]=!0,i.default.cloneElement(e,t)}return i.default.cloneElement(e,{key:r})})}let p=function(e){let{children:t}=e,n=(0,i.useContext)(c.AmpStateContext),r=(0,i.useContext)(s.HeadManagerContext);return(0,a.jsx)(l.default,{reduceComponentsToState:f,headManager:r,inAmpMode:(0,d.isInAmpMode)(n),children:t})};("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},96496:function(e,t){function n(e){let{widthInt:t,heightInt:n,blurWidth:r,blurHeight:o,blurDataURL:a,objectFit:i}=e,l=r?40*r:t,c=o?40*o:n,s=l&&c?"viewBox='0 0 "+l+" "+c+"'":"";return"%3Csvg xmlns='http://www.w3.org/2000/svg' "+s+"%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='"+(s?"none":"contain"===i?"xMidYMid":"cover"===i?"xMidYMid slice":"none")+"' style='filter: url(%23b);' href='"+a+"'/%3E%3C/svg%3E"}Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"getImageBlurSvg",{enumerable:!0,get:function(){return n}})},62589:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"ImageConfigContext",{enumerable:!0,get:function(){return a}});let r=n(47043)._(n(2265)),o=n(90128),a=r.default.createContext(o.imageConfigDefault)},90128:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),function(e,t){for(var n in t)Object.defineProperty(e,n,{enumerable:!0,get:t[n]})}(t,{VALID_LOADERS:function(){return n},imageConfigDefault:function(){return r}});let n=["default","imgix","cloudinary","akamai","custom"],r={deviceSizes:[640,750,828,1080,1200,1920,2048,3840],imageSizes:[16,32,48,64,96,128,256,384],path:"/_next/image",loader:"default",loaderFile:"",domains:[],disableStaticImages:!1,minimumCacheTTL:60,formats:["image/webp"],dangerouslyAllowSVG:!1,contentSecurityPolicy:"script-src 'none'; frame-src 'none'; sandbox;",contentDispositionType:"inline",localPatterns:void 0,remotePatterns:[],qualities:void 0,unoptimized:!1}},48461:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),function(e,t){for(var n in t)Object.defineProperty(e,n,{enumerable:!0,get:t[n]})}(t,{default:function(){return c},getImageProps:function(){return l}});let r=n(47043),o=n(55346),a=n(65878),i=r._(n(5084));function l(e){let{props:t}=(0,o.getImgProps)(e,{defaultLoader:i.default,imgConf:{deviceSizes:[640,750,828,1080,1200,1920,2048,3840],imageSizes:[16,32,48,64,96,128,256,384],path:"/_next/image",loader:"default",dangerouslyAllowSVG:!1,unoptimized:!1}});for(let[e,n]of Object.entries(t))void 0===n&&delete t[e];return{props:t}}let c=a.Image},5084:function(e,t){function n(e){var t;let{config:n,src:r,width:o,quality:a}=e,i=a||(null==(t=n.qualities)?void 0:t.reduce((e,t)=>Math.abs(t-75){}:r.useLayoutEffect,i=o?()=>{}:r.useEffect;function l(e){let{headManager:t,reduceComponentsToState:n}=e;function l(){if(t&&t.mountedInstances){let o=r.Children.toArray(Array.from(t.mountedInstances).filter(Boolean));t.updateHead(n(o,e))}}if(o){var c;null==t||null==(c=t.mountedInstances)||c.add(e.children),l()}return a(()=>{var n;return null==t||null==(n=t.mountedInstances)||n.add(e.children),()=>{var n;null==t||null==(n=t.mountedInstances)||n.delete(e.children)}}),a(()=>(t&&(t._pendingUpdate=l),()=>{t&&(t._pendingUpdate=l)})),i(()=>(t&&t._pendingUpdate&&(t._pendingUpdate(),t._pendingUpdate=null),()=>{t&&t._pendingUpdate&&(t._pendingUpdate(),t._pendingUpdate=null)})),null}},1479:function(e,t){t.Z={'code[class*="language-"]':{background:"hsl(230, 1%, 98%)",color:"hsl(230, 8%, 24%)",fontFamily:'"Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace',direction:"ltr",textAlign:"left",whiteSpace:"pre",wordSpacing:"normal",wordBreak:"normal",lineHeight:"1.5",MozTabSize:"2",OTabSize:"2",tabSize:"2",WebkitHyphens:"none",MozHyphens:"none",msHyphens:"none",hyphens:"none"},'pre[class*="language-"]':{background:"hsl(230, 1%, 98%)",color:"hsl(230, 8%, 24%)",fontFamily:'"Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace',direction:"ltr",textAlign:"left",whiteSpace:"pre",wordSpacing:"normal",wordBreak:"normal",lineHeight:"1.5",MozTabSize:"2",OTabSize:"2",tabSize:"2",WebkitHyphens:"none",MozHyphens:"none",msHyphens:"none",hyphens:"none",padding:"1em",margin:"0.5em 0",overflow:"auto",borderRadius:"0.3em"},'code[class*="language-"]::-moz-selection':{background:"hsl(230, 1%, 90%)",color:"inherit"},'code[class*="language-"] *::-moz-selection':{background:"hsl(230, 1%, 90%)",color:"inherit"},'pre[class*="language-"] *::-moz-selection':{background:"hsl(230, 1%, 90%)",color:"inherit"},'code[class*="language-"]::selection':{background:"hsl(230, 1%, 90%)",color:"inherit"},'code[class*="language-"] *::selection':{background:"hsl(230, 1%, 90%)",color:"inherit"},'pre[class*="language-"] *::selection':{background:"hsl(230, 1%, 90%)",color:"inherit"},':not(pre) > code[class*="language-"]':{padding:"0.2em 0.3em",borderRadius:"0.3em",whiteSpace:"normal"},comment:{color:"hsl(230, 4%, 64%)",fontStyle:"italic"},prolog:{color:"hsl(230, 4%, 64%)"},cdata:{color:"hsl(230, 4%, 64%)"},doctype:{color:"hsl(230, 8%, 24%)"},punctuation:{color:"hsl(230, 8%, 24%)"},entity:{color:"hsl(230, 8%, 24%)",cursor:"help"},"attr-name":{color:"hsl(35, 99%, 36%)"},"class-name":{color:"hsl(35, 99%, 36%)"},boolean:{color:"hsl(35, 99%, 36%)"},constant:{color:"hsl(35, 99%, 36%)"},number:{color:"hsl(35, 99%, 36%)"},atrule:{color:"hsl(35, 99%, 36%)"},keyword:{color:"hsl(301, 63%, 40%)"},property:{color:"hsl(5, 74%, 59%)"},tag:{color:"hsl(5, 74%, 59%)"},symbol:{color:"hsl(5, 74%, 59%)"},deleted:{color:"hsl(5, 74%, 59%)"},important:{color:"hsl(5, 74%, 59%)"},selector:{color:"hsl(119, 34%, 47%)"},string:{color:"hsl(119, 34%, 47%)"},char:{color:"hsl(119, 34%, 47%)"},builtin:{color:"hsl(119, 34%, 47%)"},inserted:{color:"hsl(119, 34%, 47%)"},regex:{color:"hsl(119, 34%, 47%)"},"attr-value":{color:"hsl(119, 34%, 47%)"},"attr-value > .token.punctuation":{color:"hsl(119, 34%, 47%)"},variable:{color:"hsl(221, 87%, 60%)"},operator:{color:"hsl(221, 87%, 60%)"},function:{color:"hsl(221, 87%, 60%)"},url:{color:"hsl(198, 99%, 37%)"},"attr-value > .token.punctuation.attr-equals":{color:"hsl(230, 8%, 24%)"},"special-attr > .token.attr-value > .token.value.css":{color:"hsl(230, 8%, 24%)"},".language-css .token.selector":{color:"hsl(5, 74%, 59%)"},".language-css .token.property":{color:"hsl(230, 8%, 24%)"},".language-css .token.function":{color:"hsl(198, 99%, 37%)"},".language-css .token.url > .token.function":{color:"hsl(198, 99%, 37%)"},".language-css .token.url > .token.string.url":{color:"hsl(119, 34%, 47%)"},".language-css .token.important":{color:"hsl(301, 63%, 40%)"},".language-css .token.atrule .token.rule":{color:"hsl(301, 63%, 40%)"},".language-javascript .token.operator":{color:"hsl(301, 63%, 40%)"},".language-javascript .token.template-string > .token.interpolation > .token.interpolation-punctuation.punctuation":{color:"hsl(344, 84%, 43%)"},".language-json .token.operator":{color:"hsl(230, 8%, 24%)"},".language-json .token.null.keyword":{color:"hsl(35, 99%, 36%)"},".language-markdown .token.url":{color:"hsl(230, 8%, 24%)"},".language-markdown .token.url > .token.operator":{color:"hsl(230, 8%, 24%)"},".language-markdown .token.url-reference.url > .token.string":{color:"hsl(230, 8%, 24%)"},".language-markdown .token.url > .token.content":{color:"hsl(221, 87%, 60%)"},".language-markdown .token.url > .token.url":{color:"hsl(198, 99%, 37%)"},".language-markdown .token.url-reference.url":{color:"hsl(198, 99%, 37%)"},".language-markdown .token.blockquote.punctuation":{color:"hsl(230, 4%, 64%)",fontStyle:"italic"},".language-markdown .token.hr.punctuation":{color:"hsl(230, 4%, 64%)",fontStyle:"italic"},".language-markdown .token.code-snippet":{color:"hsl(119, 34%, 47%)"},".language-markdown .token.bold .token.content":{color:"hsl(35, 99%, 36%)"},".language-markdown .token.italic .token.content":{color:"hsl(301, 63%, 40%)"},".language-markdown .token.strike .token.content":{color:"hsl(5, 74%, 59%)"},".language-markdown .token.strike .token.punctuation":{color:"hsl(5, 74%, 59%)"},".language-markdown .token.list.punctuation":{color:"hsl(5, 74%, 59%)"},".language-markdown .token.title.important > .token.punctuation":{color:"hsl(5, 74%, 59%)"},bold:{fontWeight:"bold"},italic:{fontStyle:"italic"},namespace:{Opacity:"0.8"},"token.tab:not(:empty):before":{color:"hsla(230, 8%, 24%, 0.2)"},"token.cr:before":{color:"hsla(230, 8%, 24%, 0.2)"},"token.lf:before":{color:"hsla(230, 8%, 24%, 0.2)"},"token.space:before":{color:"hsla(230, 8%, 24%, 0.2)"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item":{marginRight:"0.4em"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > button":{background:"hsl(230, 1%, 90%)",color:"hsl(230, 6%, 44%)",padding:"0.1em 0.4em",borderRadius:"0.3em"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > a":{background:"hsl(230, 1%, 90%)",color:"hsl(230, 6%, 44%)",padding:"0.1em 0.4em",borderRadius:"0.3em"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > span":{background:"hsl(230, 1%, 90%)",color:"hsl(230, 6%, 44%)",padding:"0.1em 0.4em",borderRadius:"0.3em"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover":{background:"hsl(230, 1%, 78%)",color:"hsl(230, 8%, 24%)"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus":{background:"hsl(230, 1%, 78%)",color:"hsl(230, 8%, 24%)"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover":{background:"hsl(230, 1%, 78%)",color:"hsl(230, 8%, 24%)"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus":{background:"hsl(230, 1%, 78%)",color:"hsl(230, 8%, 24%)"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover":{background:"hsl(230, 1%, 78%)",color:"hsl(230, 8%, 24%)"},"div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus":{background:"hsl(230, 1%, 78%)",color:"hsl(230, 8%, 24%)"},".line-highlight.line-highlight":{background:"hsla(230, 8%, 24%, 0.05)"},".line-highlight.line-highlight:before":{background:"hsl(230, 1%, 90%)",color:"hsl(230, 8%, 24%)",padding:"0.1em 0.6em",borderRadius:"0.3em",boxShadow:"0 2px 0 0 rgba(0, 0, 0, 0.2)"},".line-highlight.line-highlight[data-end]:after":{background:"hsl(230, 1%, 90%)",color:"hsl(230, 8%, 24%)",padding:"0.1em 0.6em",borderRadius:"0.3em",boxShadow:"0 2px 0 0 rgba(0, 0, 0, 0.2)"},"pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before":{backgroundColor:"hsla(230, 8%, 24%, 0.05)"},".line-numbers.line-numbers .line-numbers-rows":{borderRightColor:"hsla(230, 8%, 24%, 0.2)"},".command-line .command-line-prompt":{borderRightColor:"hsla(230, 8%, 24%, 0.2)"},".line-numbers .line-numbers-rows > span:before":{color:"hsl(230, 1%, 62%)"},".command-line .command-line-prompt > span:before":{color:"hsl(230, 1%, 62%)"},".rainbow-braces .token.token.punctuation.brace-level-1":{color:"hsl(5, 74%, 59%)"},".rainbow-braces .token.token.punctuation.brace-level-5":{color:"hsl(5, 74%, 59%)"},".rainbow-braces .token.token.punctuation.brace-level-9":{color:"hsl(5, 74%, 59%)"},".rainbow-braces .token.token.punctuation.brace-level-2":{color:"hsl(119, 34%, 47%)"},".rainbow-braces .token.token.punctuation.brace-level-6":{color:"hsl(119, 34%, 47%)"},".rainbow-braces .token.token.punctuation.brace-level-10":{color:"hsl(119, 34%, 47%)"},".rainbow-braces .token.token.punctuation.brace-level-3":{color:"hsl(221, 87%, 60%)"},".rainbow-braces .token.token.punctuation.brace-level-7":{color:"hsl(221, 87%, 60%)"},".rainbow-braces .token.token.punctuation.brace-level-11":{color:"hsl(221, 87%, 60%)"},".rainbow-braces .token.token.punctuation.brace-level-4":{color:"hsl(301, 63%, 40%)"},".rainbow-braces .token.token.punctuation.brace-level-8":{color:"hsl(301, 63%, 40%)"},".rainbow-braces .token.token.punctuation.brace-level-12":{color:"hsl(301, 63%, 40%)"},"pre.diff-highlight > code .token.token.deleted:not(.prefix)":{backgroundColor:"hsla(353, 100%, 66%, 0.15)"},"pre > code.diff-highlight .token.token.deleted:not(.prefix)":{backgroundColor:"hsla(353, 100%, 66%, 0.15)"},"pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection":{backgroundColor:"hsla(353, 95%, 66%, 0.25)"},"pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection":{backgroundColor:"hsla(353, 95%, 66%, 0.25)"},"pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection":{backgroundColor:"hsla(353, 95%, 66%, 0.25)"},"pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection":{backgroundColor:"hsla(353, 95%, 66%, 0.25)"},"pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection":{backgroundColor:"hsla(353, 95%, 66%, 0.25)"},"pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection":{backgroundColor:"hsla(353, 95%, 66%, 0.25)"},"pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection":{backgroundColor:"hsla(353, 95%, 66%, 0.25)"},"pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection":{backgroundColor:"hsla(353, 95%, 66%, 0.25)"},"pre.diff-highlight > code .token.token.inserted:not(.prefix)":{backgroundColor:"hsla(137, 100%, 55%, 0.15)"},"pre > code.diff-highlight .token.token.inserted:not(.prefix)":{backgroundColor:"hsla(137, 100%, 55%, 0.15)"},"pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection":{backgroundColor:"hsla(135, 73%, 55%, 0.25)"},"pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection":{backgroundColor:"hsla(135, 73%, 55%, 0.25)"},"pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection":{backgroundColor:"hsla(135, 73%, 55%, 0.25)"},"pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection":{backgroundColor:"hsla(135, 73%, 55%, 0.25)"},"pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection":{backgroundColor:"hsla(135, 73%, 55%, 0.25)"},"pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection":{backgroundColor:"hsla(135, 73%, 55%, 0.25)"},"pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection":{backgroundColor:"hsla(135, 73%, 55%, 0.25)"},"pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection":{backgroundColor:"hsla(135, 73%, 55%, 0.25)"},".prism-previewer.prism-previewer:before":{borderColor:"hsl(0, 0, 95%)"},".prism-previewer-gradient.prism-previewer-gradient div":{borderColor:"hsl(0, 0, 95%)",borderRadius:"0.3em"},".prism-previewer-color.prism-previewer-color:before":{borderRadius:"0.3em"},".prism-previewer-easing.prism-previewer-easing:before":{borderRadius:"0.3em"},".prism-previewer.prism-previewer:after":{borderTopColor:"hsl(0, 0, 95%)"},".prism-previewer-flipped.prism-previewer-flipped.after":{borderBottomColor:"hsl(0, 0, 95%)"},".prism-previewer-angle.prism-previewer-angle:before":{background:"hsl(0, 0%, 100%)"},".prism-previewer-time.prism-previewer-time:before":{background:"hsl(0, 0%, 100%)"},".prism-previewer-easing.prism-previewer-easing":{background:"hsl(0, 0%, 100%)"},".prism-previewer-angle.prism-previewer-angle circle":{stroke:"hsl(230, 8%, 24%)",strokeOpacity:"1"},".prism-previewer-time.prism-previewer-time circle":{stroke:"hsl(230, 8%, 24%)",strokeOpacity:"1"},".prism-previewer-easing.prism-previewer-easing circle":{stroke:"hsl(230, 8%, 24%)",fill:"transparent"},".prism-previewer-easing.prism-previewer-easing path":{stroke:"hsl(230, 8%, 24%)"},".prism-previewer-easing.prism-previewer-easing line":{stroke:"hsl(230, 8%, 24%)"}}},82422:function(e,t,n){var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M5 13l4 4L19 7"}))});t.Z=o},51853:function(e,t,n){var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"}))});t.Z=o},3477:function(e,t,n){var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"}))});t.Z=o},71437:function(e,t,n){var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15 12a3 3 0 11-6 0 3 3 0 016 0z"}),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"}))});t.Z=o},82376:function(e,t,n){var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"}))});t.Z=o},17732:function(e,t,n){var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"}))});t.Z=o},3837:function(e,t,n){var r=n(2265);let o=r.forwardRef(function(e,t){return r.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:2,stroke:"currentColor","aria-hidden":"true",ref:t},e),r.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M6 18L18 6M6 6l12 12"}))});t.Z=o},19616:function(e,t,n){n.d(t,{G:function(){return i}});var r=n(2265);let o={enabled:!0,leading:!1,trailing:!0,wait:0,onExecute:()=>{}};class a{constructor(e,t){this.fn=e,this._canLeadingExecute=!0,this._isPending=!1,this._executionCount=0,this._options={...o,...t}}setOptions(e){return this._options={...this._options,...e},this._options.enabled||(this._isPending=!1),this._options}getOptions(){return this._options}maybeExecute(...e){this._options.leading&&this._canLeadingExecute&&(this.executeFunction(...e),this._canLeadingExecute=!1),(this._options.leading||this._options.trailing)&&(this._isPending=!0),this._timeoutId&&clearTimeout(this._timeoutId),this._timeoutId=setTimeout(()=>{this._canLeadingExecute=!0,this._isPending=!1,this._options.trailing&&this.executeFunction(...e)},this._options.wait)}executeFunction(...e){this._options.enabled&&(this.fn(...e),this._executionCount++,this._options.onExecute(this))}cancel(){this._timeoutId&&(clearTimeout(this._timeoutId),this._canLeadingExecute=!0,this._isPending=!1)}getExecutionCount(){return this._executionCount}getIsPending(){return this._options.enabled&&this._isPending}}function i(e,t){let[n,o]=(0,r.useState)(e),i=function(e,t){let[n]=(0,r.useState)(()=>{var n;return Object.getOwnPropertyNames(Object.getPrototypeOf(n=new a(e,t))).filter(e=>"function"==typeof n[e]).reduce((e,t)=>{let r=n[t];return"function"==typeof r&&(e[t]=r.bind(n)),e},{})});return n.setOptions(t),n}(o,t);return[n,i.maybeExecute,i]}},21770:function(e,t,n){n.d(t,{D:function(){return u}});var r=n(2265),o=n(2894),a=n(18238),i=n(24112),l=n(45345),c=class extends i.l{#e;#t=void 0;#n;#r;constructor(e,t){super(),this.#e=e,this.setOptions(t),this.bindMethods(),this.#o()}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){let t=this.options;this.options=this.#e.defaultMutationOptions(e),(0,l.VS)(this.options,t)||this.#e.getMutationCache().notify({type:"observerOptionsUpdated",mutation:this.#n,observer:this}),t?.mutationKey&&this.options.mutationKey&&(0,l.Ym)(t.mutationKey)!==(0,l.Ym)(this.options.mutationKey)?this.reset():this.#n?.state.status==="pending"&&this.#n.setOptions(this.options)}onUnsubscribe(){this.hasListeners()||this.#n?.removeObserver(this)}onMutationUpdate(e){this.#o(),this.#a(e)}getCurrentResult(){return this.#t}reset(){this.#n?.removeObserver(this),this.#n=void 0,this.#o(),this.#a()}mutate(e,t){return this.#r=t,this.#n?.removeObserver(this),this.#n=this.#e.getMutationCache().build(this.#e,this.options),this.#n.addObserver(this),this.#n.execute(e)}#o(){let e=this.#n?.state??(0,o.R)();this.#t={...e,isPending:"pending"===e.status,isSuccess:"success"===e.status,isError:"error"===e.status,isIdle:"idle"===e.status,mutate:this.mutate,reset:this.reset}}#a(e){a.V.batch(()=>{if(this.#r&&this.hasListeners()){let t=this.#t.variables,n=this.#t.context;e?.type==="success"?(this.#r.onSuccess?.(e.data,t,n),this.#r.onSettled?.(e.data,null,t,n)):e?.type==="error"&&(this.#r.onError?.(e.error,t,n),this.#r.onSettled?.(void 0,e.error,t,n))}this.listeners.forEach(e=>{e(this.#t)})})}},s=n(29827),d=n(51172);function u(e,t){let n=(0,s.NL)(t),[o]=r.useState(()=>new c(n,e));r.useEffect(()=>{o.setOptions(e)},[o,e]);let i=r.useSyncExternalStore(r.useCallback(e=>o.subscribe(a.V.batchCalls(e)),[o]),()=>o.getCurrentResult(),()=>o.getCurrentResult()),l=r.useCallback((e,t)=>{o.mutate(e,t).catch(d.Z)},[o]);if(i.error&&(0,d.L)(o.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:l,mutateAsync:i.mutate}}}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1223-de5e7e4f043a5233.js b/litellm/proxy/_experimental/out/_next/static/chunks/1223-de5e7e4f043a5233.js deleted file mode 100644 index b7e8c2b8f91..00000000000 --- a/litellm/proxy/_experimental/out/_next/static/chunks/1223-de5e7e4f043a5233.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1223],{54250:function(e,t,n){n.d(t,{Z:function(){return v}});var o=n(5853),r=n(2265),i=n(44140),l=n(34237),a=n(33044),u=n(58747),s=n(4537),c=n(97324),d=n(1153),f=n(96398);let p=(0,d.fn)("SearchSelect"),m=(0,d.fn)("SearchSelect"),v=r.forwardRef((e,t)=>{let{defaultValue:n,value:d,onValueChange:v,placeholder:b="Select...",disabled:g=!1,icon:h,enableClear:x=!0,children:O,className:I}=e,y=(0,o._T)(e,["defaultValue","value","onValueChange","placeholder","disabled","icon","enableClear","children","className"]),[S,R]=(0,r.useState)(""),[T,E]=(0,i.Z)(n,d),{reactElementChildren:C,valueToNameMapping:k}=(0,r.useMemo)(()=>{let e=r.Children.toArray(O).filter(r.isValidElement);return{reactElementChildren:e,valueToNameMapping:(0,f.sl)(e)}},[O]),M=(0,r.useMemo)(()=>(0,f.n0)(S,C),[S,C]);return r.createElement(l.h,Object.assign({as:"div",ref:t,defaultValue:T,value:T,onChange:e=>{null==v||v(e),E(e)},disabled:g,className:(0,c.q)("w-full min-w-[10rem] relative text-tremor-default",I)},y),e=>{let{value:t}=e;return r.createElement(r.Fragment,null,r.createElement(l.h.Button,{className:"w-full"},h&&r.createElement("span",{className:(0,c.q)("absolute inset-y-0 left-0 flex items-center ml-px pl-2.5")},r.createElement(h,{className:(0,c.q)(p("Icon"),"flex-none h-5 w-5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})),r.createElement(l.h.Input,{className:(0,c.q)("w-full outline-none text-left whitespace-nowrap truncate rounded-tremor-default focus:ring-2 transition duration-100 text-tremor-default pr-14 border py-2","border-tremor-border shadow-tremor-input focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted","dark:border-dark-tremor-border dark:shadow-dark-tremor-input dark:focus:border-dark-tremor-brand-subtle dark:focus:ring-dark-tremor-brand-muted",h?"pl-10":"pl-3",g?"placeholder:text-tremor-content-subtle dark:placeholder:text-tremor-content-subtle":"placeholder:text-tremor-content dark:placeholder:text-tremor-content",(0,f.um)((0,f.Uh)(t),g)),placeholder:b,onChange:e=>R(e.target.value),displayValue:e=>{var t;return null!==(t=k.get(e))&&void 0!==t?t:""}}),r.createElement("div",{className:(0,c.q)("absolute inset-y-0 right-0 flex items-center pr-2.5")},r.createElement(u.Z,{className:(0,c.q)(p("arrowDownIcon"),"flex-none h-5 w-5","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")}))),x&&T?r.createElement("button",{type:"button",className:(0,c.q)("absolute inset-y-0 right-0 flex items-center mr-8"),onClick:e=>{e.preventDefault(),E(""),R(""),null==v||v("")}},r.createElement(s.Z,{className:(0,c.q)(m("clearIcon"),"flex-none h-4 w-4","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")})):null,M.length>0&&r.createElement(a.u,{className:"absolute z-10 w-full",enter:"transition ease duration-100 transform",enterFrom:"opacity-0 -translate-y-4",enterTo:"opacity-100 translate-y-0",leave:"transition ease duration-100 transform",leaveFrom:"opacity-100 translate-y-0",leaveTo:"opacity-0 -translate-y-4"},r.createElement(l.h.Options,{className:(0,c.q)("divide-y overflow-y-auto outline-none rounded-tremor-default text-tremor-default max-h-[228px] left-0 border my-1","bg-tremor-background border-tremor-border divide-tremor-border shadow-tremor-dropdown","dark:bg-dark-tremor-background dark:border-dark-tremor-border dark:divide-dark-tremor-border dark:shadow-dark-tremor-dropdown")},M)))})});v.displayName="SearchSelect"},70450:function(e,t,n){n.d(t,{Z:function(){return s}});var o=n(5853),r=n(2265),i=n(97324),l=n(1153),a=n(34237);let u=(0,l.fn)("SearchSelectItem"),s=r.forwardRef((e,t)=>{let{value:n,icon:l,className:s,children:c}=e,d=(0,o._T)(e,["value","icon","className","children"]);return r.createElement(a.h.Option,Object.assign({className:(0,i.q)(u("root"),"flex justify-start items-center cursor-default text-tremor-default p-2.5","ui-active:bg-tremor-background-muted ui-active:text-tremor-content-strong ui-selected:text-tremor-content-strong ui-selected:bg-tremor-background-muted text-tremor-content-emphasis","dark:ui-active:bg-dark-tremor-background-muted dark:ui-active:text-dark-tremor-content-strong dark:ui-selected:text-dark-tremor-content-strong dark:ui-selected:bg-dark-tremor-background-muted dark:text-dark-tremor-content-emphasis",s),ref:t,key:n,value:n},d),l&&r.createElement(l,{className:(0,i.q)(u("icon"),"flex-none h-5 w-5 mr-3","text-tremor-content-subtle","dark:text-dark-tremor-content-subtle")}),r.createElement("span",{className:"whitespace-nowrap truncate"},null!=c?c:n))});s.displayName="SearchSelectItem"},34237:function(e,t,n){let o,r,i,l;function a(){return(a=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0;i--){var l=e[i];if(!n.has(l.lane)){var a=r.get(l.lane);if(null==a||l.end>a.end?r.set(l.lane,l):l.end0?Math.min.apply(Math,o.pendingMeasuredCacheIndexes):0;o.pendingMeasuredCacheIndexes=[];for(var u=o.measurementsCache.slice(0,a),s=a;s0&&t>0?function(e){for(var t=e.measurements,n=e.outerSize,o=e.scrollOffset,r=t.length-1,i=O(0,r,function(e){return t[e].start},o),l=i;l=o.scrollOffset+n?"end":"start"),"start"===t||("end"===t?e-=n:"center"===t&&(e-=n/2));var r=o.options.horizontal?"scrollWidth":"scrollHeight";return Math.max(Math.min((o.scrollElement?"document"in o.scrollElement?o.scrollElement.document.documentElement[r]:o.scrollElement[r]:0)-o.getSize(),e),0)},this.getOffsetForIndex=function(e,t){void 0===t&&(t="auto"),e=Math.max(0,Math.min(e,o.options.count-1));var n=f(o.getMeasurements()[e]);if("auto"===t){if(n.end>=o.scrollOffset+o.getSize()-o.options.scrollPaddingEnd)t="end";else{if(!(n.start<=o.scrollOffset+o.options.scrollPaddingStart))return[o.scrollOffset,t];t="start"}}var r="end"===t?n.end+o.options.scrollPaddingEnd:n.start-o.options.scrollPaddingStart;return[o.getOffsetForAlignment(r,t),t]},this.isDynamicMode=function(){return o.measureElementCache.size>0},this.cancelScrollToIndex=function(){null!==o.scrollToIndexTimeoutId&&(clearTimeout(o.scrollToIndexTimeoutId),o.scrollToIndexTimeoutId=null)},this.scrollToOffset=function(e,t){var n=void 0===t?{}:t,r=n.align,i=n.behavior;o.cancelScrollToIndex(),"smooth"===i&&o.isDynamicMode()&&console.warn("The `smooth` scroll behavior is not fully supported with dynamic size."),o._scrollToOffset(o.getOffsetForAlignment(e,void 0===r?"start":r),{adjustments:void 0,behavior:i})},this.scrollToIndex=function(e,t){var n=void 0===t?{}:t,r=n.align,i=n.behavior;e=Math.max(0,Math.min(e,o.options.count-1)),o.cancelScrollToIndex(),"smooth"===i&&o.isDynamicMode()&&console.warn("The `smooth` scroll behavior is not fully supported with dynamic size.");var l=o.getOffsetForIndex(e,void 0===r?"auto":r),a=l[0],u=l[1];o._scrollToOffset(a,{adjustments:void 0,behavior:i}),"smooth"!==i&&o.isDynamicMode()&&(o.scrollToIndexTimeoutId=setTimeout(function(){o.scrollToIndexTimeoutId=null,o.measureElementCache.has(o.options.getItemKey(e))&&1>Math.abs(o.getOffsetForIndex(e,u)[0]-o.scrollOffset)||o.scrollToIndex(e,{align:u,behavior:i})}))},this.scrollBy=function(e,t){var n=(void 0===t?{}:t).behavior;o.cancelScrollToIndex(),"smooth"===n&&o.isDynamicMode()&&console.warn("The `smooth` scroll behavior is not fully supported with dynamic size."),o._scrollToOffset(o.scrollOffset+e,{adjustments:void 0,behavior:n})},this.getTotalSize=function(){var e;return((null==(e=o.getMeasurements()[o.options.count-1])?void 0:e.end)||o.options.paddingStart)-o.options.scrollMargin+o.options.paddingEnd},this._scrollToOffset=function(e,t){var n=t.adjustments,r=t.behavior;o.options.scrollToFn(e,{behavior:r,adjustments:n},o)},this.measure=function(){o.itemSizeCache=new Map,o.notify(!1)},this.setOptions(e),this.scrollRect=this.options.initialRect,this.scrollOffset=this.options.initialOffset,this.measurementsCache=this.options.initialMeasurementsCache,this.measurementsCache.forEach(function(e){o.itemSizeCache.set(e.key,e.size)}),this.maybeNotify()},O=function(e,t,n,o){for(;e<=t;){var r=(e+t)/2|0,i=n(r);if(io))return r;t=r-1}}return e>0?e-1:0},I="undefined"!=typeof document?u.useLayoutEffect:u.useEffect,y=n(78138),S=n(62963),R=n(90945),T=n(13323),E=n(17684),C=n(64518),k=n(31948),M=n(32539),w=n(40048),z=n(80004),P=n(93689),F=n(15518),D=n(40293);function N(e,t){let n=(0,u.useRef)([]),o=(0,T.z)(e);(0,u.useEffect)(()=>{let e=[...n.current];for(let[r,i]of t.entries())if(n.current[r]!==i){let r=o(t,e);return n.current=t,r}},[o,...t])}var A=n(38198),_=n(37863);let L=[];!function(e){function t(){"loading"!==document.readyState&&(e(),document.removeEventListener("DOMContentLoaded",t))}"undefined"!=typeof window&&"undefined"!=typeof document&&(document.addEventListener("DOMContentLoaded",t),t())}(()=>{function e(e){e.target instanceof HTMLElement&&e.target!==document.body&&L[0]!==e.target&&(L.unshift(e.target),(L=L.filter(e=>null!=e&&e.isConnected)).splice(10))}window.addEventListener("click",e,{capture:!0}),window.addEventListener("mousedown",e,{capture:!0}),window.addEventListener("focus",e,{capture:!0}),document.body.addEventListener("click",e,{capture:!0}),document.body.addEventListener("mousedown",e,{capture:!0}),document.body.addEventListener("focus",e,{capture:!0})});var V=n(47634),j=n(34778),q=n(16015),B=n(37105),K=n(56314),U=n(24536),Z=n(52108),Y=n(27847),G=n(37388),H=n(40257),W=((o=W||{})[o.Open=0]="Open",o[o.Closed=1]="Closed",o),J=((r=J||{})[r.Single=0]="Single",r[r.Multi=1]="Multi",r),Q=((i=Q||{})[i.Pointer=0]="Pointer",i[i.Focus=1]="Focus",i[i.Other=2]="Other",i),X=((l=X||{})[l.OpenCombobox=0]="OpenCombobox",l[l.CloseCombobox=1]="CloseCombobox",l[l.GoToOption=2]="GoToOption",l[l.RegisterOption=3]="RegisterOption",l[l.UnregisterOption=4]="UnregisterOption",l[l.RegisterLabel=5]="RegisterLabel",l[l.SetActivationTrigger=6]="SetActivationTrigger",l[l.UpdateVirtualOptions=7]="UpdateVirtualOptions",l);function $(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:e=>e,n=null!==e.activeOptionIndex?e.options[e.activeOptionIndex]:null,o=t(e.options.slice()),r=o.length>0&&null!==o[0].dataRef.current.order?o.sort((e,t)=>e.dataRef.current.order-t.dataRef.current.order):(0,B.z2)(o,e=>e.dataRef.current.domRef.current),i=n?r.indexOf(n):null;return -1===i&&(i=null),{options:r,activeOptionIndex:i}}let ee={1(e){var t;return null!=(t=e.dataRef.current)&&t.disabled||1===e.comboboxState?e:{...e,activeOptionIndex:null,comboboxState:1}},0(e){var t,n;if(null!=(t=e.dataRef.current)&&t.disabled||0===e.comboboxState)return e;if(null!=(n=e.dataRef.current)&&n.value){let t=e.dataRef.current.calculateIndex(e.dataRef.current.value);if(-1!==t)return{...e,activeOptionIndex:t,comboboxState:0}}return{...e,comboboxState:0}},2(e,t){var n,o,r,i,l;if(null!=(n=e.dataRef.current)&&n.disabled||null!=(o=e.dataRef.current)&&o.optionsRef.current&&!(null!=(r=e.dataRef.current)&&r.optionsPropsRef.current.static)&&1===e.comboboxState)return e;if(e.virtual){let n=t.focus===j.T.Specific?t.idx:(0,j.d)(t,{resolveItems:()=>e.virtual.options,resolveActiveIndex:()=>{var t,n;return null!=(n=null!=(t=e.activeOptionIndex)?t:e.virtual.options.findIndex(t=>!e.virtual.disabled(t)))?n:null},resolveDisabled:e.virtual.disabled,resolveId(){throw Error("Function not implemented.")}}),o=null!=(i=t.trigger)?i:2;return e.activeOptionIndex===n&&e.activationTrigger===o?e:{...e,activeOptionIndex:n,activationTrigger:o}}let a=$(e);if(null===a.activeOptionIndex){let e=a.options.findIndex(e=>!e.dataRef.current.disabled);-1!==e&&(a.activeOptionIndex=e)}let u=t.focus===j.T.Specific?t.idx:(0,j.d)(t,{resolveItems:()=>a.options,resolveActiveIndex:()=>a.activeOptionIndex,resolveId:e=>e.id,resolveDisabled:e=>e.dataRef.current.disabled}),s=null!=(l=t.trigger)?l:2;return e.activeOptionIndex===u&&e.activationTrigger===s?e:{...e,...a,activeOptionIndex:u,activationTrigger:s}},3:(e,t)=>{var n,o,r;if(null!=(n=e.dataRef.current)&&n.virtual)return{...e,options:[...e.options,t.payload]};let i=t.payload,l=$(e,e=>(e.push(i),e));null===e.activeOptionIndex&&null!=(o=e.dataRef.current)&&o.isSelected(t.payload.dataRef.current.value)&&(l.activeOptionIndex=l.options.indexOf(i));let a={...e,...l,activationTrigger:2};return null!=(r=e.dataRef.current)&&r.__demoMode&&void 0===e.dataRef.current.value&&(a.activeOptionIndex=0),a},4:(e,t)=>{var n;if(null!=(n=e.dataRef.current)&&n.virtual)return{...e,options:e.options.filter(e=>e.id!==t.id)};let o=$(e,e=>{let n=e.findIndex(e=>e.id===t.id);return -1!==n&&e.splice(n,1),e});return{...e,...o,activationTrigger:2}},5:(e,t)=>e.labelId===t.id?e:{...e,labelId:t.id},6:(e,t)=>e.activationTrigger===t.trigger?e:{...e,activationTrigger:t.trigger},7:(e,t)=>{var n;if((null==(n=e.virtual)?void 0:n.options)===t.options)return e;let o=e.activeOptionIndex;if(null!==e.activeOptionIndex){let n=t.options.indexOf(e.virtual.options[e.activeOptionIndex]);o=-1!==n?n:null}return{...e,activeOptionIndex:o,virtual:Object.assign({},e.virtual,{options:t.options})}}},et=(0,u.createContext)(null);function en(e){let t=(0,u.useContext)(et);if(null===t){let t=Error("<".concat(e," /> is missing a parent component."));throw Error.captureStackTrace&&Error.captureStackTrace(t,en),t}return t}et.displayName="ComboboxActionsContext";let eo=(0,u.createContext)(null);function er(e){var t,n,o,r,i,l;let c=el("VirtualProvider"),[d,f]=(0,u.useMemo)(()=>{let e=c.optionsRef.current;if(!e)return[0,0];let t=window.getComputedStyle(e);return[parseFloat(t.paddingBlockStart||t.paddingTop),parseFloat(t.paddingBlockEnd||t.paddingBottom)]},[c.optionsRef.current]),p=(n={scrollPaddingStart:d,scrollPaddingEnd:f,count:c.virtual.options.length,estimateSize:()=>40,getScrollElement(){var e;return null!=(e=c.optionsRef.current)?e:null},overscan:12},o=a({observeElementRect:v,observeElementOffset:b,scrollToFn:h},n),r=u.useReducer(function(){return{}},{})[1],i=a({},o,{onChange:function(e,t){t?(0,s.flushSync)(r):r(),null==o.onChange||o.onChange(e,t)}}),(l=u.useState(function(){return new x(i)})[0]).setOptions(i),u.useEffect(function(){return l._didMount()},[]),I(function(){return l._willUpdate()}),l),[m,g]=(0,u.useState)(0);return(0,C.e)(()=>{g(e=>e+1)},[null==(t=c.virtual)?void 0:t.options]),u.createElement(eo.Provider,{value:p},u.createElement("div",{style:{position:"relative",width:"100%",height:"".concat(p.getTotalSize(),"px")},ref:e=>{e&&(void 0===H||void 0===H.env.JEST_WORKER_ID)&&0!==c.activationTrigger&&null!==c.activeOptionIndex&&c.virtual.options.length>c.activeOptionIndex&&p.scrollToIndex(c.activeOptionIndex)}},p.getVirtualItems().map(t=>{var n;return u.createElement(u.Fragment,{key:t.key},u.cloneElement(null==(n=e.children)?void 0:n.call(e,{option:c.virtual.options[t.index],open:0===c.comboboxState}),{key:"".concat(m,"-").concat(t.key),"data-index":t.index,"aria-setsize":c.virtual.options.length,"aria-posinset":t.index+1,style:{position:"absolute",top:0,left:0,transform:"translateY(".concat(t.start,"px)"),overflowAnchor:"none"}}))})))}let ei=(0,u.createContext)(null);function el(e){let t=(0,u.useContext)(ei);if(null===t){let t=Error("<".concat(e," /> is missing a parent component."));throw Error.captureStackTrace&&Error.captureStackTrace(t,el),t}return t}function ea(e,t){return(0,U.E)(t.type,ee,e,t)}ei.displayName="ComboboxDataContext";let eu=u.Fragment,es=Y.AN.RenderStrategy|Y.AN.Static,ec=(0,Y.yV)(function(e,t){let{value:n,defaultValue:o,onChange:r,form:i,name:l,by:a=null,disabled:s=!1,__demoMode:c=!1,nullable:d=!1,multiple:f=!1,immediate:p=!1,virtual:m=null,...v}=e,[b=f?[]:void 0,g]=(0,S.q)(n,r,o),[h,x]=(0,u.useReducer)(ea,{dataRef:(0,u.createRef)(),comboboxState:c?0:1,options:[],virtual:null,activeOptionIndex:null,activationTrigger:2,labelId:null}),O=(0,u.useRef)(!1),I=(0,u.useRef)({static:!1,hold:!1}),y=(0,u.useRef)(null),E=(0,u.useRef)(null),k=(0,u.useRef)(null),w=(0,u.useRef)(null),z=(0,T.z)("string"==typeof a?(e,t)=>(null==e?void 0:e[a])===(null==t?void 0:t[a]):null!=a?a:(e,t)=>e===t),P=(0,T.z)(e=>h.options.findIndex(t=>z(t.dataRef.current.value,e))),F=(0,u.useCallback)(e=>(0,U.E)(N.mode,{1:()=>b.some(t=>z(t,e)),0:()=>z(b,e)}),[b]),D=(0,T.z)(e=>h.activeOptionIndex===P(e)),N=(0,u.useMemo)(()=>({...h,immediate:!1,optionsPropsRef:I,labelRef:y,inputRef:E,buttonRef:k,optionsRef:w,value:b,defaultValue:o,disabled:s,mode:f?1:0,virtual:h.virtual,get activeOptionIndex(){if(O.current&&null===h.activeOptionIndex&&h.options.length>0){let e=h.options.findIndex(e=>!e.dataRef.current.disabled);if(-1!==e)return e}return h.activeOptionIndex},calculateIndex:P,compare:z,isSelected:F,isActive:D,nullable:d,__demoMode:c}),[b,o,s,f,d,c,h,null]);(0,C.e)(()=>{},[null,void 0]),(0,C.e)(()=>{h.dataRef.current=N},[N]),(0,M.O)([N.buttonRef,N.inputRef,N.optionsRef],()=>Q.closeCombobox(),0===N.comboboxState);let L=(0,u.useMemo)(()=>{var e,t,n;return{open:0===N.comboboxState,disabled:s,activeIndex:N.activeOptionIndex,activeOption:null===N.activeOptionIndex?null:N.virtual?N.virtual.options[null!=(e=N.activeOptionIndex)?e:0]:null!=(n=null==(t=N.options[N.activeOptionIndex])?void 0:t.dataRef.current.value)?n:null,value:b}},[N,s,b]),V=(0,T.z)(()=>{if(null!==N.activeOptionIndex){if(N.virtual)W(N.virtual.options[N.activeOptionIndex]);else{let{dataRef:e}=N.options[N.activeOptionIndex];W(e.current.value)}Q.goToOption(j.T.Specific,N.activeOptionIndex)}}),q=(0,T.z)(()=>{x({type:0}),O.current=!0}),B=(0,T.z)(()=>{x({type:1}),O.current=!1}),Z=(0,T.z)((e,t,n)=>(O.current=!1,e===j.T.Specific?x({type:2,focus:j.T.Specific,idx:t,trigger:n}):x({type:2,focus:e,trigger:n}))),G=(0,T.z)((e,t)=>(x({type:3,payload:{id:e,dataRef:t}}),()=>{N.isActive(t.current.value)&&(O.current=!0),x({type:4,id:e})})),H=(0,T.z)(e=>(x({type:5,id:e}),()=>x({type:5,id:null}))),W=(0,T.z)(e=>(0,U.E)(N.mode,{0:()=>null==g?void 0:g(e),1(){let t=N.value.slice(),n=t.findIndex(t=>z(t,e));return -1===n?t.push(e):t.splice(n,1),null==g?void 0:g(t)}})),J=(0,T.z)(e=>{x({type:6,trigger:e})}),Q=(0,u.useMemo)(()=>({onChange:W,registerOption:G,registerLabel:H,goToOption:Z,closeCombobox:B,openCombobox:q,setActivationTrigger:J,selectActiveOption:V}),[]),X=(0,u.useRef)(null),$=(0,R.G)();return(0,u.useEffect)(()=>{X.current&&void 0!==o&&$.addEventListener(X.current,"reset",()=>{null==g||g(o)})},[X,g]),u.createElement(et.Provider,{value:Q},u.createElement(ei.Provider,{value:N},u.createElement(_.up,{value:(0,U.E)(N.comboboxState,{0:_.ZM.Open,1:_.ZM.Closed})},null!=l&&null!=b&&(0,K.t)({[l]:b}).map((e,t)=>{let[n,o]=e;return u.createElement(A._,{features:A.A.Hidden,ref:0===t?e=>{var t;X.current=null!=(t=null==e?void 0:e.closest("form"))?t:null}:void 0,...(0,Y.oA)({key:n,as:"input",type:"hidden",hidden:!0,readOnly:!0,form:i,name:n,value:o})})}),(0,Y.sY)({ourProps:null===t?{}:{ref:t},theirProps:v,slot:L,defaultTag:eu,name:"Combobox"}))))}),ed=(0,Y.yV)(function(e,t){var n;let o=el("Combobox.Button"),r=en("Combobox.Button"),i=(0,P.T)(o.buttonRef,t),l=(0,E.M)(),{id:a="headlessui-combobox-button-".concat(l),...s}=e,c=(0,R.G)(),d=(0,T.z)(e=>{switch(e.key){case G.R.ArrowDown:return e.preventDefault(),e.stopPropagation(),1===o.comboboxState&&r.openCombobox(),c.nextFrame(()=>{var e;return null==(e=o.inputRef.current)?void 0:e.focus({preventScroll:!0})});case G.R.ArrowUp:return e.preventDefault(),e.stopPropagation(),1===o.comboboxState&&(r.openCombobox(),c.nextFrame(()=>{o.value||r.goToOption(j.T.Last)})),c.nextFrame(()=>{var e;return null==(e=o.inputRef.current)?void 0:e.focus({preventScroll:!0})});case G.R.Escape:return 0!==o.comboboxState?void 0:(e.preventDefault(),o.optionsRef.current&&!o.optionsPropsRef.current.static&&e.stopPropagation(),r.closeCombobox(),c.nextFrame(()=>{var e;return null==(e=o.inputRef.current)?void 0:e.focus({preventScroll:!0})}));default:return}}),f=(0,T.z)(e=>{if((0,V.P)(e.currentTarget))return e.preventDefault();0===o.comboboxState?r.closeCombobox():(e.preventDefault(),r.openCombobox()),c.nextFrame(()=>{var e;return null==(e=o.inputRef.current)?void 0:e.focus({preventScroll:!0})})}),p=(0,y.v)(()=>{if(o.labelId)return[o.labelId,a].join(" ")},[o.labelId,a]),m=(0,u.useMemo)(()=>({open:0===o.comboboxState,disabled:o.disabled,value:o.value}),[o]),v={ref:i,id:a,type:(0,z.f)(e,o.buttonRef),tabIndex:-1,"aria-haspopup":"listbox","aria-controls":null==(n=o.optionsRef.current)?void 0:n.id,"aria-expanded":0===o.comboboxState,"aria-labelledby":p,disabled:o.disabled,onClick:f,onKeyDown:d};return(0,Y.sY)({ourProps:v,theirProps:s,slot:m,defaultTag:"button",name:"Combobox.Button"})}),ef=Object.assign(ec,{Input:(0,Y.yV)(function(e,t){var n,o,r,i,l,a;let s=(0,E.M)(),{id:c="headlessui-combobox-input-".concat(s),onChange:d,displayValue:f,type:p="text",...m}=e,v=el("Combobox.Input"),b=en("Combobox.Input"),g=(0,P.T)(v.inputRef,t),h=(0,w.i)(v.inputRef),x=(0,u.useRef)(!1),O=(0,R.G)(),I=(0,T.z)(()=>{b.onChange(null),v.optionsRef.current&&(v.optionsRef.current.scrollTop=0),b.goToOption(j.T.Nothing)});N((e,t)=>{let[n,o]=e,[r,i]=t;if(x.current)return;let l=v.inputRef.current;l&&((0===i&&1===o||n!==r)&&(l.value=n),requestAnimationFrame(()=>{if(x.current||!l||(null==h?void 0:h.activeElement)!==l)return;let{selectionStart:e,selectionEnd:t}=l;0===Math.abs((null!=t?t:0)-(null!=e?e:0))&&0===e&&l.setSelectionRange(l.value.length,l.value.length)}))},["function"==typeof f&&void 0!==v.value?null!=(a=f(v.value))?a:"":"string"==typeof v.value?v.value:"",v.comboboxState,h]),N((e,t)=>{let[n]=e,[o]=t;if(0===n&&1===o){if(x.current)return;let e=v.inputRef.current;if(!e)return;let t=e.value,{selectionStart:n,selectionEnd:o,selectionDirection:r}=e;e.value="",e.value=t,null!==r?e.setSelectionRange(n,o,r):e.setSelectionRange(n,o)}},[v.comboboxState]);let S=(0,u.useRef)(!1),C=(0,T.z)(()=>{S.current=!0}),k=(0,T.z)(()=>{O.nextFrame(()=>{S.current=!1})}),M=(0,T.z)(e=>{switch(x.current=!0,e.key){case G.R.Enter:if(x.current=!1,0!==v.comboboxState||S.current)return;if(e.preventDefault(),e.stopPropagation(),null===v.activeOptionIndex){b.closeCombobox();return}b.selectActiveOption(),0===v.mode&&b.closeCombobox();break;case G.R.ArrowDown:return x.current=!1,e.preventDefault(),e.stopPropagation(),(0,U.E)(v.comboboxState,{0:()=>b.goToOption(j.T.Next),1:()=>b.openCombobox()});case G.R.ArrowUp:return x.current=!1,e.preventDefault(),e.stopPropagation(),(0,U.E)(v.comboboxState,{0:()=>b.goToOption(j.T.Previous),1:()=>{b.openCombobox(),O.nextFrame(()=>{v.value||b.goToOption(j.T.Last)})}});case G.R.Home:if(e.shiftKey)break;return x.current=!1,e.preventDefault(),e.stopPropagation(),b.goToOption(j.T.First);case G.R.PageUp:return x.current=!1,e.preventDefault(),e.stopPropagation(),b.goToOption(j.T.First);case G.R.End:if(e.shiftKey)break;return x.current=!1,e.preventDefault(),e.stopPropagation(),b.goToOption(j.T.Last);case G.R.PageDown:return x.current=!1,e.preventDefault(),e.stopPropagation(),b.goToOption(j.T.Last);case G.R.Escape:return x.current=!1,0!==v.comboboxState?void 0:(e.preventDefault(),v.optionsRef.current&&!v.optionsPropsRef.current.static&&e.stopPropagation(),v.nullable&&0===v.mode&&null===v.value&&I(),b.closeCombobox());case G.R.Tab:if(x.current=!1,0!==v.comboboxState)return;0===v.mode&&1!==v.activationTrigger&&b.selectActiveOption(),b.closeCombobox()}}),z=(0,T.z)(e=>{null==d||d(e),v.nullable&&0===v.mode&&""===e.target.value&&I(),b.openCombobox()}),F=(0,T.z)(e=>{var t,n,o;let r=null!=(t=e.relatedTarget)?t:L.find(t=>t!==e.currentTarget);if(x.current=!1,!(null!=(n=v.optionsRef.current)&&n.contains(r))&&!(null!=(o=v.buttonRef.current)&&o.contains(r))&&0===v.comboboxState)return e.preventDefault(),0===v.mode&&(v.nullable&&null===v.value?I():1!==v.activationTrigger&&b.selectActiveOption()),b.closeCombobox()}),D=(0,T.z)(e=>{var t,n,o;let r=null!=(t=e.relatedTarget)?t:L.find(t=>t!==e.currentTarget);null!=(n=v.buttonRef.current)&&n.contains(r)||null!=(o=v.optionsRef.current)&&o.contains(r)||v.disabled||v.immediate&&0!==v.comboboxState&&(b.openCombobox(),O.nextFrame(()=>{b.setActivationTrigger(1)}))}),A=(0,y.v)(()=>{if(v.labelId)return[v.labelId].join(" ")},[v.labelId]),_=(0,u.useMemo)(()=>({open:0===v.comboboxState,disabled:v.disabled}),[v]),V={ref:g,id:c,role:"combobox",type:p,"aria-controls":null==(n=v.optionsRef.current)?void 0:n.id,"aria-expanded":0===v.comboboxState,"aria-activedescendant":null===v.activeOptionIndex?void 0:v.virtual?null==(o=v.options.find(e=>{var t;return!(null!=(t=v.virtual)&&t.disabled(e.dataRef.current.value))&&v.compare(e.dataRef.current.value,v.virtual.options[v.activeOptionIndex])}))?void 0:o.id:null==(r=v.options[v.activeOptionIndex])?void 0:r.id,"aria-labelledby":A,"aria-autocomplete":"list",defaultValue:null!=(l=null!=(i=e.defaultValue)?i:void 0!==v.defaultValue?null==f?void 0:f(v.defaultValue):null)?l:v.defaultValue,disabled:v.disabled,onCompositionStart:C,onCompositionEnd:k,onKeyDown:M,onChange:z,onFocus:D,onBlur:F};return(0,Y.sY)({ourProps:V,theirProps:m,slot:_,defaultTag:"input",name:"Combobox.Input"})}),Button:ed,Label:(0,Y.yV)(function(e,t){let n=(0,E.M)(),{id:o="headlessui-combobox-label-".concat(n),...r}=e,i=el("Combobox.Label"),l=en("Combobox.Label"),a=(0,P.T)(i.labelRef,t);(0,C.e)(()=>l.registerLabel(o),[o]);let s=(0,T.z)(()=>{var e;return null==(e=i.inputRef.current)?void 0:e.focus({preventScroll:!0})}),c=(0,u.useMemo)(()=>({open:0===i.comboboxState,disabled:i.disabled}),[i]);return(0,Y.sY)({ourProps:{ref:a,id:o,onClick:s},theirProps:r,slot:c,defaultTag:"label",name:"Combobox.Label"})}),Options:(0,Y.yV)(function(e,t){let n=(0,E.M)(),{id:o="headlessui-combobox-options-".concat(n),hold:r=!1,...i}=e,l=el("Combobox.Options"),a=(0,P.T)(l.optionsRef,t),s=(0,_.oJ)(),c=null!==s?(s&_.ZM.Open)===_.ZM.Open:0===l.comboboxState;(0,C.e)(()=>{var t;l.optionsPropsRef.current.static=null!=(t=e.static)&&t},[l.optionsPropsRef,e.static]),(0,C.e)(()=>{l.optionsPropsRef.current.hold=r},[l.optionsPropsRef,r]),function(e){let{container:t,accept:n,walk:o,enabled:r=!0}=e,i=(0,u.useRef)(n),l=(0,u.useRef)(o);(0,u.useEffect)(()=>{i.current=n,l.current=o},[n,o]),(0,C.e)(()=>{if(!t||!r)return;let e=(0,D.r)(t);if(!e)return;let n=i.current,o=l.current,a=Object.assign(e=>n(e),{acceptNode:n}),u=e.createTreeWalker(t,NodeFilter.SHOW_ELEMENT,a,!1);for(;u.nextNode();)o(u.currentNode)},[t,r,i,l])}({container:l.optionsRef.current,enabled:0===l.comboboxState,accept:e=>"option"===e.getAttribute("role")?NodeFilter.FILTER_REJECT:e.hasAttribute("role")?NodeFilter.FILTER_SKIP:NodeFilter.FILTER_ACCEPT,walk(e){e.setAttribute("role","none")}});let d=(0,y.v)(()=>{var e,t;return null!=(t=l.labelId)?t:null==(e=l.buttonRef.current)?void 0:e.id},[l.labelId,l.buttonRef.current]),f=(0,u.useMemo)(()=>({open:0===l.comboboxState,option:void 0}),[l]),p={"aria-labelledby":d,role:"listbox","aria-multiselectable":1===l.mode||void 0,id:o,ref:a};return l.virtual&&0===l.comboboxState&&Object.assign(i,{children:u.createElement(er,null,i.children)}),(0,Y.sY)({ourProps:p,theirProps:i,slot:f,defaultTag:"ul",features:es,visible:c,name:"Combobox.Options"})}),Option:(0,Y.yV)(function(e,t){var n;let o=(0,E.M)(),{id:r="headlessui-combobox-option-".concat(o),disabled:i=!1,value:l,order:a=null,...s}=e,c=el("Combobox.Option"),d=en("Combobox.Option"),f=c.virtual?c.activeOptionIndex===c.calculateIndex(l):null!==c.activeOptionIndex&&(null==(n=c.options[c.activeOptionIndex])?void 0:n.id)===r,p=c.isSelected(l),m=(0,u.useRef)(null),v=(0,k.E)({disabled:i,value:l,domRef:m,order:a}),b=(0,u.useContext)(eo),g=(0,P.T)(t,m,b?b.measureElement:null),h=(0,T.z)(()=>d.onChange(l));(0,C.e)(()=>d.registerOption(r,v),[v,r]);let x=(0,u.useRef)(!(c.virtual||c.__demoMode));(0,C.e)(()=>{if(!c.virtual||!c.__demoMode)return;let e=(0,q.k)();return e.requestAnimationFrame(()=>{x.current=!0}),e.dispose},[c.virtual,c.__demoMode]),(0,C.e)(()=>{if(!x.current||0!==c.comboboxState||!f||0===c.activationTrigger)return;let e=(0,q.k)();return e.requestAnimationFrame(()=>{var e,t;null==(t=null==(e=m.current)?void 0:e.scrollIntoView)||t.call(e,{block:"nearest"})}),e.dispose},[m,f,c.comboboxState,c.activationTrigger,c.activeOptionIndex]);let O=(0,T.z)(e=>{var t;if(i||null!=(t=c.virtual)&&t.disabled(l))return e.preventDefault();h(),(0,Z.tq)()||requestAnimationFrame(()=>{var e;return null==(e=c.inputRef.current)?void 0:e.focus({preventScroll:!0})}),0===c.mode&&requestAnimationFrame(()=>d.closeCombobox())}),I=(0,T.z)(()=>{var e;if(i||null!=(e=c.virtual)&&e.disabled(l))return d.goToOption(j.T.Nothing);let t=c.calculateIndex(l);d.goToOption(j.T.Specific,t)}),y=(0,F.g)(),S=(0,T.z)(e=>y.update(e)),R=(0,T.z)(e=>{var t;if(!y.wasMoved(e)||i||null!=(t=c.virtual)&&t.disabled(l)||f)return;let n=c.calculateIndex(l);d.goToOption(j.T.Specific,n,0)}),M=(0,T.z)(e=>{var t;y.wasMoved(e)&&(i||null!=(t=c.virtual)&&t.disabled(l)||f&&(c.optionsPropsRef.current.hold||d.goToOption(j.T.Nothing)))}),w=(0,u.useMemo)(()=>({active:f,selected:p,disabled:i}),[f,p,i]);return(0,Y.sY)({ourProps:{id:r,ref:g,role:"option",tabIndex:!0===i?void 0:-1,"aria-disabled":!0===i||void 0,"aria-selected":p,disabled:void 0,onClick:O,onFocus:I,onPointerEnter:S,onMouseEnter:S,onPointerMove:R,onMouseMove:R,onPointerLeave:M,onMouseLeave:M},theirProps:s,slot:w,defaultTag:"li",name:"Combobox.Option"})})})}}]); \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/131-66e1fb73fd8f2361.js b/litellm/proxy/_experimental/out/_next/static/chunks/131-2eebef89ebe87d26.js similarity index 100% rename from litellm/proxy/_experimental/out/_next/static/chunks/131-66e1fb73fd8f2361.js rename to litellm/proxy/_experimental/out/_next/static/chunks/131-2eebef89ebe87d26.js diff --git a/litellm/proxy/_experimental/out/_next/static/chunks/1486-75f734aab34a8112.js b/litellm/proxy/_experimental/out/_next/static/chunks/1486-75f734aab34a8112.js new file mode 100644 index 00000000000..fdf73cd3d09 --- /dev/null +++ b/litellm/proxy/_experimental/out/_next/static/chunks/1486-75f734aab34a8112.js @@ -0,0 +1 @@ +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1486],{12660:function(e,t,r){"use strict";r.d(t,{Z:function(){return l}});var n=r(1119),o=r(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M917.7 148.8l-42.4-42.4c-1.6-1.6-3.6-2.3-5.7-2.3s-4.1.8-5.7 2.3l-76.1 76.1a199.27 199.27 0 00-112.1-34.3c-51.2 0-102.4 19.5-141.5 58.6L432.3 308.7a8.03 8.03 0 000 11.3L704 591.7c1.6 1.6 3.6 2.3 5.7 2.3 2 0 4.1-.8 5.7-2.3l101.9-101.9c68.9-69 77-175.7 24.3-253.5l76.1-76.1c3.1-3.2 3.1-8.3 0-11.4zM769.1 441.7l-59.4 59.4-186.8-186.8 59.4-59.4c24.9-24.9 58.1-38.7 93.4-38.7 35.3 0 68.4 13.7 93.4 38.7 24.9 24.9 38.7 58.1 38.7 93.4 0 35.3-13.8 68.4-38.7 93.4zm-190.2 105a8.03 8.03 0 00-11.3 0L501 613.3 410.7 523l66.7-66.7c3.1-3.1 3.1-8.2 0-11.3L441 408.6a8.03 8.03 0 00-11.3 0L363 475.3l-43-43a7.85 7.85 0 00-5.7-2.3c-2 0-4.1.8-5.7 2.3L206.8 534.2c-68.9 69-77 175.7-24.3 253.5l-76.1 76.1a8.03 8.03 0 000 11.3l42.4 42.4c1.6 1.6 3.6 2.3 5.7 2.3s4.1-.8 5.7-2.3l76.1-76.1c33.7 22.9 72.9 34.3 112.1 34.3 51.2 0 102.4-19.5 141.5-58.6l101.9-101.9c3.1-3.1 3.1-8.2 0-11.3l-43-43 66.7-66.7c3.1-3.1 3.1-8.2 0-11.3l-36.6-36.2zM441.7 769.1a131.32 131.32 0 01-93.4 38.7c-35.3 0-68.4-13.7-93.4-38.7a131.32 131.32 0 01-38.7-93.4c0-35.3 13.7-68.4 38.7-93.4l59.4-59.4 186.8 186.8-59.4 59.4z"}}]},name:"api",theme:"outlined"},i=r(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,n.Z)({},e,{ref:t,icon:a}))})},5540:function(e,t,r){"use strict";r.d(t,{Z:function(){return l}});var n=r(1119),o=r(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"}},{tag:"path",attrs:{d:"M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"}}]},name:"clock-circle",theme:"outlined"},i=r(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,n.Z)({},e,{ref:t,icon:a}))})},78355:function(e,t,r){"use strict";r.d(t,{Z:function(){return l}});var n=r(1119),o=r(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M893.3 293.3L730.7 130.7c-7.5-7.5-16.7-13-26.7-16V112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V338.5c0-17-6.7-33.2-18.7-45.2zM384 184h256v104H384V184zm456 656H184V184h136v136c0 17.7 14.3 32 32 32h320c17.7 0 32-14.3 32-32V205.8l136 136V840zM512 442c-79.5 0-144 64.5-144 144s64.5 144 144 144 144-64.5 144-144-64.5-144-144-144zm0 224c-44.2 0-80-35.8-80-80s35.8-80 80-80 80 35.8 80 80-35.8 80-80 80z"}}]},name:"save",theme:"outlined"},i=r(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,n.Z)({},e,{ref:t,icon:a}))})},8881:function(e,t,r){"use strict";r.d(t,{Z:function(){return l}});var n=r(1119),o=r(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372 0-89 31.3-170.8 83.5-234.8l523.3 523.3C682.8 852.7 601 884 512 884zm288.5-137.2L277.2 223.5C341.2 171.3 423 140 512 140c205.4 0 372 166.6 372 372 0 89-31.3 170.8-83.5 234.8z"}}]},name:"stop",theme:"outlined"},i=r(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,n.Z)({},e,{ref:t,icon:a}))})},35291:function(e,t,r){"use strict";r.d(t,{Z:function(){return l}});var n=r(1119),o=r(2265),a={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"}}]},name:"warning",theme:"outlined"},i=r(55015),l=o.forwardRef(function(e,t){return o.createElement(i.Z,(0,n.Z)({},e,{ref:t,icon:a}))})},59664:function(e,t,r){"use strict";r.d(t,{Z:function(){return S}});var n=r(5853),o=r(2265),a=r(47625),i=r(93765),l=r(54061),s=r(97059),c=r(62994),u=r(25311),d=(0,i.z)({chartName:"LineChart",GraphicalChild:l.x,axisComponents:[{axisType:"xAxis",AxisComp:s.K},{axisType:"yAxis",AxisComp:c.B}],formatAxisMap:u.t9}),p=r(56940),m=r(8147),f=r(22190),h=r(81889),g=r(65278),v=r(98593),y=r(69448),b=r(32644),k=r(7084),x=r(26898),w=r(97324),C=r(1153);let S=o.forwardRef((e,t)=>{let{data:r=[],categories:i=[],index:u,colors:S=x.s,valueFormatter:E=C.Cj,startEndOnly:_=!1,showXAxis:O=!0,showYAxis:j=!0,yAxisWidth:z=56,intervalType:N="equidistantPreserveStart",animationDuration:T=900,showAnimation:L=!1,showTooltip:Z=!0,showLegend:P=!0,showGridLines:R=!0,autoMinValue:F=!1,curveType:M="linear",minValue:A,maxValue:B,connectNulls:I=!1,allowDecimals:q=!0,noDataText:D,className:W,onValueChange:V,enableLegendSlider:K=!1,customTooltip:H,rotateLabelX:G,tickGap:Y=5}=e,X=(0,n._T)(e,["data","categories","index","colors","valueFormatter","startEndOnly","showXAxis","showYAxis","yAxisWidth","intervalType","animationDuration","showAnimation","showTooltip","showLegend","showGridLines","autoMinValue","curveType","minValue","maxValue","connectNulls","allowDecimals","noDataText","className","onValueChange","enableLegendSlider","customTooltip","rotateLabelX","tickGap"]),U=O||j?20:0,[$,Q]=(0,o.useState)(60),[J,ee]=(0,o.useState)(void 0),[et,er]=(0,o.useState)(void 0),en=(0,b.me)(i,S),eo=(0,b.i4)(F,A,B),ea=!!V;function ei(e){ea&&(e===et&&!J||(0,b.FB)(r,e)&&J&&J.dataKey===e?(er(void 0),null==V||V(null)):(er(e),null==V||V({eventType:"category",categoryClicked:e})),ee(void 0))}return o.createElement("div",Object.assign({ref:t,className:(0,w.q)("w-full h-80",W)},X),o.createElement(a.h,{className:"h-full w-full"},(null==r?void 0:r.length)?o.createElement(d,{data:r,onClick:ea&&(et||J)?()=>{ee(void 0),er(void 0),null==V||V(null)}:void 0},R?o.createElement(p.q,{className:(0,w.q)("stroke-1","stroke-tremor-border","dark:stroke-dark-tremor-border"),horizontal:!0,vertical:!1}):null,o.createElement(s.K,{padding:{left:U,right:U},hide:!O,dataKey:u,interval:_?"preserveStartEnd":N,tick:{transform:"translate(0, 6)"},ticks:_?[r[0][u],r[r.length-1][u]]:void 0,fill:"",stroke:"",className:(0,w.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),tickLine:!1,axisLine:!1,minTickGap:Y,angle:null==G?void 0:G.angle,dy:null==G?void 0:G.verticalShift,height:null==G?void 0:G.xAxisHeight}),o.createElement(c.B,{width:z,hide:!j,axisLine:!1,tickLine:!1,type:"number",domain:eo,tick:{transform:"translate(-3, 0)"},fill:"",stroke:"",className:(0,w.q)("text-tremor-label","fill-tremor-content","dark:fill-dark-tremor-content"),tickFormatter:E,allowDecimals:q}),o.createElement(m.u,{wrapperStyle:{outline:"none"},isAnimationActive:!1,cursor:{stroke:"#d1d5db",strokeWidth:1},content:Z?e=>{let{active:t,payload:r,label:n}=e;return H?o.createElement(H,{payload:null==r?void 0:r.map(e=>{var t;return Object.assign(Object.assign({},e),{color:null!==(t=en.get(e.dataKey))&&void 0!==t?t:k.fr.Gray})}),active:t,label:n}):o.createElement(v.ZP,{active:t,payload:r,label:n,valueFormatter:E,categoryColors:en})}:o.createElement(o.Fragment,null),position:{y:0}}),P?o.createElement(f.D,{verticalAlign:"top",height:$,content:e=>{let{payload:t}=e;return(0,g.Z)({payload:t},en,Q,et,ea?e=>ei(e):void 0,K)}}):null,i.map(e=>{var t;return o.createElement(l.x,{className:(0,w.q)((0,C.bM)(null!==(t=en.get(e))&&void 0!==t?t:k.fr.Gray,x.K.text).strokeColor),strokeOpacity:J||et&&et!==e?.3:1,activeDot:e=>{var t;let{cx:n,cy:a,stroke:i,strokeLinecap:l,strokeLinejoin:s,strokeWidth:c,dataKey:u}=e;return o.createElement(h.o,{className:(0,w.q)("stroke-tremor-background dark:stroke-dark-tremor-background",V?"cursor-pointer":"",(0,C.bM)(null!==(t=en.get(u))&&void 0!==t?t:k.fr.Gray,x.K.text).fillColor),cx:n,cy:a,r:5,fill:"",stroke:i,strokeLinecap:l,strokeLinejoin:s,strokeWidth:c,onClick:(t,n)=>{n.stopPropagation(),ea&&(e.index===(null==J?void 0:J.index)&&e.dataKey===(null==J?void 0:J.dataKey)||(0,b.FB)(r,e.dataKey)&&et&&et===e.dataKey?(er(void 0),ee(void 0),null==V||V(null)):(er(e.dataKey),ee({index:e.index,dataKey:e.dataKey}),null==V||V(Object.assign({eventType:"dot",categoryClicked:e.dataKey},e.payload))))}})},dot:t=>{var n;let{stroke:a,strokeLinecap:i,strokeLinejoin:l,strokeWidth:s,cx:c,cy:u,dataKey:d,index:p}=t;return(0,b.FB)(r,e)&&!(J||et&&et!==e)||(null==J?void 0:J.index)===p&&(null==J?void 0:J.dataKey)===e?o.createElement(h.o,{key:p,cx:c,cy:u,r:5,stroke:a,fill:"",strokeLinecap:i,strokeLinejoin:l,strokeWidth:s,className:(0,w.q)("stroke-tremor-background dark:stroke-dark-tremor-background",V?"cursor-pointer":"",(0,C.bM)(null!==(n=en.get(d))&&void 0!==n?n:k.fr.Gray,x.K.text).fillColor)}):o.createElement(o.Fragment,{key:p})},key:e,name:e,type:M,dataKey:e,stroke:"",strokeWidth:2,strokeLinejoin:"round",strokeLinecap:"round",isAnimationActive:L,animationDuration:T,connectNulls:I})}),V?i.map(e=>o.createElement(l.x,{className:(0,w.q)("cursor-pointer"),strokeOpacity:0,key:e,name:e,type:M,dataKey:e,stroke:"transparent",fill:"transparent",legendType:"none",tooltipType:"none",strokeWidth:12,connectNulls:I,onClick:(e,t)=>{t.stopPropagation();let{name:r}=e;ei(r)}})):null):o.createElement(y.Z,{noDataText:D})))});S.displayName="LineChart"},92858:function(e,t,r){"use strict";r.d(t,{Z:function(){return N}});var n=r(5853),o=r(2265),a=r(62963),i=r(90945),l=r(13323),s=r(17684),c=r(80004),u=r(93689),d=r(38198),p=r(47634),m=r(56314),f=r(27847),h=r(64518);let g=(0,o.createContext)(null),v=Object.assign((0,f.yV)(function(e,t){let r=(0,s.M)(),{id:n="headlessui-description-".concat(r),...a}=e,i=function e(){let t=(0,o.useContext)(g);if(null===t){let t=Error("You used a component, but it is not inside a relevant parent.");throw Error.captureStackTrace&&Error.captureStackTrace(t,e),t}return t}(),l=(0,u.T)(t);(0,h.e)(()=>i.register(n),[n,i.register]);let c={ref:l,...i.props,id:n};return(0,f.sY)({ourProps:c,theirProps:a,slot:i.slot||{},defaultTag:"p",name:i.name||"Description"})}),{});var y=r(37388);let b=(0,o.createContext)(null),k=Object.assign((0,f.yV)(function(e,t){let r=(0,s.M)(),{id:n="headlessui-label-".concat(r),passive:a=!1,...i}=e,l=function e(){let t=(0,o.useContext)(b);if(null===t){let t=Error("You used a