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/cookbook/litellm_proxy_server/secret_manager/custom_secret_manager_config.yaml b/cookbook/litellm_proxy_server/secret_manager/custom_secret_manager_config.yaml new file mode 100644 index 00000000000..3598a9b1b65 --- /dev/null +++ b/cookbook/litellm_proxy_server/secret_manager/custom_secret_manager_config.yaml @@ -0,0 +1,20 @@ +general_settings: + master_key: os.environ/LITELLM_MASTER_KEY + key_management_system: "custom" + key_management_settings: + custom_secret_manager: my_secret_manager.InMemorySecretManager + store_virtual_keys: true + prefix_for_stored_virtual_keys: "litellm/" + access_mode: "read_and_write" + +model_list: + - model_name: gpt-4 + litellm_params: + model: openai/gpt-4 + api_key: os.environ/OPENAI_API_KEY # Read from custom secret manager + + - model_name: claude-3-5-sonnet + litellm_params: + model: anthropic/claude-3-5-sonnet-20241022 + api_key: os.environ/ANTHROPIC_API_KEY # Read from custom secret manager + diff --git a/cookbook/litellm_proxy_server/secret_manager/my_secret_manager.py b/cookbook/litellm_proxy_server/secret_manager/my_secret_manager.py new file mode 100644 index 00000000000..b3c1bf608e2 --- /dev/null +++ b/cookbook/litellm_proxy_server/secret_manager/my_secret_manager.py @@ -0,0 +1,79 @@ +""" +Example custom secret manager for LiteLLM Proxy. + +This is a simple in-memory secret manager for testing purposes. +In production, replace this with your actual secret management system. +""" + +from typing import Optional, Union + +import httpx + +from litellm.integrations.custom_secret_manager import CustomSecretManager + + +class InMemorySecretManager(CustomSecretManager): + def __init__(self): + super().__init__(secret_manager_name="in_memory_secrets") + # Store your secrets in memory + print("INITIALIZING CUSTOM SECRET MANAGER IN MEMORY") + self.secrets = {} + print("CUSTOM SECRET MANAGER IN MEMORY INITIALIZED") + + async def async_read_secret( + self, + secret_name: str, + optional_params: Optional[dict] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + ) -> Optional[str]: + """Read secret asynchronously""" + print("READING SECRET ASYNCHRONOUSLY") + print("SECRET NAME: %s", secret_name) + print("SECRET: %s", self.secrets.get(secret_name)) + return self.secrets.get(secret_name) + + def sync_read_secret( + self, + secret_name: str, + optional_params: Optional[dict] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + ) -> Optional[str]: + """Read secret synchronously""" + from litellm._logging import verbose_proxy_logger + + verbose_proxy_logger.info(f"CUSTOM SECRET MANAGER: LOOKING FOR SECRET: {secret_name}") + value = self.secrets.get(secret_name) + verbose_proxy_logger.info(f"CUSTOM SECRET MANAGER: READ SECRET: {value}") + return value + + async def async_write_secret( + self, + secret_name: str, + secret_value: str, + description: Optional[str] = None, + optional_params: Optional[dict] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + tags: Optional[Union[dict, list]] = None, + ) -> dict: + """Write a secret to the in-memory store""" + self.secrets[secret_name] = secret_value + print("ALL SECRETS=%s", self.secrets) + return { + "status": "success", + "secret_name": secret_name, + "description": description, + } + + async def async_delete_secret( + self, + secret_name: str, + recovery_window_in_days: Optional[int] = 7, + optional_params: Optional[dict] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + ) -> dict: + """Delete a secret from the in-memory store""" + if secret_name in self.secrets: + del self.secrets[secret_name] + return {"status": "deleted", "secret_name": secret_name} + return {"status": "not_found", "secret_name": secret_name} + 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 4178724e6e4..0cbdf761fe8 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -8,16 +8,36 @@ ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/python:latest-dev FROM $LITELLM_BUILD_IMAGE AS builder WORKDIR /app -# Install build dependencies +# Install build dependencies including Node.js for UI build USER root -RUN apk add --no-cache build-base bash \ +RUN apk add --no-cache build-base bash nodejs npm \ && pip install --no-cache-dir --upgrade pip build # Copy project files COPY . . +# Set LITELLM_NON_ROOT flag for build time +ENV LITELLM_NON_ROOT=true + # Build Admin UI -RUN chmod +x docker/build_admin_ui.sh && ./docker/build_admin_ui.sh +RUN mkdir -p /tmp/litellm_ui && \ + cd ui/litellm-dashboard && \ + if [ -f "../../enterprise/enterprise_ui/enterprise_colors.json" ]; then \ + cp ../../enterprise/enterprise_ui/enterprise_colors.json ./ui_colors.json; \ + fi && \ + npm install && \ + npm run build && \ + cp -r ./out/* /tmp/litellm_ui/ && \ + cd /tmp/litellm_ui && \ + for html_file in *.html; do \ + if [ "$html_file" != "index.html" ] && [ -f "$html_file" ]; then \ + folder_name="${html_file%.html}" && \ + mkdir -p "$folder_name" && \ + mv "$html_file" "$folder_name/index.html"; \ + fi; \ + done && \ + cd /app/ui/litellm-dashboard && \ + rm -rf ./out # Build package and wheel dependencies RUN rm -rf dist/* && python -m build && \ @@ -42,12 +62,17 @@ COPY --from=builder /app/docker/supervisord.conf /etc/supervisord.conf COPY --from=builder /app/schema.prisma /app/schema.prisma COPY --from=builder /app/dist/*.whl . COPY --from=builder /wheels/ /wheels/ +COPY --from=builder /tmp/litellm_ui /tmp/litellm_ui # Install package from wheel and dependencies 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 @@ -56,7 +81,6 @@ RUN pip uninstall jwt -y && \ pip uninstall PyJWT -y && \ pip install PyJWT==2.9.0 --no-cache-dir -# --- Prisma Handling for Non-Root User --- # Set Prisma cache directories ENV PRISMA_BINARY_CACHE_DIR=/nonexistent ENV NPM_CONFIG_CACHE=/.npm @@ -68,25 +92,20 @@ RUN pip install --no-cache-dir prisma && \ # Create directories and set permissions for non-root user RUN mkdir -p /nonexistent /.npm && \ - chown -R nobody:nogroup /app && \ - chown -R nobody:nogroup /nonexistent /.npm && \ + chown -R nobody:nogroup /app /tmp/litellm_ui /nonexistent /.npm && \ PRISMA_PATH=$(python -c "import os, prisma; print(os.path.dirname(prisma.__file__))") && \ chown -R nobody:nogroup $PRISMA_PATH && \ LITELLM_PKG_MIGRATIONS_PATH="$(python -c 'import os, litellm_proxy_extras; print(os.path.dirname(litellm_proxy_extras.__file__))' 2>/dev/null || echo '')/migrations" && \ [ -n "$LITELLM_PKG_MIGRATIONS_PATH" ] && chown -R nobody:nogroup $LITELLM_PKG_MIGRATIONS_PATH -# --- OpenShift Compatibility: Apply Red Hat recommended pattern --- -# Get paths for directories that need write access at runtime +# OpenShift compatibility RUN PRISMA_PATH=$(python -c "import os, prisma; print(os.path.dirname(prisma.__file__))") && \ LITELLM_PROXY_EXTRAS_PATH=$(python -c "import os, litellm_proxy_extras; print(os.path.dirname(litellm_proxy_extras.__file__))" 2>/dev/null || echo "") && \ - # Set group ownership to 0 (root group) for OpenShift compatibility && \ - chgrp -R 0 $PRISMA_PATH && \ + chgrp -R 0 $PRISMA_PATH /tmp/litellm_ui && \ [ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chgrp -R 0 $LITELLM_PROXY_EXTRAS_PATH || true && \ - # Mirror owner permissions to group (g=u) as recommended by Red Hat && \ - chmod -R g=u $PRISMA_PATH && \ + chmod -R g=u $PRISMA_PATH /tmp/litellm_ui && \ [ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chmod -R g=u $LITELLM_PROXY_EXTRAS_PATH || true && \ - # Ensure directories are writable by group && \ - chmod -R g+w $PRISMA_PATH && \ + chmod -R g+w $PRISMA_PATH /tmp/litellm_ui && \ [ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chmod -R g+w $LITELLM_PROXY_EXTRAS_PATH || true # Switch to non-root user @@ -94,14 +113,14 @@ USER nobody # Set HOME for prisma generate to have a writable directory ENV HOME=/app + +# Set LITELLM_NON_ROOT flag for runtime +ENV LITELLM_NON_ROOT=true + RUN prisma generate -# --- End of Prisma Handling --- EXPOSE 4000/tcp -# Set entrypoint and command ENTRYPOINT ["/app/docker/prod_entrypoint.sh"] -# Append "--detailed_debug" to the end of CMD to view detailed debug logs -# CMD ["--port", "4000", "--detailed_debug"] -CMD ["--port", "4000"] +CMD ["--port", "4000"] \ No newline at end of file diff --git a/docs/my-website/docs/benchmarks.md b/docs/my-website/docs/benchmarks.md index f60fa4fcd14..f00732450d1 100644 --- a/docs/my-website/docs/benchmarks.md +++ b/docs/my-website/docs/benchmarks.md @@ -122,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/containers.md b/docs/my-website/docs/containers.md index 367308eb005..597e0e2e4c6 100644 --- a/docs/my-website/docs/containers.md +++ b/docs/my-website/docs/containers.md @@ -1,6 +1,3 @@ -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - # /containers Manage OpenAI code interpreter containers (sessions) for executing code in isolated environments. @@ -14,17 +11,15 @@ Manage OpenAI code interpreter containers (sessions) for executing code in isola | Spend Management | ✅ Budget tracking and rate limiting | | Supported Providers | `openai`| -## **Supported Providers**: -- [OpenAI](#quick-start) - -## Quick Start +:::tip Containers provide isolated execution environments for code interpreter sessions. You can create, list, retrieve, and delete containers. -### SDK, PROXY, and OpenAI Client +::: - - +## **LiteLLM Python SDK Usage** + +### Quick Start **Create a Container** @@ -46,22 +41,33 @@ container = litellm.create_container( print(f"Container ID: {container.id}") print(f"Container Name: {container.name}") - -### ASYNC USAGE ### -# container = await litellm.acreate_container( -# name="My Code Interpreter Container", -# custom_llm_provider="openai", -# expires_after={ -# "anchor": "last_active_at", -# "minutes": 20 -# } -# ) ``` -**List Containers** +### Async Usage ```python -from litellm import list_containers, alist_containers +from litellm import acreate_container +import os + +os.environ["OPENAI_API_KEY"] = "sk-.." + +container = await acreate_container( + name="My Code Interpreter Container", + custom_llm_provider="openai", + expires_after={ + "anchor": "last_active_at", + "minutes": 20 + } +) + +print(f"Container ID: {container.id}") +print(f"Container Name: {container.name}") +``` + +### List Containers + +```python +from litellm import list_containers import os os.environ["OPENAI_API_KEY"] = "sk-.." @@ -75,19 +81,28 @@ containers = list_containers( print(f"Found {len(containers.data)} containers") for container in containers.data: print(f" - {container.id}: {container.name}") - -### ASYNC USAGE ### -# containers = await alist_containers( -# custom_llm_provider="openai", -# limit=20, -# order="desc" -# ) ``` -**Retrieve a Container** +**Async Usage:** ```python -from litellm import retrieve_container, aretrieve_container +from litellm import alist_containers + +containers = await alist_containers( + custom_llm_provider="openai", + limit=20, + order="desc" +) + +print(f"Found {len(containers.data)} containers") +for container in containers.data: + print(f" - {container.id}: {container.name}") +``` + +### Retrieve a Container + +```python +from litellm import retrieve_container import os os.environ["OPENAI_API_KEY"] = "sk-.." @@ -100,18 +115,27 @@ container = retrieve_container( print(f"Container: {container.name}") print(f"Status: {container.status}") print(f"Created: {container.created_at}") - -### ASYNC USAGE ### -# container = await aretrieve_container( -# container_id="cntr_123...", -# custom_llm_provider="openai" -# ) ``` -**Delete a Container** +**Async Usage:** ```python -from litellm import delete_container, adelete_container +from litellm import aretrieve_container + +container = await aretrieve_container( + container_id="cntr_123...", + custom_llm_provider="openai" +) + +print(f"Container: {container.name}") +print(f"Status: {container.status}") +print(f"Created: {container.created_at}") +``` + +### Delete a Container + +```python +from litellm import delete_container import os os.environ["OPENAI_API_KEY"] = "sk-.." @@ -123,16 +147,30 @@ result = delete_container( print(f"Deleted: {result.deleted}") print(f"Container ID: {result.id}") - -### ASYNC USAGE ### -# result = await adelete_container( -# container_id="cntr_123...", -# custom_llm_provider="openai" -# ) ``` - - +**Async Usage:** + +```python +from litellm import adelete_container + +result = await adelete_container( + container_id="cntr_123...", + custom_llm_provider="openai" +) + +print(f"Deleted: {result.deleted}") +print(f"Container ID: {result.id}") +``` + +## **LiteLLM Proxy Usage** + +LiteLLM provides OpenAI API compatible container endpoints for managing code interpreter sessions: + +- `/v1/containers` - Create and list containers +- `/v1/containers/{container_id}` - Retrieve and delete containers + +**Setup** ```bash $ export OPENAI_API_KEY="sk-..." @@ -208,10 +246,13 @@ curl -X DELETE "http://localhost:4000/v1/containers/cntr_123..." \ -H "Authorization: Bearer sk-1234" ``` - - +## **Using OpenAI Client with LiteLLM Proxy** -**Setup** +You can use the standard OpenAI Python client to interact with LiteLLM's container endpoints. This provides a familiar interface while leveraging LiteLLM's proxy features. + +### Setup + +First, configure your OpenAI client to point to your LiteLLM proxy: ```python from openai import OpenAI @@ -222,7 +263,7 @@ client = OpenAI( ) ``` -**Create a Container** +### Create a Container ```python container = client.containers.create( @@ -239,7 +280,7 @@ print(f"Container Name: {container.name}") print(f"Created at: {container.created_at}") ``` -**List Containers** +### List Containers ```python containers = client.containers.list( @@ -252,7 +293,7 @@ for container in containers.data: print(f" - {container.id}: {container.name}") ``` -**Retrieve a Container** +### Retrieve a Container ```python container = client.containers.retrieve( @@ -265,7 +306,7 @@ print(f"Status: {container.status}") print(f"Last active: {container.last_active_at}") ``` -**Delete a Container** +### Delete a Container ```python result = client.containers.delete( @@ -277,8 +318,62 @@ print(f"Deleted: {result.deleted}") print(f"Container ID: {result.id}") ``` - - +### Complete Workflow Example + +Here's a complete example showing the full container management workflow: + +```python +from openai import OpenAI + +# Initialize client +client = OpenAI( + api_key="sk-1234", + base_url="http://localhost:4000" +) + +# 1. Create a container +print("Creating container...") +container = client.containers.create( + name="My Code Interpreter Session", + expires_after={ + "anchor": "last_active_at", + "minutes": 20 + }, + extra_body={"custom_llm_provider": "openai"} +) + +container_id = container.id +print(f"Container created. ID: {container_id}") + +# 2. List all containers +print("\nListing containers...") +containers = client.containers.list( + extra_body={"custom_llm_provider": "openai"} +) + +for c in containers.data: + print(f" - {c.id}: {c.name} (Status: {c.status})") + +# 3. Retrieve specific container +print(f"\nRetrieving container {container_id}...") +retrieved = client.containers.retrieve( + container_id=container_id, + extra_body={"custom_llm_provider": "openai"} +) + +print(f"Container: {retrieved.name}") +print(f"Status: {retrieved.status}") +print(f"Last active: {retrieved.last_active_at}") + +# 4. Delete container +print(f"\nDeleting container {container_id}...") +result = client.containers.delete( + container_id=container_id, + extra_body={"custom_llm_provider": "openai"} +) + +print(f"Deleted: {result.deleted}") +``` ## Container Parameters @@ -356,3 +451,15 @@ print(f"Container ID: {result.id}") } ``` +## **Supported Providers** + +| Provider | Support Status | Notes | +|-------------|----------------|-------| +| OpenAI | ✅ Supported | Full support for all container operations | + +:::info + +Currently, only OpenAI supports container management for code interpreter sessions. Support for additional providers may be added in the future. + +::: + 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/mcp.md b/docs/my-website/docs/mcp.md index 10405c1b493..c735b8ecdd9 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -107,6 +107,26 @@ For stdio MCP servers, select "Standard Input/Output (stdio)" as the transport t style={{width: '80%', display: 'block', margin: '0'}} /> +
+
+ +### Static Headers + +Sometimes your MCP server needs specific headers on every request. Maybe it's an API key, maybe it's a custom header the server expects. Instead of configuring auth, you can just set them directly. + + + +These headers get sent with every request to the server. That's it. + + +**When to use this:** +- Your server needs custom headers that don't fit the standard auth patterns +- You want full control over exactly what headers are sent +- You're debugging and need to quickly add headers without changing auth configuration + @@ -175,6 +195,7 @@ mcp_servers: | `authorization` | `Authorization: ` | - **Extra Headers**: Optional list of additional header names that should be forwarded from client to the MCP server +- **Static Headers**: Optional map of header key/value pairs to include every request to the MCP server. - **Spec Version**: Optional MCP specification version (defaults to `2025-06-18`) Examples for each auth type: @@ -217,28 +238,15 @@ mcp_servers: auth_type: "bearer_token" auth_value: "ghp_example_token" extra_headers: ["custom_key", "x-custom-header"] # These headers will be forwarded from client -``` -### Static Headers - -Sometimes your MCP server needs specific headers on every request. Maybe it's an API key, maybe it's a custom header the server expects. Instead of configuring auth, you can just set them directly. - -```yaml title="config.yaml" showLineNumbers -mcp_servers: + # Example with static headers my_mcp_server: url: "https://my-mcp-server.com/mcp" - static_headers: + static_headers: # These headers will be requested to the MCP server X-API-Key: "abc123" X-Custom-Header: "some-value" ``` -These headers get sent with every request to the server. That's it. - -**When to use this:** -- Your server needs custom headers that don't fit the standard auth patterns -- You want full control over exactly what headers are sent -- You're debugging and need to quickly add headers without changing auth configuration - ### MCP Aliases You can define aliases for your MCP servers in the `litellm_settings` section. This allows you to: 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/ocr.md b/docs/my-website/docs/ocr.md index 2cb87edc461..93cb74ee69f 100644 --- a/docs/my-website/docs/ocr.md +++ b/docs/my-website/docs/ocr.md @@ -5,7 +5,7 @@ | Cost Tracking | ✅ | | Logging | ✅ (Basic Logging not supported) | | Load Balancing | ✅ | -| Supported Providers | `mistral`, `azure_ai` | +| Supported Providers | `mistral`, `azure_ai`, `vertex_ai` | :::tip @@ -262,4 +262,5 @@ The response follows Mistral's OCR format with the following structure: |-------------|--------------------| | Mistral AI | [Usage](#quick-start) | | Azure AI | [Usage](../docs/providers/azure_ocr) | +| Vertex AI | [Usage](../docs/providers/vertex_ocr) | diff --git a/docs/my-website/docs/pass_through/openai_passthrough.md b/docs/my-website/docs/pass_through/openai_passthrough.md index 27123695751..d7c98eba7b3 100644 --- a/docs/my-website/docs/pass_through/openai_passthrough.md +++ b/docs/my-website/docs/pass_through/openai_passthrough.md @@ -19,6 +19,9 @@ Simply replace `https://api.openai.com` with `LITELLM_PROXY_BASE_URL/openai` ## Usage Examples +Requirements: +Set `OPENAI_API_KEY` in your environment variables. + ### Assistants API #### Create OpenAI Client 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/azure_document_intelligence.md b/docs/my-website/docs/providers/azure_document_intelligence.md new file mode 100644 index 00000000000..edc3c616fa7 --- /dev/null +++ b/docs/my-website/docs/providers/azure_document_intelligence.md @@ -0,0 +1,408 @@ +# Azure Document Intelligence OCR + +## Overview + +| Property | Details | +|-------|-------| +| Description | Azure Document Intelligence (formerly Form Recognizer) provides advanced document analysis capabilities including text extraction, layout analysis, and structure recognition | +| Provider Route on LiteLLM | `azure_ai/doc-intelligence/` | +| Supported Operations | `/ocr` | +| Link to Provider Doc | [Azure Document Intelligence ↗](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/) + +Extract text and analyze document structure using Azure Document Intelligence's powerful prebuilt models. + +## Quick Start + +### **LiteLLM SDK** + +```python showLineNumbers title="SDK Usage" +import litellm +import os + +# Set environment variables +os.environ["AZURE_DOCUMENT_INTELLIGENCE_API_KEY"] = "your-api-key" +os.environ["AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT"] = "https://your-resource.cognitiveservices.azure.com" + +# OCR with PDF URL +response = litellm.ocr( + model="azure_ai/doc-intelligence/prebuilt-layout", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + } +) + +# Access extracted text +for page in response.pages: + print(f"Page {page.index}:") + print(page.markdown) +``` + +### **LiteLLM PROXY** + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + - model_name: azure-doc-intel + litellm_params: + model: azure_ai/doc-intelligence/prebuilt-layout + api_key: os.environ/AZURE_DOCUMENT_INTELLIGENCE_API_KEY + api_base: os.environ/AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT + model_info: + mode: ocr +``` + +**Start Proxy** +```bash +litellm --config proxy_config.yaml +``` + +**Call OCR via Proxy** +```bash showLineNumbers title="cURL Request" +curl -X POST http://localhost:4000/ocr \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-api-key" \ + -d '{ + "model": "azure-doc-intel", + "document": { + "type": "document_url", + "document_url": "https://arxiv.org/pdf/2201.04234" + } + }' +``` + +## How It Works + +Azure Document Intelligence uses an asynchronous API pattern. LiteLLM AI Gateway handles the request/response transformation and polling automatically. + +### Complete Flow Diagram + +```mermaid +sequenceDiagram + participant Client + box rgb(200, 220, 255) LiteLLM AI Gateway + participant LiteLLM + end + participant Azure as Azure Document Intelligence + + Client->>LiteLLM: POST /ocr (Mistral format) + Note over LiteLLM: Transform to Azure format + + LiteLLM->>Azure: POST :analyze + Azure-->>LiteLLM: 202 Accepted + polling URL + + Note over LiteLLM: Automatic Polling + loop Every 2-10 seconds + LiteLLM->>Azure: GET polling URL + Azure-->>LiteLLM: Status: running + end + + LiteLLM->>Azure: GET polling URL + Azure-->>LiteLLM: Status: succeeded + results + + Note over LiteLLM: Transform to Mistral format + LiteLLM-->>Client: OCR Response (Mistral format) +``` + +### What LiteLLM Does For You + +When you call `litellm.ocr()` via SDK or `/ocr` via Proxy: + +1. **Request Transformation**: Converts Mistral OCR format → Azure Document Intelligence format +2. **Submits Document**: Sends transformed request to Azure DI API +3. **Handles 202 Response**: Captures the `Operation-Location` URL from response headers +4. **Automatic Polling**: + - Polls the operation URL at intervals specified by `retry-after` header (default: 2 seconds) + - Continues until status is `succeeded` or `failed` + - Respects Azure's rate limiting via `retry-after` headers +5. **Response Transformation**: Converts Azure DI format → Mistral OCR format +6. **Returns Result**: Sends unified Mistral format response to client + +**Polling Configuration:** +- Default timeout: 120 seconds +- Configurable via `AZURE_OPERATION_POLLING_TIMEOUT` environment variable +- Uses sync (`time.sleep()`) or async (`await asyncio.sleep()`) based on call type + +:::info +**Typical processing time**: 2-10 seconds depending on document size and complexity +::: + +## Supported Models + +Azure Document Intelligence offers several prebuilt models optimized for different use cases: + +### prebuilt-layout (Recommended) + +Best for general document OCR with structure preservation. + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +```python showLineNumbers title="Layout Model - SDK" +import litellm +import os + +os.environ["AZURE_DOCUMENT_INTELLIGENCE_API_KEY"] = "your-api-key" +os.environ["AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT"] = "https://your-resource.cognitiveservices.azure.com" + +response = litellm.ocr( + model="azure_ai/doc-intelligence/prebuilt-layout", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + } +) +``` + + + + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + - model_name: azure-layout + litellm_params: + model: azure_ai/doc-intelligence/prebuilt-layout + api_key: os.environ/AZURE_DOCUMENT_INTELLIGENCE_API_KEY + api_base: os.environ/AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT + model_info: + mode: ocr +``` + +**Usage:** +```bash +curl -X POST http://localhost:4000/ocr \ + -H "Authorization: Bearer your-api-key" \ + -d '{"model": "azure-layout", "document": {"type": "document_url", "document_url": "https://example.com/doc.pdf"}}' +``` + + + + +**Features:** +- Text extraction with markdown formatting +- Table detection and extraction +- Document structure analysis +- Paragraph and section recognition + +**Pricing:** $10 per 1,000 pages + +### prebuilt-read + +Optimized for reading text from documents - fastest and most cost-effective. + + + + +```python showLineNumbers title="Read Model - SDK" +import litellm +import os + +os.environ["AZURE_DOCUMENT_INTELLIGENCE_API_KEY"] = "your-api-key" +os.environ["AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT"] = "https://your-resource.cognitiveservices.azure.com" + +response = litellm.ocr( + model="azure_ai/doc-intelligence/prebuilt-read", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + } +) +``` + + + + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + - model_name: azure-read + litellm_params: + model: azure_ai/doc-intelligence/prebuilt-read + api_key: os.environ/AZURE_DOCUMENT_INTELLIGENCE_API_KEY + api_base: os.environ/AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT + model_info: + mode: ocr +``` + +**Usage:** +```bash +curl -X POST http://localhost:4000/ocr \ + -H "Authorization: Bearer your-api-key" \ + -d '{"model": "azure-read", "document": {"type": "document_url", "document_url": "https://example.com/doc.pdf"}}' +``` + + + + +**Features:** +- Fast text extraction +- Optimized for reading-heavy documents +- Basic structure recognition + +**Pricing:** $1.50 per 1,000 pages + +### prebuilt-document + +General-purpose document analysis with key-value pairs. + + + + +```python showLineNumbers title="Document Model - SDK" +import litellm +import os + +os.environ["AZURE_DOCUMENT_INTELLIGENCE_API_KEY"] = "your-api-key" +os.environ["AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT"] = "https://your-resource.cognitiveservices.azure.com" + +response = litellm.ocr( + model="azure_ai/doc-intelligence/prebuilt-document", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + } +) +``` + + + + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + - model_name: azure-document + litellm_params: + model: azure_ai/doc-intelligence/prebuilt-document + api_key: os.environ/AZURE_DOCUMENT_INTELLIGENCE_API_KEY + api_base: os.environ/AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT + model_info: + mode: ocr +``` + +**Usage:** +```bash +curl -X POST http://localhost:4000/ocr \ + -H "Authorization: Bearer your-api-key" \ + -d '{"model": "azure-document", "document": {"type": "document_url", "document_url": "https://example.com/doc.pdf"}}' +``` + + + + +**Pricing:** $10 per 1,000 pages + +## Document Types + +Azure Document Intelligence supports various document formats. + +### PDF Documents + +```python showLineNumbers title="PDF OCR" +response = litellm.ocr( + model="azure_ai/doc-intelligence/prebuilt-layout", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + } +) +``` + +### Image Documents + +```python showLineNumbers title="Image OCR" +response = litellm.ocr( + model="azure_ai/doc-intelligence/prebuilt-layout", + document={ + "type": "image_url", + "image_url": "https://example.com/image.png" + } +) +``` + +**Supported image formats:** JPEG, PNG, BMP, TIFF + +### Base64 Encoded Documents + +```python showLineNumbers title="Base64 PDF" +import base64 + +# Read and encode PDF +with open("document.pdf", "rb") as f: + pdf_base64 = base64.b64encode(f.read()).decode() + +response = litellm.ocr( + model="azure_ai/doc-intelligence/prebuilt-layout", + document={ + "type": "document_url", + "document_url": f"data:application/pdf;base64,{pdf_base64}" + } +) +``` + +## Response Format + +```python showLineNumbers title="Response Structure" +# Response has the following structure +response.pages # List of pages with extracted text +response.model # Model used +response.object # "ocr" +response.usage_info # Token usage information + +# Access page content +for page in response.pages: + print(f"Page {page.index}:") + print(page.markdown) + + # Page dimensions (in pixels) + if page.dimensions: + print(f"Width: {page.dimensions.width}px") + print(f"Height: {page.dimensions.height}px") +``` + +## Async Support + +```python showLineNumbers title="Async Usage" +import litellm +import asyncio + +async def process_document(): + response = await litellm.aocr( + model="azure_ai/doc-intelligence/prebuilt-layout", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + } + ) + return response + +# Run async function +response = asyncio.run(process_document()) +``` + +## Cost Tracking + +LiteLLM automatically tracks costs for Azure Document Intelligence OCR: + +| Model | Cost per 1,000 Pages | +|-------|---------------------| +| prebuilt-read | $1.50 | +| prebuilt-layout | $10.00 | +| prebuilt-document | $10.00 | + +```python showLineNumbers title="View Cost" +response = litellm.ocr( + model="azure_ai/doc-intelligence/prebuilt-layout", + document={"type": "document_url", "document_url": "https://..."} +) + +# Access cost information +print(f"Cost: ${response._hidden_params.get('response_cost', 0)}") +``` + +## Additional Resources + +- [Azure Document Intelligence Documentation](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/) +- [Pricing Details](https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/) +- [Supported File Formats](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/concept-model-overview) +- [LiteLLM OCR Documentation](https://docs.litellm.ai/docs/ocr) + diff --git a/docs/my-website/docs/providers/azure_ocr.md b/docs/my-website/docs/providers/azure_ocr.md index c93e995c43e..5d79cc05338 100644 --- a/docs/my-website/docs/providers/azure_ocr.md +++ b/docs/my-website/docs/providers/azure_ocr.md @@ -1,4 +1,4 @@ -# Azure AI OCR +# Azure AI OCR (Mistral) ## Overview diff --git a/docs/my-website/docs/providers/bedrock_agentcore.md b/docs/my-website/docs/providers/bedrock_agentcore.md new file mode 100644 index 00000000000..43df7f82519 --- /dev/null +++ b/docs/my-website/docs/providers/bedrock_agentcore.md @@ -0,0 +1,246 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Bedrock AgentCore + +Call Bedrock AgentCore in the OpenAI Request/Response format. + +| Property | Details | +|----------|---------| +| Description | Amazon Bedrock AgentCore provides direct access to hosted agent runtimes for executing agentic workflows with foundation models. | +| Provider Route on LiteLLM | `bedrock/agentcore/{AGENT_RUNTIME_ARN}` | +| Provider Doc | [AWS Bedrock AgentCore ↗](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agentcore_InvokeAgentRuntime.html) | + +## Quick Start + +### Model Format to LiteLLM + +To call a bedrock agent runtime through LiteLLM, use the following model format. + +Here the `model=bedrock/agentcore/` tells LiteLLM to call the bedrock `InvokeAgentRuntime` API. + +```shell showLineNumbers title="Model Format to LiteLLM" +bedrock/agentcore/{AGENT_RUNTIME_ARN} +``` + +**Example:** +- `bedrock/agentcore/arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-agent-runtime` + +You can find the Agent Runtime ARN in your AWS Bedrock console under AgentCore. + +### LiteLLM Python SDK + +```python showLineNumbers title="Basic AgentCore Completion" +import litellm + +# Make a completion request to your AgentCore runtime +response = litellm.completion( + model="bedrock/agentcore/arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-agent-runtime", + messages=[ + { + "role": "user", + "content": "Explain machine learning in simple terms" + } + ], +) + +print(response.choices[0].message.content) +print(f"Usage: {response.usage}") +``` + +```python showLineNumbers title="Streaming AgentCore Responses" +import litellm + +# Stream responses from your AgentCore runtime +response = litellm.completion( + model="bedrock/agentcore/arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-agent-runtime", + messages=[ + { + "role": "user", + "content": "What are the key principles of software architecture?" + } + ], + stream=True, +) + +for chunk in response: + if chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="") +``` + +### LiteLLM Proxy + +#### 1. Configure your model in config.yaml + + + + +```yaml showLineNumbers title="LiteLLM Proxy Configuration" +model_list: + - model_name: agentcore-runtime-1 + litellm_params: + model: bedrock/agentcore/arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-agent-runtime + aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID + aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY + aws_region_name: us-west-2 + + - model_name: agentcore-runtime-2 + litellm_params: + model: bedrock/agentcore/arn:aws:bedrock-agentcore:us-east-1:987654321098:runtime/production-runtime + aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID + aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY + aws_region_name: us-east-1 +``` + + + + +#### 2. Start the LiteLLM Proxy + +```bash showLineNumbers title="Start LiteLLM Proxy" +litellm --config config.yaml +``` + +#### 3. Make requests to your AgentCore runtimes + + + + +```bash showLineNumbers title="Basic AgentCore Request" +curl http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_API_KEY" \ + -d '{ + "model": "agentcore-runtime-1", + "messages": [ + { + "role": "user", + "content": "Summarize the main benefits of cloud computing" + } + ] + }' +``` + +```bash showLineNumbers title="Streaming AgentCore Request" +curl http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_API_KEY" \ + -d '{ + "model": "agentcore-runtime-2", + "messages": [ + { + "role": "user", + "content": "Explain the differences between SQL and NoSQL databases" + } + ], + "stream": true + }' +``` + + + + + +```python showLineNumbers title="Using OpenAI SDK with LiteLLM Proxy" +from openai import OpenAI + +# Initialize client with your LiteLLM proxy URL +client = OpenAI( + base_url="http://localhost:4000", + api_key="your-litellm-api-key" +) + +# Make a completion request to your AgentCore runtime +response = client.chat.completions.create( + model="agentcore-runtime-1", + messages=[ + { + "role": "user", + "content": "What are best practices for API design?" + } + ] +) + +print(response.choices[0].message.content) +``` + +```python showLineNumbers title="Streaming with OpenAI SDK" +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:4000", + api_key="your-litellm-api-key" +) + +# Stream AgentCore responses +stream = client.chat.completions.create( + model="agentcore-runtime-2", + messages=[ + { + "role": "user", + "content": "Describe the microservices architecture pattern" + } + ], + stream=True +) + +for chunk in stream: + if chunk.choices[0].delta.content is not None: + print(chunk.choices[0].delta.content, end="") +``` + + + + +## Provider-specific Parameters + +AgentCore supports additional parameters that can be passed to customize the runtime invocation. + + + + +```python showLineNumbers title="Using AgentCore-specific parameters" +from litellm import completion + +response = litellm.completion( + model="bedrock/agentcore/arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-agent-runtime", + messages=[ + { + "role": "user", + "content": "Analyze this data and provide insights", + } + ], + qualifier="production", # PROVIDER-SPECIFIC: Runtime qualifier/version + runtimeSessionId="session-abc-123", # PROVIDER-SPECIFIC: Custom session ID +) +``` + + + + +```yaml showLineNumbers title="LiteLLM Proxy Configuration with Parameters" +model_list: + - model_name: agentcore-runtime-prod + litellm_params: + model: bedrock/agentcore/arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-agent-runtime + aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID + aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY + aws_region_name: us-west-2 + qualifier: production +``` + + + + +### Available Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `qualifier` | string | Optional runtime qualifier/version to invoke a specific version of the agent runtime | +| `runtimeSessionId` | string | Optional custom session ID (must be 33+ characters). If not provided, LiteLLM generates one automatically | + +## Further Reading + +- [AWS Bedrock AgentCore Documentation](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agentcore_InvokeAgentRuntime.html) +- [LiteLLM Authentication to Bedrock](https://docs.litellm.ai/docs/providers/bedrock#boto3---authentication) + diff --git a/docs/my-website/docs/providers/custom.md b/docs/my-website/docs/providers/custom.md deleted file mode 100644 index 81b92f0a031..00000000000 --- a/docs/my-website/docs/providers/custom.md +++ /dev/null @@ -1,69 +0,0 @@ -# Custom LLM API-Endpoints -LiteLLM supports Custom deploy api endpoints - -LiteLLM Expects the following input and output for custom LLM API endpoints - -### Model Details - -For calls to your custom API base ensure: -* Set `api_base="your-api-base"` -* Add `custom/` as a prefix to the `model` param. If your API expects `meta-llama/Llama-2-13b-hf` set `model=custom/meta-llama/Llama-2-13b-hf` - -| Model Name | Function Call | -|------------------|--------------------------------------------| -| meta-llama/Llama-2-13b-hf | `response = completion(model="custom/meta-llama/Llama-2-13b-hf", messages=messages, api_base="https://your-custom-inference-endpoint")` | -| meta-llama/Llama-2-13b-hf | `response = completion(model="custom/meta-llama/Llama-2-13b-hf", messages=messages, api_base="https://api.autoai.dev/inference")` | - -### Example Call to Custom LLM API using LiteLLM -```python -from litellm import completion -response = completion( - model="custom/meta-llama/Llama-2-13b-hf", - messages= [{"content": "what is custom llama?", "role": "user"}], - temperature=0.2, - max_tokens=10, - api_base="https://api.autoai.dev/inference", - request_timeout=300, -) -print("got response\n", response) -``` - -#### Setting your Custom API endpoint - -Inputs to your custom LLM api bases should follow this format: - -```python -resp = requests.post( - your-api_base, - json={ - 'model': 'meta-llama/Llama-2-13b-hf', # model name - 'params': { - 'prompt': ["The capital of France is P"], - 'max_tokens': 32, - 'temperature': 0.7, - 'top_p': 1.0, - 'top_k': 40, - } - } -) -``` - -Outputs from your custom LLM api bases should follow this format: -```python -{ - 'data': [ - { - 'prompt': 'The capital of France is P', - 'output': [ - 'The capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France is PARIS.\nThe capital of France' - ], - 'params': { - 'temperature': 0.7, - 'top_k': 40, - 'top_p': 1 - } - } - ], - 'message': 'ok' -} -``` \ No newline at end of file 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/providers/vertex_ocr.md b/docs/my-website/docs/providers/vertex_ocr.md new file mode 100644 index 00000000000..4e3d4b0a063 --- /dev/null +++ b/docs/my-website/docs/providers/vertex_ocr.md @@ -0,0 +1,237 @@ +# Vertex AI OCR + +## Overview + +| Property | Details | +|-------|-------| +| Description | Vertex AI OCR provides document intelligence capabilities powered by Mistral, enabling text extraction from PDFs and images | +| Provider Route on LiteLLM | `vertex_ai/` | +| Supported Operations | `/ocr` | +| Link to Provider Doc | [Vertex AI ↗](https://cloud.google.com/vertex-ai) + +Extract text from documents and images using Vertex AI's OCR models, powered by Mistral. + +## Quick Start + +### **LiteLLM SDK** + +```python showLineNumbers title="SDK Usage" +import litellm +import os + +# Set environment variables +os.environ["VERTEXAI_PROJECT"] = "your-project-id" +os.environ["VERTEXAI_LOCATION"] = "us-central1" + +# OCR with PDF URL +response = litellm.ocr( + model="vertex_ai/mistral-ocr-2505", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + } +) + +# Access extracted text +for page in response.pages: + print(page.text) +``` + +### **LiteLLM PROXY** + +```yaml showLineNumbers title="proxy_config.yaml" +model_list: + - model_name: vertex-ocr + litellm_params: + model: vertex_ai/mistral-ocr-2505 + vertex_project: os.environ/VERTEXAI_PROJECT + vertex_location: os.environ/VERTEXAI_LOCATION + vertex_credentials: path/to/service-account.json # Optional + model_info: + mode: ocr +``` + +**Start Proxy** +```bash +litellm --config proxy_config.yaml +``` + +**Call OCR via Proxy** +```bash showLineNumbers title="cURL Request" +curl -X POST http://localhost:4000/ocr \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-api-key" \ + -d '{ + "model": "vertex-ocr", + "document": { + "type": "document_url", + "document_url": "https://arxiv.org/pdf/2201.04234" + } + }' +``` + +## Authentication + +Vertex AI OCR supports multiple authentication methods: + +### Service Account JSON + +```python showLineNumbers title="Service Account Auth" +response = litellm.ocr( + model="vertex_ai/mistral-ocr-2505", + document={"type": "document_url", "document_url": "https://..."}, + vertex_project="your-project-id", + vertex_location="us-central1", + vertex_credentials="path/to/service-account.json" +) +``` + +### Application Default Credentials + +```python showLineNumbers title="Default Credentials" +# Relies on GOOGLE_APPLICATION_CREDENTIALS environment variable +response = litellm.ocr( + model="vertex_ai/mistral-ocr-2505", + document={"type": "document_url", "document_url": "https://..."}, + vertex_project="your-project-id", + vertex_location="us-central1" +) +``` + +## Document Types + +Vertex AI OCR supports both PDFs and images. + +### PDF Documents + +```python showLineNumbers title="PDF OCR" +response = litellm.ocr( + model="vertex_ai/mistral-ocr-2505", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + }, + vertex_project="your-project-id", + vertex_location="us-central1" +) +``` + +### Image Documents + +```python showLineNumbers title="Image OCR" +response = litellm.ocr( + model="vertex_ai/mistral-ocr-2505", + document={ + "type": "image_url", + "image_url": "https://example.com/image.png" + }, + vertex_project="your-project-id", + vertex_location="us-central1" +) +``` + +### Base64 Encoded Documents + +```python showLineNumbers title="Base64 PDF" +import base64 + +# Read and encode PDF +with open("document.pdf", "rb") as f: + pdf_base64 = base64.b64encode(f.read()).decode() + +response = litellm.ocr( + model="vertex_ai/mistral-ocr-2505", + document={ + "type": "document_url", + "document_url": f"data:application/pdf;base64,{pdf_base64}" + }, + vertex_project="your-project-id", + vertex_location="us-central1" +) +``` + +## Supported Parameters + +```python showLineNumbers title="All Parameters" +response = litellm.ocr( + model="vertex_ai/mistral-ocr-2505", + document={ # Required: Document to process + "type": "document_url", + "document_url": "https://..." + }, + vertex_project="your-project-id", # Required: GCP project ID + vertex_location="us-central1", # Optional: Defaults to us-central1 + vertex_credentials="path/to/key.json", # Optional: Service account key + include_image_base64=True, # Optional: Include base64 images + pages=[0, 1, 2], # Optional: Specific pages to process + image_limit=10 # Optional: Limit number of images +) +``` + +## Response Format + +```python showLineNumbers title="Response Structure" +# Response has the following structure +response.pages # List of pages with extracted text +response.model # Model used +response.object # "ocr" +response.usage_info # Token usage information + +# Access page content +for page in response.pages: + print(f"Page {page.page_number}:") + print(page.text) +``` + +## Async Support + +```python showLineNumbers title="Async Usage" +import litellm + +response = await litellm.aocr( + model="vertex_ai/mistral-ocr-2505", + document={ + "type": "document_url", + "document_url": "https://example.com/document.pdf" + }, + vertex_project="your-project-id", + vertex_location="us-central1" +) +``` + +## Cost Tracking + +LiteLLM automatically tracks costs for Vertex AI OCR: + +- **Cost per page**: $0.0005 (based on $1.50 per 1,000 pages) + +```python showLineNumbers title="View Cost" +response = litellm.ocr( + model="vertex_ai/mistral-ocr-2505", + document={"type": "document_url", "document_url": "https://..."}, + vertex_project="your-project-id" +) + +# Access cost information +print(f"Cost: ${response._hidden_params.get('response_cost', 0)}") +``` + +## Important Notes + +:::info URL Conversion +Vertex AI OCR endpoints don't have internet access. LiteLLM automatically converts public URLs to base64 data URIs before sending requests to Vertex AI. +::: + +:::tip Regional Availability +Mistral OCR is available in multiple regions. Specify `vertex_location` to use a region closer to your data: +- `us-central1` (default) +- `europe-west1` +- `asia-southeast1` +::: + +## Supported Models + +- `mistral-ocr-2505` - Latest Mistral OCR model on Vertex AI + +Use the Vertex AI provider prefix: `vertex_ai/` + 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/cost_tracking.md b/docs/my-website/docs/proxy/cost_tracking.md index da8b6f5c525..019cd62c620 100644 --- a/docs/my-website/docs/proxy/cost_tracking.md +++ b/docs/my-website/docs/proxy/cost_tracking.md @@ -9,7 +9,7 @@ Track spend for keys, users, and teams across 100+ LLMs. LiteLLM automatically tracks spend for all known models. See our [model cost map](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json) :::tip Keep Pricing Data Updated -[Sync model pricing data from GitHub](../sync_models_github.md) to ensure accurate cost tracking. +[Sync model pricing data from GitHub](./sync_models_github.md) to ensure accurate cost tracking. ::: ### How to Track Spend with LiteLLM diff --git a/docs/my-website/docs/proxy/email.md b/docs/my-website/docs/proxy/email.md index 1ee67e82308..da8fc57deea 100644 --- a/docs/my-website/docs/proxy/email.md +++ b/docs/my-website/docs/proxy/email.md @@ -18,7 +18,7 @@ Send LiteLLM Proxy users emails for specific events. | Category | Details | |----------|---------| -| Supported Events | • User added as a user on LiteLLM Proxy
• Proxy API Key created for user | +| Supported Events | • User added as a user on LiteLLM Proxy
• Proxy API Key created for user
• Proxy API Key rotated for user | | Supported Email Integrations | • Resend API
• SMTP | ## Usage @@ -123,6 +123,35 @@ On the Create Key Modal, Select Advanced Settings > Set Send Email to True. style={{width: '70%', display: 'block', margin: '0 0 2rem 0'}} /> +### 3. Proxy API Key Rotated for User + +This email is sent when you rotate an API key for a user on LiteLLM Proxy. + + + +**How to trigger this event** + +On the LiteLLM Proxy UI, go to Virtual Keys > Click on a key > Click "Regenerate Key" + +:::info + +Ensure there is a `user_id` attached to the key. This would have been set when creating the key. + +::: + + + +After regenerating the key, the user will receive an email notification with: +- Security-focused messaging about the rotation +- The new API key (or a placeholder if `EMAIL_INCLUDE_API_KEY=false`) +- Instructions to update their applications +- Security best practices ## Email Customization @@ -141,6 +170,8 @@ LiteLLM allows you to customize various aspects of your email notifications. Bel | Email Signature | `EMAIL_SIGNATURE` | string (HTML) | Standard LiteLLM footer | `"

Best regards,
Your Team

Visit us

"` | HTML-formatted footer for all emails | | Invitation Subject | `EMAIL_SUBJECT_INVITATION` | string | "LiteLLM: New User Invitation" | `"Welcome to Your Company!"` | Subject line for invitation emails | | Key Creation Subject | `EMAIL_SUBJECT_KEY_CREATED` | string | "LiteLLM: API Key Created" | `"Your New API Key is Ready"` | Subject line for key creation emails | +| Key Rotation Subject | `EMAIL_SUBJECT_KEY_ROTATED` | string | "LiteLLM: API Key Rotated" | `"Your API Key Has Been Rotated"` | Subject line for key rotation emails | +| Include API Key | `EMAIL_INCLUDE_API_KEY` | boolean | true | `"false"` | Whether to include the actual API key in emails (set to false for enhanced security) | | Proxy Base URL | `PROXY_BASE_URL` | string | http://0.0.0.0:4000 | `"https://proxy.your-company.com"` | Base URL for the LiteLLM Proxy (used in email links) | @@ -181,11 +212,44 @@ EMAIL_SIGNATURE="

Best regards,
Your Company Team

+ + +""" + diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index a067d285245..b71cba62046 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -688,16 +688,19 @@ class LangFuseLogger: "completion_tokens": _usage_obj.completion_tokens, "total_cost": cost if self._supports_costs() else None, } + cache_read_input_tokens = _usage_obj.get( + "cache_read_input_tokens", 0 + ) + # According to langfuse documentation: "the input value must be reduced by the number of cache_read_input_tokens" + input_tokens = _usage_obj.prompt_tokens - cache_read_input_tokens usage_details = LangfuseUsageDetails( - input=_usage_obj.prompt_tokens, + input=input_tokens, output=_usage_obj.completion_tokens, total=_usage_obj.total_tokens, cache_creation_input_tokens=_usage_obj.get( "cache_creation_input_tokens", 0 ), - cache_read_input_tokens=_usage_obj.get( - "cache_read_input_tokens", 0 - ), + cache_read_input_tokens=cache_read_input_tokens, ) generation_name = clean_metadata.pop("generation_name", None) diff --git a/litellm/integrations/langfuse/langfuse_otel.py b/litellm/integrations/langfuse/langfuse_otel.py index fc010928101..6992ea17cc8 100644 --- a/litellm/integrations/langfuse/langfuse_otel.py +++ b/litellm/integrations/langfuse/langfuse_otel.py @@ -87,31 +87,10 @@ class LangfuseOtelLogger(OpenTelemetry): return metadata @staticmethod - def _set_langfuse_specific_attributes(span: Span, kwargs, response_obj): - """ - Sets Langfuse specific metadata attributes onto the OTEL span. - - All keys supported by the vanilla Langfuse integration are mapped to - OTEL-safe attribute names defined in LangfuseSpanAttributes. Complex - values (lists/dicts) are serialised to JSON strings for OTEL - compatibility. - """ + def _set_metadata_attributes(span: Span, metadata: dict): + """Helper to set metadata attributes from mapping.""" from litellm.integrations.arize._utils import safe_set_attribute - from litellm.litellm_core_utils.safe_json_dumps import safe_dumps - # 1) Environment variable override - langfuse_environment = os.environ.get("LANGFUSE_TRACING_ENVIRONMENT") - if langfuse_environment: - safe_set_attribute( - span, - LangfuseSpanAttributes.LANGFUSE_ENVIRONMENT.value, - langfuse_environment, - ) - - # 2) Dynamic metadata from kwargs / headers - metadata = LangfuseOtelLogger._extract_langfuse_metadata(kwargs) - - # Mapping from metadata key -> OTEL attribute enum mapping = { "generation_name": LangfuseSpanAttributes.GENERATION_NAME, "generation_id": LangfuseSpanAttributes.GENERATION_ID, @@ -135,7 +114,6 @@ class LangfuseOtelLogger(OpenTelemetry): for key, enum_attr in mapping.items(): if key in metadata and metadata[key] is not None: value = metadata[key] - # Lists / dicts must be stringified for OTEL if isinstance(value, (list, dict)): try: value = json.dumps(value) @@ -143,117 +121,105 @@ class LangfuseOtelLogger(OpenTelemetry): value = str(value) safe_set_attribute(span, enum_attr.value, value) - # 3) Set observation input/output for better UI display - # - # These Langfuse-specific attributes provide better UI display, - # especially for tool calls and function calling. - # Set observation input (messages) - messages = kwargs.get("messages") - if messages: - safe_set_attribute( - span, - LangfuseSpanAttributes.OBSERVATION_INPUT.value, - safe_dumps(messages), - ) + @staticmethod + def _set_observation_output(span: Span, response_obj): + """Helper to set observation output attributes.""" + from litellm.integrations.arize._utils import safe_set_attribute + from litellm.litellm_core_utils.safe_json_dumps import safe_dumps - # Set observation output (response with tool_calls if present) - if response_obj and hasattr(response_obj, "get"): - # Handle chat completions API (choices field) - choices = response_obj.get("choices", []) - if choices: - # Extract the first choice's message - first_choice = choices[0] - message = first_choice.get("message", {}) + if not response_obj or not hasattr(response_obj, "get"): + return - # Check if there are tool_calls - tool_calls = message.get("tool_calls") - if tool_calls: - # Transform tool_calls to Langfuse-expected format - transformed_tool_calls = [] - for tool_call in tool_calls: - function = tool_call.get("function", {}) - arguments_str = function.get("arguments", "{}") + choices = response_obj.get("choices", []) + if choices: + first_choice = choices[0] + message = first_choice.get("message", {}) + tool_calls = message.get("tool_calls") + if tool_calls: + transformed_tool_calls = [] + for tool_call in tool_calls: + function = tool_call.get("function", {}) + arguments_str = function.get("arguments", "{}") + try: + arguments_obj = ( + json.loads(arguments_str) + if isinstance(arguments_str, str) + else arguments_str + ) + except json.JSONDecodeError: + arguments_obj = {} + langfuse_tool_call = { + "id": response_obj.get("id", ""), + "name": function.get("name", ""), + "call_id": tool_call.get("id", ""), + "type": "function_call", + "arguments": arguments_obj, + } + transformed_tool_calls.append(langfuse_tool_call) + safe_set_attribute(span, LangfuseSpanAttributes.OBSERVATION_OUTPUT.value, safe_dumps(transformed_tool_calls)) + else: + output_data = {} + if message.get("role"): + output_data["role"] = message.get("role") + if message.get("content") is not None: + output_data["content"] = message.get("content") + if output_data: + safe_set_attribute(span, LangfuseSpanAttributes.OBSERVATION_OUTPUT.value, safe_dumps(output_data)) - # Parse arguments from JSON string to object - try: - arguments_obj = ( - json.loads(arguments_str) - if isinstance(arguments_str, str) - else arguments_str - ) - except json.JSONDecodeError: - arguments_obj = {} - - # Create Langfuse-compatible tool call object + output = response_obj.get("output", []) + if output: + output_items_data: list[dict] = [] + for item in output: + if hasattr(item, "type"): + item_type = item.type + if item_type == "reasoning" and hasattr(item, "summary"): + for summary in item.summary: + if hasattr(summary, "text"): + output_items_data.append({"role": "reasoning_summary", "content": summary.text}) + elif item_type == "message": + output_items_data.append({ + "role": getattr(item, "role", "assistant"), + "content": getattr(getattr(item, "content", [{}])[0], "text", "") + }) + elif item_type == "function_call": + arguments_str = getattr(item, "arguments", "{}") + arguments_obj = json.loads(arguments_str) if isinstance(arguments_str, str) else arguments_str langfuse_tool_call = { - "id": response_obj.get("id", ""), - "name": function.get("name", ""), - "call_id": tool_call.get("id", ""), + "id": getattr(item, "id", ""), + "name": getattr(item, "name", ""), + "call_id": getattr(item, "call_id", ""), "type": "function_call", "arguments": arguments_obj, } - transformed_tool_calls.append(langfuse_tool_call) + output_items_data.append(langfuse_tool_call) + if output_items_data: + safe_set_attribute(span, LangfuseSpanAttributes.OBSERVATION_OUTPUT.value, safe_dumps(output_items_data)) - # Set the observation output with transformed tool_calls - safe_set_attribute( - span, - LangfuseSpanAttributes.OBSERVATION_OUTPUT.value, - safe_dumps(transformed_tool_calls), - ) - else: - # No tool_calls, use regular content-based output - output_data = {} + @staticmethod + def _set_langfuse_specific_attributes(span: Span, kwargs, response_obj): + """ + Sets Langfuse specific metadata attributes onto the OTEL span. - if message.get("role"): - output_data["role"] = message.get("role") + All keys supported by the vanilla Langfuse integration are mapped to + OTEL-safe attribute names defined in LangfuseSpanAttributes. Complex + values (lists/dicts) are serialised to JSON strings for OTEL + compatibility. + """ + from litellm.integrations.arize._utils import safe_set_attribute + from litellm.litellm_core_utils.safe_json_dumps import safe_dumps - if message.get("content") is not None: - output_data["content"] = message.get("content") + langfuse_environment = os.environ.get("LANGFUSE_TRACING_ENVIRONMENT") + if langfuse_environment: + safe_set_attribute(span, LangfuseSpanAttributes.LANGFUSE_ENVIRONMENT.value, langfuse_environment) - if output_data: - safe_set_attribute( - span, - LangfuseSpanAttributes.OBSERVATION_OUTPUT.value, - safe_dumps(output_data), - ) + metadata = LangfuseOtelLogger._extract_langfuse_metadata(kwargs) + LangfuseOtelLogger._set_metadata_attributes(span=span, metadata=metadata) - # Handle responses API (output field) - output = response_obj.get("output", []) - if output: - output_data = [] - for item in output: - if hasattr(item, "type"): - item_type = item.type - - if item_type == "reasoning" and hasattr(item, "summary"): - for summary in item.summary: - if hasattr(summary, "text"): - output_data.append({ - "role": "reasoning_summary", - "content": summary.text - }) - elif item_type == "message": - output_data.append({ - "role": getattr(item, "role", "assistant"), - "content": getattr(getattr(item, "content", [{}])[0], "text", "") - }) - elif item_type == "function_call": - arguments_str = getattr(item, "arguments", "{}") - arguments_obj = json.loads(arguments_str) if isinstance(arguments_str, str) else arguments_str - langfuse_tool_call = { - "id": getattr(item, "id", ""), - "name": getattr(item, "name", ""), - "call_id": getattr(item, "call_id", ""), - "type": "function_call", - "arguments": arguments_obj, - } - output_data.append(langfuse_tool_call) - if output_data: - safe_set_attribute( - span, - LangfuseSpanAttributes.OBSERVATION_OUTPUT.value, - safe_dumps(output_data), - ) + messages = kwargs.get("messages") + if messages: + safe_set_attribute(span, LangfuseSpanAttributes.OBSERVATION_INPUT.value, safe_dumps(messages)) + + LangfuseOtelLogger._set_observation_output(span=span, response_obj=response_obj) @staticmethod def _get_langfuse_otel_host() -> Optional[str]: diff --git a/litellm/integrations/langfuse/langfuse_otel_attributes.py b/litellm/integrations/langfuse/langfuse_otel_attributes.py index f14412ad53b..fb4a0a6a36c 100644 --- a/litellm/integrations/langfuse/langfuse_otel_attributes.py +++ b/litellm/integrations/langfuse/langfuse_otel_attributes.py @@ -5,13 +5,11 @@ Relevant Issue: https://github.com/BerriAI/litellm/issues/13764 """ import json -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Union -from numpy import isin from pydantic import BaseModel from typing_extensions import override -import litellm from litellm.integrations.opentelemetry_utils.base_otel_llm_obs_attributes import ( BaseLLMObsOTELAttributes, safe_set_attribute, 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/opentelemetry_utils/base_otel_llm_obs_attributes.py b/litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py index 8d7b2d681ab..f74da8231f3 100644 --- a/litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py +++ b/litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import TYPE_CHECKING, Any, Dict, List, Union +from typing import TYPE_CHECKING, Any, Dict, Union if TYPE_CHECKING: from opentelemetry.trace import Span diff --git a/litellm/integrations/opik/opik.py b/litellm/integrations/opik/opik.py index c28aa14a11e..7b687d34d1c 100644 --- a/litellm/integrations/opik/opik.py +++ b/litellm/integrations/opik/opik.py @@ -3,10 +3,9 @@ Opik Logger that logs LLM events to an Opik server """ import asyncio -from datetime import timezone -import json import traceback -from typing import Dict, List +from datetime import datetime +from typing import Any, Dict, Optional from litellm._logging import verbose_logger from litellm.integrations.custom_batch_logger import CustomBatchLogger @@ -16,12 +15,22 @@ from litellm.llms.custom_httpx.http_handler import ( httpxSpecialProvider, ) -from .utils import ( - create_usage_object, - create_uuid7, - get_opik_config_variable, - get_traces_and_spans_from_payload, -) +from . import opik_payload_builder, utils + +try: + from opik.api_objects import opik_client +except Exception: + opik_client = None + + +def _should_skip_event(kwargs: Dict[str, Any]) -> bool: + """Check if event should be skipped due to missing standard_logging_object.""" + if kwargs.get("standard_logging_object") is None: + verbose_logger.debug( + "OpikLogger skipping event; no standard_logging_object found" + ) + return True + return False class OpikLogger(CustomBatchLogger): @@ -29,76 +38,140 @@ class OpikLogger(CustomBatchLogger): Opik Logger for logging events to an Opik Server """ - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: self.async_httpx_client = get_async_httpx_client( llm_provider=httpxSpecialProvider.LoggingCallback ) self.sync_httpx_client = _get_httpx_client() - self.opik_project_name = get_opik_config_variable( - "project_name", - user_value=kwargs.get("project_name", None), - default_value="Default Project", + self.opik_project_name: str = ( + utils.get_opik_config_variable( + "project_name", + user_value=kwargs.get("project_name", None), + default_value="Default Project", + ) + or "Default Project" ) - opik_base_url = get_opik_config_variable( - "url_override", - user_value=kwargs.get("url", None), - default_value="https://www.comet.com/opik/api", + opik_base_url: str = ( + utils.get_opik_config_variable( + "url_override", + user_value=kwargs.get("url", None), + default_value="https://www.comet.com/opik/api", + ) + or "https://www.comet.com/opik/api" ) - opik_api_key = get_opik_config_variable( + opik_api_key: Optional[str] = utils.get_opik_config_variable( "api_key", user_value=kwargs.get("api_key", None), default_value=None ) - opik_workspace = get_opik_config_variable( + opik_workspace: Optional[str] = utils.get_opik_config_variable( "workspace", user_value=kwargs.get("workspace", None), default_value=None ) - self.trace_url = f"{opik_base_url}/v1/private/traces/batch" - self.span_url = f"{opik_base_url}/v1/private/spans/batch" + self.trace_url: str = f"{opik_base_url}/v1/private/traces/batch" + self.span_url: str = f"{opik_base_url}/v1/private/spans/batch" - self.headers = {} + self.headers: Dict[str, str] = {} if opik_workspace: self.headers["Comet-Workspace"] = opik_workspace if opik_api_key: self.headers["authorization"] = opik_api_key - self.opik_workspace = opik_workspace - self.opik_api_key = opik_api_key + self.opik_workspace: Optional[str] = opik_workspace + self.opik_api_key: Optional[str] = opik_api_key try: asyncio.create_task(self.periodic_flush()) - self.flush_lock = asyncio.Lock() + self.flush_lock: Optional[asyncio.Lock] = asyncio.Lock() except Exception as e: verbose_logger.exception( f"OpikLogger - Asynchronous processing not initialized as we are not running in an async context {str(e)}" ) self.flush_lock = None + # Initialize _opik_client attribute + if opik_client is not None: + self._opik_client = opik_client.get_client_cached() + else: + self._opik_client = None + super().__init__(**kwargs, flush_lock=self.flush_lock) - async def async_log_success_event(self, kwargs, response_obj, start_time, end_time): + async def async_log_success_event( + self, + kwargs: Dict[str, Any], + response_obj: Any, + start_time: datetime, + end_time: datetime, + ) -> None: try: - opik_payload = self._create_opik_payload( + if _should_skip_event(kwargs): + return + + # Build payload using the payload builder + trace_payload, span_payload = opik_payload_builder.build_opik_payload( kwargs=kwargs, response_obj=response_obj, start_time=start_time, end_time=end_time, + project_name=self.opik_project_name, ) - self.log_queue.extend(opik_payload) - verbose_logger.debug( - f"OpikLogger added event to log_queue - Will flush in {self.flush_interval} seconds..." - ) + if self._opik_client is not None: + # Opik native client is available, use it to send data + if trace_payload is not None: + self._opik_client.trace( + id=trace_payload.id, + name=trace_payload.name, + start_time=datetime.fromisoformat(trace_payload.start_time), + end_time=datetime.fromisoformat(trace_payload.end_time), + input=trace_payload.input, + output=trace_payload.output, + metadata=trace_payload.metadata, + tags=trace_payload.tags, + thread_id=trace_payload.thread_id, + project_name=trace_payload.project_name, + ) - if len(self.log_queue) >= self.batch_size: - verbose_logger.debug("OpikLogger - Flushing batch") - await self.flush_queue() + self._opik_client.span( + id=span_payload.id, + trace_id=span_payload.trace_id, + parent_span_id=span_payload.parent_span_id, + name=span_payload.name, + type=span_payload.type, + model=span_payload.model, + start_time=datetime.fromisoformat(span_payload.start_time), + end_time=datetime.fromisoformat(span_payload.end_time), + input=span_payload.input, + output=span_payload.output, + metadata=span_payload.metadata, + tags=span_payload.tags, + usage=span_payload.usage, + project_name=span_payload.project_name, + provider=span_payload.provider, + total_cost=span_payload.total_cost, + ) + else: + # Add payloads to LiteLLM queue + if trace_payload is not None: + self.log_queue.append(trace_payload.__dict__) + self.log_queue.append(span_payload.__dict__) + + verbose_logger.debug( + f"OpikLogger added event to log_queue - Will flush in {self.flush_interval} seconds..." + ) + + if len(self.log_queue) >= self.batch_size: + verbose_logger.debug("OpikLogger - Flushing batch") + await self.flush_queue() except Exception as e: verbose_logger.exception( f"OpikLogger failed to log success event - {str(e)}\n{traceback.format_exc()}" ) - def _sync_send(self, url: str, headers: Dict[str, str], batch: Dict): + def _sync_send( + self, url: str, headers: Dict[str, str], batch: Dict[str, Any] + ) -> None: try: response = self.sync_httpx_client.post( url=url, headers=headers, json=batch # type: ignore @@ -113,30 +186,82 @@ class OpikLogger(CustomBatchLogger): f"OpikLogger failed to send batch - {str(e)}\n{traceback.format_exc()}" ) - def log_success_event(self, kwargs, response_obj, start_time, end_time): + def log_success_event( + self, + kwargs: Dict[str, Any], + response_obj: Any, + start_time: datetime, + end_time: datetime, + ) -> None: try: - opik_payload = self._create_opik_payload( + if _should_skip_event(kwargs): + return + + # Build payload using the payload builder + trace_payload, span_payload = opik_payload_builder.build_opik_payload( kwargs=kwargs, response_obj=response_obj, start_time=start_time, end_time=end_time, + project_name=self.opik_project_name, ) + if self._opik_client is not None: + # Opik native client is available, use it to send data + if trace_payload is not None: + self._opik_client.trace( + id=trace_payload.id, + name=trace_payload.name, + start_time=datetime.fromisoformat(trace_payload.start_time), + end_time=datetime.fromisoformat(trace_payload.end_time), + input=trace_payload.input, + output=trace_payload.output, + metadata=trace_payload.metadata, + tags=trace_payload.tags, + thread_id=trace_payload.thread_id, + project_name=trace_payload.project_name, + ) - traces, spans = get_traces_and_spans_from_payload(opik_payload) - if len(traces) > 0: - self._sync_send( - url=self.trace_url, headers=self.headers, batch={"traces": traces} + self._opik_client.span( + id=span_payload.id, + trace_id=span_payload.trace_id, + parent_span_id=span_payload.parent_span_id, + name=span_payload.name, + type=span_payload.type, + model=span_payload.model, + start_time=datetime.fromisoformat(span_payload.start_time), + end_time=datetime.fromisoformat(span_payload.end_time), + input=span_payload.input, + output=span_payload.output, + metadata=span_payload.metadata, + tags=span_payload.tags, + usage=span_payload.usage, + project_name=span_payload.project_name, + provider=span_payload.provider, + total_cost=span_payload.total_cost, ) - if len(spans) > 0: + else: + # Opik native client is not available, use LiteLLM queue to send data + if trace_payload is not None: + self._sync_send( + url=self.trace_url, + headers=self.headers, + batch={"traces": [trace_payload.__dict__]}, + ) + + # Always send span self._sync_send( - url=self.span_url, headers=self.headers, batch={"spans": spans} + url=self.span_url, + headers=self.headers, + batch={"spans": [span_payload.__dict__]}, ) except Exception as e: verbose_logger.exception( f"OpikLogger failed to log success event - {str(e)}\n{traceback.format_exc()}" ) - async def _submit_batch(self, url: str, headers: Dict[str, str], batch: Dict): + async def _submit_batch( + self, url: str, headers: Dict[str, str], batch: Dict[str, Any] + ) -> None: try: response = await self.async_httpx_client.post( url=url, headers=headers, json=batch # type: ignore @@ -154,8 +279,8 @@ class OpikLogger(CustomBatchLogger): except Exception as e: verbose_logger.exception(f"OpikLogger failed to send batch - {str(e)}") - def _create_opik_headers(self): - headers = {} + def _create_opik_headers(self) -> Dict[str, str]: + headers: Dict[str, str] = {} if self.opik_workspace: headers["Comet-Workspace"] = self.opik_workspace @@ -163,13 +288,13 @@ class OpikLogger(CustomBatchLogger): headers["authorization"] = self.opik_api_key return headers - async def async_send_batch(self): + async def async_send_batch(self) -> None: verbose_logger.info("Calling async_send_batch") if not self.log_queue: return # Split the log_queue into traces and spans - traces, spans = get_traces_and_spans_from_payload(self.log_queue) + traces, spans = utils.get_traces_and_spans_from_payload(self.log_queue) # Send trace batch if len(traces) > 0: @@ -182,183 +307,3 @@ class OpikLogger(CustomBatchLogger): url=self.span_url, headers=self.headers, batch={"spans": spans} ) verbose_logger.info(f"Sent {len(spans)} spans") - - def _create_opik_payload( # noqa: PLR0915 - self, kwargs, response_obj, start_time, end_time - ) -> List[Dict]: - # Get metadata - _litellm_params = kwargs.get("litellm_params", {}) or {} - litellm_params_metadata = _litellm_params.get("metadata", {}) or {} - - # Extract opik metadata - litellm_opik_metadata = litellm_params_metadata.get("opik", {}) - - # Use standard_logging_object to create metadata and input/output data - standard_logging_object = kwargs.get("standard_logging_object", None) - if standard_logging_object is None: - verbose_logger.debug( - "OpikLogger skipping event; no standard_logging_object found" - ) - return [] - - # Update litellm_opik_metadata with opik metadata from requester - standard_logging_metadata = standard_logging_object.get("metadata", {}) or {} - requester_metadata = standard_logging_metadata.get("requester_metadata", {}) or {} - - # If requester_metadata is empty, try to get it from user_api_key_auth_metadata saved in api key - if not requester_metadata: - requester_metadata = standard_logging_metadata.get( - "user_api_key_auth_metadata", {} - ) or {} - - requester_opik_metadata = requester_metadata.get("opik", {}) or {} - litellm_opik_metadata.update(requester_opik_metadata) - - verbose_logger.debug( - f"litellm_opik_metadata - {json.dumps(litellm_opik_metadata, default=str)}" - ) - - project_name = litellm_opik_metadata.get("project_name", self.opik_project_name) - - # Extract trace_id and parent_span_id - current_span_data = litellm_opik_metadata.get("current_span_data", None) - if isinstance(current_span_data, dict): - trace_id = current_span_data.get("trace_id", None) - parent_span_id = current_span_data.get("id", None) - elif current_span_data: - trace_id = current_span_data.trace_id - parent_span_id = current_span_data.id - else: - trace_id = None - parent_span_id = None - - # Create Opik tags - opik_tags = litellm_opik_metadata.get("tags", []) - if kwargs.get("custom_llm_provider"): - opik_tags.append(kwargs["custom_llm_provider"]) - - # Get thread_id if present - thread_id = litellm_opik_metadata.get("thread_id", None) - - # Override with any opik_ headers from proxy request - proxy_server_request = _litellm_params.get("proxy_server_request", {}) or {} - proxy_headers = proxy_server_request.get("headers", {}) or {} - for key, value in proxy_headers.items(): - if key.startswith("opik_"): - param_key = key.replace("opik_", "", 1) - if param_key == "project_name" and value: - project_name = value - elif param_key == "thread_id" and value: - thread_id = value - elif param_key == "tags" and value: - try: - parsed_tags = json.loads(value) - if isinstance(parsed_tags, list): - opik_tags.extend(parsed_tags) - except (json.JSONDecodeError, TypeError): - pass - - # Create input and output data - input_data = standard_logging_object.get("messages", {}) - output_data = standard_logging_object.get("response", {}) - - # Create usage object - usage = create_usage_object(response_obj["usage"]) - - # Define span and trace names - span_name = "%s_%s_%s" % ( - response_obj.get("model", "unknown-model"), - response_obj.get("object", "unknown-object"), - response_obj.get("created", 0), - ) - trace_name = response_obj.get("object", "unknown type") - - # Create metadata object, we add the opik metadata first and then - # update it with the standard_logging_object metadata - metadata = litellm_opik_metadata - if "current_span_data" in metadata: - del metadata["current_span_data"] - metadata["created_from"] = "litellm" - - metadata.update(standard_logging_metadata) - if "call_type" in standard_logging_object: - metadata["type"] = standard_logging_object["call_type"] - if "status" in standard_logging_object: - metadata["status"] = standard_logging_object["status"] - if "response_cost" in kwargs: - metadata["cost"] = { - "total_tokens": kwargs["response_cost"], - "currency": "USD", - } - if "response_cost_failure_debug_info" in kwargs: - metadata["response_cost_failure_debug_info"] = kwargs[ - "response_cost_failure_debug_info" - ] - if "model_map_information" in standard_logging_object: - metadata["model_map_information"] = standard_logging_object[ - "model_map_information" - ] - if "model" in standard_logging_object: - metadata["model"] = standard_logging_object["model"] - if "model_id" in standard_logging_object: - metadata["model_id"] = standard_logging_object["model_id"] - if "model_group" in standard_logging_object: - metadata["model_group"] = standard_logging_object["model_group"] - if "api_base" in standard_logging_object: - metadata["api_base"] = standard_logging_object["api_base"] - if "cache_hit" in standard_logging_object: - metadata["cache_hit"] = standard_logging_object["cache_hit"] - if "saved_cache_cost" in standard_logging_object: - metadata["saved_cache_cost"] = standard_logging_object["saved_cache_cost"] - if "error_str" in standard_logging_object: - metadata["error_str"] = standard_logging_object["error_str"] - if "model_parameters" in standard_logging_object: - metadata["model_parameters"] = standard_logging_object["model_parameters"] - if "hidden_params" in standard_logging_object: - metadata["hidden_params"] = standard_logging_object["hidden_params"] - - payload = [] - if trace_id is None: - trace_id = create_uuid7() - verbose_logger.debug( - f"OpikLogger creating payload for trace with id {trace_id}" - ) - payload.append( - { - "project_name": project_name, - "id": trace_id, - "name": trace_name, - "start_time": start_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), - "end_time": end_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), - "input": input_data, - "output": output_data, - "metadata": metadata, - "tags": opik_tags, - "thread_id": thread_id, - } - ) - - span_id = create_uuid7() - verbose_logger.debug( - f"OpikLogger creating payload for trace with id {trace_id} and span with id {span_id}" - ) - payload.append( - { - "id": span_id, - "project_name": project_name, - "trace_id": trace_id, - "parent_span_id": parent_span_id, - "name": span_name, - "type": "llm", - "start_time": start_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), - "end_time": end_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), - "input": input_data, - "output": output_data, - "metadata": metadata, - "tags": opik_tags, - "thread_id": thread_id, - "usage": usage, - } - ) - verbose_logger.debug(f"Payload: {payload}") - return payload diff --git a/litellm/integrations/opik/opik_payload_builder/__init__.py b/litellm/integrations/opik/opik_payload_builder/__init__.py new file mode 100644 index 00000000000..c57fceaa110 --- /dev/null +++ b/litellm/integrations/opik/opik_payload_builder/__init__.py @@ -0,0 +1,10 @@ +""" +Opik payload builder namespace. + +Public API: + build_opik_payload - Main function to create Opik trace and span payloads +""" + +from .api import build_opik_payload + +__all__ = ["build_opik_payload"] diff --git a/litellm/integrations/opik/opik_payload_builder/api.py b/litellm/integrations/opik/opik_payload_builder/api.py new file mode 100644 index 00000000000..99dbea165e9 --- /dev/null +++ b/litellm/integrations/opik/opik_payload_builder/api.py @@ -0,0 +1,121 @@ +"""Public API for Opik payload building.""" + +from datetime import datetime +from typing import Any, Dict, Optional, Tuple + +from litellm.integrations.opik import utils + +from . import extractors, payload_builders, types + + +def build_opik_payload( + kwargs: Dict[str, Any], + response_obj: Dict[str, Any], + start_time: datetime, + end_time: datetime, + project_name: str, +) -> Tuple[Optional[types.TracePayload], types.SpanPayload]: + """ + Build Opik trace and span payloads from LiteLLM completion data. + + This is the main public API for creating Opik payloads. It: + 1. Extracts all necessary data from LiteLLM kwargs and response + 2. Decides whether to create a new trace or attach to existing + 3. Builds trace payload (if new trace) + 4. Builds span payload (always) + + Args: + kwargs: LiteLLM kwargs containing request metadata and logging data + response_obj: LiteLLM response object containing model response + start_time: Request start time + end_time: Request end time + project_name: Default Opik project name + + Returns: + Tuple of (optional trace payload, span payload) + - First element is TracePayload if creating a new trace, None if attaching to existing + - Second element is always SpanPayload + """ + standard_logging_object = kwargs["standard_logging_object"] + + # Extract litellm params and metadata + litellm_params = kwargs.get("litellm_params", {}) or {} + litellm_metadata = litellm_params.get("metadata", {}) or {} + standard_logging_metadata = standard_logging_object.get("metadata", {}) or {} + + # Extract and merge Opik metadata + opik_metadata = extractors.extract_opik_metadata( + litellm_metadata, standard_logging_metadata + ) + + # Extract project name + current_project_name = opik_metadata.get("project_name", project_name) + + # Extract trace identifiers + current_span_data = opik_metadata.get("current_span_data") + trace_id, parent_span_id = extractors.extract_span_identifiers(current_span_data) + + # Extract tags and thread_id + tags = extractors.extract_tags(opik_metadata, kwargs.get("custom_llm_provider")) + thread_id = opik_metadata.get("thread_id") + + # Apply proxy header overrides + proxy_request = litellm_params.get("proxy_server_request", {}) or {} + proxy_headers = proxy_request.get("headers", {}) or {} + current_project_name, tags, thread_id = extractors.apply_proxy_header_overrides( + current_project_name, tags, thread_id, proxy_headers + ) + + # Build shared metadata + metadata = extractors.extract_and_build_metadata( + opik_metadata=opik_metadata, + standard_logging_metadata=standard_logging_metadata, + standard_logging_object=standard_logging_object, + litellm_kwargs=kwargs, + ) + + # Get input/output data + input_data = standard_logging_object.get("messages", {}) + output_data = standard_logging_object.get("response", {}) + + # Decide whether to create a new trace or attach to existing + trace_payload: Optional[types.TracePayload] = None + if trace_id is None: + trace_id = utils.create_uuid7() + trace_payload = payload_builders.build_trace_payload( + project_name=current_project_name, + trace_id=trace_id, + response_obj=response_obj, + start_time=start_time, + end_time=end_time, + input_data=input_data, + output_data=output_data, + metadata=metadata, + tags=tags, + thread_id=thread_id, + ) + + # Always create a span + usage = utils.create_usage_object(response_obj["usage"]) + + # Extract provider and cost + provider = extractors.normalize_provider_name(kwargs.get("custom_llm_provider")) + cost = kwargs.get("response_cost") + + span_payload = payload_builders.build_span_payload( + project_name=current_project_name, + trace_id=trace_id, + parent_span_id=parent_span_id, + response_obj=response_obj, + start_time=start_time, + end_time=end_time, + input_data=input_data, + output_data=output_data, + metadata=metadata, + tags=tags, + usage=usage, + provider=provider, + cost=cost, + ) + + return trace_payload, span_payload diff --git a/litellm/integrations/opik/opik_payload_builder/extractors.py b/litellm/integrations/opik/opik_payload_builder/extractors.py new file mode 100644 index 00000000000..e4ff021778a --- /dev/null +++ b/litellm/integrations/opik/opik_payload_builder/extractors.py @@ -0,0 +1,221 @@ +"""Data extraction functions for Opik payload building.""" + +import json +from typing import Any, Dict, List, Optional, Tuple + +from litellm import _logging + + +def normalize_provider_name(provider: Optional[str]) -> Optional[str]: + """ + Normalize LiteLLM provider names to standardized string names. + + Args: + provider: LiteLLM internal provider name + + Returns: + Normalized provider name or the original if no mapping exists + """ + if provider is None: + return None + + # Provider mapping to names used in Opik + provider_mapping = { + "openai": "openai", + "vertex_ai-language-models": "google_vertexai", + "gemini": "google_ai", + "anthropic": "anthropic", + "vertex_ai-anthropic_models": "anthropic_vertexai", + "bedrock": "bedrock", + "bedrock_converse": "bedrock", + "groq": "groq", + } + + return provider_mapping.get(provider, provider) + + +def extract_opik_metadata( + litellm_metadata: Dict[str, Any], + standard_logging_metadata: Dict[str, Any], +) -> Dict[str, Any]: + """ + Extract and merge Opik metadata from request and requester. + + Args: + litellm_metadata: Metadata from litellm_params + standard_logging_metadata: Metadata from standard_logging_object + + Returns: + Merged Opik metadata dictionary + """ + opik_meta = litellm_metadata.get("opik", {}).copy() + + requester_metadata = standard_logging_metadata.get("requester_metadata", {}) or {} + requester_opik = requester_metadata.get("opik", {}) or {} + opik_meta.update(requester_opik) + + _logging.verbose_logger.debug( + f"litellm_opik_metadata - {json.dumps(opik_meta, default=str)}" + ) + + return opik_meta + + +def extract_span_identifiers( + current_span_data: Any, +) -> Tuple[Optional[str], Optional[str]]: + """ + Extract trace_id and parent_span_id from current_span_data. + + Args: + current_span_data: Either dict with trace_id/id keys or Opik object + + Returns: + Tuple of (trace_id, parent_span_id), both optional + """ + if current_span_data is None: + return None, None + + if isinstance(current_span_data, dict): + return (current_span_data.get("trace_id"), current_span_data.get("id")) + + try: + return current_span_data.trace_id, current_span_data.id + except AttributeError: + _logging.verbose_logger.warning( + f"Unexpected current_span_data format: {type(current_span_data)}" + ) + return None, None + + +def extract_tags( + opik_metadata: Dict[str, Any], + custom_llm_provider: Optional[str], +) -> List[str]: + """ + Extract and build list of tags. + + Args: + opik_metadata: Opik metadata dictionary + custom_llm_provider: LLM provider name to add as tag + + Returns: + List of tags + """ + tags = list(opik_metadata.get("tags", [])) + + if custom_llm_provider: + tags.append(custom_llm_provider) + + return tags + + +def apply_proxy_header_overrides( + project_name: str, + tags: List[str], + thread_id: Optional[str], + proxy_headers: Dict[str, Any], +) -> Tuple[str, List[str], Optional[str]]: + """ + Apply overrides from proxy request headers (opik_* prefix). + + Args: + project_name: Current project name + tags: Current tags list + thread_id: Current thread ID + proxy_headers: HTTP headers from proxy request + + Returns: + Tuple of (project_name, tags, thread_id) with overrides applied + """ + for key, value in proxy_headers.items(): + if not key.startswith("opik_") or not value: + continue + + param_key = key.replace("opik_", "", 1) + + if param_key == "project_name": + project_name = value + elif param_key == "thread_id": + thread_id = value + elif param_key == "tags": + try: + parsed_tags = json.loads(value) + if isinstance(parsed_tags, list): + tags.extend(parsed_tags) + except (json.JSONDecodeError, TypeError): + _logging.verbose_logger.warning( + f"Failed to parse tags from header: {value}" + ) + + return project_name, tags, thread_id + + +def extract_and_build_metadata( + opik_metadata: Dict[str, Any], + standard_logging_metadata: Dict[str, Any], + standard_logging_object: Dict[str, Any], + litellm_kwargs: Dict[str, Any], +) -> Dict[str, Any]: + """ + Build the complete metadata dictionary from all available sources. + + This combines: + - Opik-specific metadata (tags, etc.) + - Standard logging metadata + - Fields from standard_logging_object (model info, status, etc.) + - Cost information from litellm_kwargs (calculated after completion) + + Args: + opik_metadata: Opik-specific metadata from request + standard_logging_metadata: Standard logging metadata + standard_logging_object: Full standard logging object with call details + litellm_kwargs: Original LiteLLM kwargs (includes response_cost) + + Returns: + Complete metadata dictionary for trace/span + """ + # Start with opik metadata (excluding current_span_data which is used for trace linking) + metadata = {k: v for k, v in opik_metadata.items() if k != "current_span_data"} + metadata["created_from"] = "litellm" + + # Merge with standard logging metadata + metadata.update(standard_logging_metadata) + + # Add fields from standard_logging_object + # These come from the LiteLLM logging infrastructure + field_mappings = { + "call_type": "type", + "status": "status", + "model": "model", + "model_id": "model_id", + "model_group": "model_group", + "api_base": "api_base", + "cache_hit": "cache_hit", + "saved_cache_cost": "saved_cache_cost", + "error_str": "error_str", + "model_parameters": "model_parameters", + "hidden_params": "hidden_params", + "model_map_information": "model_map_information", + } + + for source_key, dest_key in field_mappings.items(): + if source_key in standard_logging_object: + metadata[dest_key] = standard_logging_object[source_key] + + # Add cost information + # response_cost is calculated by LiteLLM after completion and added to kwargs + # See: litellm/litellm_core_utils/llm_response_utils/response_metadata.py + if "response_cost" in litellm_kwargs: + metadata["cost"] = { + "total_tokens": litellm_kwargs["response_cost"], + "currency": "USD", + } + + # Add debug info if cost calculation failed + if "response_cost_failure_debug_info" in litellm_kwargs: + metadata["response_cost_failure_debug_info"] = litellm_kwargs[ + "response_cost_failure_debug_info" + ] + + return metadata diff --git a/litellm/integrations/opik/opik_payload_builder/payload_builders.py b/litellm/integrations/opik/opik_payload_builder/payload_builders.py new file mode 100644 index 00000000000..4656924fdb5 --- /dev/null +++ b/litellm/integrations/opik/opik_payload_builder/payload_builders.py @@ -0,0 +1,89 @@ +"""Payload builders for Opik traces and spans.""" + +from datetime import datetime, timezone +from typing import Any, Dict, List, Optional + +from litellm import _logging +from litellm.integrations.opik import utils + +from . import types + + +def build_trace_payload( + project_name: str, + trace_id: str, + response_obj: Dict[str, Any], + start_time: datetime, + end_time: datetime, + input_data: Any, + output_data: Any, + metadata: Dict[str, Any], + tags: List[str], + thread_id: Optional[str], +) -> types.TracePayload: + """Build a complete trace payload.""" + trace_name = response_obj.get("object", "unknown type") + + return types.TracePayload( + project_name=project_name, + id=trace_id, + name=trace_name, + start_time=( + start_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + ), + end_time=end_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), + input=input_data, + output=output_data, + metadata=metadata, + tags=tags, + thread_id=thread_id, + ) + + +def build_span_payload( + project_name: str, + trace_id: str, + parent_span_id: Optional[str], + response_obj: Dict[str, Any], + start_time: datetime, + end_time: datetime, + input_data: Any, + output_data: Any, + metadata: Dict[str, Any], + tags: List[str], + usage: Dict[str, int], + provider: Optional[str] = None, + cost: Optional[float] = None, +) -> types.SpanPayload: + """Build a complete span payload.""" + span_id = utils.create_uuid7() + + model = response_obj.get("model", "unknown-model") + obj_type = response_obj.get("object", "unknown-object") + created = response_obj.get("created", 0) + span_name = f"{model}_{obj_type}_{created}" + + _logging.verbose_logger.debug( + f"OpikLogger creating span with id {span_id} for trace {trace_id}" + ) + + return types.SpanPayload( + id=span_id, + project_name=project_name, + trace_id=trace_id, + parent_span_id=parent_span_id, + name=span_name, + type="llm", + model=model, + start_time=( + start_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + ), + end_time=end_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), + input=input_data, + output=output_data, + metadata=metadata, + tags=tags, + usage=usage, + provider=provider, + total_cost=cost, + ) diff --git a/litellm/integrations/opik/opik_payload_builder/types.py b/litellm/integrations/opik/opik_payload_builder/types.py new file mode 100644 index 00000000000..070cb11489a --- /dev/null +++ b/litellm/integrations/opik/opik_payload_builder/types.py @@ -0,0 +1,46 @@ +"""Type definitions for Opik payload building.""" + +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional, Tuple, Union + + +@dataclass +class TracePayload: + """Opik trace payload structure""" + + project_name: str + id: str + name: str + start_time: str + end_time: str + input: Any + output: Any + metadata: Dict[str, Any] + tags: List[str] + thread_id: Optional[str] = None + + +@dataclass +class SpanPayload: + """Opik span payload structure""" + + id: str + project_name: str + trace_id: str + name: str + type: Literal["llm"] + model: str + start_time: str + end_time: str + input: Any + output: Any + metadata: Dict[str, Any] + tags: List[str] + usage: Dict[str, int] + parent_span_id: Optional[str] = None + provider: Optional[str] = None + total_cost: Optional[float] = None + + +PayloadItem = Union[TracePayload, SpanPayload] +TraceSpanPayloadTuple = Tuple[Optional[TracePayload], SpanPayload] diff --git a/litellm/integrations/opik/utils.py b/litellm/integrations/opik/utils.py index 7b3b64dcf38..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 Dict, Final, List, Optional +from typing import Any, Dict, Final, List, Optional, Tuple CONFIG_FILE_PATH_DEFAULT: Final[str] = "~/.opik.config" @@ -99,12 +99,26 @@ def create_usage_object(usage): return usage_dict -def _remove_nulls(x): - x_ = {k: v for k, v in x.items() if v is not None} - return x_ +def _remove_nulls(x: Dict[str, Any]) -> Dict[str, Any]: + """Remove None values from dict.""" + return {k: v for k, v in x.items() if v is not None} -def get_traces_and_spans_from_payload(payload: List): +def get_traces_and_spans_from_payload( + payload: List[Dict[str, Any]] +) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: + """ + Separate traces and spans from payload. + + Traces are identified by not having a "type" field. + Spans are identified by having a "type" field. + + Args: + payload: List of dicts containing trace and span data + + Returns: + Tuple of (traces, spans) where both are lists of dicts with null values removed + """ traces = [_remove_nulls(x) for x in payload if "type" not in x] spans = [_remove_nulls(x) for x in payload if "type" in x] return traces, spans diff --git a/litellm/integrations/s3.py b/litellm/integrations/s3.py index 53caeb0d198..2e70b1d6519 100644 --- a/litellm/integrations/s3.py +++ b/litellm/integrations/s3.py @@ -181,13 +181,13 @@ class S3Logger: def get_s3_object_key( s3_path: str, - team_alias_prefix: str, + prefix: str, start_time: datetime, s3_file_name: str, ) -> str: s3_object_key = ( (s3_path.rstrip("/") + "/" if s3_path else "") - + team_alias_prefix + + prefix + start_time.strftime("%Y-%m-%d") + "/" + s3_file_name diff --git a/litellm/integrations/s3_v2.py b/litellm/integrations/s3_v2.py index cc44450e737..534b85e4752 100644 --- a/litellm/integrations/s3_v2.py +++ b/litellm/integrations/s3_v2.py @@ -50,6 +50,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): s3_config=None, s3_use_team_prefix: bool = False, s3_strip_base64_files: bool = False, + s3_use_key_prefix: bool = False, **kwargs, ): try: @@ -57,12 +58,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): f"in init s3 logger - s3_callback_params {litellm.s3_callback_params}" ) - # IMPORTANT: We use a concurrent limit of 1 to upload to s3 - # Files should get uploaded BUT they should not impact latency of LLM calling logic - self.async_httpx_client = get_async_httpx_client( - llm_provider=httpxSpecialProvider.LoggingCallback, - ) - + # Initialize S3 params first to get the correct s3_verify value self._init_s3_params( s3_bucket_name=s3_bucket_name, s3_region_name=s3_region_name, @@ -81,10 +77,21 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): s3_config=s3_config, s3_path=s3_path, s3_use_team_prefix=s3_use_team_prefix, - s3_strip_base64_files=s3_strip_base64_files + s3_strip_base64_files=s3_strip_base64_files, + s3_use_key_prefix=s3_use_key_prefix ) verbose_logger.debug(f"s3 logger using endpoint url {s3_endpoint_url}") + # IMPORTANT + # Create httpx client AFTER _init_s3_params so we have the correct s3_verify value + verbose_logger.debug( + f"s3_v2 logger creating async httpx client with s3_verify={self.s3_verify}" + ) + self.async_httpx_client = get_async_httpx_client( + llm_provider=httpxSpecialProvider.LoggingCallback, + params={"ssl_verify": self.s3_verify} + ) + asyncio.create_task(self.periodic_flush()) self.flush_lock = asyncio.Lock() @@ -127,6 +134,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): s3_path: Optional[str] = None, s3_use_team_prefix: bool = False, s3_strip_base64_files: bool = False, + s3_use_key_prefix: bool = False, ): """ Initialize the s3 params for this logging callback @@ -147,9 +155,11 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): litellm.s3_callback_params.get("s3_api_version") or s3_api_version ) self.s3_use_ssl = ( - litellm.s3_callback_params.get("s3_use_ssl", True) or s3_use_ssl + litellm.s3_callback_params.get("s3_use_ssl", True) if litellm.s3_callback_params.get("s3_use_ssl") is not None else s3_use_ssl + ) + self.s3_verify = ( + litellm.s3_callback_params.get("s3_verify") if litellm.s3_callback_params.get("s3_verify") is not None else s3_verify ) - self.s3_verify = litellm.s3_callback_params.get("s3_verify") or s3_verify self.s3_endpoint_url = ( litellm.s3_callback_params.get("s3_endpoint_url") or s3_endpoint_url ) @@ -197,6 +207,11 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): or s3_use_team_prefix ) + self.s3_use_key_prefix = ( + bool(litellm.s3_callback_params.get("s3_use_key_prefix", False)) + or s3_use_key_prefix + ) + self.s3_strip_base64_files = ( bool(litellm.s3_callback_params.get("s3_strip_base64_files", False)) or s3_strip_base64_files @@ -247,7 +262,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): ) except Exception as e: verbose_logger.exception(f"s3 Layer Error - {str(e)}") - pass + self.handle_callback_failure(callback_name="S3Logger") async def async_upload_data_to_s3( self, batch_logging_element: s3BatchLoggingElement @@ -279,6 +294,9 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): verbose_logger.debug( f"s3_v2 logger - uploading data to s3 - {batch_logging_element.s3_object_key}" ) + verbose_logger.debug( + f"s3_v2 logger - s3_verify setting: {self.s3_verify}" + ) # Prepare the URL url = f"https://{self.s3_bucket_name}.s3.{self.s3_region_name}.amazonaws.com/{batch_logging_element.s3_object_key}" @@ -331,6 +349,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): response.raise_for_status() except Exception as e: verbose_logger.exception(f"Error uploading to s3: {str(e)}") + self.handle_callback_failure(callback_name="S3Logger") async def async_send_batch(self): """ @@ -373,36 +392,36 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): return None if self.s3_strip_base64_files: - import asyncio - standard_logging_payload = asyncio.run(self._strip_base64_from_messages(standard_logging_payload)) + standard_logging_payload = self._strip_base64_from_messages_sync(standard_logging_payload) - team_alias = standard_logging_payload["metadata"].get("user_api_key_team_alias") + # Base prefix (default empty) + prefix_components = [] + if self.s3_use_team_prefix: + team_alias = standard_logging_payload.get("metadata", {}).get("user_api_key_team_alias", None) + if team_alias: + prefix_components.append(team_alias) + if self.s3_use_key_prefix: + user_api_key_alias = standard_logging_payload.get("metadata", {}).get("user_api_key_alias", None) + if user_api_key_alias: + prefix_components.append(user_api_key_alias) - team_alias_prefix = "" - if ( - litellm.enable_preview_features - and self.s3_use_team_prefix - and team_alias is not None - ): - team_alias_prefix = f"{team_alias}/" + + # Construct full prefix path + prefix_path = "/".join(prefix_components) + if prefix_path: + prefix_path += "/" s3_file_name = ( litellm.utils.get_logging_id(start_time, standard_logging_payload) or "" ) + verbose_logger.debug(f"Creating s3 file with prefix_components={prefix_components},prefix_path={prefix_path} and {s3_file_name}") s3_object_key = get_s3_object_key( s3_path=cast(Optional[str], self.s3_path) or "", - team_alias_prefix=team_alias_prefix, + prefix=prefix_path, start_time=start_time, s3_file_name=s3_file_name, ) - - s3_object_download_filename = ( - "time-" - + start_time.strftime("%Y-%m-%dT%H-%M-%S-%f") - + "_" - + standard_logging_payload["id"] - + ".json" - ) + verbose_logger.debug(f"s3_object_key={s3_object_key}") s3_object_download_filename = f"time-{start_time.strftime('%Y-%m-%dT%H-%M-%S-%f')}_{standard_logging_payload['id']}.json" @@ -477,12 +496,15 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): # Prepare the signed headers signed_headers = dict(aws_request.headers.items()) - httpx_client = _get_httpx_client() + httpx_client = _get_httpx_client( + params={"ssl_verify": self.s3_verify} if self.s3_verify is not None else None + ) # Make the request response = httpx_client.put(url, data=json_string, headers=signed_headers) response.raise_for_status() except Exception as e: verbose_logger.exception(f"Error uploading to s3: {str(e)}") + self.handle_callback_failure(callback_name="S3Logger") async def _download_object_from_s3(self, s3_object_key: str) -> Optional[dict]: """ 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 fac57e038f0..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"] @@ -2162,6 +2132,11 @@ class Logging(LiteLLMLoggingBaseClass): ) if capture_exception: # log this error to sentry for debugging capture_exception(e) + # Track callback logging failures in Prometheus + try: + self._handle_callback_failure(callback=callback) + except Exception: + pass except Exception as e: verbose_logger.exception( "LiteLLM.LoggingError: [Non-Blocking] Exception occurred while success logging {}".format( @@ -2246,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: @@ -2259,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( @@ -2275,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, @@ -2467,8 +2442,31 @@ class Logging(LiteLLMLoggingBaseClass): verbose_logger.error( f"LiteLLM.LoggingError: [Non-Blocking] Exception occurred while success logging {traceback.format_exc()}" ) + self._handle_callback_failure(callback=callback) pass + 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"): + 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)}") + def _failure_handler_helper_fn( self, exception, traceback_exception, start_time=None, end_time=None ): @@ -2497,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 @@ -2804,6 +2802,8 @@ class Logging(LiteLLMLoggingBaseClass): str(e), callback ) ) + # Track callback logging failures in Prometheus + self._handle_callback_failure(callback=callback) def _get_trace_id(self, service_name: Literal["langfuse"]) -> Optional[str]: """ @@ -2936,15 +2936,19 @@ class Logging(LiteLLMLoggingBaseClass): Helper to get the name of a callback function Args: - cb: The callback function/string to get the name of + cb: The callback object/function/string to get the name of Returns: The name of the callback """ + if isinstance(cb, str): + return cb 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: @@ -3000,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 @@ -3407,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) @@ -3433,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 ( @@ -3567,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) @@ -4269,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 @@ -4333,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, ) @@ -4496,7 +4506,7 @@ class StandardLoggingPayloadSetup: def _get_status_fields( status: StandardLoggingPayloadStatus, - guardrail_information: Optional[list[dict]], + guardrail_information: Optional[List[dict]], error_str: Optional[str], ) -> "StandardLoggingPayloadStatusFields": """ @@ -4835,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/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 33658d49063..69e3cc43322 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -94,6 +94,15 @@ def handle_messages_with_content_list_to_str_conversion( return messages +def strip_name_from_message(message: AllMessageValues, allowed_name_roles: List[str] = ["user"]) -> AllMessageValues: + """ + Removes 'name' from message + """ + msg_copy = message.copy() + if msg_copy.get("role") not in allowed_name_roles: + msg_copy.pop("name", None) # type: ignore + return msg_copy + def strip_name_from_messages( messages: List[AllMessageValues], allowed_name_roles: List[str] = ["user"] ) -> List[AllMessageValues]: diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index ba83e3c72c1..717c2607657 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -1,4 +1,5 @@ import copy +import hashlib import json import mimetypes import re @@ -1490,7 +1491,7 @@ def convert_to_anthropic_tool_invoke( _content_element = add_cache_control_to_content( anthropic_content_element=_anthropic_tool_use_param, - orignal_content_element=dict(tool), + original_content_element=dict(tool), ) if "cache_control" in _content_element: @@ -1512,9 +1513,9 @@ def add_cache_control_to_content( AnthropicMessagesToolUseParam, ChatCompletionThinkingBlock, ], - orignal_content_element: Union[dict, AllMessageValues], + original_content_element: Union[dict, AllMessageValues], ): - cache_control_param = orignal_content_element.get("cache_control") + cache_control_param = original_content_element.get("cache_control") if cache_control_param is not None and isinstance(cache_control_param, dict): transformed_param = ChatCompletionCachedContent(**cache_control_param) # type: ignore @@ -1727,7 +1728,7 @@ def anthropic_messages_pt( # noqa: PLR0915 ) _content_element = add_cache_control_to_content( anthropic_content_element=_anthropic_content_element, - orignal_content_element=dict(m), + original_content_element=dict(m), ) if "cache_control" in _content_element: @@ -1745,7 +1746,7 @@ def anthropic_messages_pt( # noqa: PLR0915 ) _content_element = add_cache_control_to_content( anthropic_content_element=_anthropic_text_content_element, - orignal_content_element=dict(m), + original_content_element=dict(m), ) _content_element = cast( AnthropicMessagesTextParam, _content_element @@ -1767,7 +1768,7 @@ def anthropic_messages_pt( # noqa: PLR0915 } _content_element = add_cache_control_to_content( anthropic_content_element=_anthropic_content_text_element, - orignal_content_element=dict(user_message_types_block), + original_content_element=dict(user_message_types_block), ) if "cache_control" in _content_element: @@ -1825,7 +1826,7 @@ def anthropic_messages_pt( # noqa: PLR0915 ) _cached_message = add_cache_control_to_content( anthropic_content_element=anthropic_message, - orignal_content_element=dict(m), + original_content_element=dict(m), ) assistant_content.append( @@ -1845,7 +1846,7 @@ def anthropic_messages_pt( # noqa: PLR0915 _content_element = add_cache_control_to_content( anthropic_content_element=_anthropic_text_content_element, - orignal_content_element=dict(assistant_content_block), + original_content_element=dict(assistant_content_block), ) if "cache_control" in _content_element: @@ -2706,12 +2707,39 @@ class BedrockImageProcessor: for video_type in supported_video_formats ) + HASH_SAMPLE_BYTES = 64 * 1024 # hash up to 64 KB of data + if is_document: + # --- Prepare normalized bytes for hashing (without modifying original) --- + if isinstance(image_bytes, str): + # Remove whitespace/newlines so base64 variations hash identically + normalized = "".join(image_bytes.split()).encode("utf-8") + else: + normalized = image_bytes + + # --- Use only the first 64 KB for speed --- + if len(normalized) <= HASH_SAMPLE_BYTES: + sample = normalized + else: + sample = normalized[:HASH_SAMPLE_BYTES] + + # --- Compute deterministic hash (sample + total length) --- + hasher = hashlib.sha256() + hasher.update(sample) + hasher.update( + str(len(normalized)).encode("utf-8") + ) # include full length for uniqueness + full_hash = hasher.hexdigest() + content_hash = full_hash[:16] # short deterministic ID + + document_name = f"DocumentPDFmessages_{content_hash}_{image_format}" + + # --- Return content block --- return BedrockContentBlock( document=BedrockDocumentBlock( source=_blob, format=image_format, - name=f"DocumentPDFmessages_{str(uuid.uuid4())}", + name=document_name, ) ) elif is_video: 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/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index a5c3862cf30..4d8e109d882 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -1305,7 +1305,7 @@ class CustomStreamWrapper: else: # openai / azure chat model if self.custom_llm_provider == "azure": if isinstance(chunk, BaseModel) and hasattr(chunk, "model"): - # for azure, we need to pass the model from the orignal chunk + # for azure, we need to pass the model from the original chunk self.model = getattr(chunk, "model", self.model) response_obj = self.handle_openai_chat_completion_chunk(chunk) if response_obj is None: diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index adaf8e46d25..5de72f45025 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) @@ -763,7 +778,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/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 922e6626f23..a786f06921f 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -167,14 +167,20 @@ class LiteLLMAnthropicMessagesAdapter: ) new_user_content_list.append(text_obj) elif content.get("type") == "image": - image_url = ChatCompletionImageUrlObject( - url=f"data:{content.get('type', '')};base64,{content.get('source', '')}" - ) - image_obj = ChatCompletionImageObject( - type="image_url", image_url=image_url + # Convert Anthropic image format to OpenAI format + source = content.get("source", {}) + openai_image_url = ( + self._translate_anthropic_image_to_openai(source) ) - new_user_content_list.append(image_obj) + if openai_image_url: + image_url_obj = ChatCompletionImageUrlObject( + url=openai_image_url + ) + image_obj = ChatCompletionImageObject( + type="image_url", image_url=image_url_obj + ) + new_user_content_list.append(image_obj) elif content.get("type") == "tool_result": if "content" not in content: tool_result = ChatCompletionToolMessage( @@ -210,13 +216,21 @@ class LiteLLMAnthropicMessagesAdapter: ) tool_message_list.append(tool_result) elif c.get("type") == "image": - image_str = f"data:{c.get('type', '')};base64,{c.get('source', '')}" + # Convert Anthropic image format to OpenAI format for tool results + source = c.get("source", {}) + openai_image_url = ( + self._translate_anthropic_image_to_openai( + source + ) + or "" + ) + tool_result = ChatCompletionToolMessage( role="tool", tool_call_id=content.get( "tool_use_id", "" ), - content=image_str, + content=openai_image_url, ) tool_message_list.append(tool_result) @@ -232,7 +246,9 @@ class LiteLLMAnthropicMessagesAdapter: ## ASSISTANT MESSAGE ## assistant_message_str: Optional[str] = None tool_calls: List[ChatCompletionAssistantToolCall] = [] - thinking_blocks: List[Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]] = [] + thinking_blocks: List[ + Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock] + ] = [] if m["role"] == "assistant": if isinstance(m.get("content"), str): assistant_message_str = str(m.get("content", "")) @@ -264,23 +280,30 @@ class LiteLLMAnthropicMessagesAdapter: type="thinking", thinking=content.get("thinking") or "", signature=content.get("signature") or "", - cache_control=content.get("cache_control", {}) + cache_control=content.get("cache_control", {}), ) thinking_blocks.append(thinking_block) elif content.get("type") == "redacted_thinking": - redacted_thinking_block = ChatCompletionRedactedThinkingBlock( - type="redacted_thinking", - data=content.get("data") or "", - cache_control=content.get("cache_control", {}) + redacted_thinking_block = ( + ChatCompletionRedactedThinkingBlock( + type="redacted_thinking", + data=content.get("data") or "", + cache_control=content.get("cache_control", {}), + ) ) thinking_blocks.append(redacted_thinking_block) - - if assistant_message_str is not None or len(tool_calls) > 0 or len(thinking_blocks) > 0: + if ( + assistant_message_str is not None + or len(tool_calls) > 0 + or len(thinking_blocks) > 0 + ): assistant_message = ChatCompletionAssistantMessage( role="assistant", content=assistant_message_str, - thinking_blocks=thinking_blocks if len(thinking_blocks) > 0 else None, + thinking_blocks=( + thinking_blocks if len(thinking_blocks) > 0 else None + ), ) if len(tool_calls) > 0: assistant_message["tool_calls"] = tool_calls @@ -406,19 +429,55 @@ class LiteLLMAnthropicMessagesAdapter: return new_kwargs - def _translate_openai_content_to_anthropic( - self, choices: List[Choices] - ) -> List[ - Union[AnthropicResponseContentBlockText, AnthropicResponseContentBlockToolUse, AnthropicResponseContentBlockThinking, AnthropicResponseContentBlockRedactedThinking] + def _translate_anthropic_image_to_openai(self, image_source: dict) -> Optional[str]: + """ + Translate Anthropic image source format to OpenAI-compatible image URL. + + Anthropic supports two image source formats: + 1. Base64: {"type": "base64", "media_type": "image/jpeg", "data": "..."} + 2. URL: {"type": "url", "url": "https://..."} + + Returns the properly formatted image URL string, or None if invalid format. + """ + if not isinstance(image_source, dict): + return None + + source_type = image_source.get("type") + + if source_type == "base64": + # Base64 image format + media_type = image_source.get("media_type", "image/jpeg") + image_data = image_source.get("data", "") + if image_data: + return f"data:{media_type};base64,{image_data}" + elif source_type == "url": + # URL-referenced image format + return image_source.get("url", "") + + return None + + def _translate_openai_content_to_anthropic(self, choices: List[Choices]) -> List[ + Union[ + AnthropicResponseContentBlockText, + AnthropicResponseContentBlockToolUse, + AnthropicResponseContentBlockThinking, + AnthropicResponseContentBlockRedactedThinking, + ] ]: new_content: List[ Union[ - AnthropicResponseContentBlockText, AnthropicResponseContentBlockToolUse, AnthropicResponseContentBlockThinking, AnthropicResponseContentBlockRedactedThinking + AnthropicResponseContentBlockText, + AnthropicResponseContentBlockToolUse, + AnthropicResponseContentBlockThinking, + AnthropicResponseContentBlockRedactedThinking, ] ] = [] for choice in choices: # Handle thinking blocks first - if hasattr(choice.message, 'thinking_blocks') and choice.message.thinking_blocks: + if ( + hasattr(choice.message, "thinking_blocks") + and choice.message.thinking_blocks + ): for thinking_block in choice.message.thinking_blocks: if thinking_block.get("type") == "thinking": thinking_value = thinking_block.get("thinking", "") @@ -426,8 +485,16 @@ class LiteLLMAnthropicMessagesAdapter: new_content.append( AnthropicResponseContentBlockThinking( type="thinking", - thinking=str(thinking_value) if thinking_value is not None else "", - signature=str(signature_value) if signature_value is not None else None, + thinking=( + str(thinking_value) + if thinking_value is not None + else "" + ), + signature=( + str(signature_value) + if signature_value is not None + else None + ), ) ) elif thinking_block.get("type") == "redacted_thinking": @@ -438,7 +505,7 @@ class LiteLLMAnthropicMessagesAdapter: data=str(data_value) if data_value is not None else "", ) ) - + # Handle tool calls if ( choice.message.tool_calls is not None @@ -450,7 +517,11 @@ class LiteLLMAnthropicMessagesAdapter: type="tool_use", id=tool_call.id, name=tool_call.function.name or "", - input=json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}, + input=( + json.loads(tool_call.function.arguments) + if tool_call.function.arguments + else {} + ), ) ) # Handle text content @@ -525,8 +596,8 @@ class LiteLLMAnthropicMessagesAdapter: name=choice.delta.tool_calls[0].function.name or "", input={}, ) - elif ( - isinstance(choice, StreamingChoices) and hasattr(choice.delta, "thinking_blocks") + elif isinstance(choice, StreamingChoices) and hasattr( + choice.delta, "thinking_blocks" ): thinking_blocks = choice.delta.thinking_blocks or [] if len(thinking_blocks) > 0: @@ -539,22 +610,26 @@ class LiteLLMAnthropicMessagesAdapter: assert isinstance(signature, str) if thinking and signature: - raise ValueError("Both `thinking` and `signature` in a single streaming chunk isn't supported.") + raise ValueError( + "Both `thinking` and `signature` in a single streaming chunk isn't supported." + ) return "thinking", ChatCompletionThinkingBlock( - type="thinking", - thinking=thinking, - signature=signature + type="thinking", thinking=thinking, signature=signature ) - return "text", TextBlock(type="text", text="") def _translate_streaming_openai_chunk_to_anthropic( self, choices: List[Union[OpenAIStreamingChoice, StreamingChoices]] ) -> Tuple[ Literal["text_delta", "input_json_delta", "thinking_delta", "signature_delta"], - Union[ContentTextBlockDelta, ContentJsonBlockDelta, ContentThinkingBlockDelta, ContentThinkingSignatureBlockDelta], + Union[ + ContentTextBlockDelta, + ContentJsonBlockDelta, + ContentThinkingBlockDelta, + ContentThinkingSignatureBlockDelta, + ], ]: text: str = "" @@ -572,7 +647,9 @@ class LiteLLMAnthropicMessagesAdapter: and tool.function.arguments is not None ): partial_json = (partial_json or "") + tool.function.arguments - elif isinstance(choice, StreamingChoices) and hasattr(choice.delta, "thinking_blocks"): + elif isinstance(choice, StreamingChoices) and hasattr( + choice.delta, "thinking_blocks" + ): thinking_blocks = choice.delta.thinking_blocks or [] if len(thinking_blocks) > 0: for thinking_block in thinking_blocks: @@ -585,19 +662,24 @@ class LiteLLMAnthropicMessagesAdapter: reasoning_content += thinking reasoning_signature += signature - - if reasoning_content and reasoning_signature: - raise ValueError("Both `reasoning` and `signature` in a single streaming chunk isn't supported.") + if reasoning_content and reasoning_signature: + raise ValueError( + "Both `reasoning` and `signature` in a single streaming chunk isn't supported." + ) if partial_json is not None: return "input_json_delta", ContentJsonBlockDelta( type="input_json_delta", partial_json=partial_json ) elif reasoning_content: - return "thinking_delta", ContentThinkingBlockDelta(type="thinking_delta", thinking=reasoning_content) + return "thinking_delta", ContentThinkingBlockDelta( + type="thinking_delta", thinking=reasoning_content + ) elif reasoning_signature: - return "signature_delta", ContentThinkingSignatureBlockDelta(type="signature_delta", signature=reasoning_signature) + return "signature_delta", ContentThinkingSignatureBlockDelta( + type="signature_delta", signature=reasoning_signature + ) else: return "text_delta", ContentTextBlockDelta(type="text_delta", text=text) 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/ocr/__init__.py b/litellm/llms/azure_ai/ocr/__init__.py index 86f7e53d60b..7182a750b45 100644 --- a/litellm/llms/azure_ai/ocr/__init__.py +++ b/litellm/llms/azure_ai/ocr/__init__.py @@ -1,5 +1,13 @@ """Azure AI OCR module.""" +from .common_utils import get_azure_ai_ocr_config +from .document_intelligence.transformation import ( + AzureDocumentIntelligenceOCRConfig, +) from .transformation import AzureAIOCRConfig -__all__ = ["AzureAIOCRConfig"] +__all__ = [ + "AzureAIOCRConfig", + "AzureDocumentIntelligenceOCRConfig", + "get_azure_ai_ocr_config", +] diff --git a/litellm/llms/azure_ai/ocr/common_utils.py b/litellm/llms/azure_ai/ocr/common_utils.py new file mode 100644 index 00000000000..ef470c74923 --- /dev/null +++ b/litellm/llms/azure_ai/ocr/common_utils.py @@ -0,0 +1,53 @@ +""" +Common utilities for Azure AI OCR providers. + +This module provides routing logic to determine which OCR configuration to use +based on the model name. +""" + +from typing import TYPE_CHECKING, Optional + +from litellm._logging import verbose_logger + +if TYPE_CHECKING: + from litellm.llms.base_llm.ocr.transformation import BaseOCRConfig + + +def get_azure_ai_ocr_config(model: str) -> Optional["BaseOCRConfig"]: + """ + Determine which Azure AI OCR configuration to use based on the model name. + + Azure AI supports multiple OCR services: + - Azure Document Intelligence: azure_ai/doc-intelligence/ + - Mistral OCR (via Azure AI): azure_ai/ + + Args: + model: The model name (e.g., "azure_ai/doc-intelligence/prebuilt-read", + "azure_ai/pixtral-12b-2409") + + Returns: + OCR configuration instance for the specified model + + Examples: + >>> get_azure_ai_ocr_config("azure_ai/doc-intelligence/prebuilt-read") + + + >>> get_azure_ai_ocr_config("azure_ai/pixtral-12b-2409") + + """ + from litellm.llms.azure_ai.ocr.document_intelligence.transformation import ( + AzureDocumentIntelligenceOCRConfig, + ) + from litellm.llms.azure_ai.ocr.transformation import AzureAIOCRConfig + + # Check for Azure Document Intelligence models + if "doc-intelligence" in model or "documentintelligence" in model: + verbose_logger.debug( + f"Routing {model} to Azure Document Intelligence OCR config" + ) + return AzureDocumentIntelligenceOCRConfig() + + # Default to Mistral-based OCR for other azure_ai models + verbose_logger.debug(f"Routing {model} to Azure AI (Mistral) OCR config") + return AzureAIOCRConfig() + diff --git a/litellm/llms/azure_ai/ocr/document_intelligence/__init__.py b/litellm/llms/azure_ai/ocr/document_intelligence/__init__.py new file mode 100644 index 00000000000..372a6a8d761 --- /dev/null +++ b/litellm/llms/azure_ai/ocr/document_intelligence/__init__.py @@ -0,0 +1,5 @@ +"""Azure Document Intelligence OCR module.""" +from .transformation import AzureDocumentIntelligenceOCRConfig + +__all__ = ["AzureDocumentIntelligenceOCRConfig"] + diff --git a/litellm/llms/azure_ai/ocr/document_intelligence/transformation.py b/litellm/llms/azure_ai/ocr/document_intelligence/transformation.py new file mode 100644 index 00000000000..b1ccfc36d0d --- /dev/null +++ b/litellm/llms/azure_ai/ocr/document_intelligence/transformation.py @@ -0,0 +1,696 @@ +""" +Azure Document Intelligence OCR transformation implementation. + +Azure Document Intelligence (formerly Form Recognizer) provides advanced document analysis capabilities. +This implementation transforms between Mistral OCR format and Azure Document Intelligence API v4.0. + +Note: Azure Document Intelligence API is async - POST returns 202 Accepted with Operation-Location header. +The operation location must be polled until the analysis completes. +""" +import asyncio +import re +import time +from typing import Any, Dict, Optional + +import httpx + +from litellm._logging import verbose_logger +from litellm.constants import ( + AZURE_DOCUMENT_INTELLIGENCE_API_VERSION, + AZURE_DOCUMENT_INTELLIGENCE_DEFAULT_DPI, + AZURE_OPERATION_POLLING_TIMEOUT, +) +from litellm.llms.base_llm.ocr.transformation import ( + BaseOCRConfig, + DocumentType, + OCRPage, + OCRPageDimensions, + OCRRequestData, + OCRResponse, + OCRUsageInfo, +) +from litellm.secret_managers.main import get_secret_str + + +class AzureDocumentIntelligenceOCRConfig(BaseOCRConfig): + """ + Azure Document Intelligence OCR transformation configuration. + + Supports Azure Document Intelligence v4.0 (2024-11-30) API. + Model route: azure_ai/doc-intelligence/ + + Supported models: + - prebuilt-layout: Extracts text with markdown, tables, and structure (closest to Mistral OCR) + - prebuilt-read: Basic text extraction optimized for reading + - prebuilt-document: General document analysis + + Reference: https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/ + """ + + def __init__(self) -> None: + super().__init__() + + def get_supported_ocr_params(self, model: str) -> list: + """ + Get supported OCR parameters for Azure Document Intelligence. + + Azure DI has minimal optional parameters compared to Mistral OCR. + Most Mistral-specific params are ignored during transformation. + """ + return [] + + 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 Azure Document Intelligence. + + Authentication uses Ocp-Apim-Subscription-Key header. + """ + # Get API key from environment if not provided + if api_key is None: + api_key = get_secret_str("AZURE_DOCUMENT_INTELLIGENCE_API_KEY") + + if api_key is None: + raise ValueError( + "Missing Azure Document Intelligence API Key - Set AZURE_DOCUMENT_INTELLIGENCE_API_KEY environment variable or pass api_key parameter" + ) + + # Validate API base/endpoint is provided + if api_base is None: + api_base = get_secret_str("AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT") + + if api_base is None: + raise ValueError( + "Missing Azure Document Intelligence Endpoint - Set AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT environment variable or pass api_base parameter" + ) + + headers = { + "Ocp-Apim-Subscription-Key": api_key, + "Content-Type": "application/json", + **headers, + } + + return headers + + def get_complete_url( + self, + api_base: Optional[str], + model: str, + optional_params: dict, + litellm_params: Optional[dict] = None, + **kwargs, + ) -> str: + """ + Get complete URL for Azure Document Intelligence endpoint. + + Format: {endpoint}/documentintelligence/documentModels/{modelId}:analyze?api-version=2024-11-30 + + Note: API version 2024-11-30 uses /documentintelligence/ path (not /formrecognizer/) + + Args: + api_base: Azure Document Intelligence endpoint (e.g., https://your-resource.cognitiveservices.azure.com) + model: Model ID (e.g., "prebuilt-layout", "prebuilt-read") + optional_params: Optional parameters + + Returns: Complete URL for Azure DI analyze endpoint + """ + if api_base is None: + raise ValueError( + "Missing Azure Document Intelligence Endpoint - Set AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT environment variable or pass api_base parameter" + ) + + # Ensure no trailing slash + api_base = api_base.rstrip("/") + + # Extract model ID from full model path if needed + # Model can be "prebuilt-layout" or "azure_ai/doc-intelligence/prebuilt-layout" + model_id = model + if "/" in model: + # Extract the last part after the last slash + model_id = model.split("/")[-1] + + # Azure Document Intelligence analyze endpoint + # Note: API version 2024-11-30+ uses /documentintelligence/ (not /formrecognizer/) + return f"{api_base}/documentintelligence/documentModels/{model_id}:analyze?api-version={AZURE_DOCUMENT_INTELLIGENCE_API_VERSION}" + + def _extract_base64_from_data_uri(self, data_uri: str) -> str: + """ + Extract base64 content from a data URI. + + Args: + data_uri: Data URI like "data:application/pdf;base64,..." + + Returns: + Base64 string without the data URI prefix + """ + # Match pattern: data:[][;base64], + match = re.match(r"data:([^;]+)(?:;base64)?,(.+)", data_uri) + if match: + return match.group(2) + return data_uri + + def transform_ocr_request( + self, + model: str, + document: DocumentType, + optional_params: dict, + headers: dict, + **kwargs, + ) -> OCRRequestData: + """ + Transform OCR request to Azure Document Intelligence format. + + Mistral OCR format: + { + "document": { + "type": "document_url", + "document_url": "https://example.com/doc.pdf" + } + } + + Azure DI format: + { + "urlSource": "https://example.com/doc.pdf" + } + OR + { + "base64Source": "base64_encoded_content" + } + + Args: + model: Model name + document: Document dict from user (Mistral format) + optional_params: Already mapped optional parameters + headers: Request headers + + Returns: + OCRRequestData with JSON data + """ + verbose_logger.debug( + f"Azure Document Intelligence transform_ocr_request - model: {model}" + ) + + if not isinstance(document, dict): + raise ValueError(f"Expected document dict, got {type(document)}") + + # Extract document URL from Mistral format + doc_type = document.get("type") + document_url = None + + if doc_type == "document_url": + document_url = document.get("document_url", "") + elif doc_type == "image_url": + document_url = document.get("image_url", "") + else: + raise ValueError( + f"Invalid document type: {doc_type}. Must be 'document_url' or 'image_url'" + ) + + if not document_url: + raise ValueError("Document URL is required") + + # Build Azure DI request + data: Dict[str, Any] = {} + + # Check if it's a data URI (base64) + if document_url.startswith("data:"): + # Extract base64 content + base64_content = self._extract_base64_from_data_uri(document_url) + data["base64Source"] = base64_content + verbose_logger.debug("Using base64Source for Azure Document Intelligence") + else: + # Regular URL + data["urlSource"] = document_url + verbose_logger.debug("Using urlSource for Azure Document Intelligence") + + # Azure DI doesn't support most Mistral-specific params + # Ignore pages, include_image_base64, etc. + + return OCRRequestData(data=data, files=None) + + def _extract_page_markdown(self, page_data: Dict[str, Any]) -> str: + """ + Extract text from Azure DI page and format as markdown. + + Azure DI provides text in 'lines' array. We concatenate them with newlines. + + Args: + page_data: Azure DI page object + + Returns: + Markdown-formatted text + """ + lines = page_data.get("lines", []) + if not lines: + return "" + + # Extract text content from each line + text_lines = [line.get("content", "") for line in lines] + + # Join with newlines to preserve structure + return "\n".join(text_lines) + + def _convert_dimensions( + self, width: float, height: float, unit: str + ) -> OCRPageDimensions: + """ + Convert Azure DI dimensions to pixels. + + Azure DI provides dimensions in inches. We convert to pixels using configured DPI. + + Args: + width: Width in specified unit + height: Height in specified unit + unit: Unit of measurement (e.g., "inch") + + Returns: + OCRPageDimensions with pixel values + """ + # Convert to pixels using configured DPI + dpi = AZURE_DOCUMENT_INTELLIGENCE_DEFAULT_DPI + if unit == "inch": + width_px = int(width * dpi) + height_px = int(height * dpi) + else: + # If unit is not inches, assume it's already in pixels + width_px = int(width) + height_px = int(height) + + return OCRPageDimensions(width=width_px, height=height_px, dpi=dpi) + + @staticmethod + def _check_timeout(start_time: float, timeout_secs: int) -> None: + """ + Check if operation has timed out. + + Args: + start_time: Start time of the operation + timeout_secs: Timeout duration in seconds + + Raises: + TimeoutError: If operation has exceeded timeout + """ + if time.time() - start_time > timeout_secs: + raise TimeoutError( + f"Azure Document Intelligence operation polling timed out after {timeout_secs} seconds" + ) + + @staticmethod + def _get_retry_after(response: httpx.Response) -> int: + """ + Get retry-after duration from response headers. + + Args: + response: HTTP response + + Returns: + Retry-after duration in seconds (default: 2) + """ + retry_after = int(response.headers.get("retry-after", "2")) + verbose_logger.debug(f"Retry polling after: {retry_after} seconds") + return retry_after + + @staticmethod + def _check_operation_status(response: httpx.Response) -> str: + """ + Check Azure DI operation status from response. + + Args: + response: HTTP response from operation endpoint + + Returns: + Operation status string + + Raises: + ValueError: If operation failed or status is unknown + """ + try: + result = response.json() + status = result.get("status") + + verbose_logger.debug(f"Azure DI operation status: {status}") + + if status == "succeeded": + return "succeeded" + elif status == "failed": + error_msg = result.get("error", {}).get("message", "Unknown error") + raise ValueError( + f"Azure Document Intelligence analysis failed: {error_msg}" + ) + elif status in ["running", "notStarted"]: + return "running" + else: + raise ValueError(f"Unknown operation status: {status}") + + except Exception as e: + if "succeeded" in str(e) or "failed" in str(e): + raise + # If we can't parse JSON, something went wrong + raise ValueError(f"Failed to parse Azure DI operation response: {e}") + + def _poll_operation_sync( + self, + operation_url: str, + headers: Dict[str, str], + timeout_secs: int, + ) -> httpx.Response: + """ + Poll Azure Document Intelligence operation until completion (sync). + + Azure DI POST returns 202 with Operation-Location header. + We need to poll that URL until status is "succeeded" or "failed". + + Args: + operation_url: The Operation-Location URL to poll + headers: Request headers (including auth) + timeout_secs: Total timeout in seconds + + Returns: + Final response with completed analysis + """ + from litellm.llms.custom_httpx.http_handler import _get_httpx_client + + client = _get_httpx_client() + start_time = time.time() + + verbose_logger.debug(f"Polling Azure DI operation: {operation_url}") + + while True: + self._check_timeout(start_time=start_time, timeout_secs=timeout_secs) + + # Poll the operation status + response = client.get(url=operation_url, headers=headers) + + # Check operation status + status = self._check_operation_status(response=response) + + if status == "succeeded": + return response + elif status == "running": + # Wait before polling again + retry_after = self._get_retry_after(response=response) + time.sleep(retry_after) + + async def _poll_operation_async( + self, + operation_url: str, + headers: Dict[str, str], + timeout_secs: int, + ) -> httpx.Response: + """ + Poll Azure Document Intelligence operation until completion (async). + + Args: + operation_url: The Operation-Location URL to poll + headers: Request headers (including auth) + timeout_secs: Total timeout in seconds + + Returns: + Final response with completed analysis + """ + import litellm + from litellm.llms.custom_httpx.http_handler import get_async_httpx_client + + client = get_async_httpx_client(llm_provider=litellm.LlmProviders.AZURE_AI) + start_time = time.time() + + verbose_logger.debug(f"Polling Azure DI operation (async): {operation_url}") + + while True: + self._check_timeout(start_time=start_time, timeout_secs=timeout_secs) + + # Poll the operation status + response = await client.get(url=operation_url, headers=headers) + + # Check operation status + status = self._check_operation_status(response=response) + + if status == "succeeded": + return response + elif status == "running": + # Wait before polling again + retry_after = self._get_retry_after(response=response) + await asyncio.sleep(retry_after) + + def transform_ocr_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: Any, + **kwargs, + ) -> OCRResponse: + """ + Transform Azure Document Intelligence response to Mistral OCR format. + + Handles async operation polling: If response is 202 Accepted, polls Operation-Location + until analysis completes. + + Azure DI response (after polling): + { + "status": "succeeded", + "analyzeResult": { + "content": "Full document text...", + "pages": [ + { + "pageNumber": 1, + "width": 8.5, + "height": 11, + "unit": "inch", + "lines": [{"content": "text", "boundingBox": [...]}] + } + ] + } + } + + Mistral OCR format: + { + "pages": [ + { + "index": 0, + "markdown": "extracted text", + "dimensions": {"width": 816, "height": 1056, "dpi": 96} + } + ], + "model": "azure_ai/doc-intelligence/prebuilt-layout", + "usage_info": {"pages_processed": 1}, + "object": "ocr" + } + + Args: + model: Model name + raw_response: Raw HTTP response from Azure DI (may be 202 Accepted) + logging_obj: Logging object + + Returns: + OCRResponse in Mistral format + """ + try: + # Check if we got 202 Accepted (async operation started) + if raw_response.status_code == 202: + verbose_logger.debug( + "Azure DI returned 202 Accepted, polling operation..." + ) + + # Get Operation-Location header + operation_url = raw_response.headers.get("Operation-Location") + if not operation_url: + raise ValueError( + "Azure Document Intelligence returned 202 but no Operation-Location header found" + ) + + # Get headers for polling (need auth) + poll_headers = { + "Ocp-Apim-Subscription-Key": raw_response.request.headers.get( + "Ocp-Apim-Subscription-Key", "" + ) + } + + # Get timeout from kwargs or use default + timeout_secs = AZURE_OPERATION_POLLING_TIMEOUT + + # Poll until operation completes + raw_response = self._poll_operation_sync( + operation_url=operation_url, + headers=poll_headers, + timeout_secs=timeout_secs, + ) + + # Now parse the completed response + response_json = raw_response.json() + + verbose_logger.debug( + f"Azure Document Intelligence response status: {response_json.get('status')}" + ) + + # Check if request succeeded + status = response_json.get("status") + if status != "succeeded": + raise ValueError( + f"Azure Document Intelligence analysis failed with status: {status}" + ) + + # Extract analyze result + analyze_result = response_json.get("analyzeResult", {}) + azure_pages = analyze_result.get("pages", []) + + # Transform pages to Mistral format + mistral_pages = [] + for azure_page in azure_pages: + page_number = azure_page.get("pageNumber", 1) + index = page_number - 1 # Convert to 0-based index + + # Extract markdown text + markdown = self._extract_page_markdown(azure_page) + + # Convert dimensions + width = azure_page.get("width", 8.5) + height = azure_page.get("height", 11) + unit = azure_page.get("unit", "inch") + dimensions = self._convert_dimensions( + width=width, height=height, unit=unit + ) + + # Build OCR page + ocr_page = OCRPage( + index=index, markdown=markdown, dimensions=dimensions + ) + mistral_pages.append(ocr_page) + + # Build usage info + usage_info = OCRUsageInfo( + pages_processed=len(mistral_pages), doc_size_bytes=None + ) + + # Return Mistral OCR response + return OCRResponse( + pages=mistral_pages, + model=model, + usage_info=usage_info, + object="ocr", + ) + + except Exception as e: + verbose_logger.error( + f"Error parsing Azure Document Intelligence response: {e}" + ) + raise e + + async def async_transform_ocr_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: Any, + **kwargs, + ) -> OCRResponse: + """ + Async transform Azure Document Intelligence response to Mistral OCR format. + + Handles async operation polling: If response is 202 Accepted, polls Operation-Location + until analysis completes using async polling. + + Args: + model: Model name + raw_response: Raw HTTP response from Azure DI (may be 202 Accepted) + logging_obj: Logging object + + Returns: + OCRResponse in Mistral format + """ + try: + # Check if we got 202 Accepted (async operation started) + if raw_response.status_code == 202: + verbose_logger.debug( + "Azure DI returned 202 Accepted, polling operation (async)..." + ) + + # Get Operation-Location header + operation_url = raw_response.headers.get("Operation-Location") + if not operation_url: + raise ValueError( + "Azure Document Intelligence returned 202 but no Operation-Location header found" + ) + + # Get headers for polling (need auth) + poll_headers = { + "Ocp-Apim-Subscription-Key": raw_response.request.headers.get( + "Ocp-Apim-Subscription-Key", "" + ) + } + + # Get timeout from kwargs or use default + timeout_secs = AZURE_OPERATION_POLLING_TIMEOUT + + # Poll until operation completes (async) + raw_response = await self._poll_operation_async( + operation_url=operation_url, + headers=poll_headers, + timeout_secs=timeout_secs, + ) + + # Now parse the completed response + response_json = raw_response.json() + + verbose_logger.debug( + f"Azure Document Intelligence response status: {response_json.get('status')}" + ) + + # Check if request succeeded + status = response_json.get("status") + if status != "succeeded": + raise ValueError( + f"Azure Document Intelligence analysis failed with status: {status}" + ) + + # Extract analyze result + analyze_result = response_json.get("analyzeResult", {}) + azure_pages = analyze_result.get("pages", []) + + # Transform pages to Mistral format + mistral_pages = [] + for azure_page in azure_pages: + page_number = azure_page.get("pageNumber", 1) + index = page_number - 1 # Convert to 0-based index + + # Extract markdown text + markdown = self._extract_page_markdown(azure_page) + + # Convert dimensions + width = azure_page.get("width", 8.5) + height = azure_page.get("height", 11) + unit = azure_page.get("unit", "inch") + dimensions = self._convert_dimensions( + width=width, height=height, unit=unit + ) + + # Build OCR page + ocr_page = OCRPage( + index=index, markdown=markdown, dimensions=dimensions + ) + mistral_pages.append(ocr_page) + + # Build usage info + usage_info = OCRUsageInfo( + pages_processed=len(mistral_pages), doc_size_bytes=None + ) + + # Return Mistral OCR response + return OCRResponse( + pages=mistral_pages, + model=model, + usage_info=usage_info, + object="ocr", + ) + + except Exception as e: + verbose_logger.error( + f"Error parsing Azure Document Intelligence response (async): {e}" + ) + raise e + diff --git a/litellm/llms/azure_ai/ocr/transformation.py b/litellm/llms/azure_ai/ocr/transformation.py index eade2dd765f..24fc9e86134 100644 --- a/litellm/llms/azure_ai/ocr/transformation.py +++ b/litellm/llms/azure_ai/ocr/transformation.py @@ -35,6 +35,7 @@ class AzureAIOCRConfig(MistralOCRConfig): model: str, api_key: Optional[str] = None, api_base: Optional[str] = None, + litellm_params: Optional[dict] = None, **kwargs, ) -> Dict: """ @@ -73,6 +74,7 @@ class AzureAIOCRConfig(MistralOCRConfig): api_base: Optional[str], model: str, optional_params: dict, + litellm_params: Optional[dict] = None, **kwargs, ) -> str: """ 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/ocr/transformation.py b/litellm/llms/base_llm/ocr/transformation.py index 2fe8f3def75..fb13332c464 100644 --- a/litellm/llms/base_llm/ocr/transformation.py +++ b/litellm/llms/base_llm/ocr/transformation.py @@ -106,6 +106,7 @@ class BaseOCRConfig: model: str, api_key: Optional[str] = None, api_base: Optional[str] = None, + litellm_params: Optional[dict] = None, **kwargs, ) -> Dict: """ @@ -119,6 +120,7 @@ class BaseOCRConfig: api_base: Optional[str], model: str, optional_params: dict, + litellm_params: Optional[dict] = None, **kwargs, ) -> str: """ @@ -196,6 +198,36 @@ class BaseOCRConfig: """ raise NotImplementedError("transform_ocr_response must be implemented by provider") + async def async_transform_ocr_response( + self, + model: str, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + **kwargs, + ) -> OCRResponse: + """ + Async transform provider-specific OCR response to standard format. + Optional method - providers can override if they need async transformations + (e.g., Azure Document Intelligence for async operation polling). + + Default implementation falls back to sync transform_ocr_response. + + Args: + model: Model name + raw_response: Raw HTTP response + logging_obj: Logging object + + Returns: + OCRResponse in standard format + """ + # Default implementation: call sync version + return self.transform_ocr_response( + model=model, + raw_response=raw_response, + logging_obj=logging_obj, + **kwargs, + ) + def get_error_class( self, error_message: str, 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/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 4c854437544..72e270428ac 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -901,7 +901,7 @@ class BaseAWSLLM: api_base: Optional[str], aws_bedrock_runtime_endpoint: Optional[str], aws_region_name: str, - endpoint_type: Optional[Literal["runtime", "agent"]] = "runtime", + endpoint_type: Optional[Literal["runtime", "agent", "agentcore"]] = "runtime", ) -> Tuple[str, str]: env_aws_bedrock_runtime_endpoint = get_secret("AWS_BEDROCK_RUNTIME_ENDPOINT") if api_base is not None: @@ -935,7 +935,7 @@ class BaseAWSLLM: return endpoint_url, proxy_endpoint_url def _select_default_endpoint_url( - self, endpoint_type: Optional[Literal["runtime", "agent"]], aws_region_name: str + self, endpoint_type: Optional[Literal["runtime", "agent", "agentcore"]], aws_region_name: str ) -> str: """ Select the default endpoint url based on the endpoint type @@ -944,6 +944,8 @@ class BaseAWSLLM: """ if endpoint_type == "agent": return f"https://bedrock-agent-runtime.{aws_region_name}.amazonaws.com" + elif endpoint_type == "agentcore": + return f"https://bedrock-agentcore.{aws_region_name}.amazonaws.com" else: return f"https://bedrock-runtime.{aws_region_name}.amazonaws.com" @@ -1091,7 +1093,7 @@ class BaseAWSLLM: def _sign_request( self, - service_name: Literal["bedrock", "sagemaker"], + service_name: Literal["bedrock", "sagemaker", "bedrock-agentcore"], headers: dict, optional_params: dict, request_data: dict, diff --git a/litellm/llms/bedrock/chat/agentcore/__init__.py b/litellm/llms/bedrock/chat/agentcore/__init__.py new file mode 100644 index 00000000000..a2f13876203 --- /dev/null +++ b/litellm/llms/bedrock/chat/agentcore/__init__.py @@ -0,0 +1,4 @@ +from .transformation import AmazonAgentCoreConfig + +__all__ = ["AmazonAgentCoreConfig"] + diff --git a/litellm/llms/bedrock/chat/agentcore/sse_iterator.py b/litellm/llms/bedrock/chat/agentcore/sse_iterator.py new file mode 100644 index 00000000000..8e0e698e615 --- /dev/null +++ b/litellm/llms/bedrock/chat/agentcore/sse_iterator.py @@ -0,0 +1,150 @@ +""" +SSE Stream Iterator for Bedrock AgentCore. + +Handles Server-Sent Events (SSE) streaming responses from AgentCore. +""" + +import json +from typing import TYPE_CHECKING + +import httpx + +from litellm._logging import verbose_logger +from litellm._uuid import uuid +from litellm.types.llms.bedrock_agentcore import AgentCoreUsage +from litellm.types.utils import Delta, ModelResponse, StreamingChoices, Usage + +if TYPE_CHECKING: + pass + + +class AgentCoreSSEStreamIterator: + """Iterator for AgentCore SSE streaming responses.""" + + def __init__(self, response: httpx.Response, model: str): + self.response = response + self.model = model + self.finished = False + self.line_iterator = self.response.iter_lines() + + def __iter__(self): + return self + + def __next__(self) -> ModelResponse: + """Parse SSE events and yield ModelResponse chunks.""" + try: + for line in self.line_iterator: + line = line.strip() + + if not line or not line.startswith('data:'): + continue + + # Extract JSON from SSE line + json_str = line[5:].strip() + if not json_str: + continue + + try: + data = json.loads(json_str) + + # Skip non-dict data + if not isinstance(data, dict): + continue + + # Process content delta events + if "event" in data and isinstance(data["event"], dict): + event_payload = data["event"] + content_block_delta = event_payload.get("contentBlockDelta") + + if content_block_delta: + delta = content_block_delta.get("delta", {}) + text = delta.get("text", "") + + if text: + # Yield chunk with text + chunk = ModelResponse( + id=f"chatcmpl-{uuid.uuid4()}", + created=0, + model=self.model, + object="chat.completion.chunk", + ) + + chunk.choices = [ + StreamingChoices( + finish_reason=None, + index=0, + delta=Delta(content=text, role="assistant"), + ) + ] + + return chunk + + # Check for metadata/usage + metadata = event_payload.get("metadata") + if metadata and "usage" in metadata: + # This is the final chunk with usage + chunk = ModelResponse( + id=f"chatcmpl-{uuid.uuid4()}", + created=0, + model=self.model, + object="chat.completion.chunk", + ) + + chunk.choices = [ + StreamingChoices( + finish_reason="stop", + index=0, + delta=Delta(), + ) + ] + + usage_data: AgentCoreUsage = metadata["usage"] # type: ignore + setattr(chunk, "usage", Usage( + prompt_tokens=usage_data.get("inputTokens", 0), + completion_tokens=usage_data.get("outputTokens", 0), + total_tokens=usage_data.get("totalTokens", 0), + )) + + self.finished = True + return chunk + + # Check for final message (alternative finish signal) + if "message" in data and isinstance(data["message"], dict): + if not self.finished: + chunk = ModelResponse( + id=f"chatcmpl-{uuid.uuid4()}", + created=0, + model=self.model, + object="chat.completion.chunk", + ) + + chunk.choices = [ + StreamingChoices( + finish_reason="stop", + index=0, + delta=Delta(), + ) + ] + + self.finished = True + return chunk + + except json.JSONDecodeError: + verbose_logger.debug(f"Skipping non-JSON SSE line: {line[:100]}") + continue + + # Stream ended naturally + raise StopIteration + + except StopIteration: + raise + except httpx.StreamConsumed: + # This is expected when the stream has been fully consumed + raise StopIteration + except httpx.StreamClosed: + # This is expected when the stream is closed + raise StopIteration + except Exception as e: + verbose_logger.error(f"Error in AgentCore SSE stream: {str(e)}") + raise StopIteration + diff --git a/litellm/llms/bedrock/chat/agentcore/transformation.py b/litellm/llms/bedrock/chat/agentcore/transformation.py new file mode 100644 index 00000000000..1bfd2809a11 --- /dev/null +++ b/litellm/llms/bedrock/chat/agentcore/transformation.py @@ -0,0 +1,588 @@ +""" +Transformation for Bedrock AgentCore + +https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agentcore_InvokeAgentRuntime.html +""" + +import json +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from urllib.parse import quote + +import httpx + +from litellm._logging import verbose_logger +from litellm._uuid import uuid +from litellm.litellm_core_utils.prompt_templates.common_utils import ( + convert_content_list_to_str, +) +from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException +from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM +from litellm.llms.bedrock.chat.agentcore.sse_iterator import AgentCoreSSEStreamIterator +from litellm.llms.bedrock.common_utils import BedrockError +from litellm.types.llms.bedrock_agentcore import ( + AgentCoreMessage, + AgentCoreParsedResponse, + AgentCoreUsage, +) +from litellm.types.llms.openai import AllMessageValues +from litellm.types.utils import Choices, Message, ModelResponse, Usage + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler + from litellm.utils import CustomStreamWrapper + + LiteLLMLoggingObj = _LiteLLMLoggingObj +else: + LiteLLMLoggingObj = Any + HTTPHandler = Any + AsyncHTTPHandler = Any + CustomStreamWrapper = Any + + +class AmazonAgentCoreConfig(BaseConfig, BaseAWSLLM): + def __init__(self, **kwargs): + BaseConfig.__init__(self, **kwargs) + BaseAWSLLM.__init__(self, **kwargs) + + def get_supported_openai_params(self, model: str) -> List[str]: + """ + Bedrock AgentCore has 0 OpenAI compatible params + """ + return [] + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool, + ) -> dict: + """ + Map OpenAI params to AgentCore params + """ + return optional_params + + def get_complete_url( + self, + api_base: Optional[str], + api_key: Optional[str], + model: str, + optional_params: dict, + litellm_params: dict, + stream: Optional[bool] = None, + ) -> str: + """ + Get the complete url for the request + """ + ### SET RUNTIME ENDPOINT ### + aws_bedrock_runtime_endpoint = optional_params.get( + "aws_bedrock_runtime_endpoint", None + ) + + # Extract ARN from model string + agent_runtime_arn = self._get_agent_runtime_arn(model) + + # Parse ARN to get region + region = self._extract_region_from_arn(agent_runtime_arn) + + # Build the base endpoint URL for AgentCore + # Note: We don't use get_runtime_endpoint as AgentCore has its own endpoint structure + if aws_bedrock_runtime_endpoint: + base_url = aws_bedrock_runtime_endpoint + else: + base_url = f"https://bedrock-agentcore.{region}.amazonaws.com" + + # Based on boto3 client.invoke_agent_runtime, the path is: + # /runtimes/{URL-ENCODED-ARN}/invocations?qualifier= + encoded_arn = quote(agent_runtime_arn, safe='') + endpoint_url = f"{base_url}/runtimes/{encoded_arn}/invocations" + + # Add qualifier as query parameter if provided + if "qualifier" in optional_params: + endpoint_url = f"{endpoint_url}?qualifier={optional_params['qualifier']}" + + return endpoint_url + + def sign_request( + self, + headers: dict, + optional_params: dict, + request_data: dict, + api_base: str, + api_key: Optional[str] = None, + model: Optional[str] = None, + stream: Optional[bool] = None, + fake_stream: Optional[bool] = None, + ) -> Tuple[dict, Optional[bytes]]: + return self._sign_request( + service_name="bedrock-agentcore", + headers=headers, + optional_params=optional_params, + request_data=request_data, + api_base=api_base, + model=model, + stream=stream, + fake_stream=fake_stream, + api_key=api_key, + ) + + def _get_agent_runtime_arn(self, model: str) -> str: + """ + Extract ARN from model string + model = "agentcore/arn:aws:bedrock-agentcore:us-west-2:888602223428:runtime/hosted_agent_r9jvp-3ySZuRHjLC" + returns: "arn:aws:bedrock-agentcore:us-west-2:888602223428:runtime/hosted_agent_r9jvp-3ySZuRHjLC" + """ + parts = model.split("/", 1) + if len(parts) != 2 or parts[0] != "agentcore": + raise ValueError( + "Invalid model format. Expected format: 'model=bedrock/agentcore/arn:aws:bedrock-agentcore:region:account:runtime/runtime_id'" + ) + return parts[1] + + def _extract_region_from_arn(self, arn: str) -> str: + """ + Extract region from ARN + arn:aws:bedrock-agentcore:us-west-2:888602223428:runtime/hosted_agent_r9jvp-3ySZuRHjLC + returns: us-west-2 + """ + parts = arn.split(":") + if len(parts) >= 4: + return parts[3] + raise ValueError(f"Invalid ARN format: {arn}") + + def _get_runtime_session_id(self, optional_params: dict) -> str: + """ + Get or generate runtime session ID (must be 33+ chars) + """ + session_id = optional_params.get("runtimeSessionId", None) + if session_id: + return session_id + + # Generate a session ID with 33+ characters + return f"litellm-session-{str(uuid.uuid4())}" + + def transform_request( + self, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + headers: dict, + ) -> dict: + """ + Transform the request to AgentCore format. + + Based on boto3's implementation: + - Session ID goes in header: X-Amzn-Bedrock-AgentCore-Runtime-Session-Id + - Qualifier goes as query parameter + - Only the payload goes in the request body + + Returns: + dict: Payload dict containing the prompt + """ + # Use the last message content as the prompt + prompt = convert_content_list_to_str(messages[-1]) + + # Create the payload - this is what goes in the body (raw JSON) + payload: dict = {"prompt": prompt} + + # Get or generate session ID - this goes in the header + runtime_session_id = self._get_runtime_session_id(optional_params) + headers["X-Amzn-Bedrock-AgentCore-Runtime-Session-Id"] = runtime_session_id + + # The request data is the payload dict (will be JSON encoded by the HTTP handler) + # Qualifier will be handled as a query parameter in get_complete_url + + return payload + + def _extract_sse_json(self, line: str) -> Optional[Dict]: + """Extract and parse JSON from an SSE data line.""" + if not line.startswith('data:'): + return None + + json_str = line[5:].strip() + if not json_str: + return None + + try: + data = json.loads(json_str) + # Skip non-dict data (some lines contain JSON strings) + return data if isinstance(data, dict) else None + except json.JSONDecodeError: + verbose_logger.debug(f"Skipping non-JSON line: {line[:100]}") + return None + + def _extract_usage_from_event(self, event_data: Dict) -> Optional[AgentCoreUsage]: + """Extract usage information from event metadata.""" + event_payload = event_data.get("event") + if not event_payload: + return None + + metadata = event_payload.get("metadata") + if metadata and "usage" in metadata: + return metadata["usage"] # type: ignore + + return None + + def _extract_content_delta(self, event_data: Dict) -> Optional[str]: + """Extract text content from contentBlockDelta event.""" + event_payload = event_data.get("event") + if not event_payload: + return None + + content_block_delta = event_payload.get("contentBlockDelta") + if not content_block_delta: + return None + + delta = content_block_delta.get("delta", {}) + return delta.get("text") + + def _extract_content_from_message(self, message: AgentCoreMessage) -> str: + """ + Extract text content from message content blocks. + This works for both SSE messages and JSON responses. + """ + content_list = message.get("content", []) + if not isinstance(content_list, list): + return "" + + return "".join( + block["text"] + for block in content_list + if isinstance(block, dict) and "text" in block + ) + + def _calculate_usage( + self, model: str, messages: List[AllMessageValues], content: str + ) -> Optional[Usage]: + """ + Calculate token usage using LiteLLM's token counter. + + Args: + model: The model name + messages: Input messages + content: Response content + + Returns: + Usage object with calculated tokens, or None if calculation fails + """ + try: + from litellm.utils import token_counter + + prompt_tokens = token_counter(model=model, messages=messages) + completion_tokens = token_counter( + model=model, + text=content, + count_response_tokens=True + ) + total_tokens = prompt_tokens + completion_tokens + + verbose_logger.debug( + f"Calculated usage - prompt: {prompt_tokens}, " + f"completion: {completion_tokens}, total: {total_tokens}" + ) + + return Usage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens, + ) + except Exception as e: + verbose_logger.warning(f"Failed to calculate token usage: {str(e)}") + return None + + def _parse_json_response(self, response_json: dict) -> AgentCoreParsedResponse: + """ + Parse direct JSON response (non-streaming). + + JSON response structure: + { + "result": { + "role": "assistant", + "content": [{"text": "..."}] + } + } + """ + result = response_json.get("result", {}) + + # Extract content using the same helper as SSE parsing + content = self._extract_content_from_message(result) # type: ignore + + # JSON responses don't include usage data + return AgentCoreParsedResponse( + content=content, + usage=None, + final_message=result # type: ignore + ) + + def _get_parsed_response( + self, raw_response: httpx.Response + ) -> AgentCoreParsedResponse: + """ + Parse AgentCore response based on content type. + + Args: + raw_response: Raw HTTP response from AgentCore + + Returns: + AgentCoreParsedResponse: Parsed response data + """ + content_type = raw_response.headers.get("content-type", "").lower() + verbose_logger.debug(f"AgentCore response Content-Type: {content_type}") + + # Parse response based on content type + if "application/json" in content_type: + # Direct JSON response + verbose_logger.debug("Parsing JSON response") + response_json = raw_response.json() + verbose_logger.debug(f"Response JSON: {response_json}") + return self._parse_json_response(response_json) + else: + # SSE stream response (text/event-stream or default) + verbose_logger.debug("Parsing SSE stream response") + response_text = raw_response.text + verbose_logger.debug(f"AgentCore response (first 500 chars): {response_text[:500]}") + return self._parse_sse_stream(response_text) + + def _parse_sse_stream(self, response_text: str) -> AgentCoreParsedResponse: + """ + Parse Server-Sent Events (SSE) stream format. + Each line starts with 'data:' followed by JSON. + + Returns: + AgentCoreParsedResponse: Parsed response with content, usage, and message + """ + final_message: Optional[AgentCoreMessage] = None + usage_data: Optional[AgentCoreUsage] = None + content_blocks: List[str] = [] + + for line in response_text.strip().split('\n'): + line = line.strip() + if not line: + continue + + data = self._extract_sse_json(line) + if not data: + continue + + verbose_logger.debug(f"SSE event keys: {list(data.keys())}") + + # Check for final complete message + if "message" in data and isinstance(data["message"], dict): + final_message = data["message"] # type: ignore + verbose_logger.debug("Found final message") + + # Process event data + if "event" in data and isinstance(data["event"], dict): + event_payload = data["event"] + verbose_logger.debug(f"Event payload keys: {list(event_payload.keys())}") + + # Extract usage metadata + if usage := self._extract_usage_from_event(data): + usage_data = usage + verbose_logger.debug(f"Found usage data: {usage_data}") + + # Collect content deltas + if text := self._extract_content_delta(data): + content_blocks.append(text) + + # Build final content + content = ( + self._extract_content_from_message(final_message) + if final_message + else "".join(content_blocks) + ) + + verbose_logger.debug(f"Final usage_data: {usage_data}") + + return AgentCoreParsedResponse( + content=content, + usage=usage_data, + final_message=final_message + ) + + def get_streaming_response( + self, + model: str, + raw_response: httpx.Response, + ) -> AgentCoreSSEStreamIterator: + """ + Return a streaming iterator for SSE responses. + + Args: + model: The model name + raw_response: Raw HTTP response with streaming data + + Returns: + AgentCoreSSEStreamIterator: Iterator that yields ModelResponse chunks + """ + return AgentCoreSSEStreamIterator(response=raw_response, model=model) + + def get_sync_custom_stream_wrapper( + self, + model: str, + custom_llm_provider: str, + logging_obj: LiteLLMLoggingObj, + api_base: str, + headers: dict, + data: dict, + messages: list, + client: Optional[Union[HTTPHandler, "AsyncHTTPHandler"]] = None, + json_mode: Optional[bool] = None, + signed_json_body: Optional[bytes] = None, + ) -> CustomStreamWrapper: + """ + Get a CustomStreamWrapper for synchronous streaming. + + This is called when stream=True is passed to completion(). + """ + from litellm.llms.custom_httpx.http_handler import ( + HTTPHandler, + _get_httpx_client, + ) + from litellm.utils import CustomStreamWrapper + + if client is None or not isinstance(client, HTTPHandler): + client = _get_httpx_client(params={}) + + # Make streaming request + response = client.post( + api_base, + headers=headers, + data=signed_json_body if signed_json_body else json.dumps(data), + stream=True, # THIS IS KEY - tells httpx to not buffer + logging_obj=logging_obj, + ) + + if response.status_code != 200: + raise BedrockError( + status_code=response.status_code, message=str(response.read()) + ) + + # Create iterator for SSE stream + completion_stream = self.get_streaming_response(model=model, raw_response=response) + + streaming_response = CustomStreamWrapper( + completion_stream=completion_stream, + model=model, + custom_llm_provider=custom_llm_provider, + logging_obj=logging_obj, + ) + + # LOGGING + logging_obj.post_call( + input=messages, + api_key="", + original_response="first stream response received", + additional_args={"complete_input_dict": data}, + ) + + return streaming_response + + @property + def has_custom_stream_wrapper(self) -> bool: + """Indicates that this config has custom streaming support.""" + return True + + @property + def supports_stream_param_in_request_body(self) -> bool: + """ + AgentCore does not allow passing `stream` in the request body. + Streaming is automatic based on the response format. + """ + return False + + def transform_response( + self, + model: str, + raw_response: httpx.Response, + model_response: ModelResponse, + logging_obj: LiteLLMLoggingObj, + request_data: dict, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + encoding: Any, + api_key: Optional[str] = None, + json_mode: Optional[bool] = None, + ) -> ModelResponse: + """ + Transform the AgentCore response to LiteLLM ModelResponse format. + AgentCore can return either JSON or SSE (Server-Sent Events) stream responses. + + Note: For streaming responses, use get_streaming_response() instead. + """ + try: + # Parse the response based on content type (JSON or SSE) + parsed_data = self._get_parsed_response(raw_response) + + content = parsed_data["content"] + usage_data = parsed_data["usage"] + + verbose_logger.debug(f"Parsed content length: {len(content)}") + verbose_logger.debug(f"Usage data: {usage_data}") + + # Create the message + message = Message(content=content, role="assistant") + + # Create choices + choice = Choices(finish_reason="stop", index=0, message=message) + + # Update model response + model_response.choices = [choice] + model_response.model = model + + # Add usage information if available + # Note: AgentCore JSON responses don't include usage data + # SSE responses may include usage in metadata events + if usage_data: + usage = Usage( + prompt_tokens=usage_data.get("inputTokens", 0), + completion_tokens=usage_data.get("outputTokens", 0), + total_tokens=usage_data.get("totalTokens", 0), + ) + setattr(model_response, "usage", usage) + else: + # Calculate token usage using LiteLLM's token counter + verbose_logger.debug("No usage data from AgentCore - calculating tokens") + calculated_usage = self._calculate_usage(model, messages, content) + if calculated_usage: + setattr(model_response, "usage", calculated_usage) + + return model_response + + except Exception as e: + verbose_logger.error( + f"Error processing Bedrock AgentCore response: {str(e)}" + ) + raise BedrockError( + message=f"Error processing response: {str(e)}", + status_code=raw_response.status_code, + ) + + def validate_environment( + self, + headers: dict, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> dict: + return headers + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + return BedrockError(status_code=status_code, message=error_message) + + def should_fake_stream( + self, + model: Optional[str], + stream: Optional[bool], + custom_llm_provider: Optional[str] = None, + ) -> bool: + return True + diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index 9b13d3df08e..02b8fd57115 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -69,11 +69,19 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): litellm_params: dict, headers: dict, ) -> dict: + # Filter out AWS authentication parameters before passing to Anthropic transformation + # AWS params should only be used for signing requests, not included in request body + filtered_params = { + k: v + for k, v in optional_params.items() + if k not in self.aws_authentication_params + } + _anthropic_request = AnthropicConfig.transform_request( self, model=model, messages=messages, - optional_params=optional_params, + optional_params=filtered_params, litellm_params=litellm_params, headers=headers, ) diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 661817e4ce6..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( @@ -445,17 +446,18 @@ class BedrockModelInfo(BaseLLMModelInfo): @staticmethod def get_bedrock_route( model: str, - ) -> Literal["converse", "invoke", "converse_like", "agent", "async_invoke"]: + ) -> Literal["converse", "invoke", "converse_like", "agent", "agentcore", "async_invoke"]: """ Get the bedrock route for the given model. """ route_mappings: Dict[ - str, Literal["invoke", "converse_like", "converse", "agent", "async_invoke"] + str, Literal["invoke", "converse_like", "converse", "agent", "agentcore", "async_invoke"] ] = { "invoke/": "invoke", "converse_like/": "converse_like", "converse/": "converse", "agent/": "agent", + "agentcore/": "agentcore", "async_invoke/": "async_invoke", } @@ -494,6 +496,13 @@ class BedrockModelInfo(BaseLLMModelInfo): """ return "agent/" in model + @staticmethod + def _explicit_agentcore_route(model: str) -> bool: + """ + Check if the model is an explicit agentcore route. + """ + return "agentcore/" in model + @staticmethod def _explicit_converse_like_route(model: str) -> bool: """ @@ -538,6 +547,65 @@ class BedrockModelInfo(BaseLLMModelInfo): return None +def get_bedrock_chat_config(model: str): + """ + Helper function to get the appropriate Bedrock chat config based on model and route. + + Args: + model: The model name/identifier + + Returns: + The appropriate Bedrock config class instance + """ + bedrock_route = BedrockModelInfo.get_bedrock_route(model) + bedrock_invoke_provider = litellm.BedrockLLM.get_bedrock_invoke_provider( + model=model + ) + base_model = BedrockModelInfo.get_base_model(model) + + # Handle explicit routes first + if bedrock_route == "converse" or bedrock_route == "converse_like": + return litellm.AmazonConverseConfig() + elif bedrock_route == "agent": + from litellm.llms.bedrock.chat.invoke_agent.transformation import ( + AmazonInvokeAgentConfig, + ) + return AmazonInvokeAgentConfig() + elif bedrock_route == "agentcore": + from litellm.llms.bedrock.chat.agentcore.transformation import ( + AmazonAgentCoreConfig, + ) + return AmazonAgentCoreConfig() + + # Handle provider-specific configs + if bedrock_invoke_provider == "amazon": + return litellm.AmazonTitanConfig() + elif bedrock_invoke_provider == "anthropic": + if ( + base_model + in litellm.AmazonAnthropicConfig.get_legacy_anthropic_model_names() + ): + return litellm.AmazonAnthropicConfig() + else: + return litellm.AmazonAnthropicClaudeConfig() + elif bedrock_invoke_provider == "meta" or bedrock_invoke_provider == "llama": + return litellm.AmazonLlamaConfig() + elif bedrock_invoke_provider == "ai21": + return litellm.AmazonAI21Config() + elif bedrock_invoke_provider == "cohere": + return litellm.AmazonCohereConfig() + elif bedrock_invoke_provider == "mistral": + return litellm.AmazonMistralConfig() + elif bedrock_invoke_provider == "deepseek_r1": + return litellm.AmazonDeepSeekR1Config() + elif bedrock_invoke_provider == "nova": + return litellm.AmazonInvokeNovaConfig() + elif bedrock_invoke_provider == "qwen3": + return litellm.AmazonQwen3Config() + else: + return litellm.AmazonInvokeConfig() + + class BedrockEventStreamDecoderBase: """ Base class for event stream decoding for Bedrock 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/aiohttp_transport.py b/litellm/llms/custom_httpx/aiohttp_transport.py index 50bbccd6a4b..769bc0fed1e 100644 --- a/litellm/llms/custom_httpx/aiohttp_transport.py +++ b/litellm/llms/custom_httpx/aiohttp_transport.py @@ -95,6 +95,15 @@ class AiohttpResponseStream(httpx.AsyncByteStream): # If the error is due to incomplete transfer encoding, we can still # return what we've received so far, similar to how httpx handles it return + except RuntimeError as e: + # Some providers (e.g., SSE streams) may close the connection + # causing aiohttp StreamReader to raise a generic RuntimeError + # with message "Connection closed.". Treat this as a graceful + # end-of-stream so downstream consumers don't error. + if "Connection closed" in str(e): + verbose_logger.debug("Upstream closed streaming connection; ending iterator gracefully") + return + raise except aiohttp.http_exceptions.TransferEncodingError as e: # Handle transfer encoding errors gracefully verbose_logger.debug(f"Transfer encoding error, but continuing: {e}") diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index dbcb81107d6..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( @@ -1286,18 +1288,19 @@ class BaseLLMHTTPHandler: Returns: (headers, complete_url, data, files) """ from litellm.llms.base_llm.ocr.transformation import OCRRequestData - headers = provider_config.validate_environment( api_key=api_key, api_base=api_base, headers=headers or {}, model=model, + litellm_params=litellm_params, ) complete_url = provider_config.get_complete_url( api_base=api_base, model=model, optional_params=optional_params, + litellm_params=litellm_params, ) # Transform the request to get data and files @@ -1358,12 +1361,14 @@ class BaseLLMHTTPHandler: api_base=api_base, headers=headers or {}, model=model, + litellm_params=litellm_params, ) complete_url = provider_config.get_complete_url( api_base=api_base, model=model, optional_params=optional_params, + litellm_params=litellm_params, ) # Use async transform (providers can override this method if they need async operations) @@ -1549,10 +1554,10 @@ class BaseLLMHTTPHandler: except Exception as e: raise self._handle_error(e=e, provider_config=provider_config) - return self._transform_ocr_response( - provider_config=provider_config, + # Use async response transform for async operations + return await provider_config.async_transform_ocr_response( model=model, - response=response, + raw_response=response, logging_obj=logging_obj, ) @@ -1942,6 +1947,7 @@ class BaseLLMHTTPHandler: _is_async: bool = False, fake_stream: bool = False, litellm_metadata: Optional[Dict[str, Any]] = None, + shared_session: Optional["ClientSession"] = None, ) -> Union[ ResponsesAPIResponse, BaseResponsesAPIStreamingIterator, @@ -1970,6 +1976,7 @@ class BaseLLMHTTPHandler: client=client if isinstance(client, AsyncHTTPHandler) else None, fake_stream=fake_stream, litellm_metadata=litellm_metadata, + shared_session=shared_session, ) if client is None or not isinstance(client, HTTPHandler): @@ -2004,6 +2011,9 @@ class BaseLLMHTTPHandler: headers=headers, ) + if extra_body: + data.update(extra_body) + ## LOGGING logging_obj.pre_call( input=input, @@ -2087,15 +2097,20 @@ class BaseLLMHTTPHandler: client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, fake_stream: bool = False, litellm_metadata: Optional[Dict[str, Any]] = None, + shared_session: Optional["ClientSession"] = None, ) -> Union[ResponsesAPIResponse, BaseResponsesAPIStreamingIterator]: """ Async version of the responses API handler. Uses async HTTP client to make requests. """ if client is None or not isinstance(client, AsyncHTTPHandler): + verbose_logger.debug( + f"Creating HTTP client for responses API with shared_session: {id(shared_session) if shared_session else None}" + ) async_httpx_client = get_async_httpx_client( llm_provider=litellm.LlmProviders(custom_llm_provider), params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + shared_session=shared_session, ) else: async_httpx_client = client @@ -2125,6 +2140,9 @@ class BaseLLMHTTPHandler: headers=headers, ) + if extra_body: + data.update(extra_body) + ## LOGGING logging_obj.pre_call( input=input, @@ -2208,15 +2226,20 @@ class BaseLLMHTTPHandler: timeout: Optional[Union[float, httpx.Timeout]] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, _is_async: bool = False, + shared_session: Optional["ClientSession"] = None, ) -> DeleteResponseResult: """ Async version of the delete response API handler. Uses async HTTP client to make requests. """ if client is None or not isinstance(client, AsyncHTTPHandler): + verbose_logger.debug( + f"Creating HTTP client for delete_response with shared_session: {id(shared_session) if shared_session else None}" + ) async_httpx_client = get_async_httpx_client( llm_provider=litellm.LlmProviders(custom_llm_provider), params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + shared_session=shared_session, ) else: async_httpx_client = client @@ -2279,6 +2302,7 @@ class BaseLLMHTTPHandler: timeout: Optional[Union[float, httpx.Timeout]] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, _is_async: bool = False, + shared_session: Optional["ClientSession"] = None, ) -> Union[DeleteResponseResult, Coroutine[Any, Any, DeleteResponseResult]]: """ Async version of the responses API handler. @@ -2295,6 +2319,7 @@ class BaseLLMHTTPHandler: extra_body=extra_body, timeout=timeout, client=client, + shared_session=shared_session, ) if client is None or not isinstance(client, HTTPHandler): sync_httpx_client = _get_httpx_client( @@ -2361,6 +2386,7 @@ class BaseLLMHTTPHandler: timeout: Optional[Union[float, httpx.Timeout]] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, _is_async: bool = False, + shared_session: Optional["ClientSession"] = None, ) -> Union[ResponsesAPIResponse, Coroutine[Any, Any, ResponsesAPIResponse]]: """ Get a response by ID @@ -2377,6 +2403,7 @@ class BaseLLMHTTPHandler: extra_body=extra_body, timeout=timeout, client=client, + shared_session=shared_session, ) if client is None or not isinstance(client, HTTPHandler): @@ -2440,14 +2467,19 @@ class BaseLLMHTTPHandler: extra_body: Optional[Dict[str, Any]] = None, timeout: Optional[Union[float, httpx.Timeout]] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + shared_session: Optional["ClientSession"] = None, ) -> ResponsesAPIResponse: """ Async version of get_responses """ if client is None or not isinstance(client, AsyncHTTPHandler): + verbose_logger.debug( + f"Creating HTTP client for get_responses with shared_session: {id(shared_session) if shared_session else None}" + ) async_httpx_client = get_async_httpx_client( llm_provider=litellm.LlmProviders(custom_llm_provider), params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + shared_session=shared_session, ) else: async_httpx_client = client @@ -2518,6 +2550,7 @@ class BaseLLMHTTPHandler: timeout: Optional[Union[float, httpx.Timeout]] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, _is_async: bool = False, + shared_session: Optional["ClientSession"] = None, ) -> Union[Dict, Coroutine[Any, Any, Dict]]: if _is_async: return self.async_list_responses_input_items( @@ -2534,6 +2567,7 @@ class BaseLLMHTTPHandler: extra_headers=extra_headers, timeout=timeout, client=client, + shared_session=shared_session, ) if client is None or not isinstance(client, HTTPHandler): @@ -2602,11 +2636,16 @@ class BaseLLMHTTPHandler: extra_headers: Optional[Dict[str, Any]] = None, timeout: Optional[Union[float, httpx.Timeout]] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + shared_session: Optional["ClientSession"] = None, ) -> Dict: if client is None or not isinstance(client, AsyncHTTPHandler): + verbose_logger.debug( + f"Creating HTTP client for list_input_items with shared_session: {id(shared_session) if shared_session else None}" + ) async_httpx_client = get_async_httpx_client( llm_provider=litellm.LlmProviders(custom_llm_provider), params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + shared_session=shared_session, ) else: async_httpx_client = client @@ -3293,6 +3332,7 @@ class BaseLLMHTTPHandler: timeout: Optional[Union[float, httpx.Timeout]] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, _is_async: bool = False, + shared_session: Optional["ClientSession"] = None, ) -> Union[ResponsesAPIResponse, Coroutine[Any, Any, ResponsesAPIResponse]]: """ Async version of the responses API handler. @@ -3309,6 +3349,7 @@ class BaseLLMHTTPHandler: extra_body=extra_body, timeout=timeout, client=client, + shared_session=shared_session, ) if client is None or not isinstance(client, HTTPHandler): sync_httpx_client = _get_httpx_client( @@ -3375,15 +3416,20 @@ class BaseLLMHTTPHandler: timeout: Optional[Union[float, httpx.Timeout]] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, _is_async: bool = False, + shared_session: Optional["ClientSession"] = None, ) -> ResponsesAPIResponse: """ Async version of the cancel response API handler. Uses async HTTP client to make requests. """ if client is None or not isinstance(client, AsyncHTTPHandler): + verbose_logger.debug( + f"Creating HTTP client for cancel_response with shared_session: {id(shared_session) if shared_session else None}" + ) async_httpx_client = get_async_httpx_client( llm_provider=litellm.LlmProviders(custom_llm_provider), params={"ssl_verify": litellm_params.get("ssl_verify", None)}, + shared_session=shared_session, ) else: async_httpx_client = client @@ -4053,7 +4099,7 @@ class BaseLLMHTTPHandler: or {}, model=model, ) - + if extra_headers: headers.update(extra_headers) @@ -4063,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 @@ -4094,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, @@ -4113,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( @@ -4160,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, @@ -4180,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, @@ -4207,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 ###### @@ -4262,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, @@ -4270,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( @@ -4328,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, @@ -4336,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( @@ -4446,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: @@ -4527,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: @@ -4662,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: @@ -4817,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: @@ -4891,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: @@ -6356,4 +6452,4 @@ class BaseLLMHTTPHandler: model=model, raw_response=response, logging_obj=logging_obj, - ) + ) \ No newline at end of file diff --git a/litellm/llms/dashscope/cost_calculator.py b/litellm/llms/dashscope/cost_calculator.py index 107eb7f5adf..9b3e3851162 100644 --- a/litellm/llms/dashscope/cost_calculator.py +++ b/litellm/llms/dashscope/cost_calculator.py @@ -14,6 +14,7 @@ from litellm.utils import get_model_info @dataclass class TokenBreakdown: """Token breakdown for cost calculation.""" + text_tokens: int cached_tokens: int completion_tokens: int @@ -23,133 +24,194 @@ class TokenBreakdown: def _extract_token_breakdown(usage: Usage) -> TokenBreakdown: """Extract token counts from usage, handling cached and reasoning tokens.""" cached_tokens = 0 - if usage.prompt_tokens_details and hasattr(usage.prompt_tokens_details, "cached_tokens"): + if usage.prompt_tokens_details and hasattr( + usage.prompt_tokens_details, "cached_tokens" + ): cached_tokens = usage.prompt_tokens_details.cached_tokens or 0 - + text_tokens = usage.prompt_tokens - cached_tokens - + reasoning_tokens = 0 - if (hasattr(usage, "completion_tokens_details") and - usage.completion_tokens_details and - hasattr(usage.completion_tokens_details, "reasoning_tokens")): + if ( + hasattr(usage, "completion_tokens_details") + and usage.completion_tokens_details + and hasattr(usage.completion_tokens_details, "reasoning_tokens") + ): reasoning_tokens = usage.completion_tokens_details.reasoning_tokens or 0 - + completion_tokens = (usage.completion_tokens or 0) - reasoning_tokens - - return TokenBreakdown(text_tokens, cached_tokens, completion_tokens, reasoning_tokens) + + return TokenBreakdown( + text_tokens, cached_tokens, completion_tokens, reasoning_tokens + ) def _calculate_tiered_cost( - tokens: int, - tiered_pricing: List[dict], + tokens: int, + tiered_pricing: List[dict], cost_key: str, - fallback_cost_key: Optional[str] = None + fallback_cost_key: Optional[str] = None, ) -> float: - """Calculate cost using tiered pricing structure. - - Finds the appropriate tier based on token count and applies that tier's rate to all tokens. + """ + Calculate cost for a given number of tokens based on a true tiered pricing structure. + + This function iterates through sorted pricing tiers, calculates the cost for the + number of tokens that fall into each tier's range, and sums them up to get the total cost. + + Args: + tokens (int): The total number of tokens to calculate the cost for. + tiered_pricing (List[dict]): A list of dictionaries, where each dictionary + represents a pricing tier. + cost_key (str): The key in the tier dictionary that holds the per-token cost + (e.g., 'input_cost_per_token'). + fallback_cost_key (Optional[str], optional): A fallback key to use if the + primary `cost_key` is not found in a tier. Defaults to None. + + Returns: + float: The total calculated cost for the given tokens. + + Example: + >>> tiered_pricing = [ + ... {"range": [0, 100000], "input_cost_per_token": 0.0001}, + ... {"range": [100000, 500000], "input_cost_per_token": 0.00005}, + ... ] + + Calculating cost for 150,000 tokens: + (100,000 * 0.0001) + (50,000 * 0.00005) = $12.5 """ if not tiered_pricing or tokens <= 0: return 0.0 - - # Find the appropriate tier for the token count - for tier in tiered_pricing: + + total_cost = 0.0 + tokens_processed = 0 + + sorted_tiers = sorted(tiered_pricing, key=lambda x: x.get("range", [0, 0])[0]) + + for tier in sorted_tiers: + if tokens_processed >= tokens: + break + tier_range = tier.get("range", []) if len(tier_range) != 2: continue - + range_start, range_end = tier_range - - # Check if tokens fall within this tier's range - if range_start <= tokens <= range_end: + + if tokens <= range_start: + continue + + tier_start = max(range_start, tokens_processed) + tier_end = min(range_end, tokens) + + if tier_end > tier_start: + tokens_in_tier = tier_end - tier_start cost_per_token = tier.get(cost_key) or tier.get(fallback_cost_key, 0) - return tokens * cost_per_token - - # If no tier matches, use the last tier (highest tier) - if tiered_pricing: - last_tier = tiered_pricing[-1] + total_cost += tokens_in_tier * cost_per_token + tokens_processed = tier_end + + # After loop, check if any tokens remain (i.e., tokens > highest tier's end range) + # and charge them at the last tier's rate. + if tokens_processed < tokens and sorted_tiers: + last_tier = sorted_tiers[-1] + remaining_tokens = tokens - tokens_processed cost_per_token = last_tier.get(cost_key) or last_tier.get(fallback_cost_key, 0) - return tokens * cost_per_token - - return 0.0 + total_cost += remaining_tokens * cost_per_token + + return total_cost -def _calculate_flat_cost(tokens: int, cost_per_token: float) -> float: - """Calculate cost using flat pricing.""" - return tokens * cost_per_token - - -def _calculate_prompt_cost(breakdown: TokenBreakdown, model_info: ModelInfo, tiered_pricing: Optional[List[dict]]) -> float: +def _calculate_prompt_cost( + breakdown: TokenBreakdown, + model_info: ModelInfo, + tiered_pricing: Optional[List[dict]], +) -> float: """Calculate total prompt cost including cached tokens.""" if tiered_pricing: text_cost = _calculate_tiered_cost( - tokens=breakdown.text_tokens, - tiered_pricing=tiered_pricing, - cost_key="input_cost_per_token" + tokens=breakdown.text_tokens, + tiered_pricing=tiered_pricing, + cost_key="input_cost_per_token", ) cache_cost = _calculate_tiered_cost( - tokens=breakdown.cached_tokens, - tiered_pricing=tiered_pricing, - cost_key="cache_read_input_token_cost" + tokens=breakdown.cached_tokens, + tiered_pricing=tiered_pricing, + cost_key="cache_read_input_token_cost", + fallback_cost_key="input_cost_per_token", ) return text_cost + cache_cost - - input_cost = model_info.get("input_cost_per_token", 0.0) - cache_cost = model_info.get("cache_read_input_token_cost", input_cost) or input_cost - - return (_calculate_flat_cost(tokens=breakdown.text_tokens, cost_per_token=input_cost) + - _calculate_flat_cost(tokens=breakdown.cached_tokens, cost_per_token=cache_cost)) + + input_cost = float(model_info.get("input_cost_per_token") or 0.0) + + # For cache_cost, first try the specific key, then fall back to input_cost. + cache_cost_val = model_info.get("cache_read_input_token_cost") + if cache_cost_val is None: + cache_cost = input_cost + else: + cache_cost = float(cache_cost_val) + + return (breakdown.text_tokens * input_cost) + (breakdown.cached_tokens * cache_cost) -def _calculate_completion_cost(breakdown: TokenBreakdown, model_info: ModelInfo, tiered_pricing: Optional[List[dict]]) -> float: +def _calculate_completion_cost( + breakdown: TokenBreakdown, + model_info: ModelInfo, + tiered_pricing: Optional[List[dict]], +) -> float: """Calculate total completion cost including reasoning tokens.""" if tiered_pricing: completion_cost = _calculate_tiered_cost( - tokens=breakdown.completion_tokens, - tiered_pricing=tiered_pricing, - cost_key="output_cost_per_token" + tokens=breakdown.completion_tokens, + tiered_pricing=tiered_pricing, + cost_key="output_cost_per_token", ) reasoning_cost = _calculate_tiered_cost( - tokens=breakdown.reasoning_tokens, - tiered_pricing=tiered_pricing, + tokens=breakdown.reasoning_tokens, + tiered_pricing=tiered_pricing, cost_key="output_cost_per_reasoning_token", - fallback_cost_key="output_cost_per_token" + fallback_cost_key="output_cost_per_token", ) return completion_cost + reasoning_cost - - output_cost = model_info.get("output_cost_per_token", 0.0) - reasoning_cost = model_info.get("output_cost_per_reasoning_token", output_cost) or output_cost - - return (_calculate_flat_cost(tokens=breakdown.completion_tokens, cost_per_token=output_cost) + - _calculate_flat_cost(tokens=breakdown.reasoning_tokens, cost_per_token=reasoning_cost)) + + output_cost = float(model_info.get("output_cost_per_token") or 0.0) + + # For reasoning_cost, first try the specific key, then fall back to output_cost. + reasoning_cost_val = model_info.get("output_cost_per_reasoning_token") + if reasoning_cost_val is None: + reasoning_cost = output_cost + else: + reasoning_cost = float(reasoning_cost_val) + + return (breakdown.completion_tokens * output_cost) + ( + breakdown.reasoning_tokens * reasoning_cost + ) def cost_per_token(model: str, usage: Usage) -> Tuple[float, float]: """ Calculate cost per token for Dashscope models. - + Supports both tiered and flat pricing with cached and reasoning tokens. - + Args: model: Model name without provider prefix usage: LiteLLM Usage block - + Returns: Tuple[float, float] - (prompt_cost_in_usd, completion_cost_in_usd) """ model_info = get_model_info(model=model, custom_llm_provider="dashscope") breakdown = _extract_token_breakdown(usage) - tiered_pricing = model_info.get("tiered_pricing") if isinstance(model_info.get("tiered_pricing"), list) else None - + tiered_pricing = ( + model_info.get("tiered_pricing") + if isinstance(model_info.get("tiered_pricing"), list) + else None + ) + prompt_cost = _calculate_prompt_cost( - breakdown=breakdown, - model_info=model_info, - tiered_pricing=tiered_pricing + breakdown=breakdown, model_info=model_info, tiered_pricing=tiered_pricing ) completion_cost = _calculate_completion_cost( - breakdown=breakdown, - model_info=model_info, - tiered_pricing=tiered_pricing + breakdown=breakdown, model_info=model_info, tiered_pricing=tiered_pricing ) - + return prompt_cost, completion_cost diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index dd136c54264..ac3be0c3518 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -26,7 +26,7 @@ from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response impo _should_convert_tool_call_to_json_mode, ) from litellm.litellm_core_utils.prompt_templates.common_utils import ( - strip_name_from_messages, + strip_name_from_message ) from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator from litellm.types.llms.anthropic import AllAnthropicToolsValues @@ -332,8 +332,11 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): _message = message.model_dump(exclude_none=True) else: _message = message + _message = strip_name_from_message(_message, allowed_name_roles=["user"]) + # Move message-level cache_control into a content block when content is a string. + if "cache_control" in _message and isinstance(_message.get("content"), str): + _message = self._move_cache_control_into_string_content_block(_message) new_messages.append(_message) - new_messages = strip_name_from_messages(new_messages) if is_async: return super()._transform_messages( @@ -344,6 +347,32 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): messages=new_messages, model=model, is_async=cast(Literal[False], False) ) + def _move_cache_control_into_string_content_block(self, message: AllMessageValues) -> AllMessageValues: + """ + Moves message-level cache_control into a content block when content is a string. + + Transforms: + {"role": "user", "content": "text", "cache_control": {...}} + Into: + {"role": "user", "content": [{"type": "text", "text": "text", "cache_control": {...}}]} + + This is required for Anthropic's prompt caching API when cache_control is specified + at the message level but content is a simple string (not already an array of content blocks). + """ + content = message.get("content") + # Create new message with cache_control moved into content block + transformed_message = cast(dict[str, Any], message.copy()) + cache_control = transformed_message.pop("cache_control") + transformed_message["content"] = [ + { + "type": "text", + "text": content, + "cache_control": cache_control, + } + ] + return cast(AllMessageValues, transformed_message) + + @staticmethod def extract_content_str( content: Optional[AllDatabricksContentValues], @@ -611,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/deepgram/audio_transcription/transformation.py b/litellm/llms/deepgram/audio_transcription/transformation.py index fe63ad11bc7..6a540d72778 100644 --- a/litellm/llms/deepgram/audio_transcription/transformation.py +++ b/litellm/llms/deepgram/audio_transcription/transformation.py @@ -150,7 +150,7 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig): segments = [] current_speaker = None - current_words = [] + current_words: list[str] = [] for word_obj in words: speaker = word_obj.get("speaker") 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/firecrawl/__init__.py b/litellm/llms/firecrawl/__init__.py new file mode 100644 index 00000000000..bacf1eac070 --- /dev/null +++ b/litellm/llms/firecrawl/__init__.py @@ -0,0 +1,7 @@ +""" +Firecrawl API integration module. +""" +from litellm.llms.firecrawl.search.transformation import FirecrawlSearchConfig + +__all__ = ["FirecrawlSearchConfig"] + diff --git a/litellm/llms/firecrawl/search/__init__.py b/litellm/llms/firecrawl/search/__init__.py new file mode 100644 index 00000000000..999dce655d5 --- /dev/null +++ b/litellm/llms/firecrawl/search/__init__.py @@ -0,0 +1,7 @@ +""" +Firecrawl Search API module. +""" +from litellm.llms.firecrawl.search.transformation import FirecrawlSearchConfig + +__all__ = ["FirecrawlSearchConfig"] + diff --git a/litellm/llms/firecrawl/search/transformation.py b/litellm/llms/firecrawl/search/transformation.py new file mode 100644 index 00000000000..af501a8eac0 --- /dev/null +++ b/litellm/llms/firecrawl/search/transformation.py @@ -0,0 +1,207 @@ +""" +Calls Firecrawl's /search endpoint to search the web. + +Firecrawl API Reference: https://docs.firecrawl.dev/api-reference/endpoint/search +""" +from typing import Dict, List, Optional, TypedDict, Union + +import httpx + +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.base_llm.search.transformation import ( + BaseSearchConfig, + SearchResponse, + SearchResult, +) +from litellm.secret_managers.main import get_secret_str + + +class _FirecrawlSearchRequestRequired(TypedDict): + """Required fields for Firecrawl Search API request.""" + query: str # Required - search query + + +class FirecrawlSearchRequest(_FirecrawlSearchRequestRequired, total=False): + """ + Firecrawl Search API request format. + Based on: https://docs.firecrawl.dev/api-reference/endpoint/search + """ + limit: int # Optional - maximum number of results to return (default 5, max 100) + sources: List[str] # Optional - sources to search ('web', 'images', 'news'), default ['web'] + categories: List[Dict[str, str]] # Optional - categories to filter by (github, research, pdf) + tbs: str # Optional - time-based search parameter + location: str # Optional - location parameter for geo-targeting + country: str # Optional - ISO country code (default 'US') + timeout: int # Optional - timeout in milliseconds (default 60000) + ignoreInvalidURLs: bool # Optional - exclude invalid URLs (default false) + scrapeOptions: Dict # Optional - options for scraping search results + + +class FirecrawlSearchConfig(BaseSearchConfig): + FIRECRAWL_API_BASE = "https://api.firecrawl.dev/v2" + + @staticmethod + def ui_friendly_name() -> str: + return "Firecrawl" + + def validate_environment( + self, + headers: Dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Dict: + """ + Validate environment and return headers. + """ + api_key = api_key or get_secret_str("FIRECRAWL_API_KEY") + if not api_key: + raise ValueError("FIRECRAWL_API_KEY is not set. Set `FIRECRAWL_API_KEY` environment variable.") + headers["Authorization"] = f"Bearer {api_key}" + headers["Content-Type"] = "application/json" + return headers + + def get_complete_url( + self, + api_base: Optional[str], + optional_params: dict, + data: Optional[Union[Dict, List[Dict]]] = None, + **kwargs, + ) -> str: + """ + Get complete URL for Search endpoint. + """ + api_base = api_base or get_secret_str("FIRECRAWL_API_BASE") or self.FIRECRAWL_API_BASE + + # Append "/search" to the api base if it's not already there + if not api_base.endswith("/search"): + api_base = f"{api_base}/search" + + return api_base + + + def transform_search_request( + self, + query: Union[str, List[str]], + optional_params: dict, + **kwargs, + ) -> Dict: + """ + Transform Search request to Firecrawl API format. + + Transforms Perplexity unified spec parameters: + - query → query (same) + - max_results → limit + - search_domain_filter → (not directly supported, can use scrapeOptions) + - country → country + - max_tokens_per_page → (not applicable, ignored) + + All other Firecrawl-specific parameters are passed through as-is. + + Args: + query: Search query (string or list of strings). Firecrawl only supports single string queries. + optional_params: Optional parameters for the request + + Returns: + Dict with typed request data following FirecrawlSearchRequest spec + """ + if isinstance(query, list): + # Firecrawl only supports single string queries, join with spaces + query = " ".join(query) + + request_data: FirecrawlSearchRequest = { + "query": query, + } + + # Transform Perplexity unified spec parameters to Firecrawl format + if "max_results" in optional_params: + request_data["limit"] = optional_params["max_results"] + + if "country" in optional_params: + request_data["country"] = optional_params["country"] + + # Convert to dict before dynamic key assignments + result_data = dict(request_data) + + # pass through all other parameters as-is + for param, value in optional_params.items(): + if param not in self.get_supported_perplexity_optional_params() and param not in result_data: + result_data[param] = value + + # By default, request markdown content if not explicitly specified + # Firecrawl doesn't return content unless explicitly requested via scrapeOptions + if "scrapeOptions" not in result_data: + result_data["scrapeOptions"] = { + "formats": ["markdown"], + "onlyMainContent": True + } + + return result_data + + def transform_search_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + **kwargs, + ) -> SearchResponse: + """ + Transform Firecrawl API response to LiteLLM unified SearchResponse format. + + Firecrawl → LiteLLM mappings: + - data.web[].title → SearchResult.title + - data.web[].url → SearchResult.url + - data.web[].description OR data.web[].markdown → SearchResult.snippet + - No date field in web results (set to None) + - No last_updated field in Firecrawl response (set to None) + + Note: Firecrawl v2 returns results organized by source type (web, images, news). + We primarily use web results for the unified format. + + Args: + raw_response: Raw httpx response from Firecrawl API + logging_obj: Logging object for tracking + + Returns: + SearchResponse with standardized format + """ + response_json = raw_response.json() + + # Transform results to SearchResult objects + results = [] + + # Process web results (primary source) + data = response_json.get("data", {}) + web_results = data.get("web", []) + + for result in web_results: + # Use markdown if available, otherwise fall back to description + snippet = result.get("markdown") or result.get("description", "") + + search_result = SearchResult( + title=result.get("title", ""), + url=result.get("url", ""), + snippet=snippet, + date=None, # Web results don't include date + last_updated=None, # Firecrawl doesn't provide last_updated in response + ) + results.append(search_result) + + # Process news results if available (they have date field) + news_results = data.get("news", []) + for result in news_results: + snippet = result.get("markdown") or result.get("snippet", "") + + search_result = SearchResult( + title=result.get("title", ""), + url=result.get("url", ""), + snippet=snippet, + date=result.get("date"), # News results include date + last_updated=None, + ) + results.append(search_result) + + return SearchResponse( + results=results, + object="search", + ) + 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/mistral/ocr/transformation.py b/litellm/llms/mistral/ocr/transformation.py index f17c872f536..ed5e2359395 100644 --- a/litellm/llms/mistral/ocr/transformation.py +++ b/litellm/llms/mistral/ocr/transformation.py @@ -74,6 +74,7 @@ class MistralOCRConfig(BaseOCRConfig): model: str, api_key: Optional[str] = None, api_base: Optional[str] = None, + litellm_params: Optional[dict] = None, **kwargs, ) -> Dict: """ @@ -104,6 +105,7 @@ class MistralOCRConfig(BaseOCRConfig): api_base: Optional[str], model: str, optional_params: dict, + litellm_params: Optional[dict] = None, **kwargs, ) -> str: """ 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/searxng/__init__.py b/litellm/llms/searxng/__init__.py new file mode 100644 index 00000000000..91d237a8a08 --- /dev/null +++ b/litellm/llms/searxng/__init__.py @@ -0,0 +1,7 @@ +""" +SearXNG API integration module. +""" +from litellm.llms.searxng.search.transformation import SearXNGSearchConfig + +__all__ = ["SearXNGSearchConfig"] + diff --git a/litellm/llms/searxng/search/__init__.py b/litellm/llms/searxng/search/__init__.py new file mode 100644 index 00000000000..cb6fccfa9d5 --- /dev/null +++ b/litellm/llms/searxng/search/__init__.py @@ -0,0 +1,7 @@ +""" +SearXNG Search API module. +""" +from litellm.llms.searxng.search.transformation import SearXNGSearchConfig + +__all__ = ["SearXNGSearchConfig"] + diff --git a/litellm/llms/searxng/search/transformation.py b/litellm/llms/searxng/search/transformation.py new file mode 100644 index 00000000000..00ad9d19485 --- /dev/null +++ b/litellm/llms/searxng/search/transformation.py @@ -0,0 +1,223 @@ +""" +Calls SearXNG's /search endpoint to search the web. + +SearXNG API Reference: https://docs.searxng.org/dev/search_api.html +""" +from typing import Dict, List, Optional, TypedDict, Union + +import httpx + +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.llms.base_llm.search.transformation import ( + BaseSearchConfig, + SearchResponse, + SearchResult, +) +from litellm.secret_managers.main import get_secret_str + + +class _SearXNGSearchRequestRequired(TypedDict): + """Required fields for SearXNG Search API request.""" + q: str # Required - search query + + +class SearXNGSearchRequest(_SearXNGSearchRequestRequired, total=False): + """ + SearXNG Search API request format. + Based on: https://docs.searxng.org/dev/search_api.html + """ + categories: str # Optional - comma-separated list of categories + engines: str # Optional - comma-separated list of engines + language: str # Optional - language code + pageno: int # Optional - page number (default 1) + time_range: str # Optional - time range filter (day, month, year) + format: str # Optional - output format (json, csv, rss) - should be 'json' + + +class SearXNGSearchConfig(BaseSearchConfig): + + @staticmethod + def ui_friendly_name() -> str: + return "SearXNG" + + def get_http_method(self): + """ + SearXNG supports both GET and POST, but we'll use GET for simplicity. + """ + return "GET" + + def validate_environment( + self, + headers: Dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Dict: + """ + Validate environment and return headers. + SearXNG is open-source and doesn't require an API key by default. + Some instances may require authentication via headers. + """ + # SearXNG typically doesn't require API keys, but support optional auth + api_key = api_key or get_secret_str("SEARXNG_API_KEY") + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + headers["Content-Type"] = "application/json" + return headers + + def get_complete_url( + self, + api_base: Optional[str], + optional_params: dict, + data: Optional[Union[Dict, List[Dict]]] = None, + **kwargs, + ) -> str: + """ + Get complete URL for Search endpoint with query parameters. + + SearXNG uses GET requests, so we build the full URL with query params here. + The transformed request body (data) contains the parameters needed for the URL. + """ + from urllib.parse import urlencode + + api_base = api_base or get_secret_str("SEARXNG_API_BASE") + + if not api_base: + raise ValueError( + "SEARXNG_API_BASE is not set. Please set the `SEARXNG_API_BASE` environment variable " + "or pass `api_base` parameter. Example: os.environ['SEARXNG_API_BASE'] = 'https://your-searxng-instance.com'" + ) + + # Append "/search" to the api base if it's not already there + if not api_base.endswith("/search"): + if api_base.endswith("/"): + api_base = f"{api_base}search" + else: + api_base = f"{api_base}/search" + + # Build query parameters from the transformed request body + if data and isinstance(data, dict) and "_searxng_params" in data: + params = data["_searxng_params"] + query_string = urlencode(params) + return f"{api_base}?{query_string}" + + return api_base + + + def transform_search_request( + self, + query: Union[str, List[str]], + optional_params: dict, + **kwargs, + ) -> Dict: + """ + Transform Search request to SearXNG API format. + + Transforms Perplexity unified spec parameters: + - query → q + - max_results → (handled via pageno, SearXNG returns ~20 results per page) + - search_domain_filter → (not directly supported) + - country → language (approximate mapping) + - max_tokens_per_page → (not applicable, ignored) + + All other SearXNG-specific parameters are passed through as-is. + + Args: + query: Search query (string or list of strings). SearXNG only supports single string queries. + optional_params: Optional parameters for the request + + Returns: + Dict with typed request data following SearXNGSearchRequest spec + """ + if isinstance(query, list): + # SearXNG only supports single string queries, join with spaces + query = " ".join(query) + + request_data: SearXNGSearchRequest = { + "q": query, + "format": "json", # Always request JSON format + } + + # Transform Perplexity unified spec parameters to SearXNG format + if "country" in optional_params: + # Map country code to language (approximate) + country = optional_params["country"].lower() + if country == "us" or country == "uk": + request_data["language"] = "en" + elif country == "de": + request_data["language"] = "de" + elif country == "fr": + request_data["language"] = "fr" + elif country == "es": + request_data["language"] = "es" + elif country == "jp": + request_data["language"] = "ja" + else: + request_data["language"] = country # Pass through as-is + + # Handle max_results via pagination (SearXNG returns ~20 results per page by default) + # For simplicity, we'll just use page 1 and let SearXNG return its default number of results + if "max_results" in optional_params: + # Note: We could calculate pageno based on max_results, but for now we'll ignore this + # and let SearXNG return its default results + pass + + # Convert to dict before dynamic key assignments + result_data = dict(request_data) + + # Pass through all other SearXNG-specific parameters as-is + for param, value in optional_params.items(): + if param not in self.get_supported_perplexity_optional_params() and param not in result_data: + result_data[param] = value + + # Store params in special key for GET request URL building + # This will be used by get_complete_url to build the query string + return {"_searxng_params": result_data} + + def transform_search_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + **kwargs, + ) -> SearchResponse: + """ + Transform SearXNG API response to LiteLLM unified SearchResponse format. + + SearXNG → LiteLLM mappings: + - results[].title → SearchResult.title + - results[].url → SearchResult.url + - results[].content → SearchResult.snippet + - results[].publishedDate OR results[].pubdate → SearchResult.date + - No last_updated field in SearXNG response (set to None) + + Args: + raw_response: Raw httpx response from SearXNG API + logging_obj: Logging object for tracking + + Returns: + SearchResponse with standardized format + """ + response_json = raw_response.json() + + # Transform results to SearchResult objects + # Note: SearXNG doesn't natively support limiting results via API params + # It returns ~20 results per page by default + results = [] + for result in response_json.get("results", []): + # Get date from either publishedDate or pubdate field + date = result.get("publishedDate") or result.get("pubdate") + + search_result = SearchResult( + title=result.get("title", ""), + url=result.get("url", ""), + snippet=result.get("content", ""), # SearXNG uses "content" for snippet + date=date, + last_updated=None, # SearXNG doesn't provide last_updated in response + ) + results.append(search_result) + + return SearchResponse( + results=results, + object="search", + ) + diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index 4f84586cfc1..08c91a6fad1 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -473,7 +473,7 @@ def _transform_request_body( labels = {k: v for k, v in rm.items() if isinstance(v, str)} filtered_params = { - k: v for k, v in optional_params.items() if k in config_fields + k: v for k, v in optional_params.items() if _get_equivalent_key(k, set(config_fields)) } generation_config: Optional[GenerationConfig] = GenerationConfig( diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 112d3783787..b8370d5fef2 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -1419,7 +1419,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): _candidates: List[Candidates], model_response: Union[ModelResponse, "ModelResponseStream"], standard_optional_params: dict, - ) -> Tuple[List[dict], List[dict], List, List]: + cumulative_tool_call_index: int = 0, + ) -> Tuple[List[dict], List[dict], List, List, int]: """ Helper method to process candidates and extract metadata @@ -1428,6 +1429,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): url_context_metadata: List[dict] safety_ratings: List citation_metadata: List + cumulative_tool_call_index: int """ from litellm.litellm_core_utils.prompt_templates.common_utils import ( is_function_call, @@ -1443,7 +1445,6 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): chat_completion_logprobs: Optional[ChoiceLogprobs] = None tools: Optional[List[ChatCompletionToolCallChunk]] = [] functions: Optional[ChatCompletionToolCallFunctionChunk] = None - cumulative_tool_call_index: int = 0 thinking_blocks: Optional[List[ChatCompletionThinkingBlock]] = None for idx, candidate in enumerate(_candidates): @@ -1563,6 +1564,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): url_context_metadata, safety_ratings, citation_metadata, + cumulative_tool_call_index, ) def transform_response( @@ -1663,6 +1665,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): url_context_metadata, safety_ratings, citation_metadata, + _, # cumulative_tool_call_index not needed in non-streaming ) = VertexGeminiConfig._process_candidates( _candidates, model_response, logging_obj.optional_params ) @@ -1740,13 +1743,15 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): messages: List[AllMessageValues], optional_params: Dict, litellm_params: Dict, - api_key: Optional[str] = None, + api_key: Optional[Union[str, Dict]] = None, api_base: Optional[str] = None, ) -> Dict: default_headers = { "Content-Type": "application/json", } - if api_key is not None: + if isinstance(api_key, dict): + default_headers.update(api_key) + elif api_key is not None: default_headers["Authorization"] = f"Bearer {api_key}" if headers is not None: default_headers.update(headers) @@ -2277,6 +2282,7 @@ class ModelResponseIterator: self.sent_first_chunk = False self.logging_obj = logging_obj self.is_function_call = check_is_function_call(logging_obj) + self.cumulative_tool_call_index: int = 0 def chunk_parser(self, chunk: dict) -> Optional["ModelResponseStream"]: try: @@ -2298,8 +2304,12 @@ class ModelResponseIterator: url_context_metadata, safety_ratings, citation_metadata, + self.cumulative_tool_call_index, ) = VertexGeminiConfig._process_candidates( - _candidates, model_response, self.logging_obj.optional_params + _candidates, + model_response, + self.logging_obj.optional_params, + cumulative_tool_call_index=self.cumulative_tool_call_index, ) setattr(model_response, "vertex_ai_grounding_metadata", grounding_metadata) # type: ignore diff --git a/litellm/llms/vertex_ai/ocr/__init__.py b/litellm/llms/vertex_ai/ocr/__init__.py new file mode 100644 index 00000000000..fa8c85da9c5 --- /dev/null +++ b/litellm/llms/vertex_ai/ocr/__init__.py @@ -0,0 +1,5 @@ +"""Vertex AI OCR module.""" +from .transformation import VertexAIOCRConfig + +__all__ = ["VertexAIOCRConfig"] + diff --git a/litellm/llms/vertex_ai/ocr/transformation.py b/litellm/llms/vertex_ai/ocr/transformation.py new file mode 100644 index 00000000000..f4482939851 --- /dev/null +++ b/litellm/llms/vertex_ai/ocr/transformation.py @@ -0,0 +1,283 @@ +""" +Vertex AI Mistral OCR transformation implementation. +""" +from typing import Dict, Optional + +from litellm._logging import verbose_logger +from litellm.litellm_core_utils.prompt_templates.image_handling import ( + async_convert_url_to_base64, + convert_url_to_base64, +) +from litellm.llms.base_llm.ocr.transformation import DocumentType, OCRRequestData +from litellm.llms.mistral.ocr.transformation import MistralOCRConfig +from litellm.llms.vertex_ai.vertex_llm_base import VertexBase + + +class VertexAIOCRConfig(MistralOCRConfig): + """ + Vertex AI Mistral OCR transformation configuration. + + Vertex AI uses Mistral's OCR API format through the Mistral publisher endpoint. + Inherits transformation logic from MistralOCRConfig since they use the same format. + + Reference: Vertex AI Mistral OCR documentation + + Important: Vertex AI OCR only supports base64 data URIs (data:image/..., data:application/pdf;base64,...). + Regular URLs are not supported. + """ + + def __init__(self) -> None: + super().__init__() + self.vertex_base = VertexBase() + + 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.vertex_base.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, + api_base: Optional[str], + model: str, + optional_params: dict, + litellm_params: Optional[dict] = None, + **kwargs, + ) -> str: + """ + Get complete URL for Vertex AI OCR endpoint. + + Vertex AI endpoint format: + https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/mistralai/ocr + + Args: + api_base: Vertex AI API base URL (optional) + model: Model name (not used in URL construction) + optional_params: Optional parameters + litellm_params: LiteLLM parameters containing vertex_project, vertex_location + + Returns: Complete URL for Vertex AI OCR endpoint + """ + # 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_location = VertexBase.safe_get_vertex_ai_location(litellm_params=litellm_params) + + if vertex_project is None: + raise ValueError( + "Missing vertex_project - Set VERTEXAI_PROJECT environment variable or pass vertex_project parameter" + ) + + if vertex_location is None: + vertex_location = "us-central1" + + # Get API base URL + if api_base is None: + api_base = f"https://{vertex_location}-aiplatform.googleapis.com" + + # Ensure no trailing slash + api_base = api_base.rstrip("/") + + # Vertex AI OCR endpoint format for Mistral publisher + # Format: https://{region}-aiplatform.googleapis.com/v1/projects/{project}/locations/{region}/publishers/mistralai/models/{model}:rawPredict + return f"{api_base}/v1/projects/{vertex_project}/locations/{vertex_location}/publishers/mistralai/models/{model}:rawPredict" + + def _convert_url_to_data_uri_sync(self, url: str) -> str: + """ + Synchronously convert a URL to a base64 data URI. + + Vertex AI OCR doesn't have internet access, so we need to fetch URLs + and convert them to base64 data URIs. + + Args: + url: The URL to convert + + Returns: + Base64 data URI string + """ + verbose_logger.debug(f"Vertex AI OCR: Converting URL to base64 data URI (sync): {url}") + + # Fetch and convert to base64 data URI + # convert_url_to_base64 already returns a full data URI like "data:image/jpeg;base64,..." + data_uri = convert_url_to_base64(url=url) + + verbose_logger.debug(f"Vertex AI OCR: Converted URL to data URI (length: {len(data_uri)})") + + return data_uri + + async def _convert_url_to_data_uri_async(self, url: str) -> str: + """ + Asynchronously convert a URL to a base64 data URI. + + Vertex AI OCR doesn't have internet access, so we need to fetch URLs + and convert them to base64 data URIs. + + Args: + url: The URL to convert + + Returns: + Base64 data URI string + """ + verbose_logger.debug(f"Vertex AI OCR: Converting URL to base64 data URI (async): {url}") + + # Fetch and convert to base64 data URI asynchronously + # async_convert_url_to_base64 already returns a full data URI like "data:image/jpeg;base64,..." + data_uri = await async_convert_url_to_base64(url=url) + + verbose_logger.debug(f"Vertex AI OCR: Converted URL to data URI (length: {len(data_uri)})") + + return data_uri + + def transform_ocr_request( + self, + model: str, + document: DocumentType, + optional_params: dict, + headers: dict, + **kwargs, + ) -> OCRRequestData: + """ + Transform OCR request for Vertex AI, converting URLs to base64 data URIs (sync). + + Vertex AI OCR doesn't have internet access, so we automatically fetch + any URLs and convert them to base64 data URIs synchronously. + + Args: + model: Model name + document: Document dict from user + optional_params: Already mapped optional parameters + headers: Request headers + **kwargs: Additional arguments + + Returns: + OCRRequestData with JSON data + """ + verbose_logger.debug("Vertex AI OCR transform_ocr_request (sync) called") + + if not isinstance(document, dict): + raise ValueError(f"Expected document dict, got {type(document)}") + + # Check if we need to convert URL to base64 + doc_type = document.get("type") + transformed_document = document.copy() + + if doc_type == "document_url": + document_url = document.get("document_url", "") + # If it's not already a data URI, convert it + if document_url and not document_url.startswith("data:"): + verbose_logger.debug( + "Vertex AI OCR: Converting document URL to base64 data URI (sync)" + ) + data_uri = self._convert_url_to_data_uri_sync(url=document_url) + transformed_document["document_url"] = data_uri + elif doc_type == "image_url": + image_url = document.get("image_url", "") + # If it's not already a data URI, convert it + if image_url and not image_url.startswith("data:"): + verbose_logger.debug( + "Vertex AI OCR: Converting image URL to base64 data URI (sync)" + ) + data_uri = self._convert_url_to_data_uri_sync(url=image_url) + transformed_document["image_url"] = data_uri + + # Call parent's transform to build the request + return super().transform_ocr_request( + model=model, + document=transformed_document, + optional_params=optional_params, + headers=headers, + **kwargs, + ) + + async def async_transform_ocr_request( + self, + model: str, + document: DocumentType, + optional_params: dict, + headers: dict, + **kwargs, + ) -> OCRRequestData: + """ + Transform OCR request for Vertex AI, converting URLs to base64 data URIs (async). + + Vertex AI OCR doesn't have internet access, so we automatically fetch + any URLs and convert them to base64 data URIs asynchronously. + + Args: + model: Model name + document: Document dict from user + optional_params: Already mapped optional parameters + headers: Request headers + **kwargs: Additional arguments + + Returns: + OCRRequestData with JSON data + """ + verbose_logger.debug(f"Vertex AI OCR async_transform_ocr_request - model: {model}") + + if not isinstance(document, dict): + raise ValueError(f"Expected document dict, got {type(document)}") + + # Check if we need to convert URL to base64 + doc_type = document.get("type") + transformed_document = document.copy() + + if doc_type == "document_url": + document_url = document.get("document_url", "") + # If it's not already a data URI, convert it + if document_url and not document_url.startswith("data:"): + verbose_logger.debug( + "Vertex AI OCR: Converting document URL to base64 data URI (async)" + ) + data_uri = await self._convert_url_to_data_uri_async(url=document_url) + transformed_document["document_url"] = data_uri + elif doc_type == "image_url": + image_url = document.get("image_url", "") + # If it's not already a data URI, convert it + if image_url and not image_url.startswith("data:"): + verbose_logger.debug( + "Vertex AI OCR: Converting image URL to base64 data URI (async)" + ) + data_uri = await self._convert_url_to_data_uri_async(url=image_url) + transformed_document["image_url"] = data_uri + + # Call parent's transform to build the request + return super().transform_ocr_request( + model=model, + document=transformed_document, + optional_params=optional_params, + headers=headers, + **kwargs, + ) + 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/count_tokens/handler.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/count_tokens/handler.py index 0f073d02269..da76b12c371 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/count_tokens/handler.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/count_tokens/handler.py @@ -9,7 +9,6 @@ from typing import Any, Dict, Optional from litellm.llms.custom_httpx.http_handler import get_async_httpx_client from litellm.llms.vertex_ai.vertex_llm_base import VertexBase -from litellm.types.llms.vertex_ai import VertexPartnerProvider class VertexAIPartnerModelsTokenCounter(VertexBase): 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/vertex_llm_base.py b/litellm/llms/vertex_ai/vertex_llm_base.py index 8f7c846bd7d..9ddbc461a70 100644 --- a/litellm/llms/vertex_ai/vertex_llm_base.py +++ b/litellm/llms/vertex_ai/vertex_llm_base.py @@ -308,9 +308,8 @@ class VertexBase: raise ValueError( "Missing gemini_api_key, please set `GEMINI_API_KEY`" ) - auth_header = ( - gemini_api_key # cloudflare expects api key as bearer token - ) + if gemini_api_key is not None: + auth_header = {"x-goog-api-key": gemini_api_key} # type: ignore[assignment] else: url = "{}:{}".format(api_base, endpoint) @@ -625,3 +624,66 @@ class VertexBase: or get_secret_str("VERTEXAI_LOCATION") or get_secret_str("VERTEX_LOCATION") ) + + @staticmethod + def safe_get_vertex_ai_project(litellm_params: dict) -> Optional[str]: + """ + Safely get Vertex AI project without mutating the litellm_params dict. + + Unlike get_vertex_ai_project(), this does NOT pop values from the dict, + making it safe to call multiple times with the same litellm_params. + + Args: + litellm_params: Dictionary containing Vertex AI parameters + + Returns: + Vertex AI project ID or None + """ + return ( + litellm_params.get("vertex_project") + or litellm_params.get("vertex_ai_project") + or litellm.vertex_project + or get_secret_str("VERTEXAI_PROJECT") + ) + + @staticmethod + def safe_get_vertex_ai_credentials(litellm_params: dict) -> Optional[str]: + """ + Safely get Vertex AI credentials without mutating the litellm_params dict. + + Unlike get_vertex_ai_credentials(), this does NOT pop values from the dict, + making it safe to call multiple times with the same litellm_params. + + Args: + litellm_params: Dictionary containing Vertex AI parameters + + Returns: + Vertex AI credentials or None + """ + return ( + litellm_params.get("vertex_credentials") + or litellm_params.get("vertex_ai_credentials") + or get_secret_str("VERTEXAI_CREDENTIALS") + ) + + @staticmethod + def safe_get_vertex_ai_location(litellm_params: dict) -> Optional[str]: + """ + Safely get Vertex AI location without mutating the litellm_params dict. + + Unlike get_vertex_ai_location(), this does NOT pop values from the dict, + making it safe to call multiple times with the same litellm_params. + + Args: + litellm_params: Dictionary containing Vertex AI parameters + + Returns: + Vertex AI location/region or None + """ + return ( + litellm_params.get("vertex_location") + or litellm_params.get("vertex_ai_location") + or litellm.vertex_location + or get_secret_str("VERTEXAI_LOCATION") + or get_secret_str("VERTEX_LOCATION") + ) 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/watsonx/chat/transformation.py b/litellm/llms/watsonx/chat/transformation.py index 2c096cafced..865dc71939d 100644 --- a/litellm/llms/watsonx/chat/transformation.py +++ b/litellm/llms/watsonx/chat/transformation.py @@ -35,6 +35,7 @@ class IBMWatsonXChatConfig(IBMWatsonXMixin, OpenAIGPTConfig): "n", "presence_penalty", "response_format", + "reasoning_effort", ] def is_tool_choice_option(self, tool_choice: Optional[Union[str, dict]]) -> bool: @@ -124,16 +125,18 @@ class IBMWatsonXChatConfig(IBMWatsonXMixin, OpenAIGPTConfig): None if model.startswith("deployment/") else api_params["project_id"] ) return payload - + @staticmethod - def _apply_prompt_template_core(model: str, messages: List[Dict[str, str]], hf_template_fn) -> Optional[str]: + def _apply_prompt_template_core( + model: str, messages: List[Dict[str, str]], hf_template_fn + ) -> Optional[str]: """Core logic for applying prompt templates""" from litellm.litellm_core_utils.prompt_templates.factory import ( custom_prompt, ibm_granite_pt, mistral_instruct_pt, ) - + if WatsonXModelPattern.GRANITE_CHAT.value in model: return ibm_granite_pt(messages=messages) elif WatsonXModelPattern.IBM_MISTRAL.value in model: @@ -147,9 +150,18 @@ class IBMWatsonXChatConfig(IBMWatsonXMixin, OpenAIGPTConfig): elif WatsonXModelPattern.LLAMA3_INSTRUCT.value in model: return custom_prompt( role_dict={ - "system": {"pre_message": "<|start_header_id|>system<|end_header_id|>\n", "post_message": "<|eot_id|>"}, - "user": {"pre_message": "<|start_header_id|>user<|end_header_id|>\n", "post_message": "<|eot_id|>"}, - "assistant": {"pre_message": "<|start_header_id|>assistant<|end_header_id|>\n", "post_message": "<|eot_id|>"}, + "system": { + "pre_message": "<|start_header_id|>system<|end_header_id|>\n", + "post_message": "<|eot_id|>", + }, + "user": { + "pre_message": "<|start_header_id|>user<|end_header_id|>\n", + "post_message": "<|eot_id|>", + }, + "assistant": { + "pre_message": "<|start_header_id|>assistant<|end_header_id|>\n", + "post_message": "<|eot_id|>", + }, }, messages=messages, initial_prompt_value="<|begin_of_text|>", @@ -158,7 +170,9 @@ class IBMWatsonXChatConfig(IBMWatsonXMixin, OpenAIGPTConfig): return None @staticmethod - async def aapply_prompt_template(model: str, messages: List[Dict[str, str]]) -> Optional[str]: + async def aapply_prompt_template( + model: str, messages: List[Dict[str, str]] + ) -> Optional[str]: """Apply prompt template (async version)""" import litellm from litellm.litellm_core_utils.prompt_templates.factory import ( @@ -204,9 +218,11 @@ class IBMWatsonXChatConfig(IBMWatsonXMixin, OpenAIGPTConfig): final_prompt_value="<|start_header_id|>assistant<|end_header_id|>\n", ) return None - + @staticmethod - def apply_prompt_template(model: str, messages: List[Dict[str, str]]) -> Optional[str]: + def apply_prompt_template( + model: str, messages: List[Dict[str, str]] + ) -> Optional[str]: """Apply prompt template (sync version)""" from litellm.litellm_core_utils.prompt_templates.factory import ( hf_chat_template, @@ -215,4 +231,3 @@ class IBMWatsonXChatConfig(IBMWatsonXMixin, OpenAIGPTConfig): return IBMWatsonXChatConfig._apply_prompt_template_core( model=model, messages=messages, hf_template_fn=hf_chat_template ) - diff --git a/litellm/llms/xai/responses/transformation.py b/litellm/llms/xai/responses/transformation.py new file mode 100644 index 00000000000..bd422c8d81e --- /dev/null +++ b/litellm/llms/xai/responses/transformation.py @@ -0,0 +1,146 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +import litellm +from litellm._logging import verbose_logger +from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig +from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams +from litellm.types.router import GenericLiteLLMParams +from litellm.types.utils import LlmProviders + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + + LiteLLMLoggingObj = _LiteLLMLoggingObj +else: + LiteLLMLoggingObj = Any + +XAI_API_BASE = "https://api.x.ai/v1" + + +class XAIResponsesAPIConfig(OpenAIResponsesAPIConfig): + """ + Configuration for XAI's Responses API. + + Inherits from OpenAIResponsesAPIConfig since XAI's Responses API is largely + compatible with OpenAI's, with a few differences: + - Does not support the 'instructions' parameter + - Requires code_interpreter tools to have 'container' field removed + - Recommends store=false when sending images + + Reference: https://docs.x.ai/docs/api-reference#create-new-response + """ + + @property + def custom_llm_provider(self) -> LlmProviders: + return LlmProviders.XAI + + def get_supported_openai_params(self, model: str) -> list: + """ + Get supported parameters for XAI Responses API. + + XAI supports most OpenAI Responses API params except 'instructions'. + """ + supported_params = super().get_supported_openai_params(model) + + # Remove 'instructions' as it's not supported by XAI + if "instructions" in supported_params: + supported_params.remove("instructions") + + return supported_params + + def map_openai_params( + self, + response_api_optional_params: ResponsesAPIOptionalRequestParams, + model: str, + drop_params: bool, + ) -> Dict: + """ + Map parameters for XAI Responses API. + + Handles XAI-specific transformations: + 1. Drops 'instructions' parameter (not supported) + 2. Transforms code_interpreter tools to remove 'container' field + 3. Sets store=false when images are detected (recommended by XAI) + """ + params = dict(response_api_optional_params) + + # Drop instructions parameter (not supported by XAI) + if "instructions" in params: + verbose_logger.debug( + "XAI Responses API does not support 'instructions' parameter. Dropping it." + ) + params.pop("instructions") + + # Transform code_interpreter tools - remove container field + if "tools" in params and params["tools"]: + tools_list = params["tools"] + # Ensure tools is a list for iteration + if not isinstance(tools_list, list): + tools_list = [tools_list] + + transformed_tools: List[Any] = [] + for tool in tools_list: + if isinstance(tool, dict) and tool.get("type") == "code_interpreter": + # XAI supports code_interpreter but doesn't use the container field + # Keep only the type field + verbose_logger.debug( + "XAI: Transforming code_interpreter tool, removing container field" + ) + transformed_tools.append({"type": "code_interpreter"}) + else: + transformed_tools.append(tool) + params["tools"] = transformed_tools + + return params + + def validate_environment( + self, headers: dict, model: str, litellm_params: Optional[GenericLiteLLMParams] + ) -> dict: + """ + Validate environment and set up headers for XAI API. + + Uses XAI_API_KEY from environment or litellm_params. + """ + litellm_params = litellm_params or GenericLiteLLMParams() + api_key = ( + litellm_params.api_key + or litellm.api_key + or get_secret_str("XAI_API_KEY") + ) + + if not api_key: + raise ValueError( + "XAI API key is required. Set XAI_API_KEY environment variable or pass api_key parameter." + ) + + headers.update( + { + "Authorization": f"Bearer {api_key}", + } + ) + return headers + + def get_complete_url( + self, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """ + Get the complete URL for XAI Responses API endpoint. + + Returns: + str: The full URL for the XAI /responses endpoint + """ + api_base = ( + api_base + or litellm.api_base + or get_secret_str("XAI_API_BASE") + or XAI_API_BASE + ) + + # Remove trailing slashes + api_base = api_base.rstrip("/") + + return f"{api_base}/responses" + diff --git a/litellm/main.py b/litellm/main.py index b7af4e8d39c..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, @@ -5406,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 @@ -5434,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" @@ -5644,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 @@ -6001,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 e20ab6f564d..f571dfb5243 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -1105,6 +1105,11 @@ "supports_tool_choice": true, "supports_vision": true }, + "azure/container": { + "code_interpreter_cost_per_session": 0.03, + "litellm_provider": "azure", + "mode": "chat" + }, "azure/eu/gpt-4o-2024-08-06": { "deprecation_date": "2026-02-27", "cache_read_input_token_cost": 1.375e-06, @@ -2364,6 +2369,35 @@ "supports_tool_choice": true, "supports_vision": true }, + "azure/gpt-5-pro": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "mode": "responses", + "output_cost_per_token": 0.00012, + "source": "https://learn.microsoft.com/en-us/azure/ai-foundry/foundry-models/concepts/models-sold-directly-by-azure?pivots=azure-openai&tabs=global-standard-aoai%2Cstandard-chat-completions%2Cglobal-standard#gpt-5", + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, "azure/gpt-image-1": { "input_cost_per_pixel": 4.0054321e-08, "litellm_provider": "azure", @@ -2472,6 +2506,96 @@ "/v1/images/generations" ] }, + "azure/gpt-image-1-mini": { + "input_cost_per_pixel": 8.0566406e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1024-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 2.0751953125e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1024-x-1536/gpt-image-1-mini": { + "input_cost_per_pixel": 2.0751953125e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1536-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 2.0345052083e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1024-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 8.056640625e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1024-x-1536/gpt-image-1-mini": { + "input_cost_per_pixel": 8.056640625e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1536-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 7.9752604167e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/high/1024-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 3.173828125e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/high/1024-x-1536/gpt-image-1-mini": { + "input_cost_per_pixel": 3.173828125e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/high/1536-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 3.1575520833e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, "azure/mistral-large-2402": { "input_cost_per_token": 8e-06, "litellm_provider": "azure", @@ -3387,6 +3511,33 @@ ], "source": "https://devblogs.microsoft.com/foundry/whats-new-in-azure-ai-foundry-august-2025/#mistral-document-ai-(ocr)-%E2%80%94-serverless-in-foundry" }, + "azure_ai/doc-intelligence/prebuilt-read": { + "litellm_provider": "azure_ai", + "ocr_cost_per_page": 1.5e-3, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/" + }, + "azure_ai/doc-intelligence/prebuilt-layout": { + "litellm_provider": "azure_ai", + "ocr_cost_per_page": 1e-2, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/" + }, + "azure_ai/doc-intelligence/prebuilt-document": { + "litellm_provider": "azure_ai", + "ocr_cost_per_page": 1e-2, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/" + }, "azure_ai/MAI-DS-R1": { "input_cost_per_token": 1.35e-06, "litellm_provider": "azure_ai", @@ -3553,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 @@ -3581,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 @@ -5604,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, @@ -7860,11 +8019,98 @@ } ] }, + "firecrawl/search": { + "litellm_provider": "firecrawl", + "mode": "search", + "tiered_pricing": [ + { + "input_cost_per_query": 1.66e-03, + "max_results_range": [ + 1, + 10 + ] + }, + { + "input_cost_per_query": 3.32e-03, + "max_results_range": [ + 11, + 20 + ] + }, + { + "input_cost_per_query": 4.98e-03, + "max_results_range": [ + 21, + 30 + ] + }, + { + "input_cost_per_query": 6.64e-03, + "max_results_range": [ + 31, + 40 + ] + }, + { + "input_cost_per_query": 8.3e-03, + "max_results_range": [ + 41, + 50 + ] + }, + { + "input_cost_per_query": 9.96e-03, + "max_results_range": [ + 51, + 60 + ] + }, + { + "input_cost_per_query": 11.62e-03, + "max_results_range": [ + 61, + 70 + ] + }, + { + "input_cost_per_query": 13.28e-03, + "max_results_range": [ + 71, + 80 + ] + }, + { + "input_cost_per_query": 14.94e-03, + "max_results_range": [ + 81, + 90 + ] + }, + { + "input_cost_per_query": 16.6e-03, + "max_results_range": [ + 91, + 100 + ] + } + ], + "metadata": { + "notes": "Firecrawl search pricing: $83 for 100,000 credits, 2 credits per 10 results. Cost = ceiling(limit/10) * 2 * $0.00083" + } + }, "perplexity/search": { "input_cost_per_query": 5e-03, "litellm_provider": "perplexity", "mode": "search" }, + "searxng/search": { + "litellm_provider": "searxng", + "mode": "search", + "input_cost_per_query": 0.0, + "metadata": { + "notes": "SearXNG is an open-source metasearch engine. Free to use when self-hosted or using public instances." + } + }, "elevenlabs/scribe_v1": { "input_cost_per_second": 6.11e-05, "litellm_provider": "elevenlabs", @@ -10042,6 +10288,98 @@ "supports_vision": true, "supports_web_search": true }, + "gemini-live-2.5-flash-preview-native-audio-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 3e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_audio_token": 1.2e-05, + "output_cost_per_token": 2e-06, + "source": "https://ai.google.dev/gemini-api/docs/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini/gemini-live-2.5-flash-preview-native-audio-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 3e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "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", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 8000000 + }, "gemini-2.5-flash-lite-preview-06-17": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_audio_token": 5e-07, @@ -11279,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", @@ -12330,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", @@ -12396,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, @@ -18211,15 +18579,6 @@ "output_cost_per_token": 2e-07, "supports_tool_choice": true }, - "openrouter/nvidia/nemotron-nano-9b-v2:free": { - "input_cost_per_token": 0, - "litellm_provider": "openrouter", - "max_tokens": 128000, - "mode": "chat", - "output_cost_per_token": 0, - "source": "https://openrouter.ai/nvidia/nemotron-nano-9b-v2:free", - "supports_tool_choice": true - }, "openrouter/openai/gpt-3.5-turbo": { "input_cost_per_token": 1.5e-06, "litellm_provider": "openrouter", @@ -22777,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", @@ -22910,6 +23281,15 @@ "supports_function_calling": true, "supports_tool_choice": true }, + "vertex_ai/mistral-ocr-2505": { + "litellm_provider": "vertex_ai", + "mode": "ocr", + "ocr_cost_per_page": 5e-4, + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://cloud.google.com/generative-ai-app-builder/pricing" + }, "vertex_ai/openai/gpt-oss-120b-maas": { "input_cost_per_token": 1.5e-07, "litellm_provider": "vertex_ai-openai_models", @@ -23022,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, @@ -23904,7 +24312,6 @@ "output_cost_per_token": 1.5e-05, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, - "supports_reasoning": true, "supports_tool_choice": true, "supports_web_search": true }, @@ -23921,7 +24328,6 @@ "cache_read_input_token_cost": 0.05e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, - "supports_reasoning": true, "supports_tool_choice": true, "supports_web_search": true }, @@ -23953,7 +24359,6 @@ "output_cost_per_token_above_128k_tokens": 30e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, - "supports_reasoning": true, "supports_tool_choice": true, "supports_web_search": true }, @@ -23969,7 +24374,6 @@ "output_cost_per_token_above_128k_tokens": 30e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, - "supports_reasoning": true, "supports_tool_choice": true, "supports_web_search": true }, @@ -24047,6 +24451,11 @@ "litellm_provider": "vertex_ai", "mode": "vector_store" }, + "openai/container": { + "code_interpreter_cost_per_session": 0.03, + "litellm_provider": "openai", + "mode": "chat" + }, "openai/sora-2": { "litellm_provider": "openai", "mode": "video_generation", @@ -24081,16 +24490,6 @@ "1280x720" ] }, - "openai/container": { - "code_interpreter_cost_per_session": 0.03, - "litellm_provider": "openai", - "mode": "container" - }, - "azure/container": { - "code_interpreter_cost_per_session": 0.03, - "litellm_provider": "azure", - "mode": "container" - }, "azure/sora-2": { "litellm_provider": "azure", "mode": "video_generation", diff --git a/litellm/ocr/main.py b/litellm/ocr/main.py index 62172b0fbae..5acab8cbf2c 100644 --- a/litellm/ocr/main.py +++ b/litellm/ocr/main.py @@ -14,6 +14,7 @@ from litellm.constants import request_timeout from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.base_llm.ocr.transformation import BaseOCRConfig, OCRResponse from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler +from litellm.types.router import GenericLiteLLMParams from litellm.utils import ProviderConfigManager, client ####### ENVIRONMENT VARIABLES ################### @@ -243,6 +244,9 @@ def ocr( f"OCR call - model: {model}, provider: {custom_llm_provider}" ) + # Get litellm params using GenericLiteLLMParams (same as responses API) + litellm_params = GenericLiteLLMParams(**kwargs) + # Extract OCR-specific parameters from kwargs supported_params = ocr_provider_config.get_supported_ocr_params(model=model) non_default_params = {} @@ -283,10 +287,7 @@ def ocr( aocr=_is_async, headers=extra_headers, provider_config=ocr_provider_config, - litellm_params={ - "api_base": api_base, - "api_key": api_key, - }, + litellm_params=dict(litellm_params), ) return response diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index 22695485741..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,18 @@ 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) + # Handle mcp_info serialization if data.mcp_info is not None: data_dict["mcp_info"] = safe_dumps(data.mcp_info) @@ -48,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]: @@ -76,12 +117,12 @@ async def get_mcp_server( """ Returns the matching mcp server from the db iff exists """ - mcp_server: Optional[LiteLLM_MCPServerTable] = ( - await prisma_client.db.litellm_mcpservertable.find_unique( - where={ - "server_id": server_id, - } - ) + mcp_server: Optional[ + LiteLLM_MCPServerTable + ] = await prisma_client.db.litellm_mcpservertable.find_unique( + where={ + "server_id": server_id, + } ) return mcp_server @@ -92,12 +133,12 @@ async def get_mcp_servers( """ Returns the matching mcp servers from the db with the server_ids """ - _mcp_servers: List[LiteLLM_MCPServerTable] = ( - await prisma_client.db.litellm_mcpservertable.find_many( - where={ - "server_id": {"in": server_ids}, - } - ) + _mcp_servers: List[ + LiteLLM_MCPServerTable + ] = await prisma_client.db.litellm_mcpservertable.find_many( + where={ + "server_id": {"in": server_ids}, + } ) final_mcp_servers: List[LiteLLM_MCPServerTable] = [] for _mcp_server in _mcp_servers: @@ -299,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 631aa2e897b..5b1dc5933c3 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -38,34 +38,33 @@ 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 -def _deserialize_env_dict(env_data: Any) -> Optional[Dict[str, str]]: +def _deserialize_json_dict(data: Any) -> Optional[Dict[str, str]]: """ - Helper function to deserialize environment dictionary from database storage. - Handles both JSON string and dictionary formats. + Deserialize optional JSON mappings stored in the database. - Args: - env_data: The environment data from database (could be JSON string or dict) - - Returns: - Dict[str, str] or None: Deserialized environment dictionary + Accepts values kept as JSON strings or materialized dictionaries and + returns None when the input is empty or cannot be decoded. """ - if not env_data: + if not data: return None - if isinstance(env_data, str): + if isinstance(data, str): try: - return json.loads(env_data) + return json.loads(data) except (json.JSONDecodeError, TypeError): # If it's not valid JSON, return as-is (shouldn't happen but safety) return None else: # Already a dictionary - return env_data + return data class MCPServerManager: @@ -398,10 +397,26 @@ class MCPServerManager: try: if mcp_server.server_id not in self.get_registry(): _mcp_info: MCPInfo = mcp_server.mcp_info or {} - # Use helper to deserialize environment dictionary + # Use helper to deserialize dictionary # Safely access env field which may not exist on Prisma model objects - env_data = getattr(mcp_server, "env", None) - env_dict = _deserialize_env_dict(env_data) + env_dict = _deserialize_json_dict(getattr(mcp_server, "env", None)) + 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 @@ -424,8 +439,10 @@ 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, # oauth specific fields client_id=getattr(mcp_server, "client_id", None), client_secret=getattr(mcp_server, "client_secret", None), 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