@@ -406,6 +406,140 @@ You can use LiteLLM through either the Proxy Server or Python SDK. Both give you
Support for more providers. Missing a provider or LLM Platform, raise a [feature request](https://github.com/BerriAI/litellm/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.yml&title=%5BFeature%5D%3A+).
+### Deploy on AWS or GCP with Terraform
+
+Run the LiteLLM proxy as a production-ready componentized stack (gateway, backend, UI on separate services; managed Postgres + Redis + object store) using the published Terraform modules. Both modules are on the [public Terraform Registry](https://registry.terraform.io/namespaces/BerriAI) — no auth needed.
+
+#### AWS — ECS Fargate + Aurora + ElastiCache + ALB
+
+[](https://console.aws.amazon.com/cloudshell/home) — opens an in-browser shell, already authenticated to your AWS account. Once inside, run:
+
+```bash
+git clone https://github.com/BerriAI/litellm.git
+cd litellm/terraform/litellm/aws/examples/default
+cp terraform.tfvars.example terraform.tfvars # edit region/tenant/env
+terraform init && terraform apply
+```
+
+[Module page →](https://registry.terraform.io/modules/BerriAI/litellm/aws/latest)
+
+Or call the module from your own root config:
+
+```hcl
+# main.tf
+terraform {
+ required_version = ">= 1.6.0"
+ required_providers {
+ aws = { source = "hashicorp/aws", version = "~> 5.60" }
+ }
+}
+
+provider "aws" {
+ region = "us-west-2"
+}
+
+module "litellm" {
+ source = "BerriAI/litellm/aws"
+ version = "~> 1.89"
+
+ region = "us-west-2"
+ azs = ["us-west-2a", "us-west-2b"]
+ tenant = "acme"
+ env = "prod"
+
+ # Production: provide an ACM cert. Without one, set allow_plaintext_alb = true
+ # (dev/trial only).
+ # acm_certificate_arn = "arn:aws:acm:us-west-2:111122223333:certificate/..."
+ allow_plaintext_alb = true
+}
+
+output "litellm_url" {
+ value = module.litellm.alb_dns_name
+}
+```
+
+```bash
+terraform init
+terraform apply
+```
+
+Provider API keys live in AWS Secrets Manager; reference ARNs via `gateway_extra_secrets`. Full input list and architecture diagram on the [registry page](https://registry.terraform.io/modules/BerriAI/litellm/aws/latest?tab=inputs).
+
+#### GCP — Cloud Run + Cloud SQL + Memorystore + HTTPS LB
+
+[](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FBerriAI%2Flitellm&cloudshell_workspace=terraform%2Flitellm%2Fgcp%2Fexamples%2Fdefault&cloudshell_tutorial=TUTORIAL.md&cloudshell_image=gcr.io/ds-artifacts-cloudshell/deploystack_custom_image&shellonly=true)
+
+Real 1-click. Opens Cloud Shell, clones this repo, and walks you through `terraform apply` via a built-in [DeployStack tutorial](./terraform/litellm/gcp/examples/default/TUTORIAL.md) — pick the project, the tutorial sets up the Artifact Registry remote repo, writes `terraform.tfvars` from your answers, and runs apply.
+
+[Module page →](https://registry.terraform.io/modules/BerriAI/litellm/google/latest)
+
+To call the module from your own config instead, Cloud Run can't pull from `ghcr.io` directly, so first set up a one-time Artifact Registry remote repo backed by GHCR:
+
+```bash
+gcloud artifacts repositories create litellm \
+ --location=us-central1 \
+ --repository-format=docker \
+ --mode=remote-repository \
+ --remote-docker-repo=https://ghcr.io \
+ --project=my-gcp-project
+```
+
+Then:
+
+```hcl
+# main.tf
+terraform {
+ required_version = ">= 1.6.0"
+ required_providers {
+ google = { source = "hashicorp/google", version = "~> 6.10" }
+ google-beta = { source = "hashicorp/google-beta", version = "~> 6.10" }
+ }
+}
+
+provider "google" { project = "my-gcp-project"; region = "us-central1" }
+provider "google-beta" { project = "my-gcp-project"; region = "us-central1" }
+
+module "litellm" {
+ source = "BerriAI/litellm/google"
+ version = "~> 1.89"
+
+ project_id = "my-gcp-project"
+ region = "us-central1"
+ tenant = "acme"
+ env = "prod"
+
+ # Replace my-gcp-project with your GCP project ID (same value as project_id above).
+ image_registry = "us-central1-docker.pkg.dev/my-gcp-project/litellm/berriai"
+
+ # Production: provide DNS already pointing at the LB IP for Google-managed certs.
+ # Without one, set allow_plaintext_lb = true (dev/trial only).
+ # lb_domains = ["proxy.example.com"]
+ allow_plaintext_lb = true
+}
+
+output "litellm_url" {
+ value = module.litellm.load_balancer_url
+}
+```
+
+```bash
+terraform init
+terraform apply
+```
+
+Provider API keys live in Secret Manager; reference resource IDs (e.g. `projects/my-gcp-project/secrets/openai-api-key`) via `gateway_extra_secrets`. Full input list and architecture diagram on the [registry page](https://registry.terraform.io/modules/BerriAI/litellm/google/latest?tab=inputs).
+
+#### Both stacks include
+
+- The full componentized split (gateway / backend / UI as independent services)
+- Managed Postgres (writer + reader) and Redis
+- Versioned object store for proxy state + file uploads
+- An auto-generated `LITELLM_MASTER_KEY` in your cloud's secret manager
+- A one-off migration job that runs `prisma migrate deploy` before the proxy starts
+- The same `proxy_config` surface as the [Helm chart](./helm/litellm/) — pass YAML as a typed map
+
+The Terraform modules live at [`terraform/litellm/aws/`](./terraform/litellm/aws/) and [`terraform/litellm/gcp/`](./terraform/litellm/gcp/) in this repo; the registry entries are read-only mirrors updated on each release.
+
### Run in Developer Mode
#### Services
1. Setup .env file in root
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 2cfdde8a517..667bdb073eb 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,5 +1,5 @@
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
FROM $UV_IMAGE AS uvbin
diff --git a/backend/routes/allowlist.py b/backend/routes/allowlist.py
index d1a576aeb33..2f65f99c292 100644
--- a/backend/routes/allowlist.py
+++ b/backend/routes/allowlist.py
@@ -120,6 +120,9 @@ BACKEND_PATH_PREFIXES: tuple[str, ...] = (
"/robots.txt",
# Health (k8s probes)
"/health",
+ # Plugin system
+ "/api/plugins",
+ "/plugin-proxy/",
)
BACKEND_EXACT_PATHS: frozenset[str] = frozenset(
diff --git a/basedpyright-code-budget.json b/basedpyright-code-budget.json
index 7ba7656e407..f5b0a9aaf81 100644
--- a/basedpyright-code-budget.json
+++ b/basedpyright-code-budget.json
@@ -121,7 +121,7 @@
},
"reportReturnType": {
"baseline": 126,
- "slack": 13
+ "slack": 100
},
"reportTypedDictNotRequiredAccess": {
"baseline": 20,
@@ -157,7 +157,7 @@
},
"reportUnnecessaryComparison": {
"baseline": 683,
- "slack": 10
+ "slack": 100
},
"reportUnnecessaryContains": {
"baseline": 4,
diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database
index e591a4a2adb..50ef55e3261 100644
--- a/docker/Dockerfile.database
+++ b/docker/Dockerfile.database
@@ -1,8 +1,8 @@
# Base image for building
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
# Runtime image
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
FROM $UV_IMAGE AS uvbin
diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root
index eafbd23fd90..ab02b43d0f9 100644
--- a/docker/Dockerfile.non_root
+++ b/docker/Dockerfile.non_root
@@ -1,6 +1,6 @@
# Base images
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG PROXY_EXTRAS_SOURCE=published
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
diff --git a/docs/plugin_architecture.md b/docs/plugin_architecture.md
new file mode 100644
index 00000000000..8801761531d
--- /dev/null
+++ b/docs/plugin_architecture.md
@@ -0,0 +1,141 @@
+# LiteLLM Plugin Architecture
+
+Plugins let external services appear as selectable modes in the litellm UI sidebar alongside the AI Gateway.
+
+---
+
+## Quick start
+
+### 1. Configure the plugin
+
+Add a `plugins` block to your litellm `config.yaml`:
+
+```yaml
+general_settings:
+ master_key: sk-...
+ plugins:
+ - name: my-plugin # unique identifier (no spaces)
+ display_name: My Plugin # shown in the UI dropdown
+ url: "https://my-plugin.example.com"
+ plugin_key: "sk-..." # plugin's own auth credential
+```
+
+`plugin_key` is injected as `Authorization: Bearer ` on every
+request proxied through `/plugin-proxy/my-plugin/*`. The caller's litellm
+credential is stripped before forwarding so the plugin never receives a live
+litellm API key.
+
+### 2. Implement two endpoints on your service
+
+| Endpoint | Method | Purpose |
+|---|---|---|
+| `GET /api/plugin-manifest` | public | Returns plugin metadata for the UI |
+| `POST /api/plugin-auth` | public | Decrypts the identity claim for seamless sign-in |
+
+#### `GET /api/plugin-manifest`
+
+```json
+{
+ "name": "my-plugin",
+ "display_name": "My Plugin",
+ "version": "1.0.0",
+ "nav_items": [
+ { "key": "home", "label": "Home", "icon": "HomeOutlined", "path": "/" },
+ { "key": "reports", "label": "Reports", "icon": "BarChartOutlined", "path": "/reports" }
+ ],
+ "capabilities": ["reports", "data"]
+}
+```
+
+#### `POST /api/plugin-auth`
+
+Receives `{ "session_claim": "" }`.
+
+The proxy never shares `LITELLM_SALT_KEY` with your plugin. Each plugin is
+provisioned with its own dedicated key, derived as
+`HMAC-SHA256(LITELLM_SALT_KEY, plugin_name)`. Compute it once on the proxy
+host and hand the result to your plugin as a secret (e.g. `PLUGIN_AUTH_KEY`):
+
+```bash
+python -c 'import base64,hmac,hashlib,os; \
+print(base64.urlsafe_b64encode(hmac.new(os.environ["LITELLM_SALT_KEY"].encode(), b"my-plugin", hashlib.sha256).digest()).decode())'
+```
+
+A compromised plugin holding only this scoped key cannot recover
+`LITELLM_SALT_KEY` or decrypt any other litellm secret.
+
+Decrypt and validate the claim with that key:
+
+```python
+import json, os, time
+from cryptography.fernet import Fernet
+
+_CLAIM_TTL_SECONDS = 30
+
+def plugin_auth(session_claim: str) -> dict:
+ cipher = Fernet(os.environ["PLUGIN_AUTH_KEY"].encode())
+ claim = json.loads(cipher.decrypt(session_claim.encode(), ttl=_CLAIM_TTL_SECONDS))
+ if claim.get("plugin") != "my-plugin":
+ raise ValueError("claim audience mismatch")
+ if int(claim.get("exp", 0)) < int(time.time()):
+ raise ValueError("claim expired")
+ return claim
+```
+
+The claim is `{ "plugin", "user_id", "user_role", "exp" }`; it carries no
+litellm bearer token. Establish the plugin's own session from `user_id` /
+`user_role` and authenticate API calls back to litellm through the
+`/plugin-proxy/my-plugin/*` reverse proxy, which injects `plugin_key` for you.
+
+---
+
+## How iframe auth works
+
+```
+litellm UI
+ ├─ GET /api/plugins/auth-token -> { session_claim }
+ └─ postMessage({ type:"litellm-auth", session_claim }, pluginOrigin)
+ │
+ ▼
+Plugin iframe browser
+ └─ POST /api/plugin-auth { session_claim }
+ │
+ ▼
+Plugin server
+ ├─ decrypt(session_claim, PLUGIN_AUTH_KEY) -> { user_id, user_role, exp }
+ └─ establish plugin session -> stored in sessionStorage
+```
+
+No litellm bearer token ever leaves the proxy; the claim only conveys the
+caller's identity and expires after 30 seconds. A postMessage intercept
+yields ciphertext that is useless without the plugin's scoped key.
+
+---
+
+## Proxy routes
+
+- `GET /api/plugins` — list registered plugins (`name`, `display_name`, `url`). `plugin_key` is **never** returned; it stays server-side. Requires an authenticated caller.
+- `GET /api/plugins/auth-token?plugin_name=` — short-lived encrypted identity claim for the named plugin. Requires `LITELLM_SALT_KEY` to be set (503 otherwise) and the plugin to be registered (404 otherwise).
+- `ANY /plugin-proxy/{name}/{path}` — authenticated reverse proxy to the plugin backend. Restricted to `proxy_admin`.
+
+---
+
+## Reverse proxy behaviour
+
+When an admin (or server-to-server caller) hits `/plugin-proxy//`, the proxy authenticates the caller locally, then rewrites the request before forwarding it to the plugin's `url`:
+
+- **Every litellm credential header is stripped** — `Authorization`, `x-api-key`, `API-Key`, `x-goog-api-key`, `Ocp-Apim-Subscription-Key`, `x-litellm-api-key`, any configured `litellm_key_header_name`, plus `Cookie`. The plugin can never be handed the caller's live litellm key.
+- **`plugin_key` is injected** as `Authorization: Bearer ` — the only credential the plugin receives.
+- **Caller identity is forwarded** as `x-litellm-user-id` and `x-litellm-user-role` so the plugin can run its own authorization. These are informational, not credentials.
+- **Responses are sandboxed** — `Content-Security-Policy: sandbox` and `X-Content-Type-Options: nosniff` are set so plugin-controlled bytes served from the litellm origin cannot execute against the dashboard.
+
+---
+
+## Security checklist
+
+- [ ] `LITELLM_SALT_KEY` is set on the proxy and never shared with the plugin
+- [ ] The plugin holds only its derived `HMAC(LITELLM_SALT_KEY, plugin_name)` key, provisioned as a dedicated secret
+- [ ] `plugin_key` is a dedicated credential scoped to the plugin (not your litellm master key)
+- [ ] Plugin's `POST /api/plugin-auth` enforces the claim's `plugin` audience and `exp` (30s TTL)
+- [ ] Plugin treats `x-litellm-user-id` / `x-litellm-user-role` as identity hints, not as proof of authentication
+- [ ] Plugin service URL uses HTTPS in production
diff --git a/enterprise/pyproject.toml b/enterprise/pyproject.toml
index d0432448433..b032942427c 100644
--- a/enterprise/pyproject.toml
+++ b/enterprise/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "litellm-enterprise"
-version = "0.1.42"
+version = "0.1.43"
description = "Package for LiteLLM Enterprise features"
readme = "README.md"
requires-python = ">=3.9"
@@ -26,7 +26,7 @@ required-version = ">=0.10.9"
module-root = ""
[tool.commitizen]
-version = "0.1.42"
+version = "0.1.43"
version_files = [
"pyproject.toml:^version",
"../pyproject.toml:litellm-enterprise==",
diff --git a/gateway/Dockerfile b/gateway/Dockerfile
index 19c8a10fdfe..716b2fa09d1 100644
--- a/gateway/Dockerfile
+++ b/gateway/Dockerfile
@@ -1,5 +1,5 @@
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
FROM $UV_IMAGE AS uvbin
diff --git a/litellm-rust/.gitignore b/litellm-rust/.gitignore
new file mode 100644
index 00000000000..b83d22266ac
--- /dev/null
+++ b/litellm-rust/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/litellm-rust/ADDING_A_PROVIDER.md b/litellm-rust/ADDING_A_PROVIDER.md
new file mode 100644
index 00000000000..2fa81798605
--- /dev/null
+++ b/litellm-rust/ADDING_A_PROVIDER.md
@@ -0,0 +1,9 @@
+# Adding a provider / route to litellm-rust
+
+Three layers, same for every route (see `ocr` and `realtime` as references):
+
+1. **Transform contract (pure)** — `crates/core/src//transformation.rs`: a `…ProviderConfig` trait (URL build + request/response transforms) + types in `types.rs`. No network, env, or auth.
+2. **Provider config (pure)** — `crates/providers/src///transformation.rs`: implement that trait as a `const __CONFIG`, mirroring the Python provider tree. Add parity unit tests.
+3. **HTTP / transport (the host)** — `crates/providers/src/.rs` (e.g. `ocr.rs`, `realtime.rs`): the callable fn (`run_ocr`, `realtime`). It resolves the key, builds the auth header, builds URL + transforms via the config, then does the network call. This is the only layer allowed to do I/O.
+
+**Calling:** the host invokes the route fn — the Python bridge calls `run_ocr`; the `ai-gateway` server calls `realtime`. Register new modules in `lib.rs` / `mod.rs`, then run `cargo fmt && cargo clippy --workspace -- -D warnings && cargo test --workspace`.
diff --git a/litellm-rust/CLAUDE.md b/litellm-rust/CLAUDE.md
new file mode 100644
index 00000000000..1d2987e0a1a
--- /dev/null
+++ b/litellm-rust/CLAUDE.md
@@ -0,0 +1,88 @@
+# CLAUDE.md
+
+This file defines the rules for Rust work in LiteLLM.
+
+## Core Boundary
+
+The `core` and `providers` crates describe work; hosts execute work.
+
+Route-level Rust structure mirrors LiteLLM's Python responsibilities:
+- `core/src//` owns the route contract, shared types, and provider
+ template traits. For OCR, this means `core/src/ocr`.
+- `providers/src///transformation.rs` owns the
+ provider-specific transform. For Mistral OCR, this means
+ `providers/src/mistral/ocr/transformation.rs`.
+- Future network execution belongs in a host/transport layer such as
+ `llm_http_handler`, not inside `core` or `providers`.
+
+Allowed in `core` and `providers`:
+- Pure request transforms
+- Pure response transforms
+- Pure stream chunk normalization
+- Shared data types and validation errors
+- Deterministic token/cost helper logic
+
+Not allowed in `core` or `providers`:
+- Network calls
+- Environment variable or secret reads
+- Filesystem access
+- Database or cache access
+- Provider SDK signing or auth flows
+- Logging callbacks, spend writes, or custom callbacks
+- Global mutable runtime state
+
+Python owns rollout state and fallback while Rust is being introduced. Rust
+paths must be off by default until parity tests prove equivalence with Python.
+
+## Production Bar
+
+Rust code in this workspace is held to a strict parity and robustness bar from
+the first PR:
+
+- Correctness parity is proven with tests. Do not rely on README claims or
+ manual inspection for a port that mirrors Python behavior.
+- Every provider transform must have unit tests for supported-parameter
+ filtering, request body shape, response normalization, missing/null fields,
+ and bad-input errors.
+- When Rust is exposed through Python, add Python tests that prove disabled,
+ enabled, and unavailable-bridge fallback behavior.
+- Avoid panics on user/provider input. Return typed errors and let the host map
+ them to Python exceptions or HTTP responses.
+- OCR handles documents that often contain personal data. Do not log document
+ contents, base64 payloads, provider response bodies, or secrets.
+- Error messages must be useful but data-minimized. Truncate or sanitize any
+ upstream body before it crosses a host boundary.
+- Treat empty or whitespace-only credentials, URLs, and config values as absent
+ at the host/config resolution layer.
+- Preserve Python output shape intentionally. If a field is always serialized as
+ `null` for Python parity, leave a short comment explaining that parity choice.
+
+## Host I/O Rules
+
+These rules apply when adding future crates or modules that execute network I/O,
+such as `ai-gateway`, router hosts, or standalone servers:
+
+- Set connect and full-request timeouts. No unbounded waits.
+- Reuse HTTP clients; do not construct clients per request.
+- Prefer rustls TLS for portable Python wheels and Linux images unless there is
+ a documented reason not to.
+- Add request IDs and structured tracing at the host layer, without logging OCR
+ document contents or secrets.
+- Do not echo raw upstream response bodies to callers. Sanitize and bound them.
+- Avoid `expect`/`unwrap` in server startup and request paths unless the panic is
+ impossible by construction and documented.
+
+## Checks
+
+Run these before pushing Rust changes. The same checks run in GitHub Actions
+for changes under `litellm-rust/`.
+
+```bash
+cd litellm-rust
+cargo fmt --check
+cargo clippy --workspace --all-targets -- -D warnings
+cargo test --workspace
+```
+
+When a Rust path is exposed through Python, add Python parity tests that compare
+the existing Python output with the Rust-backed output.
diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock
new file mode 100644
index 00000000000..a269a224d97
--- /dev/null
+++ b/litellm-rust/Cargo.lock
@@ -0,0 +1,1872 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "base64",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sha1",
+ "sync_wrapper",
+ "tokio",
+ "tokio-tungstenite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+
+[[package]]
+name = "cc"
+version = "1.2.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indoc"
+version = "2.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "litellm-ai-gateway"
+version = "0.1.0"
+dependencies = [
+ "axum",
+ "futures-util",
+ "litellm-core",
+ "litellm-providers",
+ "pyo3",
+ "serde",
+ "serde_json",
+ "subtle",
+ "tokio",
+]
+
+[[package]]
+name = "litellm-core"
+version = "0.1.0"
+dependencies = [
+ "rand 0.8.6",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "litellm-providers"
+version = "0.1.0"
+dependencies = [
+ "futures-channel",
+ "futures-util",
+ "litellm-core",
+ "reqwest",
+ "serde_json",
+ "tokio",
+ "tokio-tungstenite",
+]
+
+[[package]]
+name = "litellm-python-bridge"
+version = "0.1.0"
+dependencies = [
+ "litellm-core",
+ "litellm-providers",
+ "pyo3",
+ "serde_json",
+]
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "memoffset",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
+dependencies = [
+ "once_cell",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "pyo3-build-config",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.4",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustls"
+version = "0.23.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tungstenite",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.8.6",
+ "rustls",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror 1.0.69",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unindent"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/litellm-rust/Cargo.toml b/litellm-rust/Cargo.toml
new file mode 100644
index 00000000000..06289e5a46f
--- /dev/null
+++ b/litellm-rust/Cargo.toml
@@ -0,0 +1,28 @@
+[workspace]
+members = [
+ "crates/core",
+ "crates/providers",
+ "crates/python-bridge",
+ "crates/ai-gateway",
+]
+resolver = "2"
+
+[workspace.package]
+edition = "2021"
+license = "MIT"
+repository = "https://github.com/BerriAI/litellm"
+
+[workspace.dependencies]
+litellm-core = { path = "crates/core" }
+litellm-providers = { path = "crates/providers" }
+axum = "0.7"
+pyo3 = "0.23.5"
+rand = "0.8"
+reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+subtle = "2"
+thiserror = "2.0"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
+tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "rustls-tls-native-roots"] }
+futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
diff --git a/litellm-rust/README.md b/litellm-rust/README.md
new file mode 100644
index 00000000000..15ad1855420
--- /dev/null
+++ b/litellm-rust/README.md
@@ -0,0 +1,34 @@
+# LiteLLM Rust
+
+This workspace contains the staged Rust implementation for LiteLLM.
+
+Rust starts as a pure transform core used by the existing Python host. Python
+continues to own auth, configuration, network I/O, retries, routing, logging,
+callbacks, spend tracking, and customer plugins until each Rust path has parity
+coverage and production evidence.
+
+## Layout
+
+```text
+crates/
+ core/ Route contracts, shared pure types, errors, and templates.
+ src/ocr/
+ providers/ Provider-specific pure transforms.
+ src/mistral/ocr/transformation.rs
+ python-bridge/ PyO3 bridge for Python LiteLLM.
+```
+
+The folder shape should follow the Python provider tree:
+`providers/src///transformation.rs`. The bridge should expose
+one function per top-level route, starting with `ocr(payload)`.
+
+## Checks
+
+Run these before pushing Rust changes. GitHub Actions runs the same checks for
+changes under `litellm-rust/`.
+
+```bash
+cargo fmt --check
+cargo clippy --workspace --all-targets -- -D warnings
+cargo test --workspace
+```
diff --git a/litellm-rust/crates/ai-gateway/AGENTS.md b/litellm-rust/crates/ai-gateway/AGENTS.md
new file mode 100644
index 00000000000..d9e6e1adde5
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/AGENTS.md
@@ -0,0 +1,50 @@
+# ai-gateway — folder architecture
+
+The Axum server that fronts the Rust gateway. It owns transport + config + auth
+only; deployment selection lives in `core::router`, transforms in `core`/`providers`.
+
+```
+src/
+ main.rs # entrypoint: build AppState (router + master key), bind, serve
+ state.rs # AppState — shared Arc + master_key
+ gil.rs # GIL-activity tracker (records Python acquisitions)
+ auth/ # authentication as an axum extractor — added to handler args
+ mod.rs # RequireMasterKey: FromRequestParts, single master key (LITELLM_MASTER_KEY)
+ routes/ # one module per route, all matching the same template
+ AGENTS.md # ← the route template (read this before adding a route)
+ mod.rs # app(): merges every module's router()
+ health.rs # simple route (one file): router() + liveness/readiness
+ gil.rs # simple route (one file): router() + GET /health/gil
+ realtime/ # route with logic → axum surface + a no-axum service:
+ mod.rs # router() + handler + WS<->events adapter (the axum surface)
+ service.rs # business logic (select deployment, call provider) — no axum, testable
+ python/ # Python interop (feature: python-config) — load-time only
+ mod.rs, config.rs, AGENTS.md
+```
+
+## Rules
+
+- **Routes follow one template.** Each route module exposes
+ `pub fn router() -> Router`; `routes/mod.rs` only merges them. Simple
+ routes are one file; non-trivial routes are a folder (`handler`/`service`/
+ `transport`). See `routes/AGENTS.md`.
+- **Auth is an extractor.** Add `crate::auth::RequireMasterKey` to a handler's
+ args; it runs during extraction. Never re-implement the check per route.
+- **Handlers are thin.** A handler validates and delegates to its `service`. No
+ business logic, no provider calls, no transforms in handlers.
+- **State is shared and cheap to clone.** Long-lived handles live behind `Arc` in
+ `state.rs`; read env/config only in `main.rs` when building state.
+
+## Auth (interim)
+
+A single **master key** (`LITELLM_MASTER_KEY`), enforced by the
+`auth::RequireMasterKey` extractor: any caller presenting it as
+`Authorization: Bearer ` may invoke the gateway. Fails closed (500) when
+unset; constant-time compare. The server binds `127.0.0.1` by default (`HOST` to
+override). Full per-key auth + budgets/rate-limits are delegated to the Python
+proxy in a later phase. Health routes don't add the extractor (unauthenticated).
+
+## Python interop
+
+Anything that calls into Python lives in `python/` and is **load-time only** — see
+`python/AGENTS.md`. The realtime data path never takes the GIL.
diff --git a/litellm-rust/crates/ai-gateway/Cargo.toml b/litellm-rust/crates/ai-gateway/Cargo.toml
new file mode 100644
index 00000000000..79bdc4bdb26
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "litellm-ai-gateway"
+version = "0.1.0"
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[[bin]]
+name = "litellm-ai-gateway"
+path = "src/main.rs"
+
+[dependencies]
+litellm-core.workspace = true
+litellm-providers.workspace = true
+axum = { workspace = true, features = ["ws"] }
+futures-util.workspace = true
+tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "time"] }
+serde.workspace = true
+serde_json.workspace = true
+subtle.workspace = true
+pyo3 = { workspace = true, features = ["auto-initialize"], optional = true }
+
+[features]
+# Build the gateway's config from the proxy YAML via an embedded Python
+# interpreter (links libpython; requires `litellm` importable at runtime).
+python-config = ["dep:pyo3"]
diff --git a/litellm-rust/crates/ai-gateway/Dockerfile b/litellm-rust/crates/ai-gateway/Dockerfile
new file mode 100644
index 00000000000..adf6fca0741
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/Dockerfile
@@ -0,0 +1,86 @@
+# Multi-stage build for the LiteLLM Rust AI Gateway (realtime WebSocket proxy).
+#
+# Build context is the **repo root** so we can install `litellm` from this repo's
+# source (the gateway loads its model_list via litellm.proxy.read_model_list,
+# which is not in any PyPI release yet) AND build the rust workspace under
+# litellm-rust/.
+#
+# docker build -f litellm-rust/crates/ai-gateway/Dockerfile -t litellm-ai-gateway .
+#
+# No secrets live in this file. Runtime config (LITELLM_MASTER_KEY,
+# OPENAI_API_KEY referenced by config.yaml, etc.) is injected as environment
+# variables at deploy time.
+
+# ---- Chef -------------------------------------------------------------------
+# cargo-chef caches the dependency build so only the gateway crate recompiles on
+# a source-only change. python3-dev is present in every rust stage because the
+# `python-config` feature links libpython via pyo3 (even in the cook step).
+FROM rust:1.90-slim-bookworm AS chef
+ENV PYO3_PYTHON=python3.11
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ python3 python3-dev pkg-config libssl-dev clang \
+ && rm -rf /var/lib/apt/lists/* \
+ && cargo install cargo-chef --locked --version 0.1.77
+WORKDIR /build/litellm-rust
+
+# ---- Planner ----------------------------------------------------------------
+# Produce the dependency recipe from the rust workspace manifests + Cargo.lock.
+FROM chef AS planner
+COPY litellm-rust/ .
+RUN cargo chef prepare --recipe-path recipe.json
+
+# ---- Builder ----------------------------------------------------------------
+FROM chef AS builder
+# Cook (compile) just the dependencies first — this layer is cached and reused
+# whenever only gateway source changes.
+COPY --from=planner /build/litellm-rust/recipe.json recipe.json
+RUN cargo chef cook --locked --release \
+ -p litellm-ai-gateway --features python-config \
+ --recipe-path recipe.json
+# Now copy the real sources and build the gateway binary. Deps are already cooked
+# above, so this step only recompiles the gateway crate.
+COPY litellm-rust/ .
+RUN cargo build --locked --release -p litellm-ai-gateway --features python-config
+
+# ---- Runtime ----------------------------------------------------------------
+# python:3.11-slim-bookworm ships libpython3.11, matching the builder's PyO3
+# 3.11 ABI so the embedded interpreter links and imports cleanly.
+FROM python:3.11-slim-bookworm AS runtime
+
+# CA certificates for outbound TLS to the OpenAI realtime endpoint.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Install litellm (with proxy extras) FROM THIS REPO'S SOURCE so
+# `import litellm.proxy.read_model_list` works — it is not on PyPI yet. Copy the
+# package + packaging metadata, then pip install the proxy extra.
+COPY pyproject.toml README.md LICENSE ./
+COPY litellm/ ./litellm/
+RUN pip install --no-cache-dir ".[proxy]"
+
+# The compiled gateway binary (pure-Rust realtime hot path; Python is load-time
+# only).
+COPY --from=builder /build/litellm-rust/target/release/litellm-ai-gateway /usr/local/bin/litellm-ai-gateway
+
+# Default config.yaml. A real deploy can override this (e.g. mount a Render
+# secret file at the same path) — never bake secrets into the image.
+COPY litellm-rust/crates/ai-gateway/config.yaml /app/config.yaml
+
+# Bind to all interfaces (Render routes to 0.0.0.0:$PORT) and load the model_list
+# from config.yaml via the embedded python config reader.
+ENV HOST=0.0.0.0 \
+ LITELLM_CONFIG_PATH=/app/config.yaml
+
+# Drop to a non-root user. The realtime hot path needs no root privileges, so
+# running unprivileged limits blast radius if the process is ever compromised.
+# The binary in /usr/local/bin is world-executable (COPY default mode 755); we
+# only need /app (and the config.yaml it reads) owned by the unprivileged user.
+RUN useradd --system --no-create-home --uid 10001 appuser \
+ && chown -R appuser:appuser /app
+USER appuser
+
+ENTRYPOINT ["/usr/local/bin/litellm-ai-gateway"]
diff --git a/litellm-rust/crates/ai-gateway/Dockerfile.dockerignore b/litellm-rust/crates/ai-gateway/Dockerfile.dockerignore
new file mode 100644
index 00000000000..030ee6a37c5
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/Dockerfile.dockerignore
@@ -0,0 +1,45 @@
+# Dockerfile-specific ignore-file for the Rust AI Gateway build.
+#
+# The build context is the repo root (so the image can pip install litellm from
+# source AND build the rust workspace). BuildKit honors `.dockerignore`
+# next to the Dockerfile and it takes precedence over the repo-root `.dockerignore`,
+# so this file shrinks the (large) repo-root context for THIS build only without
+# touching the root `.dockerignore` used by the main litellm images.
+#
+# Strategy: ignore everything, then re-include only what the build needs:
+# - litellm/ (pip install . needs the full package + proxy reader)
+# - litellm-rust/ (the rust workspace; Cargo.lock + crate sources)
+# - pyproject.toml / README.md / LICENSE (packaging metadata for pip install)
+*
+
+# --- re-include the build inputs ---
+!litellm/
+!litellm-rust/
+!pyproject.toml
+!README.md
+!LICENSE
+
+# --- prune heavy / irrelevant subpaths back out of the re-included trees ---
+# Rust build artifacts (huge; regenerated in the builder).
+**/target/
+# Python caches and compiled bytecode.
+**/__pycache__/
+**/*.pyc
+**/*.pyo
+**/.pytest_cache/
+**/.ruff_cache/
+**/.mypy_cache/
+# Node / UI build output bundled under the python package (not needed to import
+# litellm.proxy.read_model_list).
+**/node_modules/
+litellm/proxy/_experimental/out/
+# Tests, logs, and local scratch.
+**/tests/
+**/test/
+*.log
+log.txt
+*.tgz
+# VCS / editor / CI metadata that may live under re-included trees.
+**/.git/
+.git/
+**/.DS_Store
diff --git a/litellm-rust/crates/ai-gateway/README.md b/litellm-rust/crates/ai-gateway/README.md
new file mode 100644
index 00000000000..3662ce2584a
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/README.md
@@ -0,0 +1,172 @@
+# LiteLLM Rust AI Gateway
+
+A minimal Axum service that fronts OpenAI's realtime API. Clients open a
+WebSocket to `GET /v1/realtime`; the gateway authenticates, selects a deployment,
+dials OpenAI upstream, and splices the two sockets frame-by-frame.
+
+- **Client endpoint:** `wss:///v1/realtime?model=` (WebSocket)
+- **Auth:** `Authorization: Bearer $LITELLM_MASTER_KEY` (fails closed if unset)
+- **Health:** `GET /health/readiness`, `GET /health/liveness`, `GET /health/gil`
+
+> **Realtime serving is pure Rust.** Python is used at **load time only** — to
+> read the config once at boot. The realtime hot path never touches Python.
+
+## Configuration (config.yaml)
+
+The gateway loads its `model_list` from a **config.yaml**, the same as the
+LiteLLM proxy. Point `LITELLM_CONFIG_PATH` at the file:
+
+```yaml
+# config.yaml
+model_list:
+ - model_name: gpt-realtime
+ litellm_params:
+ model: openai/gpt-realtime
+ api_key: os.environ/OPENAI_API_KEY
+```
+
+```bash
+LITELLM_CONFIG_PATH=./config.yaml ./litellm-ai-gateway
+```
+
+At boot the gateway calls into `litellm.proxy.read_model_list`, which reuses the
+**real proxy config reader** (`ProxyConfig.get_config`). That means everything
+the proxy supports in config.yaml works here too:
+
+- `include:` to merge in other config files,
+- `os.environ/VAR` secret references (resolved via the secret manager, never
+ inlined),
+- DB-stored models (when a database is configured).
+
+Secrets stay out of the config — reference them with `os.environ/...` and set
+the env var at deploy time. The shipped Docker image is built with the
+`python-config` feature and **bundles litellm**, so config loading works out of
+the box; the default baked config lives at `/app/config.yaml` and can be
+overridden at deploy time (e.g. a Render secret file mounted at the same path).
+
+### Environment variables
+
+| Var | Required | Default | Purpose |
+|---|---|---|---|
+| `LITELLM_CONFIG_PATH` | yes (config mode) | — | Path to the config.yaml the gateway loads its `model_list` from. The Docker image defaults this to `/app/config.yaml`. |
+| `LITELLM_MASTER_KEY` | yes | — | Bearer token clients must send. Unset ⇒ all `/v1/realtime` requests are rejected (fail closed). |
+| `OPENAI_API_KEY` | yes | — | Upstream OpenAI key. Referenced by config.yaml as `os.environ/OPENAI_API_KEY` for the gateway→OpenAI dial. |
+| `HOST` | no | `127.0.0.1` | **Set to `0.0.0.0` in any container/deploy** or external traffic is refused. |
+| `PORT` | no | `4001` | Listen port. Render and most PaaS inject this automatically. |
+
+> Secrets (`LITELLM_MASTER_KEY`, `OPENAI_API_KEY`) are never baked into the image
+> or `render.yaml` — inject them at deploy time only.
+
+### Lean env stand-in (fallback)
+
+If the binary is built **without** `python-config` (default features), or
+`LITELLM_CONFIG_PATH` is unset, the gateway falls back to a single-deployment
+stand-in built from the environment:
+
+| Var | Default | Purpose |
+|---|---|---|
+| `OPENAI_REALTIME_MODEL` | `gpt-realtime` | The single deployment's model name (also the `?model=` clients pass). |
+
+This mode links no libpython and needs no config file, but it only supports one
+hard-coded OpenAI deployment. **config.yaml is the recommended path** — use the
+stand-in only for the leanest possible build.
+
+## Build & run with Docker
+
+The image is built `--features python-config` and installs litellm **from this
+repo's source** (the config reader is newer than any PyPI release), so the build
+**context is the repo root**:
+
+```bash
+# from the repo root
+docker build -f litellm-rust/crates/ai-gateway/Dockerfile -t litellm-ai-gateway .
+
+docker run --rm -p 4001:4001 \
+ -e HOST=0.0.0.0 -e PORT=4001 \
+ -e LITELLM_MASTER_KEY=sk-local \
+ -e OPENAI_API_KEY=$OPENAI_API_KEY \
+ litellm-ai-gateway # LITELLM_CONFIG_PATH defaults to /app/config.yaml
+
+# smoke test
+curl -s -o /dev/null -w '%{http_code}\n' localhost:4001/health/readiness # -> 200
+curl -s -o /dev/null -w '%{http_code}\n' localhost:4001/v1/realtime # -> 401 (auth fails closed)
+```
+
+On boot you should see `loaded model_list from /app/config.yaml via python
+config reader` — that confirms the config path (not the env stand-in fallback).
+To use your own config, mount it over the default:
+
+```bash
+docker run --rm -p 4001:4001 \
+ -e HOST=0.0.0.0 -e LITELLM_MASTER_KEY=sk-local -e OPENAI_API_KEY=$OPENAI_API_KEY \
+ -v $(pwd)/my-config.yaml:/app/config.yaml:ro \
+ litellm-ai-gateway
+```
+
+### Cargo-only (no Docker)
+
+```bash
+# config.yaml mode — needs litellm importable in the active python env
+LITELLM_CONFIG_PATH=./crates/ai-gateway/config.yaml \
+ cargo run --release -p litellm-ai-gateway --features python-config
+
+# env stand-in mode — no python, no config
+cargo run --release -p litellm-ai-gateway
+```
+
+## Deploy on Render
+
+The service is a Docker **web service**; Render terminates TLS and supports
+WebSockets, so the public endpoint is `wss://.onrender.com/v1/realtime`.
+
+### Option A — Blueprint (`render.yaml`)
+
+`crates/ai-gateway/render.yaml` describes the service (Docker runtime,
+`healthCheckPath: /health/readiness`, repo-root `dockerContext: .`,
+`dockerfilePath: ./litellm-rust/crates/ai-gateway/Dockerfile`,
+`LITELLM_CONFIG_PATH: /app/config.yaml`). `LITELLM_MASTER_KEY` and
+`OPENAI_API_KEY` are `sync: false` — set them in the dashboard after the first
+deploy. To use a non-default model_list, mount a **Render Secret File** at
+`/app/config.yaml`. Point a Render Blueprint at this repo/branch and apply.
+
+### Option B — Render API
+
+```bash
+# create a Docker web service from this repo+branch, then set env vars:
+curl -X POST https://api.render.com/v1/services \
+ -H "Authorization: Bearer $RENDER_API_KEY" -H "Content-Type: application/json" \
+ -d '{
+ "type": "web_service", "name": "litellm-rust-ai-gateway",
+ "ownerId": "", "repo": "https://github.com/BerriAI/litellm",
+ "branch": "",
+ "serviceDetails": {
+ "env": "docker",
+ "envSpecificDetails": {
+ "dockerfilePath": "./litellm-rust/crates/ai-gateway/Dockerfile",
+ "dockerContext": "."
+ },
+ "healthCheckPath": "/health/readiness"
+ }
+ }'
+# then set env vars LITELLM_MASTER_KEY, OPENAI_API_KEY, HOST=0.0.0.0,
+# LITELLM_CONFIG_PATH=/app/config.yaml
+```
+
+Health check path **must** be `/health/readiness`. `autoDeploy` is off by default
+in the blueprint — trigger deploys manually (or flip it on) to pick up new commits.
+
+## Scaling
+
+Concurrency is what matters, not total connections: each in-flight session holds
+one client socket + one upstream socket. To scale, raise the instance count /
+enable autoscaling on the Render service (e.g. baseline 10, max 100). Each
+instance needs file descriptors for `2 × peak_concurrent_sessions` — raise
+`ulimit -n` if you push very high concurrency.
+
+## Latency note
+
+The gateway adds the cost of one extra hop: client→gateway, then a fresh
+gateway→OpenAI realtime handshake (TLS + WS upgrade + `session.created`). In
+benchmarks this is ~100–150 ms of added session-establishment time; first-audio
+and steady-state streaming add no measurable overhead. To minimize it, deploy the
+gateway in the Render region with the lowest RTT to OpenAI's realtime endpoint.
diff --git a/litellm-rust/crates/ai-gateway/benchmarks/realtime/README.md b/litellm-rust/crates/ai-gateway/benchmarks/realtime/README.md
new file mode 100644
index 00000000000..84e926af243
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/benchmarks/realtime/README.md
@@ -0,0 +1,55 @@
+# Realtime gateway benchmark — pool on/off
+
+Measures what the gateway adds over talking to OpenAI's realtime WebSocket
+directly, and what the pre-warmed connection pool removes. See
+`../../src/routes/realtime/README.md` for how the pool works.
+
+## Results
+
+5000 calls / 500 concurrency, gateway at 10 instances, pool ON
+(`REALTIME_POOL_SIZE=64`), upstream OpenAI `gpt-realtime`. Each leg run twice.
+Times in **ms**. Phases per connection: **dial** = TCP+TLS+WS upgrade,
+**session** = upgrade → `session.created` (the phase the pool removes),
+**1st-audio** = `response.create` → first audio delta (OpenAI inference),
+**total** = full wall-clock.
+
+| metric | Direct OpenAI | Gateway (pool ON) | Overhead (ms) | vs OpenAI |
+| ------------------ | ------------- | ----------------- | ------------- | ---------- |
+| success rate (%) | 99.8 | 99.8 | — | — |
+| dial p50 (ms) | 276 | 158 | −118 | **faster** |
+| session p50 (ms) | 7 | 0 | −7 | **faster** |
+| 1st-audio p50 (ms) | 440 | 664 | +224 | slower¹ |
+| total p50 (ms) | 816 | 1010 | +194 | slower¹ |
+| total p95 (ms) | 2152 | 1970 | −182 | **faster** |
+| total p99 (ms) | 2692 | 2610 | −82 | **faster** |
+
+The gateway is **faster than direct on 4 of 6 metrics**. The warm pool makes the
+**session phase sub-millisecond** at the median — ~76% of connects hit the pool,
+~70% had session < 1 ms. ¹ The two "slower" rows are not gateway overhead:
+`1st-audio` is OpenAI's own inference time (the gateway only relays it), which ran
+slower during the gateway legs and drags `total p50` with it.
+
+**Pool OFF** (control, `REALTIME_POOL_SIZE=0`): session p50 was **367 ms** — the
+fresh-dial overhead the pool removes.
+
+## Reproduce
+
+The load generator lives in a separate repo:
+**https://github.com/ishaan-berri/litellm-realtime-bench**
+
+```bash
+git clone https://github.com/ishaan-berri/litellm-realtime-bench
+cd litellm-realtime-bench && go build -o wsbench .
+
+# Direct to OpenAI (baseline)
+./wsbench -host api.openai.com -key "$OPENAI_API_KEY" -m gpt-realtime -n 5000 -c 500 -t 60
+
+# Through the gateway — run once with pool ON, once with REALTIME_POOL_SIZE=0
+./wsbench -host -key "$LITELLM_MASTER_KEY" -m gpt-realtime -n 5000 -c 500 -t 60
+```
+
+Run the gateway with the env stand-in (`OPENAI_REALTIME_MODEL=gpt-realtime`,
+`OPENAI_API_KEY`, `LITELLM_MASTER_KEY`, `REALTIME_POOL_SIZE`, `HOST=0.0.0.0`). At
+500 concurrency over N instances, size the pool to `≈ 500 / N` per instance (64 was
+used here for 10 instances). The bench repo's README covers running 500-concurrency
+legs from a hosted multi-vCPU runner. **Never commit keys — pass them via `-key`.**
diff --git a/litellm-rust/crates/ai-gateway/config.yaml b/litellm-rust/crates/ai-gateway/config.yaml
new file mode 100644
index 00000000000..ac598c220dd
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/config.yaml
@@ -0,0 +1,13 @@
+# Sample realtime config for the LiteLLM Rust AI Gateway.
+#
+# The gateway loads this model_list at boot via the embedded python config
+# reader (litellm.proxy.read_model_list), which reuses the proxy's own reader —
+# so include:, os.environ/ secrets, and DB-stored models all work here too.
+#
+# Secrets are referenced (never inlined) via os.environ/. A real deploy can
+# override this file (e.g. mount a Render secret file at LITELLM_CONFIG_PATH).
+model_list:
+ - model_name: gpt-realtime
+ litellm_params:
+ model: openai/gpt-realtime
+ api_key: os.environ/OPENAI_API_KEY
diff --git a/litellm-rust/crates/ai-gateway/render.yaml b/litellm-rust/crates/ai-gateway/render.yaml
new file mode 100644
index 00000000000..4170849f65d
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/render.yaml
@@ -0,0 +1,35 @@
+# Render blueprint for the LiteLLM Rust AI Gateway (realtime WebSocket proxy).
+#
+# Single instance for now (no autoscaling). The public endpoint is a
+# WebSocket served over TLS: wss://.onrender.com/v1/realtime
+#
+# Paths are relative to the **repo root** (Render's convention). The build
+# context is the repo root so the image can install litellm from source — the
+# gateway loads its model_list via litellm.proxy.read_model_list at boot.
+#
+# Secrets (LITELLM_MASTER_KEY, OPENAI_API_KEY) are marked sync: false — set
+# them in the Render dashboard or via the API, never inline here.
+services:
+ - type: web
+ name: litellm-rust-ai-gateway
+ runtime: docker
+ plan: standard
+ dockerfilePath: ./litellm-rust/crates/ai-gateway/Dockerfile
+ dockerContext: .
+ healthCheckPath: /health/readiness
+ numInstances: 1
+ envVars:
+ # The gateway loads its model_list from this config.yaml via the embedded
+ # python config reader. The image bakes a default config at /app/config.yaml;
+ # a real deploy can override it by mounting a Render secret file at this
+ # same path (Dashboard → Environment → Secret Files) — never inline secrets.
+ - key: LITELLM_CONFIG_PATH
+ value: /app/config.yaml
+ - key: HOST
+ value: 0.0.0.0
+ # Bearer token clients must send on /v1/realtime (fail closed if unset).
+ - key: LITELLM_MASTER_KEY
+ sync: false
+ # Referenced by config.yaml as os.environ/OPENAI_API_KEY for the upstream dial.
+ - key: OPENAI_API_KEY
+ sync: false
diff --git a/litellm-rust/crates/ai-gateway/src/auth/mod.rs b/litellm-rust/crates/ai-gateway/src/auth/mod.rs
new file mode 100644
index 00000000000..e2dd51f656d
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/auth/mod.rs
@@ -0,0 +1,54 @@
+//! Gateway authentication, as an axum **extractor** (the idiomatic pattern —
+//! keeps handlers clean and auth testable).
+//!
+//! For now this is a single **master key**: any caller presenting it as
+//! `Authorization: Bearer ` may invoke the gateway. Per-key auth, budgets,
+//! and rate limits are delegated to the Python proxy in a later phase.
+//!
+//! A handler opts in by adding [`RequireMasterKey`] to its arguments; auth then
+//! runs during extraction, before the handler body. Routes never re-implement it.
+
+use axum::extract::FromRequestParts;
+use axum::http::header::AUTHORIZATION;
+use axum::http::request::Parts;
+use axum::http::StatusCode;
+use subtle::ConstantTimeEq;
+
+use crate::state::AppState;
+
+/// Extractor that requires the configured master key as a bearer token.
+///
+/// Rejections: `500` when no master key is configured (permanent
+/// misconfiguration, not a transient outage); `401` on a missing/incorrect
+/// token. The comparison is constant-time.
+pub struct RequireMasterKey;
+
+#[axum::async_trait]
+impl FromRequestParts for RequireMasterKey {
+ type Rejection = (StatusCode, String);
+
+ async fn from_request_parts(
+ parts: &mut Parts,
+ state: &AppState,
+ ) -> Result {
+ let Some(expected) = state.master_key.as_deref() else {
+ return Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "gateway auth not configured (set LITELLM_MASTER_KEY)".to_string(),
+ ));
+ };
+ let provided = parts
+ .headers
+ .get(AUTHORIZATION)
+ .and_then(|value| value.to_str().ok())
+ .and_then(|value| value.strip_prefix("Bearer "))
+ .map(str::trim);
+ match provided {
+ Some(token) if bool::from(token.as_bytes().ct_eq(expected.as_bytes())) => Ok(Self),
+ _ => Err((
+ StatusCode::UNAUTHORIZED,
+ "missing or invalid bearer token".to_string(),
+ )),
+ }
+ }
+}
diff --git a/litellm-rust/crates/ai-gateway/src/gil.rs b/litellm-rust/crates/ai-gateway/src/gil.rs
new file mode 100644
index 00000000000..c749f722c73
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/gil.rs
@@ -0,0 +1,58 @@
+//! GIL-activity tracking.
+//!
+//! Every acquisition of the Python GIL is recorded here so the `/health/gil`
+//! endpoint can report whether Python was touched recently. The design goal is
+//! that the GIL is acquired **only at load time** (config read) and never on the
+//! realtime hot path — polling this endpoint during traffic should show the
+//! count holding steady and `acquired_last_30s` falling to `false`.
+
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+/// Window (seconds) for the "recently acquired" signal.
+pub const RECENT_WINDOW_SECS: u64 = 30;
+
+static GIL_ACQUISITIONS: AtomicU64 = AtomicU64::new(0);
+/// Unix seconds of the last acquisition; `0` means "never".
+static LAST_GIL_UNIX_SECS: AtomicU64 = AtomicU64::new(0);
+
+fn now_unix_secs() -> u64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|d| d.as_secs())
+ .unwrap_or(0)
+}
+
+/// Record that the GIL was just acquired. Call immediately before taking the GIL.
+///
+/// Only invoked under the `python-config` feature; without it the gateway never
+/// touches Python, so the recorder is unused (and the endpoint reports zero).
+#[cfg_attr(not(feature = "python-config"), allow(dead_code))]
+pub fn record_acquisition() {
+ GIL_ACQUISITIONS.fetch_add(1, Ordering::Relaxed);
+ LAST_GIL_UNIX_SECS.store(now_unix_secs(), Ordering::Relaxed);
+}
+
+/// Point-in-time view of GIL activity.
+pub struct GilSnapshot {
+ pub total_acquisitions: u64,
+ pub seconds_since_last: Option,
+ pub acquired_last_30s: bool,
+}
+
+/// Read the current GIL-activity snapshot.
+pub fn snapshot() -> GilSnapshot {
+ let total = GIL_ACQUISITIONS.load(Ordering::Relaxed);
+ let last = LAST_GIL_UNIX_SECS.load(Ordering::Relaxed);
+ let seconds_since_last = if last == 0 {
+ None
+ } else {
+ Some(now_unix_secs().saturating_sub(last))
+ };
+ let acquired_last_30s = seconds_since_last.is_some_and(|secs| secs <= RECENT_WINDOW_SECS);
+ GilSnapshot {
+ total_acquisitions: total,
+ seconds_since_last,
+ acquired_last_30s,
+ }
+}
diff --git a/litellm-rust/crates/ai-gateway/src/main.rs b/litellm-rust/crates/ai-gateway/src/main.rs
new file mode 100644
index 00000000000..71e4a6836ad
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/main.rs
@@ -0,0 +1,152 @@
+//! LiteLLM AI Gateway — a minimal Axum server fronting the Rust router.
+//!
+//! Flow: client → `POST /v1/realtime` → `router.realtime()` selects a deployment
+//! (simple-shuffle) → `providers::realtime::realtime()` invokes OpenAI. The
+//! server owns transport + config; routing lives in the `router` crate.
+
+mod auth;
+mod gil;
+#[cfg(feature = "python-config")]
+mod python;
+mod routes;
+mod state;
+
+use std::sync::Arc;
+
+use litellm_core::router::{Deployment, LiteLLMParams, Router};
+use litellm_providers::realtime_pool::{upstream_key, PoolConfig, RealtimePool};
+
+use crate::state::AppState;
+
+/// Bind to localhost by default so the gateway is not a public, unauthenticated
+/// provider proxy out of the box. Override with `HOST` (e.g. `0.0.0.0`).
+const DEFAULT_HOST: &str = "127.0.0.1";
+const DEFAULT_PORT: u16 = 4001;
+
+#[tokio::main]
+async fn main() {
+ // Trim before storing so it matches the trimmed bearer token in `auth`
+ // (avoids a silent auth failure when the env var has surrounding whitespace).
+ let master_key: Option> = std::env::var("LITELLM_MASTER_KEY")
+ .ok()
+ .map(|key| key.trim().to_string())
+ .filter(|key| !key.is_empty())
+ .map(Arc::from);
+ if master_key.is_none() {
+ eprintln!(
+ "warning: LITELLM_MASTER_KEY is not set; /v1/realtime will reject all requests (fail closed)"
+ );
+ }
+
+ let router = Arc::new(build_router());
+
+ // Build the pre-warmed realtime pool and register each deployment's upstream
+ // so the background replenisher starts warming it. `REALTIME_POOL_SIZE=0`
+ // yields a disabled pool → every connect fresh-dials (original behavior).
+ let pool_config = PoolConfig::from_env();
+ let realtime_pool = RealtimePool::spawn(pool_config);
+ if pool_config.enabled() {
+ register_deployments(&router, &realtime_pool);
+ eprintln!(
+ "realtime connection pool enabled: target {} warm sockets/key, max idle {}s",
+ pool_config.target_size,
+ pool_config.max_idle.as_secs()
+ );
+ } else {
+ eprintln!(
+ "realtime connection pool disabled (REALTIME_POOL_SIZE=0); fresh-dialing each connect"
+ );
+ }
+
+ let state = AppState {
+ router,
+ master_key,
+ realtime_pool,
+ };
+
+ let host = std::env::var("HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string());
+ let port = resolve_port();
+
+ let listener = tokio::net::TcpListener::bind((host.as_str(), port))
+ .await
+ .expect("failed to bind listener");
+ eprintln!("litellm-ai-gateway listening on {host}:{port}");
+ axum::serve(listener, routes::app(state))
+ .await
+ .expect("server error");
+}
+
+/// Register every deployment's upstream key with the pool so the replenisher
+/// pre-warms it. Mirrors `service::run`'s key derivation (strip `openai/`, resolve
+/// api_key); deployments whose key can't be resolved are skipped (they fresh-dial
+/// and surface the auth error on the request path, as before).
+fn register_deployments(router: &Router, pool: &RealtimePool) {
+ for deployment in router.deployments() {
+ let params = &deployment.litellm_params;
+ let provider_model = params
+ .model
+ .strip_prefix("openai/")
+ .unwrap_or(¶ms.model);
+ if let Some(key) = upstream_key(
+ provider_model,
+ params.api_key.as_deref(),
+ params.api_base.as_deref(),
+ ) {
+ pool.register(key);
+ }
+ }
+}
+
+/// Resolve `PORT`, warning (rather than silently defaulting) on an invalid value.
+fn resolve_port() -> u16 {
+ match std::env::var("PORT") {
+ Ok(raw) => raw.parse().unwrap_or_else(|_| {
+ eprintln!("warning: PORT={raw:?} is not a valid port; using {DEFAULT_PORT}");
+ DEFAULT_PORT
+ }),
+ Err(_) => DEFAULT_PORT,
+ }
+}
+
+/// Build the router. With the `python-config` feature and `LITELLM_CONFIG_PATH`
+/// set, load the resolved `model_list` from the proxy config via the embedded
+/// Python reader (load time only). Otherwise fall back to the env stand-in.
+fn build_router() -> Router {
+ #[cfg(feature = "python-config")]
+ if let Ok(config_path) = std::env::var("LITELLM_CONFIG_PATH") {
+ match python::config::load_router_from_config(&config_path) {
+ Ok(router) => {
+ eprintln!("loaded model_list from {config_path} via python config reader");
+ return router;
+ }
+ Err(err) => {
+ eprintln!("config load failed ({err}); falling back to env deployment");
+ }
+ }
+ }
+ build_router_from_env()
+}
+
+/// Build a minimal single-deployment `model_list` from the environment.
+///
+/// A real deployment loads `model_list` from config; this is the minimal stand-in
+/// so the gateway has one OpenAI deployment to route to.
+fn build_router_from_env() -> Router {
+ let model =
+ std::env::var("OPENAI_REALTIME_MODEL").unwrap_or_else(|_| "gpt-realtime".to_string());
+ let api_key = std::env::var("OPENAI_API_KEY").ok();
+ if api_key.is_none() {
+ eprintln!(
+ "warning: OPENAI_API_KEY is not set; realtime requests will fail with auth errors"
+ );
+ }
+ let deployment = Deployment {
+ model_name: model.clone(),
+ litellm_params: LiteLLMParams {
+ model,
+ api_key,
+ api_base: None,
+ },
+ };
+ Router::new(vec![deployment])
+}
diff --git a/litellm-rust/crates/ai-gateway/src/python/AGENTS.md b/litellm-rust/crates/ai-gateway/src/python/AGENTS.md
new file mode 100644
index 00000000000..47aa117e0b9
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/python/AGENTS.md
@@ -0,0 +1,27 @@
+# ai-gateway/src/python — Python interop (load-time only)
+
+Functions here embed the Python interpreter (pyo3) and take the GIL to call into
+`litellm` (e.g. read the proxy `model_list`). Compiled only under the
+`python-config` feature.
+
+## Hard rule: non-hot-path functions only
+
+Everything in this folder MUST run **at most once per process lifetime — at
+startup / load time** (config read, warm-up). NEVER call into Python on the
+request path:
+
+- No GIL acquisition per request, per connection, or per realtime event.
+- No Python call inside a route handler, the router's hot path, or any loop that
+ scales with traffic.
+
+**Why:** the GIL serializes execution and would cap throughput; the realtime data
+path must stay pure Rust. Every acquisition is recorded by `crate::gil` — poll
+`GET /health/gil`, and `total_acquisitions` MUST stay flat under load.
+
+## How to add one
+
+Resolve whatever Python-derived data you need **once at boot** and hand the rest
+of the gateway an owned, plain-Rust value (e.g. build a `Router` from the
+resolved `model_list`). Record the acquisition via `crate::gil::record_acquisition()`
+immediately before taking the GIL. If a function would need to run per request,
+it does not belong here — move the work to Rust, or pre-resolve it at startup.
diff --git a/litellm-rust/crates/ai-gateway/src/python/config.rs b/litellm-rust/crates/ai-gateway/src/python/config.rs
new file mode 100644
index 00000000000..6ec9595469d
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/python/config.rs
@@ -0,0 +1,39 @@
+//! Build the router by calling the Python proxy config reader (load time only).
+//!
+//! Embeds the interpreter via pyo3 and calls
+//! `litellm.proxy.read_model_list.read_model_list`, which reuses the proxy's
+//! `os.environ/` + secret-manager resolution. The GIL is taken **once at boot**
+//! (and recorded in [`crate::gil`]); the realtime hot path never touches Python.
+//!
+//! Compiled only under the `python-config` feature.
+
+use litellm_core::error::CoreError;
+use litellm_core::router::{Deployment, Router};
+use litellm_core::CoreResult;
+use pyo3::prelude::*;
+
+use crate::gil;
+
+/// Load the router's `model_list` from `config_path` via the Python reader.
+pub fn load_router_from_config(config_path: &str) -> CoreResult {
+ gil::record_acquisition();
+ Python::with_gil(|py| {
+ let model_list = py
+ .import("litellm.proxy.read_model_list")
+ .and_then(|module| module.getattr("read_model_list"))
+ .and_then(|reader| reader.call1((config_path,)))
+ .map_err(|err| CoreError::Routing(format!("read_model_list failed: {err}")))?;
+
+ let model_list_json: String = py
+ .import("json")
+ .and_then(|json| json.getattr("dumps"))
+ .and_then(|dumps| dumps.call1((model_list,)))
+ .and_then(|encoded| encoded.extract())
+ .map_err(|err| CoreError::Routing(format!("serializing model_list failed: {err}")))?;
+
+ let deployments: Vec = serde_json::from_str(&model_list_json)
+ .map_err(|err| CoreError::Routing(format!("parsing model_list failed: {err}")))?;
+
+ Ok(Router::new(deployments))
+ })
+}
diff --git a/litellm-rust/crates/ai-gateway/src/python/mod.rs b/litellm-rust/crates/ai-gateway/src/python/mod.rs
new file mode 100644
index 00000000000..a677bade676
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/python/mod.rs
@@ -0,0 +1,4 @@
+//! Python interop for the gateway. See `AGENTS.md`: **load-time / non-hot-path
+//! only.** Compiled only under the `python-config` feature.
+
+pub mod config;
diff --git a/litellm-rust/crates/ai-gateway/src/routes/AGENTS.md b/litellm-rust/crates/ai-gateway/src/routes/AGENTS.md
new file mode 100644
index 00000000000..02c5f18c4f3
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/routes/AGENTS.md
@@ -0,0 +1,38 @@
+# routes/ — the route template
+
+Every route follows the **same shape** so the layout is predictable. The rule:
+
+> **Each route module exposes `pub fn router() -> Router`.**
+> `routes/mod.rs::app` merges them all and applies state once. Adding a route is:
+> create the module, then add one `.merge(::router())` line.
+
+## Default: one file
+A route is a single file containing `router()` + its handler(s) (handlers stay
+private). This is the norm — don't split until it hurts.
+```
+pub fn router() -> Router { Router::new().route(PATH, get(handle)) }
+async fn handle(...) -> impl IntoResponse { ... }
+```
+`health.rs` and `gil.rs` are examples.
+
+## Split out `service` when there's real logic
+When a route has business logic worth testing without axum, put it in a sibling
+`service` (a file, or a folder if the route grows). The route file stays the
+**axum surface** (router + handler + any socket/SSE adapter); `service` is plain
+Rust with **no axum types**. `realtime/` is the example:
+```
+realtime/
+ mod.rs # axum surface: router() + handler + the WS<->events adapter
+ service.rs # pure logic: select deployment + call provider (no axum) — testable
+```
+Split `service` further (or add `transport`, `repo`, …) only once a single file
+genuinely gets hard to read.
+
+## Invariants
+- **Auth is an extractor, not a manual call.** A handler requires auth by adding
+ `crate::auth::RequireMasterKey` to its arguments; it runs during extraction.
+ Never re-implement the check per route.
+- **Handlers contain no business logic; `service` contains no axum types.**
+- A route owns its paths in its own `router()`; `mod.rs` only merges.
+- Cross-cutting concerns (logging, CORS, timeouts) → Tower layers in `mod.rs`,
+ not duplicated in handlers.
diff --git a/litellm-rust/crates/ai-gateway/src/routes/gil.rs b/litellm-rust/crates/ai-gateway/src/routes/gil.rs
new file mode 100644
index 00000000000..0db0c6f0b14
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/routes/gil.rs
@@ -0,0 +1,30 @@
+//! `GET /health/gil` — poll to confirm Python is only touched at load time.
+//! Simple-route template: a `router()` plus its handler, in one file.
+
+use axum::routing::get;
+use axum::{Json, Router};
+use serde::Serialize;
+
+use crate::gil;
+use crate::state::AppState;
+
+/// This route's contribution to the app router.
+pub fn router() -> Router {
+ Router::new().route("/health/gil", get(status))
+}
+
+#[derive(Debug, Serialize)]
+struct GilStatusResponse {
+ gil_acquired_last_30s: bool,
+ total_acquisitions: u64,
+ seconds_since_last: Option,
+}
+
+async fn status() -> Json {
+ let snapshot = gil::snapshot();
+ Json(GilStatusResponse {
+ gil_acquired_last_30s: snapshot.acquired_last_30s,
+ total_acquisitions: snapshot.total_acquisitions,
+ seconds_since_last: snapshot.seconds_since_last,
+ })
+}
diff --git a/litellm-rust/crates/ai-gateway/src/routes/health.rs b/litellm-rust/crates/ai-gateway/src/routes/health.rs
new file mode 100644
index 00000000000..15c67fea325
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/routes/health.rs
@@ -0,0 +1,24 @@
+//! Health probes. Simple-route template: a `router()` plus its handlers, in one file.
+
+use axum::http::StatusCode;
+use axum::routing::get;
+use axum::Router;
+
+use crate::state::AppState;
+
+/// This route's contribution to the app router.
+pub fn router() -> Router {
+ Router::new()
+ .route("/health/liveness", get(liveness))
+ .route("/health/readiness", get(readiness))
+}
+
+/// The process is up.
+async fn liveness() -> StatusCode {
+ StatusCode::OK
+}
+
+/// The server is ready to accept traffic.
+async fn readiness() -> StatusCode {
+ StatusCode::OK
+}
diff --git a/litellm-rust/crates/ai-gateway/src/routes/mod.rs b/litellm-rust/crates/ai-gateway/src/routes/mod.rs
new file mode 100644
index 00000000000..c6b9573781a
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/routes/mod.rs
@@ -0,0 +1,23 @@
+//! HTTP routes.
+//!
+//! **Template:** every route module exposes `pub fn router() -> Router`
+//! that mounts its own paths; [`app`] merges them. A trivial route is a single
+//! file (`health.rs`, `gil.rs`); a non-trivial one is a folder (`realtime/`) with
+//! `handler` (entry) + `service` (logic) + `transport` (adapters). See AGENTS.md.
+
+pub mod gil;
+pub mod health;
+pub mod realtime;
+
+use axum::Router;
+
+use crate::state::AppState;
+
+/// Assemble the application router by merging every route module's `router()`.
+pub fn app(state: AppState) -> Router {
+ Router::new()
+ .merge(health::router())
+ .merge(gil::router())
+ .merge(realtime::router())
+ .with_state(state)
+}
diff --git a/litellm-rust/crates/ai-gateway/src/routes/realtime/README.md b/litellm-rust/crates/ai-gateway/src/routes/realtime/README.md
new file mode 100644
index 00000000000..3301576bb85
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/routes/realtime/README.md
@@ -0,0 +1,87 @@
+# Realtime route (`GET /v1/realtime`)
+
+Proxies OpenAI's realtime WebSocket. `mod.rs` is the axum surface (handler +
+socket↔events adapter); `service.rs` is the pure logic (select a deployment, then
+splice client ↔ upstream). The pool itself lives in
+`crates/providers/src/realtime_pool.rs`.
+
+## Connection pooling
+
+### The problem
+
+The gateway's realtime overhead lives **entirely in session establishment**. On each
+client connect it dials a *fresh* upstream WS to OpenAI and waits for
+`session.created` before it can serve. Measured at 5000 calls / 500 concurrency, the
+fresh-dial session phase is **~360 ms** vs **~7 ms** direct; dial, first-audio, and
+streaming add ~0. So the one lever is removing that per-connect handshake from the
+critical path.
+
+### The idea
+
+Keep a few upstream OpenAI sockets **already connected and already past
+`session.created`** (buffered). On a client connect, hand off a warm socket — relay
+its buffered `session.created` instantly (a local `Vec::pop`, sub-millisecond) and
+splice exactly as a fresh dial would. A background task keeps the pool topped up. On
+a miss or dead socket we fall back to fresh-dial: the pool is a latency optimization,
+never a correctness dependency.
+
+```
+ ┌───────────────────────────────────────┐
+ client connect ──────► │ routes/realtime → service::run │
+ │ pool.take(key) │
+ │ hit → relay buffered │
+ │ session.created, then splice │
+ │ miss → fresh dial (original path) │
+ └───────────────┬───────────────────────┘
+ │ replenish (async, concurrent)
+ ┌───────────────▼───────────────────────┐
+ background task ─────► │ RealtimePool: per-key warm sockets │
+ │ each = { ws, buffered session.created}│
+ │ liveness-checked before handoff │
+ └─────────────────────────────────────────┘
+```
+
+A warm session is indistinguishable from a fresh one: OpenAI sends `session.created`
+unprompted on connect, we pre-read exactly that one frame and relay it on handoff,
+and we send nothing else on the socket before a client exists — so the client's first
+`session.update` behaves identically either way.
+
+### Sizing
+
+Each warm socket serves **exactly one** session (realtime isn't multiplexed), so the
+pool is sized to the **peak concurrent connects per instance**, not total live
+connections:
+
+```
+REALTIME_POOL_SIZE ≈ peak_concurrency / instance_count
+```
+
+e.g. 500 concurrency over 10 instances → ~50–64 per instance. The replenisher dials
+the missing sockets **concurrently**, so a drained pool refills in ~one handshake
+window and keeps supply close to the connect rate. Over-provisioning just burns idle
+upstream sockets, which is why warm sockets are short-lived
+(`REALTIME_POOL_MAX_IDLE_SECS`).
+
+### Config
+
+| env | default | meaning |
+| ----------------------------- | ------- | --------------------------------------------------------------- |
+| `REALTIME_POOL_SIZE` | `4` | target warm sockets per key. `0` disables pooling (fresh-dial). |
+| `REALTIME_POOL_MAX_IDLE_SECS` | `30` | max time a warm socket sits before it's closed and replaced. |
+
+### Notes
+
+- **Miss / dead socket → fresh dial.** Burst beyond warm supply, or a socket that
+ died, never blocks or fails — it falls back to the original path. The pool can only
+ make a connect faster, never slower or more fragile.
+- **Auth scope.** The pool key includes `api_key`, so a warm socket is only handed to
+ a request resolving to the same key — no cross-tenant reuse.
+- **Idle billing.** Warm sockets are liveness-checked at handoff and capped at
+ `REALTIME_POOL_MAX_IDLE_SECS` to bound idle billing and dodge OpenAI's idle timeout.
+- **Replenish backoff.** If a key's warm-up dials all fail (invalid credentials, an
+ unreachable upstream), the replenisher puts that key into exponential backoff
+ (500 ms → 30 s cap) instead of re-dialing it every tick. This bounds connection
+ attempts against a broken key so it can't exhaust upstream rate limits and degrade
+ valid cold-path traffic; the backoff resets the moment a dial succeeds.
+
+Benchmarks and repro: `../../benchmarks/realtime/README.md`.
diff --git a/litellm-rust/crates/ai-gateway/src/routes/realtime/mod.rs b/litellm-rust/crates/ai-gateway/src/routes/realtime/mod.rs
new file mode 100644
index 00000000000..695e0c6bb39
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/routes/realtime/mod.rs
@@ -0,0 +1,88 @@
+//! `GET /v1/realtime` (WebSocket).
+//!
+//! This file is the **axum surface**: `router()`, the handler, and the small
+//! socket↔events adapter. The pure logic (no axum) lives in [`service`]. Auth is
+//! the `RequireMasterKey` extractor, so the handler stays thin.
+
+mod service;
+
+use std::sync::Arc;
+
+use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
+use axum::extract::{Query, State};
+use axum::http::StatusCode;
+use axum::response::Response;
+use axum::routing::get;
+use axum::Router;
+use futures_util::{SinkExt, StreamExt};
+use litellm_core::realtime::types::RealtimeEvent;
+use litellm_core::router::Router as ModelRouter;
+use litellm_providers::realtime_pool::RealtimePool;
+use serde::Deserialize;
+
+use crate::auth::RequireMasterKey;
+use crate::state::AppState;
+
+/// This route's contribution to the app router.
+pub fn router() -> Router {
+ Router::new().route("/v1/realtime", get(handle))
+}
+
+#[derive(Debug, Deserialize)]
+struct RealtimeQuery {
+ model: String,
+}
+
+/// Auth runs via the `RequireMasterKey` extractor. We validate the model BEFORE
+/// the upgrade so failures are clean HTTP (400/404), not a socket that opens then
+/// closes, then hand the socket to `bridge`.
+async fn handle(
+ _auth: RequireMasterKey,
+ ws: WebSocketUpgrade,
+ State(state): State,
+ Query(query): Query,
+) -> Result {
+ if query.model.trim().is_empty() {
+ return Err((
+ StatusCode::BAD_REQUEST,
+ "missing 'model' query param".to_string(),
+ ));
+ }
+ if !state.router.has_deployment(&query.model) {
+ return Err((
+ StatusCode::NOT_FOUND,
+ format!("no deployment for model '{}'", query.model),
+ ));
+ }
+
+ let router = state.router.clone();
+ let pool = state.realtime_pool.clone();
+ let model = query.model;
+ Ok(ws.on_upgrade(move |socket| bridge(socket, router, pool, model)))
+}
+
+/// Adapt the axum socket (text frames) to the typed-event `Stream`/`Sink` the
+/// service wants, keeping axum types out of `service`.
+async fn bridge(
+ socket: WebSocket,
+ router: Arc,
+ pool: Arc,
+ model: String,
+) {
+ let (ws_sink, ws_stream) = socket.split();
+
+ let client_in = ws_stream.filter_map(|message| async move {
+ match message {
+ Ok(Message::Text(text)) => serde_json::from_str::(&text).ok(),
+ _ => None,
+ }
+ });
+ let client_out = ws_sink.with(|event: RealtimeEvent| async move {
+ Ok::(Message::Text(
+ serde_json::to_string(&event).unwrap_or_default(),
+ ))
+ });
+
+ futures_util::pin_mut!(client_in, client_out);
+ let _ = service::run(&router, &pool, &model, None, client_in, client_out).await;
+}
diff --git a/litellm-rust/crates/ai-gateway/src/routes/realtime/service.rs b/litellm-rust/crates/ai-gateway/src/routes/realtime/service.rs
new file mode 100644
index 00000000000..0cbd00d664f
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/routes/realtime/service.rs
@@ -0,0 +1,76 @@
+//! Business logic: select a deployment with the (pure) core router, then call the
+//! provider splice. The seam between `core::router` (selection only) and
+//! `providers` (the actual WebSocket I/O).
+//!
+//! On connect we try a pre-warmed upstream from the pool (handshake already paid,
+//! `session.created` buffered) and relay it instantly. On a pool miss or dead warm
+//! socket we fresh-dial exactly as before — the pool is never on the critical path
+//! for correctness, only latency.
+
+use std::time::Duration;
+
+use futures_util::{Sink, Stream};
+use litellm_core::error::CoreError;
+use litellm_core::realtime::types::RealtimeEvent;
+use litellm_core::router::Router;
+use litellm_core::CoreResult;
+use litellm_providers::realtime_pool::{upstream_key, RealtimePool};
+
+/// Select a deployment for `model` and splice the client stream to the provider.
+///
+/// `pool` supplies a pre-warmed upstream when one is available; otherwise we
+/// fresh-dial. A disabled pool always misses, so this collapses to the original
+/// fresh-dial behavior.
+pub async fn run(
+ router: &Router,
+ pool: &RealtimePool,
+ model: &str,
+ idle_timeout: Option,
+ client_in: In,
+ client_out: Out,
+) -> CoreResult<()>
+where
+ In: Stream + Unpin + Send,
+ Out: Sink + Unpin + Send,
+ >::Error: std::fmt::Display,
+{
+ let deployment = router.get_available_deployment(model).ok_or_else(|| {
+ CoreError::Routing(format!("no deployment available for model '{model}'"))
+ })?;
+ let params = &deployment.litellm_params;
+ // Strip a leading `openai/` so the OpenAI-only realtime fn gets the bare model.
+ let provider_model = params
+ .model
+ .strip_prefix("openai/")
+ .unwrap_or(¶ms.model);
+
+ // Warm path: take a pooled upstream (handshake already paid) and relay its
+ // buffered session.created immediately. On miss/dead socket fall through.
+ if let Some(key) = upstream_key(
+ provider_model,
+ params.api_key.as_deref(),
+ params.api_base.as_deref(),
+ ) {
+ if let Some(handoff) = pool.take(&key) {
+ return litellm_providers::realtime::realtime_warm(
+ provider_model,
+ handoff,
+ idle_timeout,
+ client_in,
+ client_out,
+ )
+ .await;
+ }
+ }
+
+ // Cold path: fresh dial (the original behavior).
+ litellm_providers::realtime::realtime(
+ provider_model,
+ params.api_key.as_deref(),
+ params.api_base.as_deref(),
+ idle_timeout,
+ client_in,
+ client_out,
+ )
+ .await
+}
diff --git a/litellm-rust/crates/ai-gateway/src/state.rs b/litellm-rust/crates/ai-gateway/src/state.rs
new file mode 100644
index 00000000000..ef96037d477
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/state.rs
@@ -0,0 +1,17 @@
+use std::sync::Arc;
+
+use litellm_core::router::Router;
+use litellm_providers::realtime_pool::RealtimePool;
+
+/// Shared application state handed to every route handler.
+#[derive(Clone)]
+pub struct AppState {
+ pub router: Arc,
+ /// The gateway master key. Any caller presenting it as a bearer token may
+ /// invoke the gateway. `None` → auth not configured (routes fail closed).
+ pub master_key: Option>,
+ /// Pre-warmed upstream realtime connection pool. Disabled
+ /// (`RealtimePool::disabled()`) when `REALTIME_POOL_SIZE=0`, in which case
+ /// every realtime connect fresh-dials exactly as before.
+ pub realtime_pool: Arc,
+}
diff --git a/litellm-rust/crates/core/CLAUDE.md b/litellm-rust/crates/core/CLAUDE.md
new file mode 100644
index 00000000000..20873878967
--- /dev/null
+++ b/litellm-rust/crates/core/CLAUDE.md
@@ -0,0 +1,47 @@
+# CLAUDE.md
+
+Rules for `litellm-rust/crates/core`.
+
+## Responsibility
+
+`core` owns shared data types, typed errors, and deterministic helper contracts.
+It must stay pure and host-independent.
+
+Allowed:
+- Shared request/response structs.
+- Typed errors with stable, non-sensitive messages.
+- Deterministic validation helpers.
+- Serialization helpers that intentionally mirror Python output shape.
+- Route templates that match Python base config responsibilities, such as
+ `ocr::transformation::OcrProviderConfig`.
+
+Not allowed:
+- Network, filesystem, database, cache, or environment access.
+- Secret reads or auth/header construction.
+- Logging callbacks, tracing spans, spend writes, or customer callbacks.
+- Provider-specific branching that belongs in `providers`.
+- Panics for user/provider-controlled input.
+
+## Typed Contracts (core rule)
+
+Trait and function boundaries MUST be strongly typed. No stringly-typed JSON
+(`&str` / `String` / `Vec` / bare `serde_json::Value`) as a transform
+input or output. Parse wire bytes into typed structs/enums at the host edge;
+`core` and `providers` operate only on those types (e.g. `RealtimeEvent`,
+`RealtimeTransformResult`, `OcrRequestData`). A `type`-style discriminator is a
+typed field on a struct, not a raw string threaded through the API.
+
+## Structure
+
+Use route names directly under `src/`: `ocr`, future `messages`,
+`chat_completions`, `embeddings`, and similar top-level LiteLLM calls. Do not
+invent broad names like `engine` for route contracts.
+
+## Parity Rules
+
+- Every shared type used by a provider transform needs unit tests for
+ serialization shape.
+- If Python parity requires always emitting a `null` field instead of omitting
+ it, document that in code and pin it with a test.
+- Error enums should preserve enough detail for Python/HTTP hosts to map errors
+ consistently without exposing document contents or upstream bodies.
diff --git a/litellm-rust/crates/core/Cargo.toml b/litellm-rust/crates/core/Cargo.toml
new file mode 100644
index 00000000000..1881bcfa602
--- /dev/null
+++ b/litellm-rust/crates/core/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "litellm-core"
+version = "0.1.0"
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+rand.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+thiserror.workspace = true
diff --git a/litellm-rust/crates/core/src/error.rs b/litellm-rust/crates/core/src/error.rs
new file mode 100644
index 00000000000..9b29260cca4
--- /dev/null
+++ b/litellm-rust/crates/core/src/error.rs
@@ -0,0 +1,35 @@
+use thiserror::Error;
+
+pub type CoreResult = Result;
+
+#[derive(Debug, Error, PartialEq, Eq)]
+pub enum CoreError {
+ #[error("expected {expected}, got {actual}")]
+ InvalidType {
+ expected: &'static str,
+ actual: &'static str,
+ },
+ #[error("missing required field: {0}")]
+ MissingField(&'static str),
+ #[error("invalid response: {0}")]
+ InvalidResponse(String),
+ #[error("{0}")]
+ Auth(String),
+ #[error("OCR request failed with status {status}: {body}")]
+ Http { status: u16, body: String },
+ #[error("OCR network error: {0}")]
+ Network(String),
+ #[error("routing error: {0}")]
+ Routing(String),
+}
+
+pub fn json_type_name(value: &serde_json::Value) -> &'static str {
+ match value {
+ serde_json::Value::Null => "null",
+ serde_json::Value::Bool(_) => "bool",
+ serde_json::Value::Number(_) => "number",
+ serde_json::Value::String(_) => "string",
+ serde_json::Value::Array(_) => "array",
+ serde_json::Value::Object(_) => "object",
+ }
+}
diff --git a/litellm-rust/crates/core/src/lib.rs b/litellm-rust/crates/core/src/lib.rs
new file mode 100644
index 00000000000..9d686626edc
--- /dev/null
+++ b/litellm-rust/crates/core/src/lib.rs
@@ -0,0 +1,6 @@
+pub mod error;
+pub mod ocr;
+pub mod realtime;
+pub mod router;
+
+pub use error::{CoreError, CoreResult};
diff --git a/litellm-rust/crates/core/src/ocr/mod.rs b/litellm-rust/crates/core/src/ocr/mod.rs
new file mode 100644
index 00000000000..ec2fbb969a6
--- /dev/null
+++ b/litellm-rust/crates/core/src/ocr/mod.rs
@@ -0,0 +1,2 @@
+pub mod transformation;
+pub mod types;
diff --git a/litellm-rust/crates/core/src/ocr/transformation.rs b/litellm-rust/crates/core/src/ocr/transformation.rs
new file mode 100644
index 00000000000..7353d9d22c4
--- /dev/null
+++ b/litellm-rust/crates/core/src/ocr/transformation.rs
@@ -0,0 +1,32 @@
+use serde_json::{Map, Value};
+
+use crate::CoreResult;
+
+use super::types::{OcrRequestData, OcrResponseData};
+
+pub trait OcrProviderConfig {
+ fn supported_ocr_params(&self) -> &'static [&'static str];
+
+ fn map_ocr_params(&self, non_default_params: &Map) -> Map {
+ let mut mapped_params = Map::new();
+ for (param, value) in non_default_params {
+ if self.supported_ocr_params().contains(¶m.as_str()) {
+ mapped_params.insert(param.clone(), value.clone());
+ }
+ }
+ mapped_params
+ }
+
+ fn transform_ocr_request(
+ &self,
+ model: &str,
+ document: Value,
+ optional_params: Map,
+ ) -> CoreResult;
+
+ fn transform_ocr_response(
+ &self,
+ model: &str,
+ response_json: Value,
+ ) -> CoreResult;
+}
diff --git a/litellm-rust/crates/core/src/ocr/types.rs b/litellm-rust/crates/core/src/ocr/types.rs
new file mode 100644
index 00000000000..1a72b8f1d66
--- /dev/null
+++ b/litellm-rust/crates/core/src/ocr/types.rs
@@ -0,0 +1,29 @@
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct OcrRequestData {
+ pub data: Value,
+ pub files: Option,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct OcrResponseData {
+ pub pages: Vec,
+ pub model: String,
+ pub document_annotation: Option,
+ pub usage_info: Option,
+ pub object: String,
+}
+
+impl OcrResponseData {
+ pub fn into_json(self) -> Value {
+ serde_json::json!({
+ "pages": self.pages,
+ "model": self.model,
+ "document_annotation": self.document_annotation,
+ "usage_info": self.usage_info,
+ "object": self.object,
+ })
+ }
+}
diff --git a/litellm-rust/crates/core/src/realtime/mod.rs b/litellm-rust/crates/core/src/realtime/mod.rs
new file mode 100644
index 00000000000..ec2fbb969a6
--- /dev/null
+++ b/litellm-rust/crates/core/src/realtime/mod.rs
@@ -0,0 +1,2 @@
+pub mod transformation;
+pub mod types;
diff --git a/litellm-rust/crates/core/src/realtime/transformation.rs b/litellm-rust/crates/core/src/realtime/transformation.rs
new file mode 100644
index 00000000000..a4baa27a6c2
--- /dev/null
+++ b/litellm-rust/crates/core/src/realtime/transformation.rs
@@ -0,0 +1,22 @@
+use crate::realtime::types::{RealtimeEvent, RealtimeTransformResult};
+use crate::CoreResult;
+
+pub trait RealtimeProviderConfig {
+ /// Build the upstream WebSocket URL (e.g. `wss://api.openai.com/v1/realtime?model=…`).
+ /// Pure string construction only — no network, no env.
+ fn complete_url(&self, api_base: Option<&str>, model: &str) -> String;
+
+ /// Transform a client → backend event before it is forwarded upstream.
+ fn transform_realtime_request(
+ &self,
+ event: &RealtimeEvent,
+ model: &str,
+ ) -> CoreResult;
+
+ /// Transform a backend → client event before it is forwarded downstream.
+ fn transform_realtime_response(
+ &self,
+ event: &RealtimeEvent,
+ model: &str,
+ ) -> CoreResult;
+}
diff --git a/litellm-rust/crates/core/src/realtime/types.rs b/litellm-rust/crates/core/src/realtime/types.rs
new file mode 100644
index 00000000000..3b59224b6e9
--- /dev/null
+++ b/litellm-rust/crates/core/src/realtime/types.rs
@@ -0,0 +1,60 @@
+use serde::{Deserialize, Serialize};
+use serde_json::{Map, Value};
+
+/// A single realtime event exchanged over the WebSocket.
+///
+/// The `type` discriminator is a typed field; the remaining fields are
+/// preserved losslessly in `data` so a transform can pass an event through, or
+/// inspect/modify specific fields, without enumerating every event variant.
+/// Wire (de)serialization happens at the host edge — `core`/`providers` operate
+/// only on this typed form.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct RealtimeEvent {
+ #[serde(rename = "type")]
+ pub event_type: String,
+ #[serde(flatten)]
+ pub data: Map,
+}
+
+/// One or more typed events produced by a realtime transform.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct RealtimeTransformResult {
+ pub events: Vec,
+}
+
+impl RealtimeTransformResult {
+ /// Forward a single event unchanged (the OpenAI baseline).
+ pub fn passthrough(event: RealtimeEvent) -> Self {
+ Self {
+ events: vec![event],
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn event(raw: &str) -> RealtimeEvent {
+ serde_json::from_str(raw).expect("valid event json")
+ }
+
+ #[test]
+ fn realtime_event_round_trips_type_and_extra_fields() {
+ let raw = r#"{"type":"response.output_text.delta","delta":"hi","response_id":"r1"}"#;
+ let parsed = event(raw);
+ assert_eq!(parsed.event_type, "response.output_text.delta");
+ assert_eq!(parsed.data.get("delta"), Some(&Value::String("hi".into())));
+ // Re-serializing yields a semantically-equal event (key order may differ).
+ let reparsed: RealtimeEvent =
+ serde_json::from_str(&serde_json::to_string(&parsed).unwrap()).unwrap();
+ assert_eq!(parsed, reparsed);
+ }
+
+ #[test]
+ fn passthrough_produces_single_element_vec() {
+ let parsed = event(r#"{"type":"session.update"}"#);
+ let result = RealtimeTransformResult::passthrough(parsed.clone());
+ assert_eq!(result.events, vec![parsed]);
+ }
+}
diff --git a/litellm-rust/crates/core/src/router/deployment.rs b/litellm-rust/crates/core/src/router/deployment.rs
new file mode 100644
index 00000000000..1ee88e682a3
--- /dev/null
+++ b/litellm-rust/crates/core/src/router/deployment.rs
@@ -0,0 +1,44 @@
+//! `model_list` data types, mirroring Python's deployment dict. Deserialize-ready
+//! so a deployment can be loaded straight from the proxy config's `model_list`.
+
+use serde::Deserialize;
+
+/// Per-deployment call parameters, mirroring Python's `litellm_params`.
+#[derive(Clone, Debug, Deserialize)]
+pub struct LiteLLMParams {
+ /// Provider model, e.g. `gpt-realtime` or `openai/gpt-realtime`.
+ pub model: String,
+ #[serde(default)]
+ pub api_key: Option,
+ #[serde(default)]
+ pub api_base: Option,
+}
+
+/// One entry of the `model_list`, mirroring Python's deployment dict.
+#[derive(Clone, Debug, Deserialize)]
+pub struct Deployment {
+ /// Public alias clients request, e.g. `gpt-realtime`.
+ pub model_name: String,
+ pub litellm_params: LiteLLMParams,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn deserializes_from_model_list_entry() {
+ let entry = r#"{
+ "model_name": "gpt-realtime",
+ "litellm_params": {"model": "openai/gpt-realtime", "api_base": "https://x"}
+ }"#;
+ let deployment: Deployment = serde_json::from_str(entry).expect("valid entry");
+ assert_eq!(deployment.model_name, "gpt-realtime");
+ assert_eq!(deployment.litellm_params.model, "openai/gpt-realtime");
+ assert_eq!(deployment.litellm_params.api_key, None);
+ assert_eq!(
+ deployment.litellm_params.api_base.as_deref(),
+ Some("https://x")
+ );
+ }
+}
diff --git a/litellm-rust/crates/core/src/router/mod.rs b/litellm-rust/crates/core/src/router/mod.rs
new file mode 100644
index 00000000000..96bc91bc6b5
--- /dev/null
+++ b/litellm-rust/crates/core/src/router/mod.rs
@@ -0,0 +1,93 @@
+//! Minimal Rust port of LiteLLM's `router.py` deployment selection.
+//!
+//! A [`Router`] is built from a `model_list` of [`Deployment`]s
+//! (`{ model_name, litellm_params: { model, api_key, api_base } }`) and selects
+//! one per request via a [`RoutingStrategy`]. For now the only strategy is
+//! `simple-shuffle` — a uniform random pick within a `model_name` group.
+//!
+//! This stays pure (no I/O): it only *chooses* a deployment. The host (the
+//! gateway) takes the chosen deployment and performs the actual provider call.
+//!
+//! - [`deployment`] — the `model_list` data types.
+//! - [`strategy`] — how a deployment is chosen.
+
+mod deployment;
+mod strategy;
+
+pub use deployment::{Deployment, LiteLLMParams};
+pub use strategy::RoutingStrategy;
+
+/// Load-balancing router over a `model_list`.
+#[derive(Clone, Debug, Default)]
+pub struct Router {
+ model_list: Vec,
+ routing_strategy: RoutingStrategy,
+}
+
+impl Router {
+ /// Build a router from a `model_list` using the default `simple-shuffle` strategy.
+ pub fn new(model_list: Vec) -> Self {
+ Self {
+ model_list,
+ routing_strategy: RoutingStrategy::SimpleShuffle,
+ }
+ }
+
+ /// All deployments in the `model_list`. Read-only; used by the host to
+ /// enumerate upstreams (e.g. to pre-warm a connection pool per deployment).
+ pub fn deployments(&self) -> &[Deployment] {
+ &self.model_list
+ }
+
+ /// Whether any deployment is registered under `model`.
+ pub fn has_deployment(&self, model: &str) -> bool {
+ self.model_list
+ .iter()
+ .any(|deployment| deployment.model_name == model)
+ }
+
+ /// Pick a deployment for `model` per the routing strategy. Returns `None`
+ /// when no deployment is registered under that `model_name`.
+ pub fn get_available_deployment(&self, model: &str) -> Option<&Deployment> {
+ let candidates: Vec<&Deployment> = self
+ .model_list
+ .iter()
+ .filter(|deployment| deployment.model_name == model)
+ .collect();
+ self.routing_strategy.select(&candidates)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn deployment(name: &str, model: &str) -> Deployment {
+ Deployment {
+ model_name: name.to_string(),
+ litellm_params: LiteLLMParams {
+ model: model.to_string(),
+ api_key: None,
+ api_base: None,
+ },
+ }
+ }
+
+ #[test]
+ fn selects_a_matching_deployment() {
+ let router = Router::new(vec![
+ deployment("gpt-realtime", "gpt-realtime"),
+ deployment("other", "other-model"),
+ ]);
+ let chosen = router
+ .get_available_deployment("gpt-realtime")
+ .expect("a deployment should match");
+ assert_eq!(chosen.model_name, "gpt-realtime");
+ }
+
+ #[test]
+ fn unknown_model_returns_none() {
+ let router = Router::new(vec![deployment("gpt-realtime", "gpt-realtime")]);
+ assert!(router.get_available_deployment("missing").is_none());
+ }
+}
diff --git a/litellm-rust/crates/core/src/router/strategy/mod.rs b/litellm-rust/crates/core/src/router/strategy/mod.rs
new file mode 100644
index 00000000000..7e8ac217db3
--- /dev/null
+++ b/litellm-rust/crates/core/src/router/strategy/mod.rs
@@ -0,0 +1,26 @@
+//! Routing policy: how the router picks one deployment from a model group.
+//!
+//! One module per strategy; [`RoutingStrategy::select`] dispatches to it. New
+//! strategies (least-busy, latency-based, …) get their own file here.
+
+mod simple_shuffle;
+
+use super::Deployment;
+
+/// How the router chooses among the deployments sharing a `model_name`.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum RoutingStrategy {
+ /// Uniform random pick among the matching deployments.
+ #[default]
+ SimpleShuffle,
+}
+
+impl RoutingStrategy {
+ /// Choose one deployment from `candidates` (all sharing the requested
+ /// `model_name`). Returns `None` when there are no candidates.
+ pub fn select<'a>(&self, candidates: &[&'a Deployment]) -> Option<&'a Deployment> {
+ match self {
+ RoutingStrategy::SimpleShuffle => simple_shuffle::select(candidates),
+ }
+ }
+}
diff --git a/litellm-rust/crates/core/src/router/strategy/simple_shuffle.rs b/litellm-rust/crates/core/src/router/strategy/simple_shuffle.rs
new file mode 100644
index 00000000000..74ce0c21e80
--- /dev/null
+++ b/litellm-rust/crates/core/src/router/strategy/simple_shuffle.rs
@@ -0,0 +1,47 @@
+//! `simple-shuffle`: a uniform random pick among the candidate deployments.
+
+use rand::seq::SliceRandom;
+
+use crate::router::Deployment;
+
+/// Uniform random choice among `candidates` (all sharing the requested
+/// `model_name`). Returns `None` when there are no candidates.
+pub fn select<'a>(candidates: &[&'a Deployment]) -> Option<&'a Deployment> {
+ candidates.choose(&mut rand::thread_rng()).copied()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::router::{Deployment, LiteLLMParams};
+
+ fn deployment(model: &str) -> Deployment {
+ Deployment {
+ model_name: "gpt-realtime".to_string(),
+ litellm_params: LiteLLMParams {
+ model: model.to_string(),
+ api_key: None,
+ api_base: None,
+ },
+ }
+ }
+
+ #[test]
+ fn picks_from_candidates() {
+ let a = deployment("key-a");
+ let b = deployment("key-b");
+ let candidates = vec![&a, &b];
+ for _ in 0..20 {
+ let chosen = select(&candidates).expect("non-empty");
+ assert!(matches!(
+ chosen.litellm_params.model.as_str(),
+ "key-a" | "key-b"
+ ));
+ }
+ }
+
+ #[test]
+ fn empty_candidates_select_none() {
+ assert!(select(&[]).is_none());
+ }
+}
diff --git a/litellm-rust/crates/providers/CLAUDE.md b/litellm-rust/crates/providers/CLAUDE.md
new file mode 100644
index 00000000000..0f7fdcda2aa
--- /dev/null
+++ b/litellm-rust/crates/providers/CLAUDE.md
@@ -0,0 +1,53 @@
+# CLAUDE.md
+
+Rules for `litellm-rust/crates/providers`.
+
+## Responsibility
+
+`providers` owns provider-specific pure transforms. It mirrors the existing
+Python provider modules closely enough that parity review is mechanical.
+
+Provider files should map to the Python provider tree:
+
+```text
+providers/src///transformation.rs
+```
+
+For example, Mistral OCR lives at
+`providers/src/mistral/ocr/transformation.rs`, matching
+`litellm/llms/mistral/ocr/transformation.py`.
+
+Allowed:
+- Provider request transforms.
+- Provider response normalization.
+- Supported-parameter filtering.
+- Provider-specific validation that does not require I/O or secrets.
+
+Not allowed:
+- HTTP clients or provider SDK calls.
+- Environment variable reads.
+- API key resolution or auth header construction.
+- Logging, callbacks, spend tracking, retries, routing, cooldowns, or fallbacks.
+- Panics on bad user/provider input.
+
+## Required Tests
+
+Every provider transform must include focused unit tests for:
+- Supported params matching the Python provider config.
+- Unknown params being dropped or transformed the same way as Python.
+- Request body shape matching Python output.
+- Response normalization with complete, missing, null, and extra fields.
+- Bad input returning typed errors.
+
+For OCR specifically, assume documents can contain personal data. Tests should
+prove transforms do not copy document contents into error messages.
+
+## Implementation Rules
+
+- Prefer static supported-parameter lists over allocating strings on every call.
+- Keep transforms deterministic and allocation-conscious, but choose clarity over
+ premature micro-optimization for tiny parameter lists.
+- Use typed errors from `core`; avoid stringly-typed error plumbing.
+- Add comments only when they explain Python-parity decisions or provider quirks.
+- Put route-level provider dispatch in a route file such as `providers/src/ocr.rs`.
+ Do not move provider-specific transform logic into the Python bridge.
diff --git a/litellm-rust/crates/providers/Cargo.toml b/litellm-rust/crates/providers/Cargo.toml
new file mode 100644
index 00000000000..c5b41424d66
--- /dev/null
+++ b/litellm-rust/crates/providers/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "litellm-providers"
+version = "0.1.0"
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+litellm-core.workspace = true
+reqwest.workspace = true
+serde_json.workspace = true
+tokio.workspace = true
+tokio-tungstenite.workspace = true
+futures-util.workspace = true
+
+[dev-dependencies]
+serde_json.workspace = true
+futures-channel = "0.3"
diff --git a/litellm-rust/crates/providers/src/lib.rs b/litellm-rust/crates/providers/src/lib.rs
new file mode 100644
index 00000000000..40e18961f43
--- /dev/null
+++ b/litellm-rust/crates/providers/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod mistral;
+pub mod ocr;
+pub mod openai;
+pub mod realtime;
+pub mod realtime_pool;
diff --git a/litellm-rust/crates/providers/src/mistral/mod.rs b/litellm-rust/crates/providers/src/mistral/mod.rs
new file mode 100644
index 00000000000..3621ff6a2fd
--- /dev/null
+++ b/litellm-rust/crates/providers/src/mistral/mod.rs
@@ -0,0 +1 @@
+pub mod ocr;
diff --git a/litellm-rust/crates/providers/src/mistral/ocr/mod.rs b/litellm-rust/crates/providers/src/mistral/ocr/mod.rs
new file mode 100644
index 00000000000..f239b6921fa
--- /dev/null
+++ b/litellm-rust/crates/providers/src/mistral/ocr/mod.rs
@@ -0,0 +1 @@
+pub mod transformation;
diff --git a/litellm-rust/crates/providers/src/mistral/ocr/transformation.rs b/litellm-rust/crates/providers/src/mistral/ocr/transformation.rs
new file mode 100644
index 00000000000..fd691177783
--- /dev/null
+++ b/litellm-rust/crates/providers/src/mistral/ocr/transformation.rs
@@ -0,0 +1,292 @@
+use litellm_core::error::{json_type_name, CoreError, CoreResult};
+use litellm_core::ocr::transformation::OcrProviderConfig;
+use litellm_core::ocr::types::{OcrRequestData, OcrResponseData};
+use serde_json::{Map, Value};
+
+const SUPPORTED_OCR_PARAMS: &[&str] = &[
+ "pages",
+ "include_image_base64",
+ "image_limit",
+ "image_min_size",
+ "bbox_annotation_format",
+ "document_annotation_format",
+ "document_annotation_prompt",
+ "extract_header",
+ "extract_footer",
+ "table_format",
+ "confidence_scores_granularity",
+ "id",
+];
+
+/// Default Mistral API base, used when the caller does not override `api_base`.
+pub const MISTRAL_DEFAULT_API_BASE: &str = "https://api.mistral.ai/v1";
+
+/// Environment variable holding the Mistral API key.
+pub const MISTRAL_API_KEY_ENV: &str = "MISTRAL_API_KEY";
+
+/// Error message raised when no Mistral API key can be resolved.
+pub const MISSING_KEY_MESSAGE: &str = "Missing Mistral API Key - A call is being made to Mistral but no key is set either in the environment variables or via params";
+
+/// Build the complete OCR endpoint URL, de-duplicating a trailing `/v1`.
+///
+/// Blank/whitespace `api_base` is treated as absent (guard at resolution time).
+pub fn complete_url(api_base: Option<&str>) -> String {
+ let base = api_base
+ .map(str::trim)
+ .filter(|base| !base.is_empty())
+ .unwrap_or(MISTRAL_DEFAULT_API_BASE)
+ .trim_end_matches('/');
+
+ if base.ends_with("/v1") {
+ format!("{base}/ocr")
+ } else {
+ format!("{base}/v1/ocr")
+ }
+}
+
+/// Resolve the Mistral API key from the explicit param or the environment.
+///
+/// Blank/whitespace values are treated as absent. Returns `CoreError::Auth`
+/// when no usable key is available.
+///
+/// Note: the env fallback only reads the process environment. Secret-manager
+/// backends (AWS/Azure/GCP/Vault) are resolved on the Python side and passed in
+/// via `api_key`; this fallback is a last resort for direct/standalone use.
+pub fn resolve_api_key(
+ api_key: Option<&str>,
+ env_lookup: &dyn Fn(&str) -> Option,
+) -> CoreResult {
+ api_key
+ .map(str::trim)
+ .filter(|key| !key.is_empty())
+ .map(str::to_string)
+ .or_else(|| env_lookup(MISTRAL_API_KEY_ENV).filter(|key| !key.trim().is_empty()))
+ .ok_or_else(|| CoreError::Auth(MISSING_KEY_MESSAGE.to_string()))
+}
+
+pub struct MistralOcrConfig;
+
+pub const MISTRAL_OCR_CONFIG: MistralOcrConfig = MistralOcrConfig;
+
+impl OcrProviderConfig for MistralOcrConfig {
+ fn supported_ocr_params(&self) -> &'static [&'static str] {
+ SUPPORTED_OCR_PARAMS
+ }
+
+ fn transform_ocr_request(
+ &self,
+ model: &str,
+ document: Value,
+ optional_params: Map,
+ ) -> CoreResult {
+ if !document.is_object() {
+ return Err(CoreError::InvalidType {
+ expected: "object",
+ actual: json_type_name(&document),
+ });
+ }
+
+ let mut data = Map::new();
+ data.insert("model".to_string(), Value::String(model.to_string()));
+ data.insert("document".to_string(), document);
+ for (param, value) in optional_params {
+ data.insert(param, value);
+ }
+
+ Ok(OcrRequestData {
+ data: Value::Object(data),
+ files: None,
+ })
+ }
+
+ fn transform_ocr_response(
+ &self,
+ model: &str,
+ response_json: Value,
+ ) -> CoreResult {
+ let response_object = response_json
+ .as_object()
+ .ok_or_else(|| CoreError::InvalidType {
+ expected: "object",
+ actual: json_type_name(&response_json),
+ })?;
+
+ let pages = response_object
+ .get("pages")
+ .and_then(Value::as_array)
+ .cloned()
+ .unwrap_or_default();
+ let model = response_object
+ .get("model")
+ .and_then(Value::as_str)
+ .unwrap_or(model)
+ .to_string();
+ let document_annotation = response_object.get("document_annotation").cloned();
+ let usage_info = response_object.get("usage_info").cloned();
+
+ Ok(OcrResponseData {
+ pages,
+ model,
+ document_annotation,
+ usage_info,
+ object: "ocr".to_string(),
+ })
+ }
+}
+
+pub fn supported_ocr_params() -> &'static [&'static str] {
+ MISTRAL_OCR_CONFIG.supported_ocr_params()
+}
+
+pub fn map_ocr_params(non_default_params: &Map) -> Map {
+ MISTRAL_OCR_CONFIG.map_ocr_params(non_default_params)
+}
+
+pub fn transform_ocr_request(
+ model: &str,
+ document: Value,
+ optional_params: Map,
+) -> CoreResult {
+ MISTRAL_OCR_CONFIG.transform_ocr_request(model, document, optional_params)
+}
+
+pub fn transform_ocr_response(model: &str, response_json: Value) -> CoreResult {
+ MISTRAL_OCR_CONFIG.transform_ocr_response(model, response_json)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn supported_params_match_python_mistral_ocr_config() {
+ assert_eq!(
+ supported_ocr_params(),
+ &[
+ "pages",
+ "include_image_base64",
+ "image_limit",
+ "image_min_size",
+ "bbox_annotation_format",
+ "document_annotation_format",
+ "document_annotation_prompt",
+ "extract_header",
+ "extract_footer",
+ "table_format",
+ "confidence_scores_granularity",
+ "id",
+ ]
+ );
+ }
+
+ #[test]
+ fn map_ocr_params_drops_unknown_params() {
+ let params = json!({
+ "extract_header": true,
+ "unsupported_param": "value",
+ "pages": [0, 1]
+ });
+ let mapped = map_ocr_params(params.as_object().unwrap());
+
+ assert_eq!(mapped.get("extract_header"), Some(&json!(true)));
+ assert_eq!(mapped.get("pages"), Some(&json!([0, 1])));
+ assert!(!mapped.contains_key("unsupported_param"));
+ }
+
+ #[test]
+ fn transform_ocr_request_builds_mistral_body() {
+ let document = json!({
+ "type": "document_url",
+ "document_url": "https://example.com/doc.pdf"
+ });
+ let optional_params = json!({
+ "include_image_base64": true,
+ "table_format": "html"
+ })
+ .as_object()
+ .unwrap()
+ .clone();
+
+ let result = transform_ocr_request("mistral-ocr-latest", document.clone(), optional_params)
+ .expect("request should transform");
+
+ assert_eq!(
+ result.data,
+ json!({
+ "model": "mistral-ocr-latest",
+ "document": document,
+ "include_image_base64": true,
+ "table_format": "html"
+ })
+ );
+ assert_eq!(result.files, None);
+ }
+
+ #[test]
+ fn transform_ocr_request_rejects_non_object_document() {
+ let err = transform_ocr_request("mistral-ocr-latest", json!("bad"), Map::new())
+ .expect_err("string document should be rejected");
+
+ assert_eq!(
+ err,
+ CoreError::InvalidType {
+ expected: "object",
+ actual: "string",
+ }
+ );
+ }
+
+ #[test]
+ fn transform_ocr_response_normalizes_mistral_json() {
+ let response = json!({
+ "pages": [{"index": 0, "markdown": "hello"}],
+ "model": "mistral-ocr-2505-completion",
+ "document_annotation": null,
+ "usage_info": {"pages_processed": 1}
+ });
+
+ let result = transform_ocr_response("mistral-ocr-latest", response)
+ .expect("response should transform");
+
+ assert_eq!(result.pages, vec![json!({"index": 0, "markdown": "hello"})]);
+ assert_eq!(result.model, "mistral-ocr-2505-completion");
+ assert_eq!(result.document_annotation, Some(Value::Null));
+ assert_eq!(result.usage_info, Some(json!({"pages_processed": 1})));
+ assert_eq!(result.object, "ocr");
+ }
+
+ #[test]
+ fn complete_url_defaults_and_dedupes_v1() {
+ assert_eq!(complete_url(None), "https://api.mistral.ai/v1/ocr");
+ assert_eq!(complete_url(Some(" ")), "https://api.mistral.ai/v1/ocr");
+ assert_eq!(
+ complete_url(Some("https://proxy.internal")),
+ "https://proxy.internal/v1/ocr"
+ );
+ assert_eq!(
+ complete_url(Some("https://proxy.internal/v1/")),
+ "https://proxy.internal/v1/ocr"
+ );
+ }
+
+ #[test]
+ fn resolve_api_key_prefers_param_then_env() {
+ let no_env = |_: &str| None;
+ assert_eq!(
+ resolve_api_key(Some("sk-param"), &no_env).unwrap(),
+ "sk-param"
+ );
+
+ let with_env = |key: &str| (key == MISTRAL_API_KEY_ENV).then(|| "sk-env".to_string());
+ assert_eq!(resolve_api_key(None, &with_env).unwrap(), "sk-env");
+ // Blank param falls through to the environment.
+ assert_eq!(resolve_api_key(Some(" "), &with_env).unwrap(), "sk-env");
+ }
+
+ #[test]
+ fn resolve_api_key_errors_when_absent() {
+ let err = resolve_api_key(None, &|_| None).expect_err("missing key should error");
+ assert_eq!(err, CoreError::Auth(MISSING_KEY_MESSAGE.to_string()));
+ }
+}
diff --git a/litellm-rust/crates/providers/src/ocr.rs b/litellm-rust/crates/providers/src/ocr.rs
new file mode 100644
index 00000000000..dcd56a5f0b4
--- /dev/null
+++ b/litellm-rust/crates/providers/src/ocr.rs
@@ -0,0 +1,127 @@
+//! End-to-end OCR orchestration.
+//!
+//! Owns the whole Mistral OCR call so the Python side stays a thin bridge:
+//! resolve the API key, build the URL + body via the pure transforms, POST it,
+//! and normalize the response. The HTTP client is built once and reused.
+
+use std::sync::OnceLock;
+use std::time::Duration;
+
+use litellm_core::error::CoreError;
+use litellm_core::ocr::transformation::OcrProviderConfig;
+use litellm_core::CoreResult;
+use serde_json::{Map, Value};
+
+use crate::mistral::ocr::transformation as mistral;
+use crate::mistral::ocr::transformation::MISTRAL_OCR_CONFIG;
+
+/// OCR over large documents can take a while; bound it generously rather than
+/// hanging forever on an unresponsive upstream. The client-level limit is the
+/// outer ceiling; callers can tighten it per request via ``run_ocr``'s ``timeout``.
+const OCR_TIMEOUT_SECS: u64 = 600;
+
+/// Maximum upstream body characters retained in error messages. OCR responses
+/// can echo document contents and prompts; keep enough for debugging without
+/// forwarding sensitive payloads across the host boundary.
+const ERROR_BODY_MAX_CHARS: usize = 256;
+
+/// Process-wide blocking HTTP client (connection pool + TLS reused across calls).
+fn http_client() -> &'static reqwest::blocking::Client {
+ static CLIENT: OnceLock = OnceLock::new();
+ CLIENT.get_or_init(|| {
+ reqwest::blocking::Client::builder()
+ .timeout(Duration::from_secs(OCR_TIMEOUT_SECS))
+ .build()
+ .expect("failed to build reqwest client")
+ })
+}
+
+fn truncate_error_body(body: &str) -> String {
+ if body.chars().count() <= ERROR_BODY_MAX_CHARS {
+ return body.to_string();
+ }
+ let truncated: String = body.chars().take(ERROR_BODY_MAX_CHARS).collect();
+ format!("{truncated}... (truncated)")
+}
+
+/// Perform a Mistral OCR call end to end and return the normalized response as
+/// JSON (the shape the Python `OCRResponse` model expects).
+///
+/// Blocking: intended to be called with the GIL released from the Python bridge.
+pub fn run_ocr(
+ model: &str,
+ document: Value,
+ api_key: Option<&str>,
+ api_base: Option<&str>,
+ optional_params: Map,
+ timeout: Option,
+) -> CoreResult {
+ let config = &MISTRAL_OCR_CONFIG;
+
+ let api_key = mistral::resolve_api_key(api_key, &|key| std::env::var(key).ok())?;
+ let url = mistral::complete_url(api_base);
+ let filtered_params = config.map_ocr_params(&optional_params);
+ let body = config
+ .transform_ocr_request(model, document, filtered_params)?
+ .data;
+
+ let mut request = http_client().post(&url).bearer_auth(&api_key).json(&body);
+ if let Some(duration) = timeout {
+ request = request.timeout(duration);
+ }
+
+ let response = request
+ .send()
+ .map_err(|err| CoreError::Network(err.to_string()))?;
+
+ let status = response.status();
+ let text = response
+ .text()
+ .map_err(|err| CoreError::Network(err.to_string()))?;
+
+ if !status.is_success() {
+ return Err(CoreError::Http {
+ status: status.as_u16(),
+ body: truncate_error_body(&text),
+ });
+ }
+
+ let response_json: Value = serde_json::from_str(&text)
+ .map_err(|err| CoreError::InvalidResponse(format!("invalid OCR response JSON: {err}")))?;
+
+ Ok(config
+ .transform_ocr_response(model, response_json)?
+ .into_json())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn truncate_error_body_passes_short_strings_through() {
+ let body = "Unauthorized";
+ assert_eq!(truncate_error_body(body), "Unauthorized");
+ }
+
+ #[test]
+ fn truncate_error_body_caps_long_payloads() {
+ let body = "x".repeat(ERROR_BODY_MAX_CHARS + 50);
+ let truncated = truncate_error_body(&body);
+
+ assert!(truncated.ends_with("... (truncated)"));
+ let prefix_chars = truncated
+ .strip_suffix("... (truncated)")
+ .expect("truncated marker present")
+ .chars()
+ .count();
+ assert_eq!(prefix_chars, ERROR_BODY_MAX_CHARS);
+ }
+
+ #[test]
+ fn truncate_error_body_does_not_split_multibyte_chars() {
+ let body = "é".repeat(ERROR_BODY_MAX_CHARS + 10);
+ let truncated = truncate_error_body(&body);
+ assert!(truncated.is_char_boundary(truncated.len()));
+ }
+}
diff --git a/litellm-rust/crates/providers/src/openai/mod.rs b/litellm-rust/crates/providers/src/openai/mod.rs
new file mode 100644
index 00000000000..403e32975cf
--- /dev/null
+++ b/litellm-rust/crates/providers/src/openai/mod.rs
@@ -0,0 +1 @@
+pub mod realtime;
diff --git a/litellm-rust/crates/providers/src/openai/realtime/mod.rs b/litellm-rust/crates/providers/src/openai/realtime/mod.rs
new file mode 100644
index 00000000000..f239b6921fa
--- /dev/null
+++ b/litellm-rust/crates/providers/src/openai/realtime/mod.rs
@@ -0,0 +1 @@
+pub mod transformation;
diff --git a/litellm-rust/crates/providers/src/openai/realtime/transformation.rs b/litellm-rust/crates/providers/src/openai/realtime/transformation.rs
new file mode 100644
index 00000000000..2e127c699e0
--- /dev/null
+++ b/litellm-rust/crates/providers/src/openai/realtime/transformation.rs
@@ -0,0 +1,189 @@
+use litellm_core::realtime::transformation::RealtimeProviderConfig;
+use litellm_core::realtime::types::{RealtimeEvent, RealtimeTransformResult};
+use litellm_core::CoreResult;
+
+/// Default OpenAI API base, used when the caller does not override `api_base`.
+pub const OPENAI_REALTIME_DEFAULT_API_BASE: &str = "https://api.openai.com";
+
+/// Path appended to the resolved host base to reach the realtime endpoint.
+pub const OPENAI_REALTIME_PATH: &str = "/v1/realtime";
+
+/// Percent-encode a query value, escaping any char outside the RFC 3986
+/// unreserved set (`A-Za-z0-9-._~`). Keeps us dependency-free; common realtime
+/// model slugs have no special chars, but this stays correct for the rest.
+fn percent_encode(value: &str) -> String {
+ let mut encoded = String::with_capacity(value.len());
+ for byte in value.bytes() {
+ let unreserved = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~');
+ if unreserved {
+ encoded.push(byte as char);
+ } else {
+ encoded.push('%');
+ encoded.push_str(&format!("{byte:02X}"));
+ }
+ }
+ encoded
+}
+
+/// Build the realtime WebSocket URL, porting Python's `OpenAIRealtime._construct_url`.
+///
+/// Blank/whitespace `api_base` is treated as absent (guard at resolution time),
+/// falling back to the default. The scheme is swapped to its WebSocket
+/// equivalent (`https://`→`wss://`, `http://`→`ws://`); bases already using
+/// `ws`/`wss` are left untouched. A bare host or unrecognized scheme defaults to
+/// secure `wss://` so we never hand a scheme-less URL to the connector (this is
+/// a deliberate hardening over Python's `_construct_url`, which would emit a
+/// scheme-less URL here). A trailing `/` is trimmed before the path and
+/// `?model=` are appended.
+pub fn complete_url(api_base: Option<&str>, model: &str) -> String {
+ let base = api_base
+ .map(str::trim)
+ .filter(|base| !base.is_empty())
+ .unwrap_or(OPENAI_REALTIME_DEFAULT_API_BASE);
+
+ let base = if let Some(rest) = base.strip_prefix("https://") {
+ format!("wss://{rest}")
+ } else if let Some(rest) = base.strip_prefix("http://") {
+ format!("ws://{rest}")
+ } else if base.starts_with("wss://") || base.starts_with("ws://") {
+ base.to_string()
+ } else {
+ format!("wss://{base}")
+ };
+
+ let base = base.trim_end_matches('/');
+
+ format!(
+ "{base}{OPENAI_REALTIME_PATH}?model={}",
+ percent_encode(model)
+ )
+}
+
+pub struct OpenAiRealtimeConfig;
+
+pub const OPENAI_REALTIME_CONFIG: OpenAiRealtimeConfig = OpenAiRealtimeConfig;
+
+impl RealtimeProviderConfig for OpenAiRealtimeConfig {
+ fn complete_url(&self, api_base: Option<&str>, model: &str) -> String {
+ complete_url(api_base, model)
+ }
+
+ fn transform_realtime_request(
+ &self,
+ event: &RealtimeEvent,
+ _model: &str,
+ ) -> CoreResult {
+ Ok(RealtimeTransformResult::passthrough(event.clone()))
+ }
+
+ fn transform_realtime_response(
+ &self,
+ event: &RealtimeEvent,
+ _model: &str,
+ ) -> CoreResult {
+ Ok(RealtimeTransformResult::passthrough(event.clone()))
+ }
+}
+
+pub fn transform_realtime_request(
+ event: &RealtimeEvent,
+ model: &str,
+) -> CoreResult {
+ OPENAI_REALTIME_CONFIG.transform_realtime_request(event, model)
+}
+
+pub fn transform_realtime_response(
+ event: &RealtimeEvent,
+ model: &str,
+) -> CoreResult {
+ OPENAI_REALTIME_CONFIG.transform_realtime_response(event, model)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn complete_url_defaults_to_openai_wss() {
+ assert_eq!(
+ complete_url(None, "gpt-4o-realtime-preview"),
+ "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview"
+ );
+ }
+
+ #[test]
+ fn complete_url_blank_base_uses_default() {
+ assert_eq!(
+ complete_url(Some(" "), "gpt-4o-realtime-preview"),
+ "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview"
+ );
+ }
+
+ #[test]
+ fn complete_url_swaps_http_to_ws() {
+ assert_eq!(
+ complete_url(Some("http://localhost:8080"), "gpt-4o-realtime-preview"),
+ "ws://localhost:8080/v1/realtime?model=gpt-4o-realtime-preview"
+ );
+ }
+
+ #[test]
+ fn complete_url_dedupes_trailing_slash() {
+ assert_eq!(
+ complete_url(Some("https://api.openai.com/"), "gpt-4o-realtime-preview"),
+ "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview"
+ );
+ }
+
+ #[test]
+ fn complete_url_custom_base() {
+ assert_eq!(
+ complete_url(Some("https://oai.azure.example"), "gpt-4o-realtime-preview"),
+ "wss://oai.azure.example/v1/realtime?model=gpt-4o-realtime-preview"
+ );
+ }
+
+ #[test]
+ fn complete_url_preserves_existing_wss_scheme() {
+ assert_eq!(
+ complete_url(Some("wss://api.openai.com"), "gpt-realtime"),
+ "wss://api.openai.com/v1/realtime?model=gpt-realtime"
+ );
+ }
+
+ #[test]
+ fn complete_url_bare_host_defaults_to_wss() {
+ assert_eq!(
+ complete_url(Some("api.openai.com"), "gpt-realtime"),
+ "wss://api.openai.com/v1/realtime?model=gpt-realtime"
+ );
+ }
+
+ #[test]
+ fn complete_url_percent_encodes_model_space() {
+ assert_eq!(
+ complete_url(None, "gpt 4o"),
+ "wss://api.openai.com/v1/realtime?model=gpt%204o"
+ );
+ }
+
+ #[test]
+ fn transform_realtime_request_passthrough_preserves_event() {
+ let event: RealtimeEvent =
+ serde_json::from_str(r#"{"type":"session.update","session":{"voice":"alloy"}}"#)
+ .expect("valid event");
+ let result =
+ transform_realtime_request(&event, "gpt-realtime").expect("passthrough is infallible");
+ assert_eq!(result.events, vec![event]);
+ }
+
+ #[test]
+ fn transform_realtime_response_passthrough_preserves_event() {
+ let event: RealtimeEvent =
+ serde_json::from_str(r#"{"type":"response.output_audio.delta","delta":"abc=="}"#)
+ .expect("valid event");
+ let result =
+ transform_realtime_response(&event, "gpt-realtime").expect("passthrough is infallible");
+ assert_eq!(result.events, vec![event]);
+ }
+}
diff --git a/litellm-rust/crates/providers/src/realtime.rs b/litellm-rust/crates/providers/src/realtime.rs
new file mode 100644
index 00000000000..398158f6dba
--- /dev/null
+++ b/litellm-rust/crates/providers/src/realtime.rs
@@ -0,0 +1,374 @@
+//! End-to-end OpenAI realtime invocation.
+//!
+//! The host-facing entry point, mirroring `providers::ocr::run_ocr`: open the
+//! WebSocket to OpenAI, then splice a client realtime stream to the upstream,
+//! driving typed events through the pure `OPENAI_REALTIME_CONFIG` transforms.
+//! Network, auth header, key resolution, and wire (de)serialization live here so
+//! the `transformation` module stays pure and typed.
+//!
+//! The dial and splice steps are factored out ([`dial_upstream`], [`splice`]) so
+//! the connection pool ([`crate::realtime_pool`]) can pre-establish an upstream,
+//! buffer its `session.created`, and later hand the live socket to the same
+//! splice loop a fresh dial uses.
+
+use std::time::Duration;
+
+use futures_util::stream::{SplitSink, SplitStream};
+use futures_util::{Sink, SinkExt, Stream, StreamExt};
+use litellm_core::error::CoreError;
+use litellm_core::realtime::transformation::RealtimeProviderConfig;
+use litellm_core::realtime::types::RealtimeEvent;
+use litellm_core::CoreResult;
+use tokio::net::TcpStream;
+use tokio_tungstenite::tungstenite::client::IntoClientRequest;
+use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
+use tokio_tungstenite::tungstenite::http::HeaderValue;
+use tokio_tungstenite::tungstenite::Message;
+use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream};
+
+use crate::openai::realtime::transformation::OPENAI_REALTIME_CONFIG;
+
+/// Environment variable holding the OpenAI API key (last-resort fallback).
+const OPENAI_API_KEY_ENV: &str = "OPENAI_API_KEY";
+
+const MISSING_KEY_MESSAGE: &str = "Missing OpenAI API Key - a realtime call is being made but no key was passed via params or the OPENAI_API_KEY environment variable";
+
+/// Default **idle** timeout: if neither side sends a frame for this long, the
+/// session is reaped. It resets on any activity, so it does not cap a healthy
+/// (continuously streaming) session — it only frees a stalled one (e.g. a
+/// half-open upstream that keeps the socket open but stops sending).
+const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 300;
+
+/// The concrete upstream WebSocket type (TLS or plain). Shared by the dial path
+/// and the pool so warm sockets and fresh sockets are the exact same type.
+pub type UpstreamWs = WebSocketStream>;
+pub(crate) type UpstreamTx = SplitSink;
+pub(crate) type UpstreamRx = SplitStream;
+
+/// Resolve the OpenAI API key from the explicit param or the environment.
+///
+/// Blank/whitespace values are treated as absent (guard at resolution time).
+pub(crate) fn resolve_api_key(api_key: Option<&str>) -> CoreResult {
+ api_key
+ .map(str::trim)
+ .filter(|key| !key.is_empty())
+ .map(str::to_string)
+ .or_else(|| {
+ std::env::var(OPENAI_API_KEY_ENV)
+ .ok()
+ .filter(|key| !key.trim().is_empty())
+ })
+ .ok_or_else(|| CoreError::Auth(MISSING_KEY_MESSAGE.to_string()))
+}
+
+/// Open the upstream WebSocket to OpenAI for `(model, api_key, api_base)`.
+///
+/// This is the dial half of [`realtime`], factored out so the pool can
+/// pre-establish sockets ahead of any client. `api_key` here is already resolved
+/// (non-blank) — the pool resolves it once when it is created.
+pub(crate) async fn dial_upstream(
+ model: &str,
+ api_key: &str,
+ api_base: Option<&str>,
+) -> CoreResult {
+ let url = OPENAI_REALTIME_CONFIG.complete_url(api_base, model);
+
+ let mut request = url
+ .as_str()
+ .into_client_request()
+ .map_err(|err| CoreError::Network(err.to_string()))?;
+ // GA realtime: only Authorization. The legacy OpenAI-Beta header triggers
+ // beta_api_shape_disabled, so we do not send it.
+ request.headers_mut().insert(
+ AUTHORIZATION,
+ HeaderValue::from_str(&format!("Bearer {api_key}"))
+ .map_err(|err| CoreError::Auth(err.to_string()))?,
+ );
+
+ let (upstream, _response) = connect_async(request)
+ .await
+ .map_err(|err| CoreError::Network(err.to_string()))?;
+ Ok(upstream)
+}
+
+/// Read the next text frame from the upstream and decode it as a typed event.
+///
+/// Used by the pool to pre-read OpenAI's unprompted `session.created`. Returns an
+/// error on a non-text frame, a closed socket, or undecodable JSON so the pool can
+/// discard a misbehaving socket rather than warm it.
+pub(crate) async fn read_event(upstream_rx: &mut UpstreamRx) -> CoreResult {
+ loop {
+ let message = upstream_rx
+ .next()
+ .await
+ .ok_or_else(|| CoreError::Network("upstream closed before first event".to_string()))?
+ .map_err(|err| CoreError::Network(err.to_string()))?;
+ match message {
+ Message::Text(text) => {
+ return serde_json::from_str(&text)
+ .map_err(|err| CoreError::InvalidResponse(err.to_string()));
+ }
+ // Ignore protocol frames (ping/pong) while waiting for the first event.
+ Message::Ping(_) | Message::Pong(_) => continue,
+ Message::Close(_) => {
+ return Err(CoreError::Network(
+ "upstream closed before first event".to_string(),
+ ))
+ }
+ _ => continue,
+ }
+ }
+}
+
+/// Splice an already-connected upstream to the client streams.
+///
+/// `prelude` is relayed to the client first (the pool passes the buffered
+/// `session.created` here; the fresh-dial path passes `None` and lets the upstream
+/// deliver it). Then a single select loop forwards both directions through the
+/// transforms until either side closes or the idle timeout fires.
+#[allow(clippy::too_many_arguments)]
+pub(crate) async fn splice(
+ model: &str,
+ mut upstream_tx: UpstreamTx,
+ mut upstream_rx: UpstreamRx,
+ prelude: Option,
+ idle_timeout: Option,
+ mut client_in: In,
+ mut client_out: Out,
+) -> CoreResult<()>
+where
+ In: Stream + Unpin + Send,
+ Out: Sink + Unpin + Send,
+ >::Error: std::fmt::Display,
+{
+ let config = &OPENAI_REALTIME_CONFIG;
+
+ // Relay a buffered backend event (warm handoff's session.created) first, so a
+ // warm session looks identical to a fresh one from the client's view.
+ if let Some(event) = prelude {
+ for outbound in config.transform_realtime_response(&event, model)?.events {
+ client_out
+ .send(outbound)
+ .await
+ .map_err(|err| CoreError::Network(err.to_string()))?;
+ }
+ }
+
+ let idle = idle_timeout.unwrap_or(Duration::from_secs(DEFAULT_IDLE_TIMEOUT_SECS));
+
+ // One loop forwarding both directions. The `sleep(idle)` arm is rebuilt every
+ // iteration, so any frame (either way) resets it — it fires only when the
+ // session has been fully idle for `idle`, reaping a stalled connection
+ // (task + upstream TCP socket) instead of leaking it.
+ loop {
+ tokio::select! {
+ // client -> upstream
+ client_event = client_in.next() => {
+ let Some(event) = client_event else { break }; // client disconnected
+ for outbound in config.transform_realtime_request(&event, model)?.events {
+ let payload = serde_json::to_string(&outbound)
+ .map_err(|err| CoreError::InvalidResponse(err.to_string()))?;
+ upstream_tx
+ .send(Message::Text(payload))
+ .await
+ .map_err(|err| CoreError::Network(err.to_string()))?;
+ }
+ }
+ // upstream -> client
+ upstream_message = upstream_rx.next() => {
+ let Some(message) = upstream_message else { break }; // upstream closed
+ match message.map_err(|err| CoreError::Network(err.to_string()))? {
+ Message::Text(text) => {
+ let event: RealtimeEvent = serde_json::from_str(&text)
+ .map_err(|err| CoreError::InvalidResponse(err.to_string()))?;
+ for outbound in config.transform_realtime_response(&event, model)?.events {
+ client_out
+ .send(outbound)
+ .await
+ .map_err(|err| CoreError::Network(err.to_string()))?;
+ }
+ }
+ Message::Close(_) => break,
+ _ => {}
+ }
+ }
+ // idle timeout: no activity from either side within `idle`
+ _ = tokio::time::sleep(idle) => break,
+ }
+ }
+ Ok(())
+}
+
+/// Splice a client realtime stream to OpenAI: forward client events upstream
+/// (via `transform_realtime_request`) and backend events downstream (via
+/// `transform_realtime_response`). Returns when either side closes.
+///
+/// Generic over the client transport (typed events) so this crate stays
+/// framework-agnostic; the gateway adapts its axum socket to these. This is the
+/// fresh-dial path: dial, then splice. The pool's warm-handoff path skips the dial
+/// and calls [`splice`] directly with a buffered `session.created`.
+pub async fn realtime(
+ model: &str,
+ api_key: Option<&str>,
+ api_base: Option<&str>,
+ idle_timeout: Option,
+ client_in: In,
+ client_out: Out,
+) -> CoreResult<()>
+where
+ In: Stream + Unpin + Send,
+ Out: Sink + Unpin + Send,
+ >::Error: std::fmt::Display,
+{
+ let api_key = resolve_api_key(api_key)?;
+ let upstream = dial_upstream(model, &api_key, api_base).await?;
+ let (upstream_tx, upstream_rx) = upstream.split();
+ splice(
+ model,
+ upstream_tx,
+ upstream_rx,
+ None,
+ idle_timeout,
+ client_in,
+ client_out,
+ )
+ .await
+}
+
+/// Splice a pre-warmed upstream (taken from [`crate::realtime_pool`]) to the
+/// client. Relays the buffered `session.created` first, then splices exactly like
+/// the fresh-dial path — so a warm session is indistinguishable from a fresh one.
+pub async fn realtime_warm(
+ model: &str,
+ handoff: crate::realtime_pool::WarmHandoff,
+ idle_timeout: Option,
+ client_in: In,
+ client_out: Out,
+) -> CoreResult<()>
+where
+ In: Stream + Unpin + Send,
+ Out: Sink + Unpin + Send,
+ >::Error: std::fmt::Display,
+{
+ splice(
+ model,
+ handoff.tx,
+ handoff.rx,
+ Some(handoff.session_created),
+ idle_timeout,
+ client_in,
+ client_out,
+ )
+ .await
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn event(raw: &str) -> RealtimeEvent {
+ serde_json::from_str(raw).expect("valid event json")
+ }
+
+ #[test]
+ fn resolve_api_key_prefers_param_then_blank_falls_through() {
+ assert_eq!(resolve_api_key(Some("sk-test")).unwrap(), "sk-test");
+ // A blank param with no env set should error.
+ if std::env::var(OPENAI_API_KEY_ENV).is_err() {
+ assert!(resolve_api_key(Some(" ")).is_err());
+ }
+ }
+
+ /// Live end-to-end check against OpenAI. Ignored by default (CI never runs
+ /// it); run explicitly with `OPENAI_API_KEY` set:
+ /// `cargo test -p litellm-providers realtime_invokes_openai -- --ignored --nocapture`
+ #[tokio::test]
+ #[ignore = "hits the live OpenAI realtime API; needs OPENAI_API_KEY"]
+ async fn realtime_invokes_openai_and_responds() {
+ use futures_channel::mpsc;
+
+ let key =
+ std::env::var(OPENAI_API_KEY_ENV).expect("set OPENAI_API_KEY to run this ignored test");
+
+ // client -> provider (we hold `client_tx` to push events upstream)
+ let (mut client_tx, client_in) = mpsc::unbounded::();
+ // provider -> client (we hold `backend_rx` to read backend events)
+ let (client_out, mut backend_rx) = mpsc::unbounded::();
+
+ // Clone the key so the spawned task owns its `String` (no borrow across await).
+ let key_owned = key.clone();
+ let call = tokio::spawn(async move {
+ realtime(
+ "gpt-realtime",
+ Some(&key_owned),
+ None,
+ None,
+ client_in,
+ client_out,
+ )
+ .await
+ });
+
+ // 1. First backend event should be session.created.
+ let first = tokio::time::timeout(Duration::from_secs(30), backend_rx.next())
+ .await
+ .expect("timed out waiting for session.created")
+ .expect("backend stream closed before session.created");
+ assert_eq!(
+ first.event_type, "session.created",
+ "expected session.created, got: {}",
+ first.event_type
+ );
+
+ // 2. Ask for a short audio response.
+ client_tx
+ .send(event(
+ r#"{"type":"conversation.item.create","item":{"type":"message","role":"user","content":[{"type":"input_text","text":"Say hi."}]}}"#,
+ ))
+ .await
+ .expect("send conversation.item.create");
+ client_tx
+ .send(event(r#"{"type":"response.create"}"#))
+ .await
+ .expect("send response.create");
+
+ // 3. Read backend events; require a non-empty audio delta, then response.done.
+ let mut saw_audio_delta = false;
+ let mut saw_done = false;
+ for _ in 0..500 {
+ let next = tokio::time::timeout(Duration::from_secs(30), backend_rx.next()).await;
+ let event = match next {
+ Ok(Some(event)) => event,
+ Ok(None) => break,
+ Err(_) => panic!("timed out waiting for backend events"),
+ };
+ match event.event_type.as_str() {
+ "response.output_audio.delta" => {
+ let delta = event
+ .data
+ .get("delta")
+ .and_then(|value| value.as_str())
+ .unwrap_or("");
+ if !delta.is_empty() {
+ saw_audio_delta = true;
+ }
+ }
+ "response.done" => {
+ saw_done = true;
+ break;
+ }
+ _ => {}
+ }
+ }
+
+ assert!(
+ saw_audio_delta,
+ "expected a response.output_audio.delta with non-empty delta"
+ );
+ assert!(saw_done, "expected a response.done event");
+
+ // Drop the client sender so the provider's to_upstream side finishes.
+ drop(client_tx);
+ let _ = call.await;
+ }
+}
diff --git a/litellm-rust/crates/providers/src/realtime_pool.rs b/litellm-rust/crates/providers/src/realtime_pool.rs
new file mode 100644
index 00000000000..1b1fc8112c5
--- /dev/null
+++ b/litellm-rust/crates/providers/src/realtime_pool.rs
@@ -0,0 +1,712 @@
+//! Pre-warmed upstream realtime connection pool.
+//!
+//! The gateway's realtime overhead lives entirely in session establishment: on
+//! every client connect it dials a fresh upstream WS to OpenAI and waits for
+//! `session.created` before it can serve. This pool keeps a small set of upstream
+//! sockets **already connected and already past `session.created`** so a connect
+//! can be served from a warm socket and the handshake is off the critical path.
+//!
+//! Layering: this stays in `providers` (axum-free) next to the dial/splice it
+//! reuses. The gateway holds an `Arc` in its state and asks for a
+//! warm socket per connect; on a miss it fresh-dials exactly as before. The pool
+//! is a latency optimization, never a correctness dependency — see the gateway's
+//! `src/routes/realtime/README.md`.
+//!
+//! ## Caveats (enforced here)
+//! - One warm socket serves exactly one session (realtime isn't multiplexed), so
+//! the pool is sized to the connect *rate*, not concurrent connections.
+//! - `session.created` is pre-read once and buffered; nothing else is read from a
+//! warm socket before handoff, so a warm session starts at OpenAI defaults just
+//! like a fresh one (`session.update` semantics unchanged).
+//! - Warm sockets are short-lived (`max_idle`) and liveness-checked at handoff to
+//! bound idle billing / dodge OpenAI's idle timeout.
+//! - On miss or dead socket the caller fresh-dials; the pool never blocks or fails
+//! a connect because it is empty.
+
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+use std::time::{Duration, Instant};
+
+use futures_util::StreamExt;
+use litellm_core::realtime::types::RealtimeEvent;
+use litellm_core::CoreResult;
+
+use crate::realtime::{
+ dial_upstream, read_event, resolve_api_key, UpstreamRx, UpstreamTx, UpstreamWs,
+};
+
+/// Default target warm sockets per key when pooling is enabled.
+pub const DEFAULT_POOL_SIZE: usize = 4;
+
+/// Default max time a warm socket may sit before it is closed and replaced.
+pub const DEFAULT_MAX_IDLE: Duration = Duration::from_secs(30);
+
+/// Env var: target warm sockets per key. `0` disables pooling (fresh-dial only).
+pub const POOL_SIZE_ENV: &str = "REALTIME_POOL_SIZE";
+
+/// Env var: max warm-socket idle lifetime, in seconds.
+pub const MAX_IDLE_ENV: &str = "REALTIME_POOL_MAX_IDLE_SECS";
+
+/// How often the background replenisher wakes to top up and reap stale sockets.
+const REPLENISH_TICK: Duration = Duration::from_millis(250);
+
+/// Backoff floor after a key's warm-up dials all fail. The first failed pass
+/// waits this long before retrying that key.
+const BACKOFF_BASE: Duration = Duration::from_millis(500);
+
+/// Backoff ceiling. A key that keeps failing (invalid credentials, an
+/// unreachable upstream) is retried at most once per this interval — instead of
+/// firing `needed` concurrent TLS dials every 250 ms tick, which would hammer
+/// the upstream and risk rate-limit exhaustion that degrades valid cold-path
+/// traffic. Backoff resets the moment a dial for the key succeeds.
+const BACKOFF_MAX: Duration = Duration::from_secs(30);
+
+/// Identifies an upstream connection: the tuple that fully determines the dial.
+/// `api_key` is included so a warm socket is only ever reused for the same key
+/// (no cross-tenant reuse).
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct UpstreamKey {
+ pub model: String,
+ pub api_key: String,
+ pub api_base: Option,
+}
+
+impl std::fmt::Debug for UpstreamKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("UpstreamKey")
+ .field("model", &self.model)
+ .field("api_key", &"[REDACTED]")
+ .field("api_base", &self.api_base)
+ .finish()
+ }
+}
+
+/// A warm upstream: split halves + the buffered `session.created` + when it was
+/// warmed (for `max_idle` expiry).
+struct WarmConnection {
+ tx: UpstreamTx,
+ rx: UpstreamRx,
+ session_created: RealtimeEvent,
+ warmed_at: Instant,
+}
+
+/// A live upstream taken from the pool, ready to splice. The caller relays
+/// `session_created` to the client first, then splices `(tx, rx)` as usual.
+pub struct WarmHandoff {
+ pub tx: UpstreamTx,
+ pub rx: UpstreamRx,
+ pub session_created: RealtimeEvent,
+}
+
+/// Pool configuration, resolved once at startup from the environment.
+#[derive(Clone, Copy, Debug)]
+pub struct PoolConfig {
+ /// Target warm sockets per key. `0` disables pooling.
+ pub target_size: usize,
+ /// Max time a warm socket may sit before it is closed and replaced.
+ pub max_idle: Duration,
+}
+
+impl Default for PoolConfig {
+ fn default() -> Self {
+ Self {
+ target_size: DEFAULT_POOL_SIZE,
+ max_idle: DEFAULT_MAX_IDLE,
+ }
+ }
+}
+
+impl PoolConfig {
+ /// Read config from the environment, falling back to defaults. An invalid
+ /// value warns and uses the default rather than failing startup.
+ pub fn from_env() -> Self {
+ let target_size = match std::env::var(POOL_SIZE_ENV) {
+ Ok(raw) => raw.trim().parse().unwrap_or_else(|_| {
+ eprintln!("warning: {POOL_SIZE_ENV}={raw:?} is not a valid size; using {DEFAULT_POOL_SIZE}");
+ DEFAULT_POOL_SIZE
+ }),
+ Err(_) => DEFAULT_POOL_SIZE,
+ };
+ let max_idle = match std::env::var(MAX_IDLE_ENV) {
+ Ok(raw) => raw
+ .trim()
+ .parse()
+ .map(Duration::from_secs)
+ .unwrap_or_else(|_| {
+ eprintln!(
+ "warning: {MAX_IDLE_ENV}={raw:?} is not a valid number of seconds; using {}s",
+ DEFAULT_MAX_IDLE.as_secs()
+ );
+ DEFAULT_MAX_IDLE
+ }),
+ Err(_) => DEFAULT_MAX_IDLE,
+ };
+ Self {
+ target_size,
+ max_idle,
+ }
+ }
+
+ /// Whether pooling is on (`target_size > 0`).
+ pub fn enabled(&self) -> bool {
+ self.target_size > 0
+ }
+}
+
+/// Per-key warm sockets, behind a single `Mutex`. Realtime warm sockets are few
+/// (the pool is small), so a plain mutex over a `VecDeque`-ish `Vec` is simpler
+/// and faster than sharding; contention is negligible at this scale.
+type Warm = HashMap>;
+
+/// Per-key replenish backoff. Absent (or `consecutive_failures == 0`) means the
+/// key is healthy and replenished every tick. After a pass whose dials all fail,
+/// `retry_after` is pushed out with exponential backoff so a broken key (invalid
+/// credentials, unreachable upstream) is not re-dialed on every 250 ms tick.
+#[derive(Default)]
+struct Backoff {
+ /// Don't attempt warm-up dials for this key until this instant. `None` =
+ /// eligible now.
+ retry_after: Option,
+ consecutive_failures: u32,
+}
+
+type Backoffs = HashMap;
+
+/// Pre-warmed upstream realtime connection pool.
+///
+/// Cheap to clone-via-`Arc`. The background replenisher is spawned by
+/// [`RealtimePool::spawn`]; a pool built with [`RealtimePool::disabled`] never
+/// warms anything and every `take` misses (callers fresh-dial).
+pub struct RealtimePool {
+ config: PoolConfig,
+ warm: Mutex,
+ /// Per-key replenish backoff so a broken key doesn't trigger unbounded
+ /// concurrent dials every tick. Separate lock from `warm` so the request
+ /// hot path (`take`) never contends on it.
+ backoff: Mutex,
+}
+
+impl RealtimePool {
+ /// A disabled pool: no background task, every `take` returns `None`.
+ pub fn disabled() -> Arc {
+ Arc::new(Self {
+ config: PoolConfig {
+ target_size: 0,
+ ..PoolConfig::default()
+ },
+ warm: Mutex::new(HashMap::new()),
+ backoff: Mutex::new(HashMap::new()),
+ })
+ }
+
+ /// Build a pool from config **without** the background replenisher. The pool
+ /// only warms when [`RealtimePool::warm_now`] is called. Used by deterministic
+ /// unit tests; production uses [`RealtimePool::spawn`].
+ #[cfg(test)]
+ fn new_unspawned(config: PoolConfig) -> Arc {
+ Arc::new(Self {
+ config,
+ warm: Mutex::new(HashMap::new()),
+ backoff: Mutex::new(HashMap::new()),
+ })
+ }
+
+ /// Build a pool from config and, if enabled, spawn the background replenisher.
+ /// Returns the shared handle the gateway stores in its state.
+ pub fn spawn(config: PoolConfig) -> Arc {
+ let pool = Arc::new(Self {
+ config,
+ warm: Mutex::new(HashMap::new()),
+ backoff: Mutex::new(HashMap::new()),
+ });
+ if config.enabled() {
+ let weak = Arc::downgrade(&pool);
+ tokio::spawn(async move {
+ let mut tick = tokio::time::interval(REPLENISH_TICK);
+ tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
+ loop {
+ tick.tick().await;
+ // Stop once the gateway has dropped its handle.
+ let Some(pool) = weak.upgrade() else { break };
+ pool.replenish_all().await;
+ }
+ });
+ }
+ pool
+ }
+
+ /// Resolved config (test/inspection).
+ pub fn config(&self) -> PoolConfig {
+ self.config
+ }
+
+ /// Register a key so the replenisher starts warming it. Idempotent. The
+ /// gateway calls this once per known deployment at startup; the pool only
+ /// warms keys it has seen, so it never dials a model nobody asked for.
+ pub fn register(&self, key: UpstreamKey) {
+ if !self.config.enabled() {
+ return;
+ }
+ self.warm.lock().unwrap().entry(key).or_default();
+ }
+
+ /// Take a warm, live socket for `key`, or `None` on miss / dead socket.
+ ///
+ /// Pops the freshest non-expired socket and liveness-checks it; a socket that
+ /// is too old or already dead is dropped (closing it) and the next candidate
+ /// tried. Never blocks: if nothing warm is live, returns `None` so the caller
+ /// fresh-dials.
+ pub fn take(&self, key: &UpstreamKey) -> Option {
+ if !self.config.enabled() {
+ return None;
+ }
+ loop {
+ let mut candidate = {
+ let mut warm = self.warm.lock().unwrap();
+ let bucket = warm.get_mut(key)?;
+ bucket.pop()?
+ };
+ // Discard sockets past their warm lifetime (idle-billing guard).
+ if candidate.warmed_at.elapsed() > self.config.max_idle {
+ continue; // drops `candidate`, closing the socket
+ }
+ // Liveness: a non-blocking check that the socket hasn't already
+ // delivered a Close/Err. A warm socket should be silent after
+ // session.created, so anything pending means it is unhealthy.
+ if is_dead(&mut candidate.rx) {
+ continue;
+ }
+ return Some(WarmHandoff {
+ tx: candidate.tx,
+ rx: candidate.rx,
+ session_created: candidate.session_created,
+ });
+ }
+ }
+
+ /// One replenish pass over every registered key: reap stale sockets, then
+ /// dial up to `target_size`. Dials run concurrently; failures are swallowed
+ /// (a key that can't be warmed just keeps fresh-dialing on the request path)
+ /// and put the key into exponential backoff so a broken key isn't re-dialed
+ /// on every tick.
+ async fn replenish_all(&self) {
+ let keys: Vec = { self.warm.lock().unwrap().keys().cloned().collect() };
+ for key in keys {
+ self.reap_stale(&key);
+ // Skip keys still in backoff from a prior all-failed pass — this is
+ // what bounds dials against an invalid/unreachable key to once per
+ // `BACKOFF_MAX` instead of `needed` dials every 250 ms tick.
+ if self.in_backoff(&key) {
+ continue;
+ }
+ let needed = {
+ let warm = self.warm.lock().unwrap();
+ let have = warm.get(&key).map(Vec::len).unwrap_or(0);
+ self.config.target_size.saturating_sub(have)
+ };
+ if needed == 0 {
+ continue;
+ }
+ // Dial the missing sockets CONCURRENTLY. A sequential loop here makes
+ // a full refill cost `needed × handshake` (~needed × 350 ms), which
+ // can't keep up with a high connect rate — the pool drains faster
+ // than it refills and most connects miss. Firing the dials together
+ // refills in ~one handshake window, keeping warm supply ≈ peak
+ // concurrent connects so the sub-ms warm handoff becomes the median,
+ // not the lucky-hit tail.
+ let dials = (0..needed).map(|_| warm_one(&key));
+ let results = futures_util::future::join_all(dials).await;
+ let mut any_ok = false;
+ // `.flatten()` keeps only the successful dials; a key that can't be
+ // warmed just keeps fresh-dialing on the request path.
+ for conn in results.into_iter().flatten() {
+ any_ok = true;
+ self.warm
+ .lock()
+ .unwrap()
+ .entry(key.clone())
+ .or_default()
+ .push(conn);
+ }
+ // Reset backoff on any success; otherwise grow it. We only ever enter
+ // backoff when a pass that *attempted* dials produced none — a `needed
+ // == 0` pass is handled by the `continue` above and never touches it.
+ self.record_replenish_outcome(&key, any_ok);
+ }
+ }
+
+ /// Whether `key` is currently in a backoff window (a prior pass failed and
+ /// the retry time hasn't arrived). Eligible keys are pruned from the backoff
+ /// map so it doesn't grow unbounded for healthy keys.
+ fn in_backoff(&self, key: &UpstreamKey) -> bool {
+ let mut backoff = self.backoff.lock().unwrap();
+ match backoff.get(key).and_then(|b| b.retry_after) {
+ Some(retry_after) if Instant::now() < retry_after => true,
+ Some(_) => {
+ // Window elapsed — allow the attempt. Keep the failure count so a
+ // still-broken key backs off further, but clear the gate so this
+ // tick proceeds.
+ if let Some(b) = backoff.get_mut(key) {
+ b.retry_after = None;
+ }
+ false
+ }
+ None => false,
+ }
+ }
+
+ /// Update a key's backoff after a replenish attempt. Success clears it;
+ /// failure grows the retry delay exponentially up to `BACKOFF_MAX`.
+ fn record_replenish_outcome(&self, key: &UpstreamKey, any_ok: bool) {
+ let mut backoff = self.backoff.lock().unwrap();
+ if any_ok {
+ backoff.remove(key);
+ return;
+ }
+ let entry = backoff.entry(key.clone()).or_default();
+ entry.consecutive_failures = entry.consecutive_failures.saturating_add(1);
+ // Exponential: BASE * 2^(failures-1), saturating at MAX. `min` of the
+ // shift exponent keeps the doubling from overflowing.
+ let shift = (entry.consecutive_failures - 1).min(16);
+ let delay = BACKOFF_BASE.saturating_mul(1u32 << shift).min(BACKOFF_MAX);
+ entry.retry_after = Some(Instant::now() + delay);
+ }
+
+ /// Drop sockets past `max_idle` or already dead for a key.
+ fn reap_stale(&self, key: &UpstreamKey) {
+ let mut warm = self.warm.lock().unwrap();
+ if let Some(bucket) = warm.get_mut(key) {
+ bucket.retain_mut(|conn| {
+ conn.warmed_at.elapsed() <= self.config.max_idle && !is_dead(&mut conn.rx)
+ });
+ }
+ }
+
+ /// Test/inspection: number of warm sockets currently held for `key`.
+ #[cfg(test)]
+ pub fn warm_len(&self, key: &UpstreamKey) -> usize {
+ self.warm
+ .lock()
+ .unwrap()
+ .get(key)
+ .map(Vec::len)
+ .unwrap_or(0)
+ }
+
+ /// Test/inspection: consecutive replenish failures recorded for `key` (0 if
+ /// the key is healthy / has no backoff entry).
+ #[cfg(test)]
+ pub fn backoff_failures(&self, key: &UpstreamKey) -> u32 {
+ self.backoff
+ .lock()
+ .unwrap()
+ .get(key)
+ .map(|b| b.consecutive_failures)
+ .unwrap_or(0)
+ }
+
+ /// Test helper: synchronously warm `target_size` sockets for `key` (no
+ /// background task). Lets tests assert handoff behavior deterministically.
+ #[cfg(test)]
+ pub async fn warm_now(&self, key: &UpstreamKey) {
+ let needed = {
+ let warm = self.warm.lock().unwrap();
+ let have = warm.get(key).map(Vec::len).unwrap_or(0);
+ self.config.target_size.saturating_sub(have)
+ };
+ for _ in 0..needed {
+ if let Ok(conn) = warm_one(key).await {
+ self.warm
+ .lock()
+ .unwrap()
+ .entry(key.clone())
+ .or_default()
+ .push(conn);
+ }
+ }
+ }
+
+ /// Test helper: insert an already-built warm connection (used to inject a
+ /// dead socket and assert it is discarded at handoff).
+ #[cfg(test)]
+ fn insert_warm(&self, key: UpstreamKey, conn: WarmConnection) {
+ self.warm.lock().unwrap().entry(key).or_default().push(conn);
+ }
+}
+
+/// Dial one upstream and pre-read its `session.created` into a [`WarmConnection`].
+///
+/// `key.api_key` is already resolved (non-blank). The first frame OpenAI sends
+/// unprompted is `session.created`; we buffer exactly that and read nothing more.
+async fn warm_one(key: &UpstreamKey) -> CoreResult {
+ let upstream: UpstreamWs =
+ dial_upstream(&key.model, &key.api_key, key.api_base.as_deref()).await?;
+ let (tx, mut rx) = upstream.split();
+ let session_created = read_event(&mut rx).await?;
+ Ok(WarmConnection {
+ tx,
+ rx,
+ session_created,
+ warmed_at: Instant::now(),
+ })
+}
+
+/// Resolve a deployment's API key into the pool key, returning `None` when no key
+/// can be resolved (those deployments simply aren't pooled — the request path
+/// still fresh-dials and surfaces the auth error there).
+pub fn upstream_key(
+ model: &str,
+ api_key: Option<&str>,
+ api_base: Option<&str>,
+) -> Option {
+ let api_key = resolve_api_key(api_key).ok()?;
+ Some(UpstreamKey {
+ model: model.to_string(),
+ api_key,
+ api_base: api_base.map(str::to_string),
+ })
+}
+
+/// Non-blocking liveness check: poll the upstream once. A warm socket is silent
+/// after `session.created`, so a pending `Close`/`Err`/`None` means it is dead.
+/// A pending data frame (shouldn't happen pre-handoff) is also treated as
+/// unhealthy — we'd rather discard and fresh-dial than hand over a socket in an
+/// unexpected state. `Pending` (the healthy case) returns `false`.
+fn is_dead(rx: &mut UpstreamRx) -> bool {
+ use futures_util::task::noop_waker_ref;
+ use futures_util::Stream;
+ use std::pin::Pin;
+ use std::task::{Context, Poll};
+
+ let mut cx = Context::from_waker(noop_waker_ref());
+ match Pin::new(rx).poll_next(&mut cx) {
+ Poll::Pending => false,
+ Poll::Ready(None) => true,
+ Poll::Ready(Some(Err(_))) => true,
+ // Any frame arriving before handoff is unexpected for a silent warm
+ // socket; treat it as unhealthy.
+ Poll::Ready(Some(Ok(_))) => true,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use futures_util::SinkExt;
+ use std::net::SocketAddr;
+ use tokio::net::TcpListener;
+ use tokio_tungstenite::tungstenite::Message;
+
+ /// An in-process fake OpenAI realtime WS server. On connect it sends
+ /// `session.created`; on `response.create` it sends `response.created` +
+ /// `response.output_audio.delta` + `response.done`. Returns its `ws://` base.
+ async fn spawn_fake_openai() -> String {
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let addr: SocketAddr = listener.local_addr().unwrap();
+ tokio::spawn(async move {
+ while let Ok((stream, _)) = listener.accept().await {
+ tokio::spawn(handle_fake_conn(stream));
+ }
+ });
+ format!("ws://{addr}")
+ }
+
+ async fn handle_fake_conn(stream: tokio::net::TcpStream) {
+ let mut ws = match tokio_tungstenite::accept_async(stream).await {
+ Ok(ws) => ws,
+ Err(_) => return,
+ };
+ // Unprompted session.created, exactly like OpenAI.
+ let _ = ws
+ .send(Message::Text(
+ r#"{"type":"session.created","session":{"id":"sess_fake"}}"#.to_string(),
+ ))
+ .await;
+ while let Some(Ok(msg)) = ws.next().await {
+ if let Message::Text(text) = msg {
+ if text.contains("response.create") {
+ for frame in [
+ r#"{"type":"response.created"}"#,
+ r#"{"type":"response.output_audio.delta","delta":"AAAA"}"#,
+ r#"{"type":"response.done"}"#,
+ ] {
+ let _ = ws.send(Message::Text(frame.to_string())).await;
+ }
+ }
+ }
+ }
+ }
+
+ fn test_config() -> PoolConfig {
+ PoolConfig {
+ target_size: 2,
+ max_idle: Duration::from_secs(30),
+ }
+ }
+
+ fn key_for(base: &str) -> UpstreamKey {
+ UpstreamKey {
+ model: "gpt-realtime".to_string(),
+ api_key: "sk-test".to_string(),
+ api_base: Some(base.to_string()),
+ }
+ }
+
+ #[tokio::test]
+ async fn warm_handoff_relays_buffered_session_created() {
+ let base = spawn_fake_openai().await;
+ let pool = RealtimePool::new_unspawned(test_config());
+ let key = key_for(&base);
+ pool.register(key.clone());
+ pool.warm_now(&key).await;
+ assert_eq!(pool.warm_len(&key), 2);
+
+ let handoff = pool.take(&key).expect("a warm socket should be available");
+ assert_eq!(handoff.session_created.event_type, "session.created");
+ assert_eq!(
+ handoff
+ .session_created
+ .data
+ .get("session")
+ .and_then(|s| s.get("id"))
+ .and_then(|v| v.as_str()),
+ Some("sess_fake")
+ );
+ // Taking one leaves one.
+ assert_eq!(pool.warm_len(&key), 1);
+ }
+
+ #[tokio::test]
+ async fn pool_miss_returns_none_for_fresh_dial_fallback() {
+ let base = spawn_fake_openai().await;
+ let pool = RealtimePool::new_unspawned(test_config());
+ let key = key_for(&base);
+ // Registered but never warmed → empty bucket → miss.
+ pool.register(key.clone());
+ assert!(pool.take(&key).is_none());
+
+ // Unknown key → miss.
+ let other = key_for("ws://127.0.0.1:1");
+ assert!(pool.take(&other).is_none());
+ }
+
+ #[tokio::test]
+ async fn disabled_pool_never_hands_off() {
+ let pool = RealtimePool::disabled();
+ let key = key_for("ws://127.0.0.1:1");
+ pool.register(key.clone());
+ assert_eq!(pool.warm_len(&key), 0);
+ assert!(pool.take(&key).is_none());
+ }
+
+ #[tokio::test]
+ async fn dead_warm_socket_is_discarded() {
+ let base = spawn_fake_openai().await;
+ let pool = RealtimePool::new_unspawned(test_config());
+ let key = key_for(&base);
+ pool.register(key.clone());
+
+ // Build one real warm connection, then kill the upstream by dropping the
+ // server side: easiest is to dial, read session.created, then close our
+ // own rx's peer. Instead we forge "dead" via an already-closed socket:
+ // dial a connection and immediately send a Close from the client side so
+ // the server closes back, then warm it. Simpler: warm normally, then
+ // mark it stale by backdating warmed_at past max_idle and confirm it's
+ // dropped — that exercises the same discard path.
+ let mut conn = warm_one(&key).await.expect("warm one");
+ conn.warmed_at = Instant::now() - Duration::from_secs(3600); // past max_idle
+ pool.insert_warm(key.clone(), conn);
+ assert_eq!(pool.warm_len(&key), 1);
+
+ // take() must discard the stale socket and report a miss.
+ assert!(pool.take(&key).is_none());
+ assert_eq!(pool.warm_len(&key), 0);
+ }
+
+ #[tokio::test]
+ async fn background_replenisher_tops_up_registered_key() {
+ let base = spawn_fake_openai().await;
+ let pool = RealtimePool::spawn(test_config());
+ let key = key_for(&base);
+ pool.register(key.clone());
+
+ // Wait (bounded) for the background task to reach the target size.
+ let mut warmed = 0;
+ for _ in 0..40 {
+ tokio::time::sleep(Duration::from_millis(50)).await;
+ warmed = pool.warm_len(&key);
+ if warmed >= test_config().target_size {
+ break;
+ }
+ }
+ assert_eq!(
+ warmed,
+ test_config().target_size,
+ "background replenisher should warm up to target_size"
+ );
+ let handoff = pool.take(&key).expect("a warm socket should be available");
+ assert_eq!(handoff.session_created.event_type, "session.created");
+ }
+
+ #[tokio::test]
+ async fn closed_upstream_socket_is_detected_dead() {
+ // A genuinely dead socket: dial the fake, read session.created, then drop
+ // the server by closing from our side and waiting for the close to land.
+ let base = spawn_fake_openai().await;
+ let pool = RealtimePool::new_unspawned(test_config());
+ let key = key_for(&base);
+ pool.register(key.clone());
+
+ let mut conn = warm_one(&key).await.expect("warm one");
+ // Close the upstream from the client side; the server echoes a close.
+ let _ = conn.tx.send(Message::Close(None)).await;
+ // Give the close a moment to arrive on rx.
+ tokio::time::sleep(Duration::from_millis(50)).await;
+ pool.insert_warm(key.clone(), conn);
+
+ // Liveness check at take() should detect the close and discard it.
+ assert!(pool.take(&key).is_none());
+ assert_eq!(pool.warm_len(&key), 0);
+ }
+
+ #[tokio::test]
+ async fn broken_key_backs_off_instead_of_dialing_every_tick() {
+ // A key whose upstream is unreachable: every warm-up dial fails.
+ let pool = RealtimePool::new_unspawned(test_config());
+ let key = key_for("ws://127.0.0.1:1"); // nothing listens here
+ pool.register(key.clone());
+
+ // First pass attempts dials, they all fail → key enters backoff, no warm
+ // sockets, one recorded failure.
+ pool.replenish_all().await;
+ assert_eq!(pool.warm_len(&key), 0);
+ assert_eq!(pool.backoff_failures(&key), 1);
+ assert!(
+ pool.in_backoff(&key),
+ "a key whose dials all failed must be in backoff"
+ );
+
+ // An immediate next pass must be SKIPPED (still in the backoff window), so
+ // it does NOT fire another round of dials — the failure count is unchanged.
+ pool.replenish_all().await;
+ assert_eq!(
+ pool.backoff_failures(&key),
+ 1,
+ "replenish during the backoff window must not re-dial the broken key"
+ );
+ }
+
+ #[tokio::test]
+ async fn healthy_key_never_enters_backoff_and_clears_after_recovery() {
+ let base = spawn_fake_openai().await;
+ let pool = RealtimePool::new_unspawned(test_config());
+ let key = key_for(&base);
+ pool.register(key.clone());
+
+ // A reachable upstream: the pass succeeds, so the key is never backed off.
+ pool.replenish_all().await;
+ assert_eq!(pool.warm_len(&key), test_config().target_size);
+ assert_eq!(pool.backoff_failures(&key), 0);
+ assert!(!pool.in_backoff(&key));
+ }
+}
diff --git a/litellm-rust/crates/python-bridge/CLAUDE.md b/litellm-rust/crates/python-bridge/CLAUDE.md
new file mode 100644
index 00000000000..efa1a554c9c
--- /dev/null
+++ b/litellm-rust/crates/python-bridge/CLAUDE.md
@@ -0,0 +1,36 @@
+# CLAUDE.md
+
+Rules for `litellm-rust/crates/python-bridge`.
+
+## Responsibility
+
+`python-bridge` is the PyO3 boundary between Python LiteLLM and Rust transforms.
+Keep this crate thin. It adapts Python objects to Rust payloads and returns
+Python-compatible dictionaries.
+
+## Bridge Shape
+
+- Prefer one stable method per top-level LiteLLM route, for example
+ `ocr(payload)`.
+- Do not add one exported PyO3 function per provider helper unless there is a
+ measured reason.
+- Provider dispatch belongs in Rust route modules such as
+ `litellm_providers::ocr`, not in this PyO3 crate.
+- Python owns rollout state and fallback. Rust should return errors; Python
+ decides whether to raise or fall back.
+
+## Data Handling
+
+- OCR payloads can contain personal data and large base64 images. Do not log
+ payloads or provider responses.
+- Avoid copying large payloads more than needed. The current JSON round-trip is
+ acceptable for the first scaffold, but future performance work should evaluate
+ direct PyO3 conversion before expanding Rust coverage to image-heavy paths.
+- Do not expose raw Rust errors that include document contents or upstream
+ bodies.
+
+## Tests
+
+- `cargo test --workspace` must compile this crate.
+- Python tests must cover bridge disabled, bridge enabled, and module-missing
+ fallback behavior for every exposed route.
diff --git a/litellm-rust/crates/python-bridge/Cargo.toml b/litellm-rust/crates/python-bridge/Cargo.toml
new file mode 100644
index 00000000000..80b6478daac
--- /dev/null
+++ b/litellm-rust/crates/python-bridge/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "litellm-python-bridge"
+version = "0.1.0"
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[lib]
+name = "litellm_python_bridge"
+crate-type = ["cdylib"]
+
+[dependencies]
+litellm-core.workspace = true
+litellm-providers.workspace = true
+pyo3 = { workspace = true, features = ["extension-module"] }
+serde_json.workspace = true
diff --git a/litellm-rust/crates/python-bridge/src/gil.rs b/litellm-rust/crates/python-bridge/src/gil.rs
new file mode 100644
index 00000000000..dc1b591735c
--- /dev/null
+++ b/litellm-rust/crates/python-bridge/src/gil.rs
@@ -0,0 +1,32 @@
+//! GIL accounting.
+//!
+//! A single chokepoint for releasing the GIL around blocking work. Every
+//! blocking call in the bridge goes through [`release_gil`] instead of calling
+//! `Python::allow_threads` directly, so the release count stays accurate and we
+//! have one place to extend later (timing histograms, per-call labels, etc.).
+
+use std::sync::atomic::{AtomicU64, Ordering};
+
+use pyo3::prelude::*;
+
+/// Number of times the bridge has released the GIL since process start.
+static GIL_RELEASES: AtomicU64 = AtomicU64::new(0);
+
+/// Release the GIL around `f`, recording the release.
+///
+/// `f` must not touch any Python state — that is what makes releasing the GIL
+/// safe. Returning the value back to Python re-acquires the GIL at the call
+/// site, after `f` has finished.
+pub fn release_gil(py: Python<'_>, f: F) -> T
+where
+ F: FnOnce() -> T + Send,
+ T: Send,
+{
+ GIL_RELEASES.fetch_add(1, Ordering::Relaxed);
+ py.allow_threads(f)
+}
+
+/// Total GIL releases performed by the bridge so far.
+pub fn release_count() -> u64 {
+ GIL_RELEASES.load(Ordering::Relaxed)
+}
diff --git a/litellm-rust/crates/python-bridge/src/lib.rs b/litellm-rust/crates/python-bridge/src/lib.rs
new file mode 100644
index 00000000000..15e93f7b00c
--- /dev/null
+++ b/litellm-rust/crates/python-bridge/src/lib.rs
@@ -0,0 +1,100 @@
+use std::time::Duration;
+
+use litellm_core::error::CoreError;
+use litellm_providers::ocr::run_ocr;
+use pyo3::exceptions::{PyRuntimeError, PyValueError};
+use pyo3::prelude::*;
+use pyo3::types::{PyAny, PyDict};
+use serde_json::{Map, Value};
+
+mod gil;
+
+fn py_to_json(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult {
+ let json = py.import("json")?;
+ let encoded: String = json.call_method1("dumps", (value,))?.extract()?;
+ serde_json::from_str(&encoded).map_err(|err| PyValueError::new_err(err.to_string()))
+}
+
+fn json_to_py(py: Python<'_>, value: Value) -> PyResult> {
+ let json = py.import("json")?;
+ let encoded =
+ serde_json::to_string(&value).map_err(|err| PyValueError::new_err(err.to_string()))?;
+ Ok(json.call_method1("loads", (encoded,))?.unbind())
+}
+
+/// Map a core error to the closest Python exception. Caller-input problems
+/// (auth, bad types, missing fields) -> `ValueError`; everything else
+/// (network, upstream status, parse failures) -> `RuntimeError`.
+fn core_error_to_pyerr(err: CoreError) -> PyErr {
+ match err {
+ CoreError::Auth(message) => PyValueError::new_err(message),
+ CoreError::InvalidType { .. } | CoreError::MissingField(_) => {
+ PyValueError::new_err(err.to_string())
+ }
+ other => PyRuntimeError::new_err(other.to_string()),
+ }
+}
+
+/// Perform a Mistral OCR call end to end and return the response as a dict.
+#[pyfunction]
+#[pyo3(signature = (model, document, api_key=None, api_base=None, optional_params=None, timeout_seconds=None))]
+fn ocr(
+ py: Python<'_>,
+ model: String,
+ document: Py,
+ api_key: Option,
+ api_base: Option,
+ optional_params: Option>,
+ timeout_seconds: Option,
+) -> PyResult> {
+ let document = py_to_json(py, document.bind(py))?;
+
+ let optional_params = match optional_params {
+ Some(params) => match py_to_json(py, params.bind(py))? {
+ Value::Object(map) => map,
+ _ => return Err(PyValueError::new_err("optional_params must be a dict")),
+ },
+ None => Map::new(),
+ };
+
+ let timeout = timeout_seconds.and_then(|secs| {
+ if secs.is_finite() && secs > 0.0 {
+ Some(Duration::from_secs_f64(secs))
+ } else {
+ None
+ }
+ });
+
+ // Release the GIL during the blocking HTTP call (counted for observability).
+ let result = gil::release_gil(py, || {
+ run_ocr(
+ &model,
+ document,
+ api_key.as_deref(),
+ api_base.as_deref(),
+ optional_params,
+ timeout,
+ )
+ });
+
+ match result {
+ Ok(value) => json_to_py(py, value),
+ Err(err) => Err(core_error_to_pyerr(err)),
+ }
+}
+
+/// Bridge GIL accounting, e.g. `{"releases": 12}`. Lets the Python side observe
+/// how often the bridge has dropped the GIL for blocking work.
+#[pyfunction]
+fn gil_stats(py: Python<'_>) -> PyResult> {
+ let stats = PyDict::new(py);
+ stats.set_item("releases", gil::release_count())?;
+ Ok(stats.into_any().unbind())
+}
+
+#[pymodule]
+fn litellm_python_bridge(module: &Bound<'_, PyModule>) -> PyResult<()> {
+ module.add_function(wrap_pyfunction!(ocr, module)?)?;
+ module.add_function(wrap_pyfunction!(gil_stats, module)?)?;
+ Ok(())
+}
diff --git a/litellm/__init__.py b/litellm/__init__.py
index cffdbacf597..d0513f77b35 100644
--- a/litellm/__init__.py
+++ b/litellm/__init__.py
@@ -80,6 +80,7 @@ from litellm.constants import (
WANDB_MODELS,
REPEATED_STREAMING_CHUNK_LIMIT,
request_timeout,
+ request_timeout_explicitly_set as request_timeout_explicitly_set,
open_ai_embedding_models,
cohere_embedding_models,
bedrock_embedding_models,
@@ -673,6 +674,7 @@ elevenlabs_models: Set = set()
dashscope_models: Set = set()
moonshot_models: Set = set()
publicai_models: Set = set()
+darkbloom_models: Set = set()
v0_models: Set = set()
morph_models: Set = set()
lambda_ai_models: Set = set()
@@ -927,6 +929,8 @@ def add_known_models(model_cost_map: Optional[Dict] = None):
moonshot_models.add(key)
elif value.get("litellm_provider") == "publicai":
publicai_models.add(key)
+ elif value.get("litellm_provider") == "darkbloom":
+ darkbloom_models.add(key)
elif value.get("litellm_provider") == "v0":
v0_models.add(key)
elif value.get("litellm_provider") == "morph":
@@ -1075,6 +1079,7 @@ model_list = list(
| dashscope_models
| moonshot_models
| publicai_models
+ | darkbloom_models
| v0_models
| morph_models
| lambda_ai_models
@@ -1179,6 +1184,7 @@ models_by_provider: dict = {
"modelscope": modelscope_models,
"moonshot": moonshot_models,
"publicai": publicai_models,
+ "darkbloom": darkbloom_models,
"v0": v0_models,
"morph": morph_models,
"lambda_ai": lambda_ai_models,
@@ -1400,7 +1406,9 @@ from .skills.main import (
)
from .containers.main import *
from .ocr.main import *
+from .ocr.rust_bridge import use_litellm_rust
from .rag.main import *
+from .sandbox.main import *
from .search.main import *
from .realtime_api.main import (
_arealtime,
@@ -1921,9 +1929,6 @@ if TYPE_CHECKING:
from .llms.fireworks_ai.completion.transformation import (
FireworksAITextCompletionConfig as FireworksAITextCompletionConfig,
)
- from .llms.fireworks_ai.audio_transcription.transformation import (
- FireworksAIAudioTranscriptionConfig as FireworksAIAudioTranscriptionConfig,
- )
from .llms.fireworks_ai.embed.fireworks_ai_transformation import (
FireworksAIEmbeddingConfig as FireworksAIEmbeddingConfig,
)
diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py
index e653b40fd04..4f131354d2e 100644
--- a/litellm/_lazy_imports_registry.py
+++ b/litellm/_lazy_imports_registry.py
@@ -260,7 +260,6 @@ LLM_CONFIG_NAMES = (
"SambaNovaEmbeddingConfig",
"FireworksAIConfig",
"FireworksAITextCompletionConfig",
- "FireworksAIAudioTranscriptionConfig",
"FireworksAIEmbeddingConfig",
"FriendliaiChatConfig",
"JinaAIEmbeddingConfig",
@@ -1027,10 +1026,6 @@ _LLM_CONFIGS_IMPORT_MAP = {
".llms.fireworks_ai.completion.transformation",
"FireworksAITextCompletionConfig",
),
- "FireworksAIAudioTranscriptionConfig": (
- ".llms.fireworks_ai.audio_transcription.transformation",
- "FireworksAIAudioTranscriptionConfig",
- ),
"FireworksAIEmbeddingConfig": (
".llms.fireworks_ai.embed.fireworks_ai_transformation",
"FireworksAIEmbeddingConfig",
diff --git a/litellm/constants.py b/litellm/constants.py
index c0e265c0e4a..212d34357f8 100644
--- a/litellm/constants.py
+++ b/litellm/constants.py
@@ -201,6 +201,18 @@ DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET = int(
# Provider-specific API base URLs
XAI_API_BASE = "https://api.x.ai/v1"
+OPEN_SANDBOX_API_BASE_ENV_VAR = "OPEN_SANDBOX_API_BASE"
+OPEN_SANDBOX_API_KEY_ENV_VAR = "OPEN_SANDBOX_API_KEY"
+OPEN_SANDBOX_DEFAULT_TEMPLATE = "opensandbox/code-interpreter:v1.1.0"
+_OPEN_SANDBOX_FALLBACK_ENTRYPOINT = "/opt/code-interpreter/code-interpreter.sh"
+OPEN_SANDBOX_DEFAULT_ENTRYPOINT = (_OPEN_SANDBOX_FALLBACK_ENTRYPOINT,)
+OPEN_SANDBOX_DEFAULT_LANGUAGE = "python"
+OPEN_SANDBOX_DEFAULT_CPU_LIMIT = "1"
+OPEN_SANDBOX_DEFAULT_MEMORY_LIMIT = "2Gi"
+OPEN_SANDBOX_EXECD_PORT = 44772
+OPEN_SANDBOX_DEFAULT_TIMEOUT = 300
+OPEN_SANDBOX_READY_TIMEOUT = 30.0
+OPEN_SANDBOX_POLL_INTERVAL = 0.2
DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET = int(
os.getenv("DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET", 1024)
@@ -456,6 +468,7 @@ HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS: float = 5.0
request_timeout: float = float(
os.getenv("REQUEST_TIMEOUT", str(int(DEFAULT_REQUEST_TIMEOUT_SECONDS)))
)
+request_timeout_explicitly_set: bool = "REQUEST_TIMEOUT" in os.environ
DEFAULT_A2A_AGENT_TIMEOUT: float = float(
os.getenv("DEFAULT_A2A_AGENT_TIMEOUT", 6000)
) # 10 minutes
@@ -867,6 +880,7 @@ openai_compatible_providers: List = [
"docker_model_runner",
"ragflow",
"pinstripes", # Pinstripes - JSON-configured provider
+ "darkbloom",
]
openai_text_completion_compatible_providers: List = (
[ # providers that support `/v1/completions`
diff --git a/litellm/integrations/code_interpreter_interception/__init__.py b/litellm/integrations/code_interpreter_interception/__init__.py
new file mode 100644
index 00000000000..2256356b6f5
--- /dev/null
+++ b/litellm/integrations/code_interpreter_interception/__init__.py
@@ -0,0 +1,15 @@
+"""
+Code Interpreter Interception Module
+
+Converts the native OpenAI Responses ``code_interpreter`` tool into a function
+tool, runs the model-emitted code in a sandbox, and feeds the result back into
+the agentic loop.
+"""
+
+from litellm.integrations.code_interpreter_interception.handler import (
+ CodeInterpreterInterceptionLogger,
+)
+
+__all__ = [
+ "CodeInterpreterInterceptionLogger",
+]
diff --git a/litellm/integrations/code_interpreter_interception/handler.py b/litellm/integrations/code_interpreter_interception/handler.py
new file mode 100644
index 00000000000..362581937d7
--- /dev/null
+++ b/litellm/integrations/code_interpreter_interception/handler.py
@@ -0,0 +1,839 @@
+"""
+Code Interpreter Interception Handler
+
+CustomLogger that swaps the native OpenAI Responses ``code_interpreter`` tool for
+a function tool, executes the code the model emits inside a sandbox, and feeds the
+captured stdout back through the typed agentic loop plan.
+"""
+
+import json
+import time
+import uuid
+from typing import Any, Literal, TypedDict, cast
+
+import litellm
+from pydantic import ValidationError
+
+from litellm._logging import verbose_logger
+from litellm.integrations.custom_logger import CustomLogger
+from litellm.types.integrations.code_interpreter_interception import (
+ CodeInterpreterInterceptionConfig,
+)
+from litellm.types.integrations.custom_logger import (
+ AgenticLoopPlan,
+ AgenticLoopRequestPatch,
+ CHAT_COMPLETION_AGENTIC_SURFACE,
+ NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES,
+ is_interception_internal_key,
+)
+from litellm.types.llms.openai import (
+ ChatCompletionAssistantMessage,
+ ChatCompletionAssistantToolCall,
+ ChatCompletionToolMessage,
+)
+from litellm.types.utils import (
+ CallTypes,
+ ChatCompletionMessageToolCall,
+ ModelResponse,
+)
+
+LITELLM_CODE_EXECUTION_TOOL_NAME = "litellm_code_execution"
+_INTERCEPTION_ACTIVE_KEY = "_code_interpreter_interception_active"
+_SANDBOX_KEY = "_code_interpreter_interception_sandbox_key"
+_CONVERTED_STREAM_KEY = "_code_interpreter_interception_converted_stream"
+_LITELLM_METADATA_KEY = "litellm_metadata"
+_CACHE_TTL_SECONDS = 15 * 60
+
+
+class CodeExecutionToolCall(TypedDict, total=False):
+ id: str | None
+ call_id: str | None
+ type: Literal["function"]
+ name: str
+ arguments: str
+
+
+class CodeInterpreterLogOutput(TypedDict):
+ type: Literal["logs"]
+ logs: str
+
+
+class CodeInterpreterCall(TypedDict):
+ id: str
+ type: Literal["code_interpreter_call"]
+ status: Literal["completed"]
+ code: str
+ container_id: str | None
+ outputs: list[CodeInterpreterLogOutput]
+
+
+class CodeExecutionFunctionParameters(TypedDict):
+ type: Literal["object"]
+ properties: dict[str, dict[str, str]]
+ required: list[str]
+
+
+class ResponsesFunctionTool(TypedDict):
+ type: Literal["function"]
+ name: str
+ description: str
+ parameters: CodeExecutionFunctionParameters
+
+
+class ChatCompletionFunctionDefinition(TypedDict):
+ name: str
+ description: str
+ parameters: CodeExecutionFunctionParameters
+
+
+class ChatCompletionFunctionTool(TypedDict):
+ type: Literal["function"]
+ function: ChatCompletionFunctionDefinition
+
+
+CodeExecutionFunctionTool = ResponsesFunctionTool | ChatCompletionFunctionTool
+
+
+class ResponsesFunctionToolChoice(TypedDict):
+ type: Literal["function"]
+ name: str
+
+
+class ChatCompletionFunctionToolChoice(TypedDict):
+ type: Literal["function"]
+ function: dict[str, str]
+
+
+CodeExecutionFunctionToolChoice = (
+ ResponsesFunctionToolChoice | ChatCompletionFunctionToolChoice
+)
+
+
+def _resolve_sandbox_tool(sandbox_tool_name: str | None) -> dict[str, Any] | None:
+ try:
+ from litellm.sandbox.sandbox_tools import resolve_sandbox_tool
+ except ImportError:
+ return None
+ return resolve_sandbox_tool(sandbox_tool_name)
+
+
+class CodeInterpreterInterceptionLogger(CustomLogger):
+ """
+ CustomLogger that implements transparent code-interpreter execution loops.
+
+ Flow:
+ 1. Replace the native ``code_interpreter`` tool with a function tool in the
+ pre-call hook so the model emits code as function-call arguments.
+ 2. Detect ``litellm_code_execution`` function calls in the model response.
+ 3. Run the emitted code in a sandbox (reused per request via a server-minted
+ sandbox key) and build a typed rerun plan that appends the
+ function_call_output.
+ """
+
+ def __init__(
+ self,
+ enabled: bool = True,
+ enabled_providers: list[str] | None = None,
+ sandbox_tool_name: str | None = None,
+ sandbox_config: Any | None = None,
+ ):
+ super().__init__()
+ self.enabled = enabled
+ self.enabled_providers = enabled_providers
+ self.sandbox_tool_name = sandbox_tool_name
+ self.sandbox_config = sandbox_config
+ self._container_cache: dict[str, tuple[Any, dict[str, Any] | None, float]] = {}
+
+ @classmethod
+ def from_config_yaml(
+ cls, config: CodeInterpreterInterceptionConfig
+ ) -> "CodeInterpreterInterceptionLogger":
+ return cls(
+ enabled=bool(config.get("enabled", True)),
+ enabled_providers=config.get("enabled_providers"),
+ sandbox_tool_name=config.get("sandbox_tool_name"),
+ )
+
+ @staticmethod
+ def initialize_from_proxy_config(
+ litellm_settings: dict[str, Any],
+ callback_specific_params: dict[str, Any],
+ ) -> "CodeInterpreterInterceptionLogger":
+ params: CodeInterpreterInterceptionConfig = {}
+ if "code_interpreter_interception_params" in litellm_settings:
+ params = litellm_settings["code_interpreter_interception_params"]
+ elif "code_interpreter_interception" in callback_specific_params and isinstance(
+ callback_specific_params["code_interpreter_interception"], dict
+ ):
+ params = cast(
+ CodeInterpreterInterceptionConfig,
+ callback_specific_params["code_interpreter_interception"],
+ )
+ return CodeInterpreterInterceptionLogger.from_config_yaml(params)
+
+ async def async_pre_call_deployment_hook(
+ self, kwargs: dict[str, Any], call_type: CallTypes | None
+ ) -> dict | None:
+ if not kwargs.get("_agentic_loop_depth"):
+ kwargs.pop(_INTERCEPTION_ACTIVE_KEY, None)
+ kwargs.pop(_SANDBOX_KEY, None)
+ self._strip_interception_metadata(kwargs)
+ if not self.enabled:
+ return None
+ if call_type not in (
+ CallTypes.responses,
+ CallTypes.aresponses,
+ CallTypes.completion,
+ CallTypes.acompletion,
+ ):
+ return None
+ if (
+ self.enabled_providers is not None
+ and self._resolve_provider(kwargs) not in self.enabled_providers
+ ):
+ return None
+
+ tools = kwargs.get("tools")
+ if not isinstance(tools, list):
+ return None
+ if not any(
+ isinstance(tool, dict) and tool.get("type") == "code_interpreter"
+ for tool in tools
+ ):
+ return None
+
+ kwargs[_INTERCEPTION_ACTIVE_KEY] = True
+ kwargs[_SANDBOX_KEY] = uuid.uuid4().hex
+ if kwargs.get("stream"):
+ kwargs["stream"] = False
+ kwargs[_CONVERTED_STREAM_KEY] = True
+ self._write_interception_metadata(kwargs)
+
+ function_tool = self._get_function_tool(call_type=call_type)
+ kwargs["tools"] = [
+ (
+ function_tool
+ if isinstance(tool, dict) and tool.get("type") == "code_interpreter"
+ else tool
+ )
+ for tool in tools
+ ]
+ if self._tool_choice_targets_code_interpreter(kwargs.get("tool_choice")):
+ kwargs["tool_choice"] = self._get_function_tool_choice(call_type=call_type)
+ return kwargs
+
+ @staticmethod
+ def _strip_interception_metadata(kwargs: dict[str, Any]) -> None:
+ metadata = kwargs.get(_LITELLM_METADATA_KEY)
+ if not isinstance(metadata, dict):
+ return
+ filtered_metadata = {
+ key: value
+ for key, value in metadata.items()
+ if not is_interception_internal_key(key)
+ and not key.startswith("_agentic_loop")
+ and key != "max_agentic_loops"
+ }
+ if filtered_metadata:
+ kwargs[_LITELLM_METADATA_KEY] = filtered_metadata
+ else:
+ kwargs.pop(_LITELLM_METADATA_KEY, None)
+
+ @staticmethod
+ def _write_interception_metadata(kwargs: dict[str, Any]) -> None:
+ metadata = kwargs.get(_LITELLM_METADATA_KEY)
+ metadata = dict(metadata) if isinstance(metadata, dict) else {}
+ for key in (_INTERCEPTION_ACTIVE_KEY, _SANDBOX_KEY, _CONVERTED_STREAM_KEY):
+ if key in kwargs:
+ metadata[key] = kwargs[key]
+ kwargs[_LITELLM_METADATA_KEY] = metadata
+
+ @staticmethod
+ def _get_function_parameters() -> CodeExecutionFunctionParameters:
+ return {
+ "type": "object",
+ "properties": {"code": {"type": "string"}},
+ "required": ["code"],
+ }
+
+ def _get_function_tool(
+ self, call_type: CallTypes | None
+ ) -> CodeExecutionFunctionTool:
+ description = "Execute python code in a sandbox and return stdout."
+ if call_type in (CallTypes.completion, CallTypes.acompletion):
+ return {
+ "type": "function",
+ "function": {
+ "name": LITELLM_CODE_EXECUTION_TOOL_NAME,
+ "description": description,
+ "parameters": self._get_function_parameters(),
+ },
+ }
+ return {
+ "type": "function",
+ "name": LITELLM_CODE_EXECUTION_TOOL_NAME,
+ "description": description,
+ "parameters": self._get_function_parameters(),
+ }
+
+ @staticmethod
+ def _get_function_tool_choice(
+ call_type: CallTypes | None,
+ ) -> CodeExecutionFunctionToolChoice:
+ if call_type in (CallTypes.completion, CallTypes.acompletion):
+ return {
+ "type": "function",
+ "function": {"name": LITELLM_CODE_EXECUTION_TOOL_NAME},
+ }
+ return {
+ "type": "function",
+ "name": LITELLM_CODE_EXECUTION_TOOL_NAME,
+ }
+
+ @staticmethod
+ def _tool_choice_targets_code_interpreter(tool_choice: Any) -> bool:
+ if not isinstance(tool_choice, dict):
+ return False
+ function = tool_choice.get("function")
+ return (
+ tool_choice.get("type") == "code_interpreter"
+ or tool_choice.get("name") == "code_interpreter"
+ or tool_choice.get("name") == LITELLM_CODE_EXECUTION_TOOL_NAME
+ or (
+ isinstance(function, dict)
+ and function.get("name") == LITELLM_CODE_EXECUTION_TOOL_NAME
+ )
+ )
+
+ def _resolve_provider(self, kwargs: dict[str, Any]) -> str | None:
+ provider = kwargs.get("custom_llm_provider")
+ if provider:
+ return provider
+ model = kwargs.get("model")
+ if not isinstance(model, str):
+ return None
+ try:
+ return litellm.get_llm_provider(model=model)[1]
+ except Exception:
+ return None
+
+ async def async_should_run_agentic_loop(
+ self,
+ response: Any,
+ model: str,
+ messages: list[dict],
+ tools: list[dict] | None,
+ stream: bool,
+ custom_llm_provider: str,
+ kwargs: dict,
+ ) -> tuple[bool, dict]:
+ if not self.enabled:
+ return False, {}
+ if not kwargs.get(_INTERCEPTION_ACTIVE_KEY):
+ return False, {}
+ if (
+ self.enabled_providers is not None
+ and custom_llm_provider not in self.enabled_providers
+ ):
+ return False, {}
+
+ tool_calls = (
+ self._extract_chat_completion_code_execution_tool_calls(response=response)
+ if kwargs.get("_agentic_loop_api_surface")
+ == CHAT_COMPLETION_AGENTIC_SURFACE
+ else self._extract_code_execution_tool_calls(response=response)
+ )
+ if not tool_calls:
+ return False, {}
+
+ return True, {"tool_calls": tool_calls}
+
+ async def async_build_agentic_loop_plan(
+ self,
+ tools: dict,
+ model: str,
+ messages: list[dict],
+ response: Any,
+ anthropic_messages_provider_config: Any,
+ anthropic_messages_optional_request_params: dict,
+ logging_obj: Any,
+ stream: bool,
+ kwargs: dict,
+ ) -> AgenticLoopPlan:
+ if kwargs.get("_agentic_loop_api_surface") == CHAT_COMPLETION_AGENTIC_SURFACE:
+ return await self._build_chat_completion_agentic_loop_plan(
+ tools=tools,
+ model=model,
+ messages=messages,
+ optional_params=anthropic_messages_optional_request_params,
+ kwargs=kwargs,
+ )
+
+ await self._prune_expired_cache()
+ tool_calls = cast(list[CodeExecutionToolCall], tools.get("tool_calls", []))
+ sandbox_key = kwargs.get(_SANDBOX_KEY)
+ container, params = await self._get_or_create_container(cache_key=sandbox_key)
+
+ try:
+ container_id = cast(str | None, getattr(container, "id", None))
+ input_list = self._normalize_messages(messages)
+ code_interpreter_calls: list[CodeInterpreterCall] = []
+ for tool_call in tool_calls:
+ arguments = tool_call.get("arguments", "")
+ code = self._parse_code(arguments)
+ stdout = await self._run_tool_call(
+ container=container, params=params, arguments=arguments
+ )
+ input_list.append(
+ {
+ "type": "function_call",
+ "call_id": tool_call.get("call_id"),
+ "name": LITELLM_CODE_EXECUTION_TOOL_NAME,
+ "arguments": arguments,
+ }
+ )
+ input_list.append(
+ {
+ "type": "function_call_output",
+ "call_id": tool_call.get("call_id"),
+ "output": stdout,
+ }
+ )
+ code_interpreter_calls.append(
+ {
+ "id": f"ci_{uuid.uuid4().hex}",
+ "type": "code_interpreter_call",
+ "status": "completed",
+ "code": code,
+ "container_id": container_id,
+ "outputs": (
+ [{"type": "logs", "logs": stdout}] if stdout else []
+ ),
+ }
+ )
+ except Exception:
+ await self._delete_container_for_cache_key(sandbox_key)
+ raise
+
+ optional_params = anthropic_messages_optional_request_params
+ request_patch = AgenticLoopRequestPatch(
+ model=model,
+ messages=input_list,
+ tools=self._get_followup_tools(
+ tools=optional_params.get("tools"),
+ call_type=CallTypes.responses,
+ ),
+ optional_params=self._get_followup_optional_params(optional_params),
+ kwargs=self._filter_agentic_loop_kwargs(kwargs),
+ )
+
+ return AgenticLoopPlan(
+ run_agentic_loop=True,
+ request_patch=request_patch,
+ metadata={
+ "tool_type": "code_interpreter",
+ "sandbox_key": sandbox_key or "",
+ "code_interpreter_calls": code_interpreter_calls,
+ },
+ )
+
+ async def _build_chat_completion_agentic_loop_plan(
+ self,
+ tools: dict[str, object],
+ model: str,
+ messages: list[dict],
+ optional_params: dict[str, object],
+ kwargs: dict[str, object],
+ ) -> AgenticLoopPlan:
+ await self._prune_expired_cache()
+ tool_calls = cast(list[CodeExecutionToolCall], tools.get("tool_calls", []))
+ sandbox_key = cast(str | None, kwargs.get(_SANDBOX_KEY))
+ container, params = await self._get_or_create_container(cache_key=sandbox_key)
+
+ try:
+ container_id = cast(str | None, getattr(container, "id", None))
+ tool_results = [
+ await self._build_chat_completion_tool_result(
+ container=container,
+ params=params,
+ tool_call=tool_call,
+ container_id=container_id,
+ )
+ for tool_call in tool_calls
+ ]
+ except Exception:
+ await self._delete_container_for_cache_key(sandbox_key)
+ raise
+ tool_messages = [result[0] for result in tool_results]
+ code_interpreter_calls = [result[1] for result in tool_results]
+
+ request_patch = AgenticLoopRequestPatch(
+ model=model,
+ messages=list(messages)
+ + [self._build_chat_completion_assistant_message(tool_calls)]
+ + tool_messages,
+ tools=self._get_followup_tools(
+ tools=optional_params.get("tools"),
+ call_type=CallTypes.completion,
+ ),
+ optional_params=self._get_followup_optional_params(optional_params),
+ kwargs=self._filter_agentic_loop_kwargs(kwargs),
+ )
+
+ return AgenticLoopPlan(
+ run_agentic_loop=True,
+ request_patch=request_patch,
+ metadata={
+ "tool_type": "code_interpreter",
+ "sandbox_key": sandbox_key or "",
+ "code_interpreter_calls": code_interpreter_calls,
+ "response_format": "openai",
+ },
+ )
+
+ async def _build_chat_completion_tool_result(
+ self,
+ container: object,
+ params: dict[str, Any] | None,
+ tool_call: CodeExecutionToolCall,
+ container_id: str | None,
+ ) -> tuple[ChatCompletionToolMessage, CodeInterpreterCall]:
+ arguments = tool_call.get("arguments", "")
+ code = self._parse_code(arguments)
+ stdout = await self._run_tool_call(
+ container=container, params=params, arguments=arguments
+ )
+ tool_call_id = (
+ tool_call.get("id") or tool_call.get("call_id") or uuid.uuid4().hex
+ )
+ return (
+ {
+ "role": "tool",
+ "tool_call_id": tool_call_id,
+ "content": stdout,
+ },
+ {
+ "id": f"ci_{uuid.uuid4().hex}",
+ "type": "code_interpreter_call",
+ "status": "completed",
+ "code": code,
+ "container_id": container_id,
+ "outputs": [{"type": "logs", "logs": stdout}] if stdout else [],
+ },
+ )
+
+ async def async_agentic_loop_cleanup_hook(
+ self, plan: AgenticLoopPlan, kwargs: dict
+ ) -> None:
+ metadata = plan.metadata or {} if plan else {}
+ await self._delete_container_for_cache_key(metadata.get("sandbox_key"))
+
+ @staticmethod
+ def _filter_agentic_loop_kwargs(kwargs: dict[str, object]) -> dict[str, object]:
+ return {
+ k: v
+ for k, v in kwargs.items()
+ if k not in {"litellm_logging_obj", "acompletion"}
+ and not is_interception_internal_key(
+ k, prefixes=NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES
+ )
+ }
+
+ def _get_followup_tools(
+ self, tools: object, call_type: CallTypes | None
+ ) -> list[dict[str, Any]] | None:
+ if not isinstance(tools, list):
+ return None
+ return [
+ (
+ self._get_function_tool(call_type=call_type)
+ if isinstance(tool, dict) and tool.get("type") == "code_interpreter"
+ else tool
+ )
+ for tool in tools
+ ]
+
+ def _get_followup_optional_params(
+ self, optional_params: dict[str, object]
+ ) -> dict[str, object]:
+ drop_tool_choice = self._tool_choice_targets_code_interpreter(
+ optional_params.get("tool_choice")
+ )
+ return {
+ k: v
+ for k, v in optional_params.items()
+ if k != "tools" and not (k == "tool_choice" and drop_tool_choice)
+ }
+
+ async def async_post_agentic_loop_response_hook(
+ self, response: Any, plan: AgenticLoopPlan, kwargs: dict
+ ) -> Any:
+ metadata = plan.metadata or {} if plan else {}
+ await self._delete_container_for_cache_key(metadata.get("sandbox_key"))
+
+ calls = metadata.get("code_interpreter_calls")
+ if not calls:
+ return response
+
+ is_dict = isinstance(response, dict)
+ output = (
+ response.get("output") if is_dict else getattr(response, "output", None)
+ )
+ if not isinstance(output, list):
+ return response
+
+ def _item_type(item: Any) -> Any:
+ return (
+ item.get("type")
+ if isinstance(item, dict)
+ else getattr(item, "type", None)
+ )
+
+ insert_at = next(
+ (i for i, item in enumerate(output) if _item_type(item) == "message"),
+ len(output),
+ )
+ new_output = output[:insert_at] + list(calls) + output[insert_at:]
+ if is_dict:
+ response["output"] = new_output
+ else:
+ response.output = new_output
+ return response
+
+ @staticmethod
+ def _parse_code(arguments: str) -> str:
+ try:
+ return json.loads(arguments).get("code", "") if arguments else ""
+ except (json.JSONDecodeError, TypeError, AttributeError):
+ return ""
+
+ async def _run_tool_call(
+ self, container: Any, params: dict[str, Any] | None, arguments: str
+ ) -> str:
+ try:
+ code = json.loads(arguments).get("code", "") if arguments else ""
+ except (json.JSONDecodeError, TypeError):
+ return "[invalid tool arguments: could not parse code]"
+
+ result = await self._run_code(container=container, params=params, code=code)
+ if getattr(result, "error", None):
+ error = result.error
+ message = (
+ error.get("value") or error.get("name")
+ if isinstance(error, dict)
+ else str(error)
+ )
+ return f"[execution error] {message}"
+ return getattr(result, "stdout", "") or ""
+
+ async def _get_or_create_container(
+ self, cache_key: str | None
+ ) -> tuple[Any, dict[str, Any] | None]:
+ if cache_key:
+ cached = self._container_cache.get(cache_key)
+ if cached is not None:
+ return cached[0], cached[1]
+
+ container, params = await self._create_container()
+ if cache_key:
+ self._container_cache[cache_key] = (container, params, time.time())
+ return container, params
+
+ async def _create_container(self) -> tuple[Any, dict[str, Any] | None]:
+ if self.sandbox_config is not None:
+ return await self.sandbox_config.acreate_sandbox(), None
+
+ params = _resolve_sandbox_tool(self.sandbox_tool_name)
+ if params is None:
+ raise ValueError(
+ "CodeInterpreterInterception: no sandbox available. Provide a "
+ "sandbox_config or configure a sandbox tool resolvable via "
+ "sandbox_tool_name."
+ )
+ container = await litellm.acreate_sandbox(
+ provider=params["sandbox_provider"],
+ api_key=params.get("api_key"),
+ api_base=params.get("api_base"),
+ )
+ return container, params
+
+ async def _run_code(
+ self, container: Any, params: dict[str, Any] | None, code: str
+ ) -> Any:
+ if self.sandbox_config is not None:
+ return await self.sandbox_config.arun_code(container=container, code=code)
+ if params is None:
+ raise ValueError(
+ "CodeInterpreterInterception: no sandbox available to run code."
+ )
+ return await litellm.arun_code(
+ provider=params["sandbox_provider"],
+ container=container,
+ code=code,
+ api_key=params.get("api_key"),
+ )
+
+ async def _delete_container(
+ self, container: Any, params: dict[str, Any] | None
+ ) -> None:
+ try:
+ if self.sandbox_config is not None:
+ await self.sandbox_config.adelete_sandbox(container=container)
+ return
+ if params is None:
+ return
+ await litellm.adelete_sandbox(
+ provider=params["sandbox_provider"],
+ container=container,
+ api_key=params.get("api_key"),
+ api_base=params.get("api_base"),
+ )
+ except Exception:
+ verbose_logger.exception(
+ "CodeInterpreterInterception: failed to delete sandbox container"
+ )
+
+ async def _delete_container_for_cache_key(self, cache_key: str | None) -> None:
+ if not cache_key:
+ return
+ cached = self._container_cache.pop(cache_key, None)
+ if cached is None:
+ return
+ await self._delete_container(container=cached[0], params=cached[1])
+
+ def _normalize_messages(self, messages: Any) -> list[dict[str, Any]]:
+ if isinstance(messages, str):
+ return [{"role": "user", "content": messages}]
+ if isinstance(messages, list):
+ return list(messages)
+ return []
+
+ def _extract_code_execution_tool_calls(
+ self, response: object
+ ) -> list[CodeExecutionToolCall]:
+ if isinstance(response, dict):
+ output = response.get("output", [])
+ else:
+ output = getattr(response, "output", []) or []
+ if not isinstance(output, list):
+ return []
+
+ return [
+ {
+ "call_id": (
+ item.get("call_id")
+ if isinstance(item, dict)
+ else getattr(item, "call_id", None)
+ ),
+ "name": LITELLM_CODE_EXECUTION_TOOL_NAME,
+ "arguments": (
+ item.get("arguments")
+ if isinstance(item, dict)
+ else getattr(item, "arguments", "")
+ ),
+ }
+ for item in output
+ if self._is_code_execution_call(item)
+ ]
+
+ def _extract_chat_completion_code_execution_tool_calls(
+ self, response: ModelResponse | dict[str, Any]
+ ) -> list[CodeExecutionToolCall]:
+ model_response = self._to_model_response(response)
+ if model_response is None:
+ return []
+ choices = model_response.choices or []
+ if not choices:
+ return []
+ message = choices[0].message
+ tool_calls = message.tool_calls or []
+
+ return [
+ normalized
+ for tool_call in tool_calls
+ if (normalized := self._normalize_chat_completion_tool_call(tool_call))
+ is not None
+ ]
+
+ @staticmethod
+ def _normalize_chat_completion_tool_call(
+ tool_call: ChatCompletionMessageToolCall,
+ ) -> CodeExecutionToolCall | None:
+ if (
+ tool_call.type != "function"
+ or tool_call.function.name != LITELLM_CODE_EXECUTION_TOOL_NAME
+ ):
+ return None
+
+ arguments = tool_call.function.arguments
+ if isinstance(arguments, dict):
+ arguments = json.dumps(arguments)
+ elif not isinstance(arguments, str):
+ arguments = "" if arguments is None else str(arguments)
+
+ return {
+ "id": tool_call.id,
+ "call_id": tool_call.id,
+ "type": "function",
+ "name": LITELLM_CODE_EXECUTION_TOOL_NAME,
+ "arguments": arguments,
+ }
+
+ @staticmethod
+ def _build_chat_completion_assistant_message(
+ tool_calls: list[CodeExecutionToolCall],
+ ) -> ChatCompletionAssistantMessage:
+ return {
+ "role": "assistant",
+ "tool_calls": [
+ cast(
+ ChatCompletionAssistantToolCall,
+ {
+ "id": tool_call.get("id"),
+ "type": "function",
+ "function": {
+ "name": LITELLM_CODE_EXECUTION_TOOL_NAME,
+ "arguments": tool_call.get("arguments", ""),
+ },
+ },
+ )
+ for tool_call in tool_calls
+ ],
+ }
+
+ @staticmethod
+ def _to_model_response(
+ response: ModelResponse | dict[str, Any],
+ ) -> ModelResponse | None:
+ if isinstance(response, ModelResponse):
+ return response
+ try:
+ return ModelResponse(**response)
+ except (TypeError, ValidationError):
+ return None
+
+ def _is_code_execution_call(self, item: Any) -> bool:
+ if isinstance(item, dict):
+ return (
+ item.get("type") == "function_call"
+ and item.get("name") == LITELLM_CODE_EXECUTION_TOOL_NAME
+ )
+ return (
+ getattr(item, "type", None) == "function_call"
+ and getattr(item, "name", None) == LITELLM_CODE_EXECUTION_TOOL_NAME
+ )
+
+ async def _prune_expired_cache(self) -> None:
+ now = time.time()
+ expired = [
+ (cache_key, container, params)
+ for cache_key, (
+ container,
+ params,
+ created_at,
+ ) in self._container_cache.items()
+ if now - created_at > _CACHE_TTL_SECONDS
+ ]
+ for cache_key, container, params in expired:
+ self._container_cache.pop(cache_key, None)
+ await self._delete_container(container=container, params=params)
diff --git a/litellm/integrations/custom_logger.py b/litellm/integrations/custom_logger.py
index 481cf7fce8e..94fb97dff53 100644
--- a/litellm/integrations/custom_logger.py
+++ b/litellm/integrations/custom_logger.py
@@ -718,6 +718,24 @@ class CustomLogger: # https://docs.litellm.ai/docs/observability/custom_callbac
"""
return response
+ async def async_agentic_loop_cleanup_hook(
+ self,
+ plan: AgenticLoopPlan,
+ kwargs: dict,
+ ) -> None:
+ """
+ Release resources held for an agentic-loop iteration.
+
+ Runs in a ``finally`` around the follow-up provider call, so it fires
+ whether the rerun returns normally, hits a loop safety abort, or raises
+ an upstream error. Implementations must be idempotent because the
+ post-response hook may already have released the same resource on the
+ success path. Use ``plan.metadata`` to locate what to clean up.
+
+ Default does nothing.
+ """
+ return None
+
async def async_should_run_chat_completion_agentic_loop(
self,
response: Any,
diff --git a/litellm/integrations/otel/model/config.py b/litellm/integrations/otel/model/config.py
index a109ba898ff..991b156ae64 100644
--- a/litellm/integrations/otel/model/config.py
+++ b/litellm/integrations/otel/model/config.py
@@ -1,6 +1,7 @@
"""Typed configuration for the OpenTelemetry instrumentation."""
from enum import Enum
+from functools import lru_cache
from typing import Any, List
from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator
@@ -47,7 +48,12 @@ class _OTelV2Flag(BaseSettings):
enabled: bool = Field(default=False, validation_alias=AliasChoices(OTEL_V2_ENV))
+@lru_cache(maxsize=1)
def is_otel_v2_enabled() -> bool:
+ # Resolved once at startup and cached: constructing the pydantic-settings
+ # model re-scans the environment and cost ~28us, which on the proxy hot path
+ # (auth, logging-callback setup) compounded into a measurable throughput
+ # regression. Tests that toggle the env must call ``is_otel_v2_enabled.cache_clear()``.
return _OTelV2Flag().enabled
diff --git a/litellm/interactions/litellm_responses_transformation/transformation.py b/litellm/interactions/litellm_responses_transformation/transformation.py
index 173d4ca8764..0ff1a97cd0b 100644
--- a/litellm/interactions/litellm_responses_transformation/transformation.py
+++ b/litellm/interactions/litellm_responses_transformation/transformation.py
@@ -300,9 +300,6 @@ class LiteLLMResponsesInteractionsConfig:
"total_output_tokens": getattr(usage, "output_tokens", 0),
}
- # Add role
- interactions_response_dict["role"] = "model"
-
# Add updated (same as created for now)
interactions_response_dict["updated"] = created
diff --git a/litellm/litellm_core_utils/chat_completion_agentic_loop.py b/litellm/litellm_core_utils/chat_completion_agentic_loop.py
new file mode 100644
index 00000000000..938e892bd50
--- /dev/null
+++ b/litellm/litellm_core_utils/chat_completion_agentic_loop.py
@@ -0,0 +1,332 @@
+# this is a patch to allow for agentic loops covering llm_http_handler.py and openai sdk based calling flows for the .completion() api
+
+import json
+from typing import cast
+
+from litellm._logging import verbose_logger
+from litellm.integrations.custom_logger import CustomLogger
+from litellm.types.integrations.custom_logger import (
+ CHAT_COMPLETION_AGENTIC_SURFACE,
+ NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES,
+ AgenticLoopPlan,
+ AgenticLoopRequestPatch,
+ is_interception_internal_key,
+)
+from litellm.types.utils import ModelResponse
+from litellm.utils import CustomStreamWrapper
+
+_FOLLOWUP_INTERNAL_PARAMS = frozenset(
+ (
+ "acompletion",
+ "litellm_logging_obj",
+ "custom_llm_provider",
+ "model_alias_map",
+ "stream_response",
+ "custom_prompt_dict",
+ "_agentic_loop_api_surface",
+ )
+)
+
+
+def _gate_overridden(callback: CustomLogger) -> bool:
+ base = CustomLogger.async_should_run_agentic_loop
+ func = type(callback).async_should_run_agentic_loop
+ return getattr(func, "__func__", func) is not getattr(base, "__func__", base)
+
+
+def _build_plan_overridden(callback: CustomLogger) -> bool:
+ base = CustomLogger.async_build_agentic_loop_plan
+ func = type(callback).async_build_agentic_loop_plan
+ return getattr(func, "__func__", func) is not getattr(base, "__func__", base)
+
+
+def _post_hook_overridden(callback: CustomLogger) -> bool:
+ base = CustomLogger.async_post_agentic_loop_response_hook
+ func = type(callback).async_post_agentic_loop_response_hook
+ return getattr(func, "__func__", func) is not getattr(base, "__func__", base)
+
+
+def _coerce_int(value: object, default: int) -> int:
+ return int(value) if isinstance(value, (int, str)) else default
+
+
+def _agentic_loop_settings(kwargs: dict[str, object]) -> tuple[int, int, list[str]]:
+ depth = _coerce_int(kwargs.get("_agentic_loop_depth"), 0)
+ max_loops = max(_coerce_int(kwargs.get("max_agentic_loops"), 3), 1)
+ raw_fingerprints = kwargs.get("_agentic_loop_fingerprints")
+ fingerprints = (
+ [str(fp) for fp in raw_fingerprints]
+ if isinstance(raw_fingerprints, list)
+ else []
+ )
+ return depth, max_loops, fingerprints
+
+
+def _fingerprint_tools(tool_calls: object) -> str:
+ try:
+ return json.dumps(tool_calls, sort_keys=True, default=str)
+ except Exception:
+ return str(tool_calls)
+
+
+def _check_agentic_loop_safety(
+ tool_calls: object,
+ fingerprints: list[str],
+ depth: int,
+ max_loops: int,
+ model: str,
+) -> str:
+ fingerprint = _fingerprint_tools(tool_calls)
+ if fingerprint in fingerprints:
+ raise ValueError(
+ "Agentic loop detected repeated tool-call fingerprint; aborting rerun"
+ )
+ if depth >= max_loops:
+ raise ValueError(f"Exceeded max_agentic_loops={max_loops} for model={model}")
+ return fingerprint
+
+
+def _wrap_response_as_fake_stream(response: object) -> object:
+ if getattr(response, "object", None) == "chat.completion.chunk":
+ return response
+ if not hasattr(response, "choices"):
+ return response
+ from litellm.llms.base_llm.base_model_iterator import (
+ convert_model_response_to_streaming,
+ )
+
+ return convert_model_response_to_streaming(cast(ModelResponse, response))
+
+
+def _add_agentic_loop_metadata(kwargs_for_followup: dict[str, object]) -> None:
+ metadata = kwargs_for_followup.get("litellm_metadata")
+ metadata = dict(metadata) if isinstance(metadata, dict) else {}
+ for key, value in kwargs_for_followup.items():
+ if (
+ key.startswith("_agentic_loop")
+ or key == "max_agentic_loops"
+ or is_interception_internal_key(key)
+ ):
+ metadata[key] = value
+ kwargs_for_followup["litellm_metadata"] = metadata
+
+
+def _filter_followup_kwargs(source: dict[str, object]) -> dict[str, object]:
+ return {
+ k: v
+ for k, v in source.items()
+ if not is_interception_internal_key(
+ k, prefixes=NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES
+ )
+ and k not in _FOLLOWUP_INTERNAL_PARAMS
+ }
+
+
+async def _execute_chat_completion_agentic_plan(
+ *,
+ plan: AgenticLoopPlan,
+ callback: CustomLogger,
+ model: str,
+ optional_params: dict[str, object],
+ kwargs: dict[str, object],
+ logging_obj: object,
+ custom_llm_provider: str,
+ depth: int,
+ max_loops: int,
+ fingerprints: list[str],
+ fingerprint: str,
+) -> object:
+ import litellm
+
+ patch = plan.request_patch or AgenticLoopRequestPatch()
+ if patch.messages is None:
+ raise ValueError("Agentic loop plan missing patched messages")
+
+ full_model_name = patch.model or model
+ if "/" not in full_model_name:
+ full_model_name = f"{custom_llm_provider}/{full_model_name}"
+
+ optional_params_for_followup = {**optional_params, **patch.optional_params}
+ if patch.tools is not None:
+ optional_params_for_followup["tools"] = patch.tools
+ if "tool_choice" not in patch.optional_params:
+ optional_params_for_followup.pop("tool_choice", None)
+
+ kwargs_for_followup = _filter_followup_kwargs(kwargs)
+ kwargs_for_followup.update(
+ {
+ k: v
+ for k, v in _filter_followup_kwargs(patch.kwargs).items()
+ if k not in optional_params_for_followup
+ }
+ )
+ kwargs_for_followup["_agentic_loop_depth"] = depth + 1
+ kwargs_for_followup["max_agentic_loops"] = max_loops
+ kwargs_for_followup["_agentic_loop_fingerprints"] = fingerprints + [fingerprint]
+ _add_agentic_loop_metadata(kwargs_for_followup)
+
+ try:
+ response_followup = await litellm.acompletion(
+ model=full_model_name,
+ messages=patch.messages,
+ **optional_params_for_followup,
+ **kwargs_for_followup,
+ )
+ if _post_hook_overridden(callback):
+ try:
+ response_followup = (
+ await callback.async_post_agentic_loop_response_hook(
+ response=response_followup, plan=plan, kwargs=kwargs
+ )
+ )
+ except Exception as e:
+ _call_id = getattr(logging_obj, "litellm_call_id", "unknown")
+ verbose_logger.exception(
+ "LiteLLM.AgenticHookError: Exception in "
+ "async_post_agentic_loop_response_hook [call_id=%s model=%s]: %s",
+ _call_id,
+ model,
+ str(e),
+ )
+ if kwargs.get("_code_interpreter_interception_converted_stream") and not depth:
+ return _wrap_response_as_fake_stream(response_followup)
+ return response_followup
+ finally:
+ try:
+ await callback.async_agentic_loop_cleanup_hook(plan=plan, kwargs=kwargs)
+ except Exception as e:
+ _call_id = getattr(logging_obj, "litellm_call_id", "unknown")
+ verbose_logger.exception(
+ "LiteLLM.AgenticHookError: Exception in "
+ "async_agentic_loop_cleanup_hook [call_id=%s model=%s]: %s",
+ _call_id,
+ model,
+ str(e),
+ )
+
+
+async def maybe_run_chat_completion_agentic_loop(
+ *,
+ response: ModelResponse,
+ model: str,
+ messages: list,
+ optional_params: dict,
+ kwargs: dict,
+ logging_obj: object,
+ custom_llm_provider: str,
+ stream: bool,
+) -> ModelResponse | CustomStreamWrapper | None:
+ import litellm
+
+ callbacks = litellm.callbacks + (
+ getattr(logging_obj, "dynamic_success_callbacks", None) or []
+ )
+ depth, max_loops, fingerprints = _agentic_loop_settings(kwargs)
+ tools = optional_params.get("tools", [])
+
+ for callback in callbacks:
+ if not isinstance(callback, CustomLogger):
+ continue
+ if not _gate_overridden(callback):
+ continue
+
+ gate_kwargs = {
+ **kwargs,
+ "_agentic_loop_api_surface": CHAT_COMPLETION_AGENTIC_SURFACE,
+ "custom_llm_provider": custom_llm_provider,
+ }
+ try:
+ should_run, tool_calls = await callback.async_should_run_agentic_loop(
+ response=response,
+ model=model,
+ messages=messages,
+ tools=tools,
+ stream=stream,
+ custom_llm_provider=custom_llm_provider,
+ kwargs=gate_kwargs,
+ )
+ except Exception as e:
+ verbose_logger.exception(
+ "LiteLLM.AgenticHookError: Exception in chat completion agentic gate: %s",
+ str(e),
+ )
+ continue
+
+ if not should_run:
+ continue
+
+ fingerprint = _check_agentic_loop_safety(
+ tool_calls=tool_calls,
+ fingerprints=fingerprints,
+ depth=depth,
+ max_loops=max_loops,
+ model=model,
+ )
+
+ try:
+ plan_kwargs = {
+ **kwargs,
+ "_agentic_loop_api_surface": CHAT_COMPLETION_AGENTIC_SURFACE,
+ "custom_llm_provider": custom_llm_provider,
+ }
+ if not _build_plan_overridden(callback):
+ return await callback.async_run_agentic_loop(
+ tools=tool_calls,
+ model=model,
+ messages=messages,
+ response=response,
+ anthropic_messages_provider_config=None,
+ anthropic_messages_optional_request_params=optional_params,
+ logging_obj=logging_obj,
+ stream=stream,
+ kwargs=plan_kwargs,
+ )
+
+ plan = await callback.async_build_agentic_loop_plan(
+ tools=tool_calls,
+ model=model,
+ messages=messages,
+ response=response,
+ anthropic_messages_provider_config=None,
+ anthropic_messages_optional_request_params=optional_params,
+ logging_obj=logging_obj,
+ stream=stream,
+ kwargs=plan_kwargs,
+ )
+
+ if plan.response_override is not None:
+ return plan.response_override
+ if plan.terminate:
+ return response
+ if not plan.run_agentic_loop:
+ continue
+
+ return await _execute_chat_completion_agentic_plan(
+ plan=plan,
+ callback=callback,
+ model=model,
+ optional_params=optional_params,
+ kwargs=kwargs,
+ logging_obj=logging_obj,
+ custom_llm_provider=custom_llm_provider,
+ depth=depth,
+ max_loops=max_loops,
+ fingerprints=fingerprints,
+ fingerprint=fingerprint,
+ )
+ except Exception as e:
+ verbose_logger.exception(
+ "LiteLLM.AgenticHookError: Exception in chat completion agentic hooks: %s",
+ str(e),
+ )
+
+ if (
+ kwargs.get("_code_interpreter_interception_converted_stream")
+ and not depth
+ and hasattr(response, "choices")
+ ):
+ return cast(
+ "ModelResponse | CustomStreamWrapper",
+ _wrap_response_as_fake_stream(response),
+ )
+ return None
diff --git a/litellm/litellm_core_utils/completion_timeout.py b/litellm/litellm_core_utils/completion_timeout.py
index 5350d88e593..70c6896a323 100644
--- a/litellm/litellm_core_utils/completion_timeout.py
+++ b/litellm/litellm_core_utils/completion_timeout.py
@@ -6,10 +6,7 @@ from typing import Callable, Optional, Union
import httpx
-from litellm.constants import (
- COMPLETION_HTTP_FALLBACK_SECONDS,
- DEFAULT_REQUEST_TIMEOUT_SECONDS,
-)
+from litellm.constants import COMPLETION_HTTP_FALLBACK_SECONDS
class CompletionTimeout:
@@ -22,17 +19,13 @@ class CompletionTimeout:
"""
Used when ``model_timeout`` and kwargs timeouts are all unset.
- ``global_timeout`` is :attr:`litellm.request_timeout` (numeric / string), not
- :class:`httpx.Timeout`.
-
- If it equals :data:`~litellm.constants.DEFAULT_REQUEST_TIMEOUT_SECONDS` (6000),
- return :data:`~litellm.constants.COMPLETION_HTTP_FALLBACK_SECONDS`. Same if
- ``None``. Otherwise return ``float(global_timeout)``.
+ ``global_timeout`` is the explicitly-configured ``litellm.request_timeout``
+ (numeric / string) or ``None`` when it was never set. ``None`` falls back to
+ :data:`~litellm.constants.COMPLETION_HTTP_FALLBACK_SECONDS`; any explicit value
+ (including ``6000``) is honored.
"""
if global_timeout is None:
return COMPLETION_HTTP_FALLBACK_SECONDS
- if float(global_timeout) == float(DEFAULT_REQUEST_TIMEOUT_SECONDS):
- return COMPLETION_HTTP_FALLBACK_SECONDS
return float(global_timeout)
@staticmethod
@@ -50,11 +43,10 @@ class CompletionTimeout:
1. ``model_timeout`` (call argument / merged ``litellm_params``)
2. ``kwargs["timeout"]``
3. ``kwargs["request_timeout"]``
- 4. Fallback from ``global_timeout`` (:attr:`litellm.request_timeout`) — if it is
- the package default (6000), use 600 instead.
+ 4. ``global_timeout`` (the explicitly-configured ``litellm.request_timeout``),
+ or 600 when nothing was configured.
Coerce :class:`httpx.Timeout` when the provider does not support it.
- Explicit ``6000`` on the model or in kwargs is kept as ``6000``.
"""
resolved: Union[float, str, httpx.Timeout]
if model_timeout is not None:
diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py
index edb97b310d7..9b2a9af4126 100644
--- a/litellm/litellm_core_utils/exception_mapping_utils.py
+++ b/litellm/litellm_core_utils/exception_mapping_utils.py
@@ -1,7 +1,7 @@
import json
import re
import traceback
-from typing import Any, Optional
+from typing import Any, Optional, Protocol, cast
import httpx
@@ -244,6 +244,2011 @@ def extract_and_raise_litellm_exception(
)
+class _ProviderHTTPException(Protocol):
+ status_code: int
+ message: str
+ response: httpx.Response
+ request: httpx.Request
+ body: object
+ code: str
+ llm_provider: str
+
+
+def _map_openai_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ # custom_llm_provider is openai, make it OpenAI
+ message = get_error_message(error_obj=original_exception)
+ if message is None:
+ if hasattr(original_exception, "message"):
+ message = original_exception.message
+ else:
+ message = str(original_exception)
+
+ if message is not None and isinstance(
+ message, str
+ ): # done to prevent user-confusion. Relevant issue - https://github.com/BerriAI/litellm/issues/1414
+ message = message.replace("OPENAI", custom_llm_provider.upper())
+ message = message.replace(
+ "openai.OpenAIError",
+ "{}.{}Error".format(custom_llm_provider, custom_llm_provider),
+ )
+ if custom_llm_provider == "openai":
+ exception_provider = "OpenAI" + "Exception"
+ else:
+ exception_provider = (
+ custom_llm_provider[0].upper() + custom_llm_provider[1:] + "Exception"
+ )
+
+ if ExceptionCheckers.is_error_str_rate_limit(error_str):
+ raise RateLimitError(
+ message=f"RateLimitError: {exception_provider} - {message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ )
+ elif ExceptionCheckers.is_error_str_context_window_exceeded(error_str):
+ raise ContextWindowExceededError(
+ message=f"ContextWindowExceededError: {exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif "invalid_request_error" in error_str and "model_not_found" in error_str:
+ raise NotFoundError(
+ message=f"{exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif "A timeout occurred" in error_str:
+ raise Timeout(
+ message=f"{exception_provider} - {message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif (
+ (
+ "invalid_request_error" in error_str
+ and "content_policy_violation" in error_str
+ )
+ or ("Invalid prompt" in error_str and "violating our usage policy" in error_str)
+ or (
+ "request was rejected as a result of the safety system" in error_str.lower()
+ )
+ ):
+ raise ContentPolicyViolationError(
+ message=f"ContentPolicyViolationError: {exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif (
+ "invalid_encrypted_content" in error_str or "could not be verified" in error_str
+ ):
+ helpful_message = (
+ f"{exception_provider} - {message}\n\n"
+ " This error occurs when load balancing Responses API across deployments with different API keys.\n"
+ " Encrypted content is tied to the organization that created it and cannot be decrypted by other organizations.\n\n"
+ " Solution: Enable 'encrypted_content_affinity' to route follow-up requests to the correct deployment:\n\n"
+ " router_settings:\n"
+ " enable_pre_call_checks: true\n"
+ " optional_pre_call_checks:\n"
+ " - encrypted_content_affinity\n\n"
+ " Learn more: https://docs.litellm.ai/docs/response_api#encrypted-content-affinity-multi-region-load-balancing"
+ )
+ raise BadRequestError(
+ message=helpful_message,
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ body=getattr(original_exception, "body", None),
+ )
+ elif (
+ "invalid_request_error" in error_str
+ and "Incorrect API key provided" not in error_str
+ ):
+ raise BadRequestError(
+ message=f"{exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ body=getattr(original_exception, "body", None),
+ )
+ elif (
+ "Web server is returning an unknown error" in error_str
+ or "The server had an error processing your request." in error_str
+ ):
+ raise litellm.InternalServerError(
+ message=f"{exception_provider} - {message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ )
+ elif "Request too large" in error_str:
+ raise RateLimitError(
+ message=f"RateLimitError: {exception_provider} - {message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif (
+ "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
+ in error_str
+ ):
+ raise AuthenticationError(
+ message=f"AuthenticationError: {exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif "Mistral API raised a streaming error" in error_str:
+ _request = httpx.Request(method="POST", url="https://api.openai.com/v1")
+ raise APIError(
+ status_code=500,
+ message=f"{exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ request=_request,
+ litellm_debug_info=extra_information,
+ )
+ elif hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"{exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"AuthenticationError: {exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 404:
+ raise NotFoundError(
+ message=f"NotFoundError: {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 == 408:
+ raise Timeout(
+ message=f"Timeout Error: {exception_provider} - {message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 422:
+ raise BadRequestError(
+ message=f"{exception_provider} - {message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ body=getattr(original_exception, "body", None),
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"RateLimitError: {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 == 500:
+ raise InternalServerError(
+ message=f"InternalServerError: {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 == 502:
+ 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:
+ raise ServiceUnavailableError(
+ message=f"ServiceUnavailableError: {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 == 504: # gateway timeout error
+ raise Timeout(
+ message=f"Timeout Error: {exception_provider} - {message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ exception_status_code=original_exception.status_code,
+ )
+ else:
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"APIError: {exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ request=getattr(original_exception, "request", None),
+ litellm_debug_info=extra_information,
+ )
+ else:
+ # if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
+ # exception_mapping_worked = True
+ raise APIConnectionError(
+ message=f"APIConnectionError: {exception_provider} - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ litellm_debug_info=extra_information,
+ request=httpx.Request(method="POST", url="https://api.openai.com/v1/"),
+ )
+
+
+def _map_anthropic_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if (
+ "prompt is too long" in error_str
+ or "prompt: length" in error_str
+ or ExceptionCheckers.is_error_str_context_window_exceeded(error_str)
+ ):
+ raise ContextWindowExceededError(
+ message="AnthropicError - {}".format(error_str),
+ model=model,
+ llm_provider="anthropic",
+ )
+ elif "overloaded_error" in error_str or "Overloaded" in error_str:
+ raise InternalServerError(
+ message="AnthropicError - {}".format(error_str),
+ model=model,
+ llm_provider="anthropic",
+ )
+ if "Invalid API Key" in error_str:
+ raise AuthenticationError(
+ message="AnthropicError - {}".format(error_str),
+ model=model,
+ llm_provider="anthropic",
+ )
+ if "content filtering policy" in error_str:
+ raise ContentPolicyViolationError(
+ message="AnthropicError - {}".format(error_str),
+ model=model,
+ llm_provider="anthropic",
+ )
+ if "Client error '400 Bad Request'" in error_str:
+ raise BadRequestError(
+ message="AnthropicError - {}".format(error_str),
+ model=model,
+ llm_provider="anthropic",
+ )
+ if hasattr(original_exception, "status_code"):
+ verbose_logger.debug(f"status_code: {original_exception.status_code}")
+ if original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"AnthropicException - {error_str}",
+ llm_provider="anthropic",
+ model=model,
+ )
+ elif (
+ original_exception.status_code == 400
+ or original_exception.status_code == 413
+ ):
+ raise BadRequestError(
+ message=f"AnthropicException - {error_str}",
+ model=model,
+ llm_provider="anthropic",
+ )
+ elif original_exception.status_code == 404:
+ raise NotFoundError(
+ message=f"AnthropicException - {error_str}",
+ model=model,
+ llm_provider="anthropic",
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"AnthropicException - {error_str}",
+ model=model,
+ llm_provider="anthropic",
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"AnthropicException - {error_str}",
+ llm_provider="anthropic",
+ model=model,
+ )
+ elif (
+ original_exception.status_code == 500
+ or original_exception.status_code == 529
+ ):
+ raise litellm.InternalServerError(
+ 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:
+ 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:
+ raise litellm.ServiceUnavailableError(
+ 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
+ raise Timeout(
+ message=f"AnthropicException Timeout - {error_str}",
+ model=model,
+ llm_provider="anthropic",
+ exception_status_code=original_exception.status_code,
+ )
+
+
+def _map_replicate_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if "Incorrect authentication token" in error_str:
+ raise AuthenticationError(
+ message=f"ReplicateException - {error_str}",
+ llm_provider="replicate",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "input is too long" in error_str:
+ raise ContextWindowExceededError(
+ message=f"ReplicateException - {error_str}",
+ model=model,
+ llm_provider="replicate",
+ response=getattr(original_exception, "response", None),
+ )
+ elif exception_type == "ModelError":
+ raise BadRequestError(
+ message=f"ReplicateException - {error_str}",
+ model=model,
+ llm_provider="replicate",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Request was throttled" in error_str:
+ raise RateLimitError(
+ message=f"ReplicateException - {error_str}",
+ llm_provider="replicate",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"ReplicateException - {original_exception.message}",
+ llm_provider="replicate",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ original_exception.status_code == 400
+ or original_exception.status_code == 413
+ ):
+ raise BadRequestError(
+ message=f"ReplicateException - {original_exception.message}",
+ model=model,
+ llm_provider="replicate",
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 422:
+ raise UnprocessableEntityError(
+ message=f"ReplicateException - {original_exception.message}",
+ model=model,
+ llm_provider="replicate",
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"ReplicateException - {original_exception.message}",
+ model=model,
+ llm_provider="replicate",
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"ReplicateException - {original_exception.message}",
+ llm_provider="replicate",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 500:
+ raise ServiceUnavailableError(
+ message=f"ReplicateException - {original_exception.message}",
+ llm_provider="replicate",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ raise APIError(
+ status_code=500,
+ message=f"ReplicateException - {str(original_exception)}",
+ llm_provider="replicate",
+ model=model,
+ request=httpx.Request(
+ method="POST",
+ url="https://api.replicate.com/v1/deployments",
+ ),
+ )
+
+
+def _map_openai_like_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if "authorization denied for" in error_str:
+
+ # Predibase returns the raw API Key in the response - this block ensures it's not returned in the exception
+ if (
+ error_str is not None
+ and isinstance(error_str, str)
+ and "bearer" in error_str.lower()
+ ):
+ # only keep the first 10 chars after the occurnence of "bearer"
+ _bearer_token_start_index = error_str.lower().find("bearer")
+ error_str = error_str[: _bearer_token_start_index + 14]
+ error_str += "XXXXXXX" + '"'
+
+ raise AuthenticationError(
+ message=f"{custom_llm_provider.capitalize()}Exception: Authentication Error - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif ExceptionCheckers.is_error_str_context_window_exceeded(error_str):
+ raise ContextWindowExceededError(
+ message=f"{custom_llm_provider.capitalize()}Exception: Context Window Error - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif "token_quota_reached" in error_str:
+ raise RateLimitError(
+ message=f"{custom_llm_provider.capitalize()}Exception: Rate Limit Errror - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ "The server received an invalid response from an upstream server." in error_str
+ ):
+ raise litellm.InternalServerError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ elif "model_no_support_for_function" in error_str:
+ raise BadRequestError(
+ message=f"{custom_llm_provider.capitalize()}Exception - Use 'watsonx_text' route instead. IBM WatsonX does not support `/text/chat` endpoint. - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ elif hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 500:
+ raise litellm.InternalServerError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ elif (
+ original_exception.status_code == 401
+ or original_exception.status_code == 403
+ ):
+ raise AuthenticationError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ elif original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ elif original_exception.status_code == 404:
+ raise NotFoundError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif (
+ original_exception.status_code == 422
+ or original_exception.status_code == 424
+ ):
+ raise BadRequestError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 503:
+ raise ServiceUnavailableError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 504: # gateway timeout error
+ raise Timeout(
+ message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ exception_status_code=original_exception.status_code,
+ )
+
+
+def _map_bedrock_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if (
+ "too many tokens" in error_str
+ or "expected maxLength:" in error_str
+ or "Input is too long" in error_str
+ or "prompt is too long" in error_str
+ or "prompt: length: 1.." in error_str
+ or "Too many input tokens" in error_str
+ ):
+ raise ContextWindowExceededError(
+ message=f"BedrockException: Context Window Error - {error_str}",
+ model=model,
+ llm_provider="bedrock",
+ )
+ elif (
+ "Conversation blocks and tool result blocks cannot be provided in the same turn."
+ in error_str
+ ):
+ raise BadRequestError(
+ message=f"BedrockException - {error_str}\n. Enable 'litellm.modify_params=True' (for PROXY do: `litellm_settings::modify_params: True`) to insert a dummy assistant message and fix this error.",
+ model=model,
+ llm_provider="bedrock",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Malformed input request" in error_str:
+ raise BadRequestError(
+ message=f"BedrockException - {error_str}",
+ model=model,
+ llm_provider="bedrock",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "A conversation must start with a user message." in error_str:
+ raise BadRequestError(
+ message=f"BedrockException - {error_str}\n. Pass in default user message via `completion(..,user_continue_message=)` or enable `litellm.modify_params=True`.\nFor Proxy: do via `litellm_settings::modify_params: True` or user_continue_message under `litellm_params`",
+ model=model,
+ llm_provider="bedrock",
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ "Unable to locate credentials" in error_str
+ or "The security token included in the request is invalid" in error_str
+ ):
+ raise AuthenticationError(
+ message=f"BedrockException Invalid Authentication - {error_str}",
+ model=model,
+ llm_provider="bedrock",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "AccessDeniedException" in error_str:
+ raise PermissionDeniedError(
+ message=f"BedrockException PermissionDeniedError - {error_str}",
+ model=model,
+ llm_provider="bedrock",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "throttlingException" in error_str or "ThrottlingException" in error_str:
+ raise RateLimitError(
+ message=f"BedrockException: Rate Limit Error - {error_str}",
+ model=model,
+ llm_provider="bedrock",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Connect timeout on endpoint URL" in error_str or "timed out" in error_str:
+ raise Timeout(
+ message=f"BedrockException: Timeout Error - {error_str}",
+ model=model,
+ llm_provider="bedrock",
+ )
+ elif "Could not process image" in error_str:
+ raise litellm.InternalServerError(
+ message=f"BedrockException - {error_str}",
+ model=model,
+ llm_provider="bedrock",
+ )
+ elif hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 500:
+ raise ServiceUnavailableError(
+ message=f"BedrockException - {original_exception.message}",
+ llm_provider="bedrock",
+ model=model,
+ response=httpx.Response(
+ status_code=500,
+ request=httpx.Request(
+ method="POST", url="https://api.openai.com/v1/"
+ ),
+ ),
+ )
+ elif original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"BedrockException - {original_exception.message}",
+ llm_provider="bedrock",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"BedrockException - {original_exception.message}",
+ llm_provider="bedrock",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 404:
+ raise NotFoundError(
+ message=f"BedrockException - {original_exception.message}",
+ llm_provider="bedrock",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"BedrockException - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 422:
+ raise BadRequestError(
+ message=f"BedrockException - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"BedrockException - {original_exception.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:
+ raise ServiceUnavailableError(
+ message=f"BedrockException - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 504: # gateway timeout error
+ raise Timeout(
+ message=f"BedrockException - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ exception_status_code=original_exception.status_code,
+ )
+
+
+def _map_sagemaker_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if "Unable to locate credentials" in error_str:
+ raise BadRequestError(
+ message=f"litellm.BadRequestError: SagemakerException - {error_str}",
+ model=model,
+ llm_provider="sagemaker",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Input validation error: `best_of` must be > 0 and <= 2" in error_str:
+ raise BadRequestError(
+ message="SagemakerException - the value of 'n' must be > 0 and <= 2 for sagemaker endpoints",
+ model=model,
+ llm_provider="sagemaker",
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ "`inputs` tokens + `max_new_tokens` must be <=" in error_str
+ or "instance type with more CPU capacity or memory" in error_str
+ ):
+ raise ContextWindowExceededError(
+ message=f"SagemakerException - {error_str}",
+ model=model,
+ llm_provider="sagemaker",
+ response=getattr(original_exception, "response", None),
+ )
+ elif hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 500:
+ raise ServiceUnavailableError(
+ message=f"SagemakerException - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=httpx.Response(
+ status_code=500,
+ request=httpx.Request(
+ method="POST", url="https://api.openai.com/v1/"
+ ),
+ ),
+ )
+ elif original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"SagemakerException - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"SagemakerException - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 404:
+ raise NotFoundError(
+ message=f"SagemakerException - {original_exception.message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"SagemakerException - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif (
+ original_exception.status_code == 422
+ or original_exception.status_code == 424
+ ):
+ raise BadRequestError(
+ message=f"SagemakerException - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"SagemakerException - {original_exception.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:
+ raise ServiceUnavailableError(
+ message=f"SagemakerException - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 504: # gateway timeout error
+ raise Timeout(
+ message=f"SagemakerException - {original_exception.message}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ exception_status_code=original_exception.status_code,
+ )
+
+
+def _map_vertex_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if (
+ "Vertex AI API has not been used in project" in error_str
+ or "Unable to find your project" in error_str
+ ):
+ raise BadRequestError(
+ message=f"litellm.BadRequestError: {custom_llm_provider}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=httpx.Response(
+ status_code=400,
+ request=httpx.Request(
+ method="POST",
+ url=" https://cloud.google.com/vertex-ai/",
+ ),
+ ),
+ litellm_debug_info=extra_information,
+ )
+ if "400 Request payload size exceeds" in error_str:
+ raise ContextWindowExceededError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ )
+ elif ExceptionCheckers.is_error_str_context_window_exceeded(error_str):
+ raise ContextWindowExceededError(
+ message=f"ContextWindowExceededError: {custom_llm_provider.capitalize()}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif "None Unknown Error." in error_str or "Content has no parts." in error_str:
+ raise litellm.InternalServerError(
+ message=f"litellm.InternalServerError: {custom_llm_provider}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=httpx.Response(
+ status_code=500,
+ content=str(original_exception),
+ request=httpx.Request(method="completion", url="https://github.com/BerriAI/litellm"), # type: ignore
+ ),
+ litellm_debug_info=extra_information,
+ )
+ elif "API key not valid." in error_str:
+ raise AuthenticationError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif "403" in error_str:
+ raise BadRequestError(
+ message=f"{custom_llm_provider.capitalize()}Exception BadRequestError - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=httpx.Response(
+ status_code=403,
+ request=httpx.Request(
+ method="POST",
+ url=" https://cloud.google.com/vertex-ai/",
+ ),
+ ),
+ litellm_debug_info=extra_information,
+ )
+ elif (
+ "The response was blocked." in error_str
+ or "Output blocked by content filtering policy"
+ in error_str # anthropic on vertex ai
+ ):
+ raise ContentPolicyViolationError(
+ message=f"{custom_llm_provider.capitalize()}Exception ContentPolicyViolationError - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ response=httpx.Response(
+ status_code=400,
+ request=httpx.Request(
+ method="POST",
+ url=" https://cloud.google.com/vertex-ai/",
+ ),
+ ),
+ )
+ 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
+ ):
+ raise RateLimitError(
+ message=f"litellm.RateLimitError: {custom_llm_provider}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ response=httpx.Response(
+ status_code=429,
+ request=httpx.Request(
+ method="POST",
+ url=" https://cloud.google.com/vertex-ai/",
+ ),
+ ),
+ )
+ elif (
+ isinstance(getattr(original_exception, "status_code", None), int)
+ and 500 <= original_exception.status_code < 600
+ and _get_body_error_code(error_str) == 429
+ ):
+ # upstream gateway wraps a 429 inside a 5xx envelope
+ # e.g. HTTP 500/503 with {"error":{"code":429,...}}.
+ # Scoped to 5xx so HTTP 400/401 with body code:429
+ # still maps to BadRequestError / AuthenticationError.
+ raise RateLimitError(
+ message=f"litellm.RateLimitError: {custom_llm_provider}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ response=httpx.Response(
+ status_code=429,
+ request=httpx.Request(
+ method="POST",
+ url=" https://cloud.google.com/vertex-ai/",
+ ),
+ ),
+ )
+ elif (
+ "500 Internal Server Error" in error_str
+ or "The model is overloaded." in error_str
+ ):
+ raise litellm.InternalServerError(
+ message=f"litellm.InternalServerError: {custom_llm_provider}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ if hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"{custom_llm_provider.capitalize()}Exception BadRequestError - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ response=httpx.Response(
+ status_code=400,
+ request=httpx.Request(
+ method="POST",
+ url="https://cloud.google.com/vertex-ai/",
+ ),
+ ),
+ )
+ if original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ if original_exception.status_code == 403:
+ raise PermissionDeniedError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=httpx.Response(
+ status_code=403,
+ request=httpx.Request(
+ method="POST",
+ url="https://cloud.google.com/vertex-ai/",
+ ),
+ ),
+ )
+ if original_exception.status_code == 404:
+ raise NotFoundError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ if original_exception.status_code == 408:
+ raise Timeout(
+ message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+
+ if original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"litellm.RateLimitError: {custom_llm_provider.capitalize()}Exception - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ response=httpx.Response(
+ status_code=429,
+ request=httpx.Request(
+ method="POST",
+ url=" https://cloud.google.com/vertex-ai/",
+ ),
+ ),
+ )
+ if original_exception.status_code == 500:
+ raise litellm.InternalServerError(
+ message=f"{custom_llm_provider.capitalize()}Exception InternalServerError - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ response=httpx.Response(
+ status_code=500,
+ content=str(original_exception),
+ request=httpx.Request(method="completion", url="https://github.com/BerriAI/litellm"), # type: ignore
+ ),
+ )
+ if original_exception.status_code == 502:
+ raise APIConnectionError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+ if original_exception.status_code == 503:
+ raise ServiceUnavailableError(
+ message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ )
+
+
+def _map_cloudflare_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if "Authentication error" in error_str:
+ raise AuthenticationError(
+ message=f"Cloudflare Exception - {original_exception.message}",
+ llm_provider="cloudflare",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ if "must have required property" in error_str:
+ raise BadRequestError(
+ message=f"Cloudflare Exception - {original_exception.message}",
+ llm_provider="cloudflare",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+
+
+def _map_cohere_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if "invalid api token" in error_str or "No API key provided." in error_str:
+ raise AuthenticationError(
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "invalid type: parameter" in error_str:
+ raise BadRequestError(
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "too many tokens" in error_str:
+ raise ContextWindowExceededError(
+ message=f"CohereException - {original_exception.message}",
+ model=model,
+ llm_provider="cohere",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "internal server error" in error_str.lower():
+ raise InternalServerError(
+ message=f"CohereException - {error_str}",
+ model=model,
+ llm_provider="cohere",
+ response=getattr(original_exception, "response", None),
+ )
+ elif hasattr(original_exception, "status_code"):
+ if (
+ original_exception.status_code == 400
+ or original_exception.status_code == 498
+ ):
+ raise BadRequestError(
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ )
+ elif original_exception.status_code == 500:
+ raise InternalServerError(
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ "CohereConnectionError" in exception_type
+ ): # cohere seems to fire these errors when we load test it (1k+ messages / min)
+ raise RateLimitError(
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "invalid type:" in error_str:
+ raise BadRequestError(
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Unexpected server error" in error_str:
+ raise InternalServerError(
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ else:
+ if hasattr(original_exception, "status_code"):
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"CohereException - {original_exception.message}",
+ llm_provider="cohere",
+ model=model,
+ request=getattr(original_exception, "request", None),
+ )
+ raise cast(Exception, original_exception)
+
+
+def _map_huggingface_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if "length limit exceeded" in error_str:
+ raise ContextWindowExceededError(
+ message=error_str,
+ model=model,
+ llm_provider="huggingface",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "A valid user token is required" in error_str:
+ raise BadRequestError(
+ message=error_str,
+ llm_provider="huggingface",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Rate limit reached" in error_str:
+ raise RateLimitError(
+ message=error_str,
+ llm_provider="huggingface",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ if hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"HuggingfaceException - {original_exception.message}",
+ llm_provider="huggingface",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"HuggingfaceException - {original_exception.message}",
+ model=model,
+ llm_provider="huggingface",
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"HuggingfaceException - {original_exception.message}",
+ model=model,
+ llm_provider="huggingface",
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"HuggingfaceException - {original_exception.message}",
+ llm_provider="huggingface",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 503:
+ raise ServiceUnavailableError(
+ message=f"HuggingfaceException - {original_exception.message}",
+ llm_provider="huggingface",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ else:
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"HuggingfaceException - {original_exception.message}",
+ llm_provider="huggingface",
+ model=model,
+ request=getattr(original_exception, "request", None),
+ )
+
+
+def _map_ai21_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if hasattr(original_exception, "message"):
+ if "Prompt has too many tokens" in original_exception.message:
+ raise ContextWindowExceededError(
+ message=f"AI21Exception - {original_exception.message}",
+ model=model,
+ llm_provider="ai21",
+ response=getattr(original_exception, "response", None),
+ )
+ if "Bad or missing API token." in original_exception.message:
+ raise BadRequestError(
+ message=f"AI21Exception - {original_exception.message}",
+ model=model,
+ llm_provider="ai21",
+ response=getattr(original_exception, "response", None),
+ )
+ if hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"AI21Exception - {original_exception.message}",
+ llm_provider="ai21",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"AI21Exception - {original_exception.message}",
+ model=model,
+ llm_provider="ai21",
+ )
+ if original_exception.status_code == 422:
+ raise BadRequestError(
+ message=f"AI21Exception - {original_exception.message}",
+ model=model,
+ llm_provider="ai21",
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"AI21Exception - {original_exception.message}",
+ llm_provider="ai21",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ else:
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"AI21Exception - {original_exception.message}",
+ llm_provider="ai21",
+ model=model,
+ request=getattr(original_exception, "request", None),
+ )
+
+
+def _map_nlp_cloud_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if "detail" in error_str:
+ if "Input text length should not exceed" in error_str:
+ raise ContextWindowExceededError(
+ message=f"NLPCloudException - {error_str}",
+ model=model,
+ llm_provider="nlp_cloud",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "value is not a valid" in error_str:
+ raise BadRequestError(
+ message=f"NLPCloudException - {error_str}",
+ model=model,
+ llm_provider="nlp_cloud",
+ response=getattr(original_exception, "response", None),
+ )
+ else:
+ raise APIError(
+ status_code=500,
+ message=f"NLPCloudException - {error_str}",
+ model=model,
+ llm_provider="nlp_cloud",
+ request=getattr(original_exception, "request", None),
+ )
+ if hasattr(
+ original_exception, "status_code"
+ ): # https://docs.nlpcloud.com/?shell#errors
+ if (
+ original_exception.status_code == 400
+ or original_exception.status_code == 406
+ or original_exception.status_code == 413
+ or original_exception.status_code == 422
+ ):
+ raise BadRequestError(
+ message=f"NLPCloudException - {original_exception.message}",
+ llm_provider="nlp_cloud",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ original_exception.status_code == 401
+ or original_exception.status_code == 403
+ ):
+ raise AuthenticationError(
+ message=f"NLPCloudException - {original_exception.message}",
+ llm_provider="nlp_cloud",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ original_exception.status_code == 522
+ or original_exception.status_code == 524
+ ):
+ raise Timeout(
+ message=f"NLPCloudException - {original_exception.message}",
+ model=model,
+ llm_provider="nlp_cloud",
+ )
+ elif (
+ original_exception.status_code == 429
+ or original_exception.status_code == 402
+ ):
+ raise RateLimitError(
+ message=f"NLPCloudException - {original_exception.message}",
+ llm_provider="nlp_cloud",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ original_exception.status_code == 500
+ or original_exception.status_code == 503
+ ):
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"NLPCloudException - {original_exception.message}",
+ llm_provider="nlp_cloud",
+ model=model,
+ request=getattr(original_exception, "request", None),
+ )
+ elif (
+ original_exception.status_code == 504
+ or original_exception.status_code == 520
+ ):
+ raise ServiceUnavailableError(
+ message=f"NLPCloudException - {original_exception.message}",
+ model=model,
+ llm_provider="nlp_cloud",
+ response=getattr(original_exception, "response", None),
+ )
+ else:
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"NLPCloudException - {original_exception.message}",
+ llm_provider="nlp_cloud",
+ model=model,
+ request=getattr(original_exception, "request", None),
+ )
+
+
+def _map_together_ai_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ try:
+ error_response = json.loads(error_str)
+ except Exception:
+ error_response = {"error": error_str}
+ if (
+ "error" in error_response
+ and "`inputs` tokens + `max_new_tokens` must be <=" in error_response["error"]
+ ):
+ raise ContextWindowExceededError(
+ message=f"TogetherAIException - {error_response['error']}",
+ model=model,
+ llm_provider="together_ai",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "error" in error_response and "invalid private key" in error_response["error"]:
+ raise AuthenticationError(
+ message=f"TogetherAIException - {error_response['error']}",
+ llm_provider="together_ai",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "error" in error_response and "INVALID_ARGUMENT" in error_response["error"]:
+ raise BadRequestError(
+ message=f"TogetherAIException - {error_response['error']}",
+ model=model,
+ llm_provider="together_ai",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "A timeout occurred" in error_str:
+ raise Timeout(
+ message=f"TogetherAIException - {error_str}",
+ model=model,
+ llm_provider="together_ai",
+ )
+ elif (
+ "error" in error_response
+ and "API key doesn't match expected format." in error_response["error"]
+ ):
+ raise BadRequestError(
+ message=f"TogetherAIException - {error_response['error']}",
+ model=model,
+ llm_provider="together_ai",
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ "error_type" in error_response and error_response["error_type"] == "validation"
+ ):
+ raise BadRequestError(
+ message=f"TogetherAIException - {error_response['error']}",
+ model=model,
+ llm_provider="together_ai",
+ response=getattr(original_exception, "response", None),
+ )
+ if hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 408:
+ raise Timeout(
+ message=f"TogetherAIException - {original_exception.message}",
+ model=model,
+ llm_provider="together_ai",
+ )
+ elif original_exception.status_code == 422:
+ raise BadRequestError(
+ message=f"TogetherAIException - {error_response['error']}",
+ model=model,
+ llm_provider="together_ai",
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"TogetherAIException - {original_exception.message}",
+ llm_provider="together_ai",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 524:
+ raise Timeout(
+ message=f"TogetherAIException - {original_exception.message}",
+ llm_provider="together_ai",
+ model=model,
+ )
+ else:
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"TogetherAIException - {original_exception.message}",
+ llm_provider="together_ai",
+ model=model,
+ request=getattr(original_exception, "request", None),
+ )
+
+
+def _map_aleph_alpha_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if "This is longer than the model's maximum context length" in error_str:
+ raise ContextWindowExceededError(
+ message=f"AlephAlphaException - {original_exception.message}",
+ llm_provider="aleph_alpha",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "InvalidToken" in error_str or "No token provided" in error_str:
+ raise BadRequestError(
+ message=f"AlephAlphaException - {original_exception.message}",
+ llm_provider="aleph_alpha",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif hasattr(original_exception, "status_code"):
+ verbose_logger.debug(f"status code: {original_exception.status_code}")
+ if original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"AlephAlphaException - {original_exception.message}",
+ llm_provider="aleph_alpha",
+ model=model,
+ )
+ elif original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"AlephAlphaException - {original_exception.message}",
+ llm_provider="aleph_alpha",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"AlephAlphaException - {original_exception.message}",
+ llm_provider="aleph_alpha",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 500:
+ raise ServiceUnavailableError(
+ message=f"AlephAlphaException - {original_exception.message}",
+ llm_provider="aleph_alpha",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ raise cast(Exception, original_exception)
+ raise cast(Exception, original_exception)
+
+
+def _map_ollama_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if isinstance(original_exception, dict):
+ error_str = original_exception.get("error", "")
+ else:
+ error_str = str(original_exception)
+ if "no such file or directory" in error_str:
+ raise BadRequestError(
+ message=f"OllamaException: Invalid Model/Model not loaded - {original_exception}",
+ model=model,
+ llm_provider="ollama",
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Failed to establish a new connection" in error_str:
+ raise ServiceUnavailableError(
+ message=f"OllamaException: {original_exception}",
+ llm_provider="ollama",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Invalid response object from API" in error_str:
+ raise BadRequestError(
+ message=f"OllamaException: {original_exception}",
+ llm_provider="ollama",
+ model=model,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Read timed out" in error_str:
+ raise Timeout(
+ message=f"OllamaException: {original_exception}",
+ llm_provider="ollama",
+ model=model,
+ )
+
+
+def _map_vllm_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 0:
+ raise APIConnectionError(
+ message=f"VLLMException - {original_exception.message}",
+ llm_provider="vllm",
+ model=model,
+ request=getattr(original_exception, "request", None),
+ )
+
+
+def _map_azure_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ message = get_error_message(error_obj=original_exception)
+ if message is None:
+ if hasattr(original_exception, "message"):
+ message = original_exception.message
+ else:
+ message = str(original_exception)
+
+ # Azure OpenAI (especially Images) often nests error details under
+ # body["error"]. Detect content policy violations using the structured
+ # payload in addition to string matching.
+ azure_error_code: Optional[str] = None
+ try:
+ body_dict = getattr(original_exception, "body", None) or {}
+ if isinstance(body_dict, dict):
+ if isinstance(body_dict.get("error"), dict):
+ azure_error_code = body_dict["error"].get("code") # type: ignore[index]
+ # Also check inner_error for
+ # ResponsibleAIPolicyViolation which indicates a
+ # content policy violation even when the top-level
+ # code is generic (e.g. "invalid_request_error").
+ if azure_error_code != "content_policy_violation":
+ _inner = body_dict["error"].get(
+ "inner_error"
+ ) or body_dict[ # type: ignore[index]
+ "error"
+ ].get(
+ "innererror"
+ ) # type: ignore[index]
+ if (
+ isinstance(_inner, dict)
+ and _inner.get("code") == "ResponsibleAIPolicyViolation"
+ ):
+ azure_error_code = "content_policy_violation"
+ else:
+ azure_error_code = body_dict.get("code")
+ except Exception:
+ azure_error_code = None
+
+ if "Internal server error" in error_str:
+ raise litellm.InternalServerError(
+ message=f"AzureException Internal server error - {message}",
+ llm_provider="azure",
+ model=model,
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "This model's maximum context length is" in error_str:
+ raise ContextWindowExceededError(
+ message=f"AzureException ContextWindowExceededError - {message}",
+ llm_provider="azure",
+ model=model,
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "DeploymentNotFound" in error_str:
+ raise NotFoundError(
+ message=f"AzureException NotFoundError - {message}",
+ llm_provider="azure",
+ model=model,
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ )
+ elif (
+ azure_error_code == "content_policy_violation"
+ or ExceptionCheckers.is_azure_content_policy_violation_error(error_str)
+ ):
+ 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 (
+ azure_error_code == "invalid_encrypted_content"
+ or "could not be verified" in error_str
+ ):
+ helpful_message = (
+ f"AzureException - {message}\n\n"
+ "This error occurs when load balancing Responses API across deployments with different API keys.\n"
+ " Encrypted content is tied to the organization that created it and cannot be decrypted by other organizations.\n\n"
+ " Solution: Enable 'encrypted_content_affinity' to route follow-up requests to the correct deployment:\n\n"
+ " router_settings:\n"
+ " enable_pre_call_checks: true\n"
+ " optional_pre_call_checks:\n"
+ " - encrypted_content_affinity\n\n"
+ " Learn more: https://docs.litellm.ai/docs/response_api#encrypted-content-affinity-multi-region-load-balancing"
+ )
+ raise BadRequestError(
+ message=helpful_message,
+ llm_provider="azure",
+ model=model,
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ body=getattr(original_exception, "body", None),
+ )
+ elif "invalid_request_error" in error_str:
+ raise BadRequestError(
+ message=f"AzureException BadRequestError - {message}",
+ llm_provider="azure",
+ model=model,
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ body=getattr(original_exception, "body", None),
+ )
+ elif (
+ "The api_key client option must be set either by passing api_key to the client or by setting"
+ in error_str
+ ):
+ raise AuthenticationError(
+ message=f"{exception_provider} AuthenticationError - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ )
+ elif "Connection error" in error_str:
+ raise APIConnectionError(
+ message=f"{exception_provider} APIConnectionError - {message}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ litellm_debug_info=extra_information,
+ )
+ elif hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"AzureException - {message}",
+ llm_provider="azure",
+ model=model,
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ body=getattr(original_exception, "body", None),
+ )
+ elif original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"AzureException AuthenticationError - {message}",
+ llm_provider="azure",
+ model=model,
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"AzureException Timeout - {message}",
+ model=model,
+ litellm_debug_info=extra_information,
+ llm_provider="azure",
+ )
+ elif original_exception.status_code == 422:
+ raise BadRequestError(
+ message=f"AzureException BadRequestError - {message}",
+ model=model,
+ llm_provider="azure",
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"AzureException RateLimitError - {message}",
+ model=model,
+ llm_provider="azure",
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 502:
+ 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:
+ raise ServiceUnavailableError(
+ message=f"AzureException ServiceUnavailableError - {message}",
+ model=model,
+ llm_provider="azure",
+ litellm_debug_info=extra_information,
+ response=getattr(original_exception, "response", None),
+ )
+ elif original_exception.status_code == 504: # gateway timeout error
+ raise Timeout(
+ message=f"AzureException Timeout - {message}",
+ model=model,
+ litellm_debug_info=extra_information,
+ llm_provider="azure",
+ exception_status_code=original_exception.status_code,
+ )
+ else:
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"AzureException APIError - {message}",
+ llm_provider="azure",
+ litellm_debug_info=extra_information,
+ model=model,
+ request=httpx.Request(method="POST", url="https://openai.com/"),
+ )
+ else:
+ # if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
+ raise APIConnectionError(
+ message=f"{exception_provider} APIConnectionError - {message}\n{_redact_string(traceback.format_exc())}",
+ llm_provider="azure",
+ model=model,
+ litellm_debug_info=extra_information,
+ request=httpx.Request(method="POST", url="https://openai.com/"),
+ )
+
+
+def _map_openrouter_exception(
+ *,
+ model: str,
+ original_exception: _ProviderHTTPException,
+ custom_llm_provider: str,
+ error_str: str,
+ exception_type: str,
+ exception_provider: str,
+ extra_information: str,
+) -> None:
+ if hasattr(original_exception, "status_code"):
+ if original_exception.status_code == 400:
+ raise BadRequestError(
+ message=f"{exception_provider} - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 401:
+ raise AuthenticationError(
+ message=f"AuthenticationError: {exception_provider} - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 404:
+ raise NotFoundError(
+ message=f"NotFoundError: {exception_provider} - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 408:
+ raise Timeout(
+ message=f"Timeout Error: {exception_provider} - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 422:
+ raise BadRequestError(
+ message=f"BadRequestError: {exception_provider} - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 429:
+ raise RateLimitError(
+ message=f"RateLimitError: {exception_provider} - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 503:
+ raise ServiceUnavailableError(
+ message=f"ServiceUnavailableError: {exception_provider} - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ response=getattr(original_exception, "response", None),
+ litellm_debug_info=extra_information,
+ )
+ elif original_exception.status_code == 504: # gateway timeout error
+ raise Timeout(
+ message=f"Timeout Error: {exception_provider} - {error_str}",
+ model=model,
+ llm_provider=custom_llm_provider,
+ litellm_debug_info=extra_information,
+ exception_status_code=original_exception.status_code,
+ )
+ else:
+ raise APIError(
+ status_code=original_exception.status_code,
+ message=f"APIError: {exception_provider} - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ request=getattr(original_exception, "request", None),
+ litellm_debug_info=extra_information,
+ )
+ else:
+ # if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
+ raise APIConnectionError(
+ message=f"APIConnectionError: {exception_provider} - {error_str}",
+ llm_provider=custom_llm_provider,
+ model=model,
+ litellm_debug_info=extra_information,
+ request=httpx.Request(method="POST", url="https://api.openai.com/v1/"),
+ )
+
+
def exception_type( # type: ignore
model,
original_exception,
@@ -259,6 +2264,9 @@ def exception_type( # type: ignore
return original_exception
exception_mapping_worked = False
exception_provider = custom_llm_provider
+ mappable_exception: _ProviderHTTPException = cast(
+ "_ProviderHTTPException", original_exception
+ )
if litellm.suppress_debug_info is False:
print() # noqa: T201
print( # noqa: T201
@@ -382,2078 +2390,199 @@ def exception_type( # type: ignore
or custom_llm_provider in litellm.openai_compatible_providers
or custom_llm_provider == "mistral"
):
- # custom_llm_provider is openai, make it OpenAI
- message = get_error_message(error_obj=original_exception)
- if message is None:
- if hasattr(original_exception, "message"):
- message = original_exception.message
- else:
- message = str(original_exception)
-
- if message is not None and isinstance(
- message, str
- ): # done to prevent user-confusion. Relevant issue - https://github.com/BerriAI/litellm/issues/1414
- message = message.replace("OPENAI", custom_llm_provider.upper())
- message = message.replace(
- "openai.OpenAIError",
- "{}.{}Error".format(custom_llm_provider, custom_llm_provider),
- )
- if custom_llm_provider == "openai":
- exception_provider = "OpenAI" + "Exception"
- else:
- exception_provider = (
- custom_llm_provider[0].upper()
- + custom_llm_provider[1:]
- + "Exception"
- )
-
- if ExceptionCheckers.is_error_str_rate_limit(error_str):
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"RateLimitError: {exception_provider} - {message}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- )
- elif ExceptionCheckers.is_error_str_context_window_exceeded(error_str):
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"ContextWindowExceededError: {exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif (
- "invalid_request_error" in error_str
- and "model_not_found" in error_str
- ):
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"{exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif "A timeout occurred" in error_str:
- exception_mapping_worked = True
- raise Timeout(
- message=f"{exception_provider} - {message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif (
- (
- "invalid_request_error" in error_str
- and "content_policy_violation" in error_str
- )
- or (
- "Invalid prompt" in error_str
- and "violating our usage policy" in error_str
- )
- or (
- "request was rejected as a result of the safety system"
- in error_str.lower()
- )
- ):
- exception_mapping_worked = True
- raise ContentPolicyViolationError(
- message=f"ContentPolicyViolationError: {exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif (
- "invalid_encrypted_content" in error_str
- or "could not be verified" in error_str
- ):
- exception_mapping_worked = True
- helpful_message = (
- f"{exception_provider} - {message}\n\n"
- " This error occurs when load balancing Responses API across deployments with different API keys.\n"
- " Encrypted content is tied to the organization that created it and cannot be decrypted by other organizations.\n\n"
- " Solution: Enable 'encrypted_content_affinity' to route follow-up requests to the correct deployment:\n\n"
- " router_settings:\n"
- " enable_pre_call_checks: true\n"
- " optional_pre_call_checks:\n"
- " - encrypted_content_affinity\n\n"
- " Learn more: https://docs.litellm.ai/docs/response_api#encrypted-content-affinity-multi-region-load-balancing"
- )
- raise BadRequestError(
- message=helpful_message,
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- body=getattr(original_exception, "body", None),
- )
- elif (
- "invalid_request_error" in error_str
- and "Incorrect API key provided" not in error_str
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- body=getattr(original_exception, "body", None),
- )
- elif (
- "Web server is returning an unknown error" in error_str
- or "The server had an error processing your request." in error_str
- ):
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- message=f"{exception_provider} - {message}",
- model=model,
- llm_provider=custom_llm_provider,
- )
- elif "Request too large" in error_str:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"RateLimitError: {exception_provider} - {message}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif (
- "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable"
- in error_str
- ):
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"AuthenticationError: {exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif "Mistral API raised a streaming error" in error_str:
- exception_mapping_worked = True
- _request = httpx.Request(
- method="POST", url="https://api.openai.com/v1"
- )
- raise APIError(
- status_code=500,
- message=f"{exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- request=_request,
- litellm_debug_info=extra_information,
- )
- elif hasattr(original_exception, "status_code"):
- exception_mapping_worked = True
- if original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"AuthenticationError: {exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 404:
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"NotFoundError: {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 == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"Timeout Error: {exception_provider} - {message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 422:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{exception_provider} - {message}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- body=getattr(original_exception, "body", None),
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"RateLimitError: {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 == 500:
- exception_mapping_worked = True
- raise InternalServerError(
- message=f"InternalServerError: {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 == 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(
- message=f"ServiceUnavailableError: {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 == 504: # gateway timeout error
- exception_mapping_worked = True
- raise Timeout(
- message=f"Timeout Error: {exception_provider} - {message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- exception_status_code=original_exception.status_code,
- )
- else:
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"APIError: {exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- request=getattr(original_exception, "request", None),
- litellm_debug_info=extra_information,
- )
- else:
- # if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
- # exception_mapping_worked = True
- raise APIConnectionError(
- message=f"APIConnectionError: {exception_provider} - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- litellm_debug_info=extra_information,
- request=httpx.Request(
- method="POST", url="https://api.openai.com/v1/"
- ),
- )
+ _map_openai_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif (
custom_llm_provider == "anthropic"
or custom_llm_provider == "anthropic_text"
): # one of the anthropics
- if (
- "prompt is too long" in error_str
- or "prompt: length" in error_str
- or ExceptionCheckers.is_error_str_context_window_exceeded(error_str)
- ):
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message="AnthropicError - {}".format(error_str),
- model=model,
- llm_provider="anthropic",
- )
- elif "overloaded_error" in error_str or "Overloaded" in error_str:
- exception_mapping_worked = True
- raise InternalServerError(
- message="AnthropicError - {}".format(error_str),
- model=model,
- llm_provider="anthropic",
- )
- if "Invalid API Key" in error_str:
- exception_mapping_worked = True
- raise AuthenticationError(
- message="AnthropicError - {}".format(error_str),
- model=model,
- llm_provider="anthropic",
- )
- if "content filtering policy" in error_str:
- exception_mapping_worked = True
- raise ContentPolicyViolationError(
- message="AnthropicError - {}".format(error_str),
- model=model,
- llm_provider="anthropic",
- )
- if "Client error '400 Bad Request'" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message="AnthropicError - {}".format(error_str),
- model=model,
- llm_provider="anthropic",
- )
- if hasattr(original_exception, "status_code"):
- verbose_logger.debug(
- f"status_code: {original_exception.status_code}"
- )
- if original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"AnthropicException - {error_str}",
- llm_provider="anthropic",
- model=model,
- )
- elif (
- original_exception.status_code == 400
- or original_exception.status_code == 413
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"AnthropicException - {error_str}",
- model=model,
- llm_provider="anthropic",
- )
- elif original_exception.status_code == 404:
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"AnthropicException - {error_str}",
- model=model,
- llm_provider="anthropic",
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"AnthropicException - {error_str}",
- model=model,
- llm_provider="anthropic",
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"AnthropicException - {error_str}",
- llm_provider="anthropic",
- model=model,
- )
- elif (
- original_exception.status_code == 500
- or original_exception.status_code == 529
- ):
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- 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
- raise litellm.ServiceUnavailableError(
- 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:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"ReplicateException - {error_str}",
- llm_provider="replicate",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "input is too long" in error_str:
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"ReplicateException - {error_str}",
- model=model,
- llm_provider="replicate",
- response=getattr(original_exception, "response", None),
- )
- elif exception_type == "ModelError":
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"ReplicateException - {error_str}",
- model=model,
- llm_provider="replicate",
- response=getattr(original_exception, "response", None),
- )
- elif "Request was throttled" in error_str:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"ReplicateException - {error_str}",
- llm_provider="replicate",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif hasattr(original_exception, "status_code"):
- if original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"ReplicateException - {original_exception.message}",
- llm_provider="replicate",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif (
- original_exception.status_code == 400
- or original_exception.status_code == 413
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"ReplicateException - {original_exception.message}",
- model=model,
- llm_provider="replicate",
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 422:
- exception_mapping_worked = True
- raise UnprocessableEntityError(
- message=f"ReplicateException - {original_exception.message}",
- model=model,
- llm_provider="replicate",
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"ReplicateException - {original_exception.message}",
- model=model,
- llm_provider="replicate",
- )
- elif original_exception.status_code == 422:
- exception_mapping_worked = True
- raise UnprocessableEntityError(
- message=f"ReplicateException - {original_exception.message}",
- llm_provider="replicate",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"ReplicateException - {original_exception.message}",
- llm_provider="replicate",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 500:
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"ReplicateException - {original_exception.message}",
- llm_provider="replicate",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- exception_mapping_worked = True
- raise APIError(
- status_code=500,
- message=f"ReplicateException - {str(original_exception)}",
- llm_provider="replicate",
+ _map_anthropic_exception(
model=model,
- request=httpx.Request(
- method="POST",
- url="https://api.replicate.com/v1/deployments",
- ),
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
+ elif custom_llm_provider == "replicate":
+ _map_replicate_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
)
elif custom_llm_provider in litellm._openai_like_providers:
- if "authorization denied for" in error_str:
- exception_mapping_worked = True
-
- # Predibase returns the raw API Key in the response - this block ensures it's not returned in the exception
- if (
- error_str is not None
- and isinstance(error_str, str)
- and "bearer" in error_str.lower()
- ):
- # only keep the first 10 chars after the occurnence of "bearer"
- _bearer_token_start_index = error_str.lower().find("bearer")
- error_str = error_str[: _bearer_token_start_index + 14]
- error_str += "XXXXXXX" + '"'
-
- raise AuthenticationError(
- message=f"{custom_llm_provider.capitalize()}Exception: Authentication Error - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif ExceptionCheckers.is_error_str_context_window_exceeded(error_str):
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"{custom_llm_provider.capitalize()}Exception: Context Window Error - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif "token_quota_reached" in error_str:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"{custom_llm_provider.capitalize()}Exception: Rate Limit Errror - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif (
- "The server received an invalid response from an upstream server."
- in error_str
- ):
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- elif "model_no_support_for_function" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{custom_llm_provider.capitalize()}Exception - Use 'watsonx_text' route instead. IBM WatsonX does not support `/text/chat` endpoint. - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- elif hasattr(original_exception, "status_code"):
- if original_exception.status_code == 500:
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- elif (
- original_exception.status_code == 401
- or original_exception.status_code == 403
- ):
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- elif original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- elif original_exception.status_code == 404:
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif (
- original_exception.status_code == 422
- or original_exception.status_code == 424
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 503:
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 504: # gateway timeout error
- exception_mapping_worked = True
- raise Timeout(
- message=f"{custom_llm_provider.capitalize()}Exception - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- exception_status_code=original_exception.status_code,
- )
+ _map_openai_like_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "bedrock":
- if (
- "too many tokens" in error_str
- or "expected maxLength:" in error_str
- or "Input is too long" in error_str
- or "prompt is too long" in error_str
- or "prompt: length: 1.." in error_str
- or "Too many input tokens" in error_str
- ):
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"BedrockException: Context Window Error - {error_str}",
- model=model,
- llm_provider="bedrock",
- )
- elif (
- "Conversation blocks and tool result blocks cannot be provided in the same turn."
- in error_str
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"BedrockException - {error_str}\n. Enable 'litellm.modify_params=True' (for PROXY do: `litellm_settings::modify_params: True`) to insert a dummy assistant message and fix this error.",
- model=model,
- llm_provider="bedrock",
- response=getattr(original_exception, "response", None),
- )
- elif "Malformed input request" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"BedrockException - {error_str}",
- model=model,
- llm_provider="bedrock",
- response=getattr(original_exception, "response", None),
- )
- elif "A conversation must start with a user message." in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"BedrockException - {error_str}\n. Pass in default user message via `completion(..,user_continue_message=)` or enable `litellm.modify_params=True`.\nFor Proxy: do via `litellm_settings::modify_params: True` or user_continue_message under `litellm_params`",
- model=model,
- llm_provider="bedrock",
- response=getattr(original_exception, "response", None),
- )
- elif (
- "Unable to locate credentials" in error_str
- or "The security token included in the request is invalid"
- in error_str
- ):
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"BedrockException Invalid Authentication - {error_str}",
- model=model,
- llm_provider="bedrock",
- response=getattr(original_exception, "response", None),
- )
- elif "AccessDeniedException" in error_str:
- exception_mapping_worked = True
- raise PermissionDeniedError(
- message=f"BedrockException PermissionDeniedError - {error_str}",
- model=model,
- llm_provider="bedrock",
- response=getattr(original_exception, "response", None),
- )
- elif (
- "throttlingException" in error_str
- or "ThrottlingException" in error_str
- ):
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"BedrockException: Rate Limit Error - {error_str}",
- model=model,
- llm_provider="bedrock",
- response=getattr(original_exception, "response", None),
- )
- elif (
- "Connect timeout on endpoint URL" in error_str
- or "timed out" in error_str
- ):
- exception_mapping_worked = True
- raise Timeout(
- message=f"BedrockException: Timeout Error - {error_str}",
- model=model,
- llm_provider="bedrock",
- )
- elif "Could not process image" in error_str:
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- message=f"BedrockException - {error_str}",
- model=model,
- llm_provider="bedrock",
- )
- elif hasattr(original_exception, "status_code"):
- if original_exception.status_code == 500:
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"BedrockException - {original_exception.message}",
- llm_provider="bedrock",
- model=model,
- response=httpx.Response(
- status_code=500,
- request=httpx.Request(
- method="POST", url="https://api.openai.com/v1/"
- ),
- ),
- )
- elif original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"BedrockException - {original_exception.message}",
- llm_provider="bedrock",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"BedrockException - {original_exception.message}",
- llm_provider="bedrock",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 404:
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"BedrockException - {original_exception.message}",
- llm_provider="bedrock",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"BedrockException - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 422:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"BedrockException - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"BedrockException - {original_exception.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(
- message=f"BedrockException - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 504: # gateway timeout error
- exception_mapping_worked = True
- raise Timeout(
- message=f"BedrockException - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- exception_status_code=original_exception.status_code,
- )
+ _map_bedrock_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif (
custom_llm_provider == "sagemaker"
or custom_llm_provider == "sagemaker_chat"
):
- if "Unable to locate credentials" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"litellm.BadRequestError: SagemakerException - {error_str}",
- model=model,
- llm_provider="sagemaker",
- response=getattr(original_exception, "response", None),
- )
- elif (
- "Input validation error: `best_of` must be > 0 and <= 2"
- in error_str
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message="SagemakerException - the value of 'n' must be > 0 and <= 2 for sagemaker endpoints",
- model=model,
- llm_provider="sagemaker",
- response=getattr(original_exception, "response", None),
- )
- elif (
- "`inputs` tokens + `max_new_tokens` must be <=" in error_str
- or "instance type with more CPU capacity or memory" in error_str
- ):
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"SagemakerException - {error_str}",
- model=model,
- llm_provider="sagemaker",
- response=getattr(original_exception, "response", None),
- )
- elif hasattr(original_exception, "status_code"):
- if original_exception.status_code == 500:
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"SagemakerException - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=httpx.Response(
- status_code=500,
- request=httpx.Request(
- method="POST", url="https://api.openai.com/v1/"
- ),
- ),
- )
- elif original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"SagemakerException - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"SagemakerException - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 404:
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"SagemakerException - {original_exception.message}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"SagemakerException - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif (
- original_exception.status_code == 422
- or original_exception.status_code == 424
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"SagemakerException - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"SagemakerException - {original_exception.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(
- message=f"SagemakerException - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 504: # gateway timeout error
- exception_mapping_worked = True
- raise Timeout(
- message=f"SagemakerException - {original_exception.message}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- exception_status_code=original_exception.status_code,
- )
+ _map_sagemaker_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif (
custom_llm_provider == LlmProviders.VERTEX_AI
or custom_llm_provider == LlmProviders.VERTEX_AI_BETA
or custom_llm_provider == LlmProviders.GEMINI
):
- if (
- "Vertex AI API has not been used in project" in error_str
- or "Unable to find your project" in error_str
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"litellm.BadRequestError: {custom_llm_provider}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- response=httpx.Response(
- status_code=400,
- request=httpx.Request(
- method="POST",
- url=" https://cloud.google.com/vertex-ai/",
- ),
- ),
- litellm_debug_info=extra_information,
- )
- if "400 Request payload size exceeds" in error_str:
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- )
- elif ExceptionCheckers.is_error_str_context_window_exceeded(error_str):
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"ContextWindowExceededError: {custom_llm_provider.capitalize()}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif (
- "None Unknown Error." in error_str
- or "Content has no parts." in error_str
- ):
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- message=f"litellm.InternalServerError: {custom_llm_provider}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- response=httpx.Response(
- status_code=500,
- content=str(original_exception),
- request=httpx.Request(method="completion", url="https://github.com/BerriAI/litellm"), # type: ignore
- ),
- litellm_debug_info=extra_information,
- )
- elif "API key not valid." in error_str:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif "403" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{custom_llm_provider.capitalize()}Exception BadRequestError - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- response=httpx.Response(
- status_code=403,
- request=httpx.Request(
- method="POST",
- url=" https://cloud.google.com/vertex-ai/",
- ),
- ),
- litellm_debug_info=extra_information,
- )
- elif (
- "The response was blocked." in error_str
- or "Output blocked by content filtering policy"
- in error_str # anthropic on vertex ai
- ):
- exception_mapping_worked = True
- raise ContentPolicyViolationError(
- message=f"{custom_llm_provider.capitalize()}Exception ContentPolicyViolationError - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- response=httpx.Response(
- status_code=400,
- request=httpx.Request(
- method="POST",
- url=" https://cloud.google.com/vertex-ai/",
- ),
- ),
- )
- 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
- ):
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"litellm.RateLimitError: {custom_llm_provider}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- response=httpx.Response(
- status_code=429,
- request=httpx.Request(
- method="POST",
- url=" https://cloud.google.com/vertex-ai/",
- ),
- ),
- )
- elif (
- isinstance(getattr(original_exception, "status_code", None), int)
- and 500 <= original_exception.status_code < 600
- and _get_body_error_code(error_str) == 429
- ):
- # upstream gateway wraps a 429 inside a 5xx envelope
- # e.g. HTTP 500/503 with {"error":{"code":429,...}}.
- # Scoped to 5xx so HTTP 400/401 with body code:429
- # still maps to BadRequestError / AuthenticationError.
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"litellm.RateLimitError: {custom_llm_provider}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- response=httpx.Response(
- status_code=429,
- request=httpx.Request(
- method="POST",
- url=" https://cloud.google.com/vertex-ai/",
- ),
- ),
- )
- elif (
- "500 Internal Server Error" in error_str
- or "The model is overloaded." in error_str
- ):
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- message=f"litellm.InternalServerError: {custom_llm_provider}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- if hasattr(original_exception, "status_code"):
- if original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{custom_llm_provider.capitalize()}Exception BadRequestError - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- response=httpx.Response(
- status_code=400,
- request=httpx.Request(
- method="POST",
- url="https://cloud.google.com/vertex-ai/",
- ),
- ),
- )
- if original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- if original_exception.status_code == 403:
- exception_mapping_worked = True
- raise PermissionDeniedError(
- message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- response=httpx.Response(
- status_code=403,
- request=httpx.Request(
- method="POST",
- url="https://cloud.google.com/vertex-ai/",
- ),
- ),
- )
- if original_exception.status_code == 404:
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- if original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- )
-
- if original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"litellm.RateLimitError: {custom_llm_provider.capitalize()}Exception - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- response=httpx.Response(
- status_code=429,
- request=httpx.Request(
- method="POST",
- url=" https://cloud.google.com/vertex-ai/",
- ),
- ),
- )
- if original_exception.status_code == 500:
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- message=f"{custom_llm_provider.capitalize()}Exception InternalServerError - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- response=httpx.Response(
- status_code=500,
- content=str(original_exception),
- request=httpx.Request(method="completion", url="https://github.com/BerriAI/litellm"), # type: ignore
- ),
- )
- if original_exception.status_code == 502:
- exception_mapping_worked = True
- raise APIConnectionError(
- message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- )
- if original_exception.status_code == 503:
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"{custom_llm_provider.capitalize()}Exception - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- )
+ _map_vertex_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "cloudflare":
- if "Authentication error" in error_str:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"Cloudflare Exception - {original_exception.message}",
- llm_provider="cloudflare",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- if "must have required property" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"Cloudflare Exception - {original_exception.message}",
- llm_provider="cloudflare",
- model=model,
- response=getattr(original_exception, "response", None),
- )
+ _map_cloudflare_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif (
custom_llm_provider == "cohere" or custom_llm_provider == "cohere_chat"
): # Cohere
- if (
- "invalid api token" in error_str
- or "No API key provided." in error_str
- ):
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "invalid type: parameter" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "too many tokens" in error_str:
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"CohereException - {original_exception.message}",
- model=model,
- llm_provider="cohere",
- response=getattr(original_exception, "response", None),
- )
- elif "internal server error" in error_str.lower():
- exception_mapping_worked = True
- raise InternalServerError(
- message=f"CohereException - {error_str}",
- model=model,
- llm_provider="cohere",
- response=getattr(original_exception, "response", None),
- )
- elif hasattr(original_exception, "status_code"):
- if (
- original_exception.status_code == 400
- or original_exception.status_code == 498
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- )
- elif original_exception.status_code == 500:
- exception_mapping_worked = True
- raise InternalServerError(
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif (
- "CohereConnectionError" in exception_type
- ): # cohere seems to fire these errors when we load test it (1k+ messages / min)
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "invalid type:" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "Unexpected server error" in error_str:
- exception_mapping_worked = True
- raise InternalServerError(
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- else:
- if hasattr(original_exception, "status_code"):
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"CohereException - {original_exception.message}",
- llm_provider="cohere",
- model=model,
- request=getattr(original_exception, "request", None),
- )
- raise original_exception
+ _map_cohere_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "huggingface":
- if "length limit exceeded" in error_str:
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=error_str,
- model=model,
- llm_provider="huggingface",
- response=getattr(original_exception, "response", None),
- )
- elif "A valid user token is required" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=error_str,
- llm_provider="huggingface",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "Rate limit reached" in error_str:
- exception_mapping_worked = True
- raise RateLimitError(
- message=error_str,
- llm_provider="huggingface",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- if hasattr(original_exception, "status_code"):
- if original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"HuggingfaceException - {original_exception.message}",
- llm_provider="huggingface",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"HuggingfaceException - {original_exception.message}",
- model=model,
- llm_provider="huggingface",
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"HuggingfaceException - {original_exception.message}",
- model=model,
- llm_provider="huggingface",
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"HuggingfaceException - {original_exception.message}",
- llm_provider="huggingface",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 503:
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"HuggingfaceException - {original_exception.message}",
- llm_provider="huggingface",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- else:
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"HuggingfaceException - {original_exception.message}",
- llm_provider="huggingface",
- model=model,
- request=getattr(original_exception, "request", None),
- )
+ _map_huggingface_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "ai21":
- if hasattr(original_exception, "message"):
- if "Prompt has too many tokens" in original_exception.message:
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"AI21Exception - {original_exception.message}",
- model=model,
- llm_provider="ai21",
- response=getattr(original_exception, "response", None),
- )
- if "Bad or missing API token." in original_exception.message:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"AI21Exception - {original_exception.message}",
- model=model,
- llm_provider="ai21",
- response=getattr(original_exception, "response", None),
- )
- if hasattr(original_exception, "status_code"):
- if original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"AI21Exception - {original_exception.message}",
- llm_provider="ai21",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"AI21Exception - {original_exception.message}",
- model=model,
- llm_provider="ai21",
- )
- if original_exception.status_code == 422:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"AI21Exception - {original_exception.message}",
- model=model,
- llm_provider="ai21",
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"AI21Exception - {original_exception.message}",
- llm_provider="ai21",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- else:
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"AI21Exception - {original_exception.message}",
- llm_provider="ai21",
- model=model,
- request=getattr(original_exception, "request", None),
- )
+ _map_ai21_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "nlp_cloud":
- if "detail" in error_str:
- if "Input text length should not exceed" in error_str:
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"NLPCloudException - {error_str}",
- model=model,
- llm_provider="nlp_cloud",
- response=getattr(original_exception, "response", None),
- )
- elif "value is not a valid" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"NLPCloudException - {error_str}",
- model=model,
- llm_provider="nlp_cloud",
- response=getattr(original_exception, "response", None),
- )
- else:
- exception_mapping_worked = True
- raise APIError(
- status_code=500,
- message=f"NLPCloudException - {error_str}",
- model=model,
- llm_provider="nlp_cloud",
- request=getattr(original_exception, "request", None),
- )
- if hasattr(
- original_exception, "status_code"
- ): # https://docs.nlpcloud.com/?shell#errors
- if (
- original_exception.status_code == 400
- or original_exception.status_code == 406
- or original_exception.status_code == 413
- or original_exception.status_code == 422
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"NLPCloudException - {original_exception.message}",
- llm_provider="nlp_cloud",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif (
- original_exception.status_code == 401
- or original_exception.status_code == 403
- ):
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"NLPCloudException - {original_exception.message}",
- llm_provider="nlp_cloud",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif (
- original_exception.status_code == 522
- or original_exception.status_code == 524
- ):
- exception_mapping_worked = True
- raise Timeout(
- message=f"NLPCloudException - {original_exception.message}",
- model=model,
- llm_provider="nlp_cloud",
- )
- elif (
- original_exception.status_code == 429
- or original_exception.status_code == 402
- ):
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"NLPCloudException - {original_exception.message}",
- llm_provider="nlp_cloud",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif (
- original_exception.status_code == 500
- or original_exception.status_code == 503
- ):
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"NLPCloudException - {original_exception.message}",
- llm_provider="nlp_cloud",
- model=model,
- request=getattr(original_exception, "request", None),
- )
- elif (
- original_exception.status_code == 504
- or original_exception.status_code == 520
- ):
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"NLPCloudException - {original_exception.message}",
- model=model,
- llm_provider="nlp_cloud",
- response=getattr(original_exception, "response", None),
- )
- else:
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"NLPCloudException - {original_exception.message}",
- llm_provider="nlp_cloud",
- model=model,
- request=getattr(original_exception, "request", None),
- )
+ _map_nlp_cloud_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "together_ai":
- try:
- error_response = json.loads(error_str)
- except Exception:
- error_response = {"error": error_str}
- if (
- "error" in error_response
- and "`inputs` tokens + `max_new_tokens` must be <="
- in error_response["error"]
- ):
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"TogetherAIException - {error_response['error']}",
- model=model,
- llm_provider="together_ai",
- response=getattr(original_exception, "response", None),
- )
- elif (
- "error" in error_response
- and "invalid private key" in error_response["error"]
- ):
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"TogetherAIException - {error_response['error']}",
- llm_provider="together_ai",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif (
- "error" in error_response
- and "INVALID_ARGUMENT" in error_response["error"]
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"TogetherAIException - {error_response['error']}",
- model=model,
- llm_provider="together_ai",
- response=getattr(original_exception, "response", None),
- )
- elif "A timeout occurred" in error_str:
- exception_mapping_worked = True
- raise Timeout(
- message=f"TogetherAIException - {error_str}",
- model=model,
- llm_provider="together_ai",
- )
- elif (
- "error" in error_response
- and "API key doesn't match expected format."
- in error_response["error"]
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"TogetherAIException - {error_response['error']}",
- model=model,
- llm_provider="together_ai",
- response=getattr(original_exception, "response", None),
- )
- elif (
- "error_type" in error_response
- and error_response["error_type"] == "validation"
- ):
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"TogetherAIException - {error_response['error']}",
- model=model,
- llm_provider="together_ai",
- response=getattr(original_exception, "response", None),
- )
- if hasattr(original_exception, "status_code"):
- if original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"TogetherAIException - {original_exception.message}",
- model=model,
- llm_provider="together_ai",
- )
- elif original_exception.status_code == 422:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"TogetherAIException - {error_response['error']}",
- model=model,
- llm_provider="together_ai",
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"TogetherAIException - {original_exception.message}",
- llm_provider="together_ai",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 524:
- exception_mapping_worked = True
- raise Timeout(
- message=f"TogetherAIException - {original_exception.message}",
- llm_provider="together_ai",
- model=model,
- )
- else:
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"TogetherAIException - {original_exception.message}",
- llm_provider="together_ai",
- model=model,
- request=getattr(original_exception, "request", None),
- )
+ _map_together_ai_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "aleph_alpha":
- if (
- "This is longer than the model's maximum context length"
- in error_str
- ):
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"AlephAlphaException - {original_exception.message}",
- llm_provider="aleph_alpha",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "InvalidToken" in error_str or "No token provided" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"AlephAlphaException - {original_exception.message}",
- llm_provider="aleph_alpha",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif hasattr(original_exception, "status_code"):
- verbose_logger.debug(
- f"status code: {original_exception.status_code}"
- )
- if original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"AlephAlphaException - {original_exception.message}",
- llm_provider="aleph_alpha",
- model=model,
- )
- elif original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"AlephAlphaException - {original_exception.message}",
- llm_provider="aleph_alpha",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"AlephAlphaException - {original_exception.message}",
- llm_provider="aleph_alpha",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 500:
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"AlephAlphaException - {original_exception.message}",
- llm_provider="aleph_alpha",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- raise original_exception
- raise original_exception
+ _map_aleph_alpha_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif (
custom_llm_provider == "ollama" or custom_llm_provider == "ollama_chat"
):
- if isinstance(original_exception, dict):
- error_str = original_exception.get("error", "")
- else:
- error_str = str(original_exception)
- if "no such file or directory" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"OllamaException: Invalid Model/Model not loaded - {original_exception}",
- model=model,
- llm_provider="ollama",
- response=getattr(original_exception, "response", None),
- )
- elif "Failed to establish a new connection" in error_str:
- exception_mapping_worked = True
- raise ServiceUnavailableError(
- message=f"OllamaException: {original_exception}",
- llm_provider="ollama",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "Invalid response object from API" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"OllamaException: {original_exception}",
- llm_provider="ollama",
- model=model,
- response=getattr(original_exception, "response", None),
- )
- elif "Read timed out" in error_str:
- exception_mapping_worked = True
- raise Timeout(
- message=f"OllamaException: {original_exception}",
- llm_provider="ollama",
- model=model,
- )
+ _map_ollama_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "vllm":
- if hasattr(original_exception, "status_code"):
- if original_exception.status_code == 0:
- exception_mapping_worked = True
- raise APIConnectionError(
- message=f"VLLMException - {original_exception.message}",
- llm_provider="vllm",
- model=model,
- request=getattr(original_exception, "request", None),
- )
+ _map_vllm_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
elif custom_llm_provider == "azure" or custom_llm_provider == "azure_text":
- message = get_error_message(error_obj=original_exception)
- if message is None:
- if hasattr(original_exception, "message"):
- message = original_exception.message
- else:
- message = str(original_exception)
-
- # Azure OpenAI (especially Images) often nests error details under
- # body["error"]. Detect content policy violations using the structured
- # payload in addition to string matching.
- azure_error_code: Optional[str] = None
- try:
- body_dict = getattr(original_exception, "body", None) or {}
- if isinstance(body_dict, dict):
- if isinstance(body_dict.get("error"), dict):
- azure_error_code = body_dict["error"].get("code") # type: ignore[index]
- # Also check inner_error for
- # ResponsibleAIPolicyViolation which indicates a
- # content policy violation even when the top-level
- # code is generic (e.g. "invalid_request_error").
- if azure_error_code != "content_policy_violation":
- _inner = body_dict["error"].get(
- "inner_error"
- ) or body_dict[ # type: ignore[index]
- "error"
- ].get(
- "innererror"
- ) # type: ignore[index]
- if (
- isinstance(_inner, dict)
- and _inner.get("code")
- == "ResponsibleAIPolicyViolation"
- ):
- azure_error_code = "content_policy_violation"
- else:
- azure_error_code = body_dict.get("code")
- except Exception:
- azure_error_code = None
-
- if "Internal server error" in error_str:
- exception_mapping_worked = True
- raise litellm.InternalServerError(
- message=f"AzureException Internal server error - {message}",
- llm_provider="azure",
- model=model,
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- )
- elif "This model's maximum context length is" in error_str:
- exception_mapping_worked = True
- raise ContextWindowExceededError(
- message=f"AzureException ContextWindowExceededError - {message}",
- llm_provider="azure",
- model=model,
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- )
- elif "DeploymentNotFound" in error_str:
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"AzureException NotFoundError - {message}",
- llm_provider="azure",
- model=model,
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- )
- elif (
- azure_error_code == "content_policy_violation"
- or ExceptionCheckers.is_azure_content_policy_violation_error(
- error_str
- )
- ):
- exception_mapping_worked = True
- 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 (
- azure_error_code == "invalid_encrypted_content"
- or "could not be verified" in error_str
- ):
- exception_mapping_worked = True
- helpful_message = (
- f"AzureException - {message}\n\n"
- "This error occurs when load balancing Responses API across deployments with different API keys.\n"
- " Encrypted content is tied to the organization that created it and cannot be decrypted by other organizations.\n\n"
- " Solution: Enable 'encrypted_content_affinity' to route follow-up requests to the correct deployment:\n\n"
- " router_settings:\n"
- " enable_pre_call_checks: true\n"
- " optional_pre_call_checks:\n"
- " - encrypted_content_affinity\n\n"
- " Learn more: https://docs.litellm.ai/docs/response_api#encrypted-content-affinity-multi-region-load-balancing"
- )
- raise BadRequestError(
- message=helpful_message,
- llm_provider="azure",
- model=model,
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- body=getattr(original_exception, "body", None),
- )
- elif "invalid_request_error" in error_str:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"AzureException BadRequestError - {message}",
- llm_provider="azure",
- model=model,
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- body=getattr(original_exception, "body", None),
- )
- elif (
- "The api_key client option must be set either by passing api_key to the client or by setting"
- in error_str
- ):
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"{exception_provider} AuthenticationError - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- )
- elif "Connection error" in error_str:
- exception_mapping_worked = True
- raise APIConnectionError(
- message=f"{exception_provider} APIConnectionError - {message}",
- llm_provider=custom_llm_provider,
- model=model,
- litellm_debug_info=extra_information,
- )
- elif hasattr(original_exception, "status_code"):
- exception_mapping_worked = True
- if original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"AzureException - {message}",
- llm_provider="azure",
- model=model,
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- body=getattr(original_exception, "body", None),
- )
- elif original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"AzureException AuthenticationError - {message}",
- llm_provider="azure",
- model=model,
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"AzureException Timeout - {message}",
- model=model,
- litellm_debug_info=extra_information,
- llm_provider="azure",
- )
- elif original_exception.status_code == 422:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"AzureException BadRequestError - {message}",
- model=model,
- llm_provider="azure",
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"AzureException RateLimitError - {message}",
- model=model,
- llm_provider="azure",
- 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(
- message=f"AzureException ServiceUnavailableError - {message}",
- model=model,
- llm_provider="azure",
- litellm_debug_info=extra_information,
- response=getattr(original_exception, "response", None),
- )
- elif original_exception.status_code == 504: # gateway timeout error
- exception_mapping_worked = True
- raise Timeout(
- message=f"AzureException Timeout - {message}",
- model=model,
- litellm_debug_info=extra_information,
- llm_provider="azure",
- exception_status_code=original_exception.status_code,
- )
- else:
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"AzureException APIError - {message}",
- llm_provider="azure",
- litellm_debug_info=extra_information,
- model=model,
- request=httpx.Request(
- method="POST", url="https://openai.com/"
- ),
- )
- else:
- # if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
- raise APIConnectionError(
- message=f"{exception_provider} APIConnectionError - {message}\n{_redact_string(traceback.format_exc())}",
- llm_provider="azure",
- model=model,
- litellm_debug_info=extra_information,
- request=httpx.Request(method="POST", url="https://openai.com/"),
- )
+ _map_azure_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
if custom_llm_provider == "openrouter":
- if hasattr(original_exception, "status_code"):
- exception_mapping_worked = True
- if original_exception.status_code == 400:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"{exception_provider} - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 401:
- exception_mapping_worked = True
- raise AuthenticationError(
- message=f"AuthenticationError: {exception_provider} - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 404:
- exception_mapping_worked = True
- raise NotFoundError(
- message=f"NotFoundError: {exception_provider} - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 408:
- exception_mapping_worked = True
- raise Timeout(
- message=f"Timeout Error: {exception_provider} - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 422:
- exception_mapping_worked = True
- raise BadRequestError(
- message=f"BadRequestError: {exception_provider} - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 429:
- exception_mapping_worked = True
- raise RateLimitError(
- message=f"RateLimitError: {exception_provider} - {error_str}",
- 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(
- message=f"ServiceUnavailableError: {exception_provider} - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- response=getattr(original_exception, "response", None),
- litellm_debug_info=extra_information,
- )
- elif original_exception.status_code == 504: # gateway timeout error
- exception_mapping_worked = True
- raise Timeout(
- message=f"Timeout Error: {exception_provider} - {error_str}",
- model=model,
- llm_provider=custom_llm_provider,
- litellm_debug_info=extra_information,
- exception_status_code=original_exception.status_code,
- )
- else:
- exception_mapping_worked = True
- raise APIError(
- status_code=original_exception.status_code,
- message=f"APIError: {exception_provider} - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- request=getattr(original_exception, "request", None),
- litellm_debug_info=extra_information,
- )
- else:
- # if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
- raise APIConnectionError(
- message=f"APIConnectionError: {exception_provider} - {error_str}",
- llm_provider=custom_llm_provider,
- model=model,
- litellm_debug_info=extra_information,
- request=httpx.Request(
- method="POST", url="https://api.openai.com/v1/"
- ),
- )
+ _map_openrouter_exception(
+ model=model,
+ original_exception=mappable_exception,
+ custom_llm_provider=custom_llm_provider,
+ error_str=error_str,
+ exception_type=exception_type,
+ exception_provider=exception_provider,
+ extra_information=extra_information,
+ )
if (
"BadRequestError.__init__() missing 1 required positional argument: 'param'"
in str(original_exception)
diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py
index e87042b9101..c22d3b99705 100644
--- a/litellm/litellm_core_utils/get_supported_openai_params.py
+++ b/litellm/litellm_core_utils/get_supported_openai_params.py
@@ -86,9 +86,7 @@ def get_supported_openai_params(
model=model
)
elif request_type == "transcription":
- return litellm.FireworksAIAudioTranscriptionConfig().get_supported_openai_params(
- model=model
- )
+ return None
else:
return litellm.FireworksAIConfig().get_supported_openai_params(model=model)
elif custom_llm_provider == "nvidia_nim":
@@ -191,7 +189,9 @@ def get_supported_openai_params(
)
elif custom_llm_provider == "sambanova":
if request_type == "embeddings":
- litellm.SambaNovaEmbeddingConfig().get_supported_openai_params(model=model)
+ return litellm.SambaNovaEmbeddingConfig().get_supported_openai_params(
+ model=model
+ )
else:
return litellm.SambanovaConfig().get_supported_openai_params(model=model)
elif custom_llm_provider == "nebius":
diff --git a/litellm/litellm_core_utils/request_timeout_resolver.py b/litellm/litellm_core_utils/request_timeout_resolver.py
new file mode 100644
index 00000000000..146c39ce9f3
--- /dev/null
+++ b/litellm/litellm_core_utils/request_timeout_resolver.py
@@ -0,0 +1,29 @@
+"""Single source of truth for whether ``litellm.request_timeout`` was configured.
+
+``litellm.request_timeout`` always holds a value (the package default,
+:data:`~litellm.constants.DEFAULT_REQUEST_TIMEOUT_SECONDS`), so a bare read can't
+tell "user asked for this" from "nobody set it". This resolver answers that:
+
+* ``request_timeout_explicitly_set`` is the authoritative signal, set when the
+ value comes from the ``REQUEST_TIMEOUT`` env var or ``litellm_settings``.
+* A runtime value that differs from the package default (e.g. ``litellm.request_timeout
+ = 300`` in SDK code) is also treated as explicit, for backwards compatibility.
+"""
+
+from __future__ import annotations
+
+from typing import Optional
+
+from litellm.constants import DEFAULT_REQUEST_TIMEOUT_SECONDS
+
+
+def get_configured_request_timeout() -> Optional[float]:
+ """Return the explicitly-configured ``litellm.request_timeout``, else ``None``."""
+ import litellm
+
+ timeout = float(litellm.request_timeout)
+ if litellm.request_timeout_explicitly_set:
+ return timeout
+ if timeout != float(DEFAULT_REQUEST_TIMEOUT_SECONDS):
+ return timeout
+ return None
diff --git a/litellm/litellm_core_utils/sensitive_data_masker.py b/litellm/litellm_core_utils/sensitive_data_masker.py
index 4928dd08386..b14e12de7cd 100644
--- a/litellm/litellm_core_utils/sensitive_data_masker.py
+++ b/litellm/litellm_core_utils/sensitive_data_masker.py
@@ -12,6 +12,7 @@ class SensitiveDataMasker:
visible_prefix: int = 4,
visible_suffix: int = 4,
mask_char: str = "*",
+ mask_short_values: bool = True,
):
self.sensitive_patterns = sensitive_patterns or {
"password",
@@ -38,12 +39,17 @@ class SensitiveDataMasker:
self.visible_prefix = visible_prefix
self.visible_suffix = visible_suffix
self.mask_char = mask_char
+ self.mask_short_values = mask_short_values
def _mask_value(self, value: str) -> str:
- if not value or len(str(value)) < (self.visible_prefix + self.visible_suffix):
- return value
-
value_str = str(value)
+ if not value_str:
+ return value
+ if len(value_str) <= (self.visible_prefix + self.visible_suffix):
+ return (
+ self.mask_char * len(value_str) if self.mask_short_values else value_str
+ )
+
masked_length = len(value_str) - (self.visible_prefix + self.visible_suffix)
# Handle the case where visible_suffix is 0 to avoid showing the entire string
diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py
index d3330c3dcec..e278483d689 100644
--- a/litellm/litellm_core_utils/streaming_handler.py
+++ b/litellm/litellm_core_utils/streaming_handler.py
@@ -6,6 +6,7 @@ import logging
import threading
import time
import traceback
+from dataclasses import dataclass
from typing import (
Any,
AsyncIterator,
@@ -97,6 +98,19 @@ def print_verbose(print_statement):
pass
+@dataclass(frozen=True, slots=True)
+class _ProviderChunkParsed:
+ response_obj: dict[str, Any]
+
+
+@dataclass(frozen=True, slots=True)
+class _ProviderChunkEarlyReturn:
+ value: Any
+
+
+_ProviderChunkResult = Union[_ProviderChunkParsed, _ProviderChunkEarlyReturn]
+
+
class CustomStreamWrapper:
def __init__(
self,
@@ -1145,381 +1159,392 @@ class CustomStreamWrapper:
del model_response.choices[0].delta.reasoning_content
return
+ def _dispatch_provider_chunk(
+ self,
+ chunk: Any,
+ model_response: ModelResponseStream,
+ completion_obj: dict[str, Any],
+ ) -> _ProviderChunkResult:
+ response_obj: dict[str, Any] = {}
+ if (
+ isinstance(chunk, ModelResponseStream)
+ and self.custom_llm_provider is not None
+ and self.custom_llm_provider in litellm._custom_providers
+ ):
+ _has_content = bool(
+ chunk.choices
+ and chunk.choices[0].delta is not None
+ and (
+ chunk.choices[0].delta.content or chunk.choices[0].delta.tool_calls
+ )
+ )
+ if self.received_finish_reason is not None:
+ if not _has_content:
+ raise StopIteration
+ if chunk.choices and chunk.choices[0].finish_reason:
+ self.received_finish_reason = chunk.choices[0].finish_reason
+ if not _has_content:
+ return _ProviderChunkEarlyReturn(None)
+ # Strip finish_reason from the content chunk so it appears
+ # only on the trailing empty-delta chunk (OpenAI spec).
+ # finish_reason_handler() will emit the proper terminal chunk.
+ chunk.choices[0].finish_reason = None # type: ignore[assignment]
+ return _ProviderChunkEarlyReturn(chunk)
+
+ if (
+ isinstance(chunk, dict)
+ and generic_chunk_has_all_required_fields(
+ chunk=chunk
+ ) # check if chunk is a generic streaming chunk
+ ) or (
+ self.custom_llm_provider
+ and self.custom_llm_provider in litellm._custom_providers
+ ):
+ if self.received_finish_reason is not None:
+ _chunk_has_content = isinstance(chunk, dict) and (
+ bool(chunk.get("text", ""))
+ or chunk.get("tool_use") is not None
+ # Usage-only final chunks are valid and needed to surface
+ # finish_reason/usage to downstream translators.
+ or chunk.get("usage") is not None
+ )
+ if not _chunk_has_content and (
+ not isinstance(chunk, dict)
+ or "provider_specific_fields" not in chunk
+ ):
+ raise StopIteration
+ anthropic_response_obj: GChunk = cast(GChunk, chunk)
+ completion_obj["content"] = anthropic_response_obj["text"]
+ if anthropic_response_obj["is_finished"]:
+ self.received_finish_reason = anthropic_response_obj["finish_reason"]
+
+ if anthropic_response_obj["finish_reason"]:
+ self.intermittent_finish_reason = anthropic_response_obj[
+ "finish_reason"
+ ]
+
+ if anthropic_response_obj["usage"] is not None:
+ setattr(
+ model_response,
+ "usage",
+ litellm.Usage(**anthropic_response_obj["usage"]),
+ )
+
+ if (
+ "tool_use" in anthropic_response_obj
+ and anthropic_response_obj["tool_use"] is not None
+ ):
+ completion_obj["tool_calls"] = [anthropic_response_obj["tool_use"]]
+
+ if (
+ "provider_specific_fields" in anthropic_response_obj
+ and anthropic_response_obj["provider_specific_fields"] is not None
+ ):
+ for key, value in anthropic_response_obj[
+ "provider_specific_fields"
+ ].items():
+ setattr(model_response, key, value)
+
+ response_obj = cast(dict[str, Any], anthropic_response_obj)
+ elif self.model == "replicate" or self.custom_llm_provider == "replicate":
+ response_obj = self.handle_replicate_chunk(chunk)
+ completion_obj["content"] = response_obj["text"]
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ elif self.custom_llm_provider and self.custom_llm_provider == "predibase":
+ response_obj = self.handle_predibase_chunk(chunk)
+ completion_obj["content"] = response_obj["text"]
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ elif (
+ self.custom_llm_provider and self.custom_llm_provider == "baseten"
+ ): # baseten doesn't provide streaming
+ completion_obj["content"] = self.handle_baseten_chunk(chunk)
+ elif (
+ self.custom_llm_provider and self.custom_llm_provider == "ai21"
+ ): # ai21 doesn't provide streaming
+ response_obj = self.handle_ai21_chunk(chunk)
+ completion_obj["content"] = response_obj["text"]
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ elif self.custom_llm_provider and self.custom_llm_provider == "maritalk":
+ response_obj = self.handle_maritalk_chunk(chunk)
+ completion_obj["content"] = response_obj["text"]
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ elif self.custom_llm_provider and self.custom_llm_provider == "vllm":
+ completion_obj["content"] = chunk[0].outputs[0].text
+ elif (
+ self.custom_llm_provider and self.custom_llm_provider == "aleph_alpha"
+ ): # aleph alpha doesn't provide streaming
+ response_obj = self.handle_aleph_alpha_chunk(chunk)
+ completion_obj["content"] = response_obj["text"]
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ elif self.custom_llm_provider == "nlp_cloud":
+ try:
+ response_obj = self.handle_nlp_cloud_chunk(chunk)
+ completion_obj["content"] = response_obj["text"]
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ except Exception as e:
+ if self.received_finish_reason:
+ raise e
+ else:
+ if self.sent_first_chunk is False:
+ raise Exception("An unknown error occurred with the stream")
+ self.received_finish_reason = "stop"
+ elif self.custom_llm_provider == "vertex_ai" and not isinstance(
+ chunk, ModelResponseStream
+ ):
+ chunk = cast(Any, chunk)
+ import proto # type: ignore
+
+ if hasattr(chunk, "candidates") is True:
+ try:
+ try:
+ completion_obj["content"] = chunk.text # type: ignore
+ except Exception as e:
+ original_exception = e
+ if "Part has no text." in str(e):
+ ## check for function calling
+ function_call = (
+ chunk.candidates[0].content.parts[0].function_call # type: ignore
+ )
+
+ args_dict = {}
+
+ # Check if it's a RepeatedComposite instance
+ for key, val in function_call.args.items():
+ if isinstance(
+ val,
+ proto.marshal.collections.repeated.RepeatedComposite, # type: ignore
+ ):
+ # If so, convert to list
+ args_dict[key] = [v for v in val]
+ else:
+ args_dict[key] = val
+
+ try:
+ args_str = json.dumps(args_dict)
+ except Exception as e:
+ raise e
+ _delta_obj = litellm.utils.Delta(
+ content=None,
+ tool_calls=[
+ {
+ "id": f"call_{str(uuid.uuid4())}",
+ "function": {
+ "arguments": args_str,
+ "name": function_call.name,
+ },
+ "type": "function",
+ }
+ ],
+ )
+ _streaming_response = StreamingChoices(delta=_delta_obj)
+ _model_response = ModelResponseStream()
+ _model_response.choices = [_streaming_response]
+ response_obj = {"original_chunk": _model_response}
+ else:
+ raise original_exception
+ if (
+ hasattr(chunk.candidates[0], "finish_reason") # type: ignore
+ and chunk.candidates[0].finish_reason.name # type: ignore
+ != "FINISH_REASON_UNSPECIFIED"
+ ): # every non-final chunk in vertex ai has this
+ self.received_finish_reason = map_finish_reason( # type: ignore
+ chunk.candidates[0].finish_reason.name
+ )
+ except Exception:
+ if chunk.candidates[0].finish_reason.name == "SAFETY": # type: ignore
+ raise Exception(
+ f"The response was blocked by VertexAI. {str(chunk)}"
+ )
+ else:
+ completion_obj["content"] = str(chunk)
+ elif self.custom_llm_provider == "petals":
+ if self.completion_stream is None or len(self.completion_stream) == 0:
+ if self.received_finish_reason is not None:
+ raise StopIteration
+ else:
+ self.received_finish_reason = "stop"
+ chunk_size = 30
+ stream = cast(Any, self.completion_stream)
+ new_chunk = stream[:chunk_size]
+ completion_obj["content"] = new_chunk
+ self.completion_stream = stream[chunk_size:]
+ elif self.custom_llm_provider == "palm":
+ # fake streaming
+ response_obj = {}
+ if self.completion_stream is None or len(self.completion_stream) == 0:
+ if self.received_finish_reason is not None:
+ raise StopIteration
+ else:
+ self.received_finish_reason = "stop"
+ chunk_size = 30
+ stream = cast(Any, self.completion_stream)
+ new_chunk = stream[:chunk_size]
+ completion_obj["content"] = new_chunk
+ self.completion_stream = stream[chunk_size:]
+ elif self.custom_llm_provider == "triton":
+ response_obj = self.handle_triton_stream(chunk)
+ completion_obj["content"] = response_obj["text"]
+ print_verbose(f"completion obj content: {completion_obj['content']}")
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ elif self.custom_llm_provider == "text-completion-openai":
+ response_obj = self.handle_openai_text_completion_chunk(chunk)
+ completion_obj["content"] = response_obj["text"]
+ print_verbose(f"completion obj content: {completion_obj['content']}")
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ if response_obj["usage"] is not None:
+ setattr(
+ model_response,
+ "usage",
+ litellm.Usage(
+ prompt_tokens=response_obj["usage"].prompt_tokens,
+ completion_tokens=response_obj["usage"].completion_tokens,
+ total_tokens=response_obj["usage"].total_tokens,
+ ),
+ )
+ elif self.custom_llm_provider == "text-completion-codestral":
+ if not isinstance(chunk, str):
+ raise ValueError(f"chunk is not a string: {chunk}")
+ response_obj = cast(
+ dict[str, Any],
+ litellm.CodestralTextCompletionConfig()._chunk_parser(chunk),
+ )
+ completion_obj["content"] = response_obj["text"]
+ print_verbose(f"completion obj content: {completion_obj['content']}")
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ if "usage" in response_obj is not None:
+ setattr(
+ model_response,
+ "usage",
+ litellm.Usage(
+ prompt_tokens=response_obj["usage"].prompt_tokens,
+ completion_tokens=response_obj["usage"].completion_tokens,
+ total_tokens=response_obj["usage"].total_tokens,
+ ),
+ )
+ elif self.custom_llm_provider == "azure_text":
+ response_obj = self.handle_azure_text_completion_chunk(chunk)
+ completion_obj["content"] = response_obj["text"]
+ print_verbose(f"completion obj content: {completion_obj['content']}")
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ elif self.custom_llm_provider == "cached_response":
+ chunk = cast(ModelResponseStream, chunk)
+ response_obj = {
+ "text": chunk.choices[0].delta.content,
+ "is_finished": True,
+ "finish_reason": chunk.choices[0].finish_reason,
+ "original_chunk": chunk,
+ "tool_calls": (
+ chunk.choices[0].delta.tool_calls
+ if hasattr(chunk.choices[0].delta, "tool_calls")
+ else None
+ ),
+ }
+
+ completion_obj["content"] = response_obj["text"]
+ if response_obj["tool_calls"] is not None:
+ completion_obj["tool_calls"] = response_obj["tool_calls"]
+ print_verbose(f"completion obj content: {completion_obj['content']}")
+ if hasattr(chunk, "id"):
+ model_response.id = chunk.id
+ self.response_id = chunk.id
+ if hasattr(chunk, "system_fingerprint"):
+ self.system_fingerprint = chunk.system_fingerprint
+ if response_obj["is_finished"]:
+ self.received_finish_reason = response_obj["finish_reason"]
+ else: # openai / azure chat model
+ if self.custom_llm_provider in [
+ LlmProviders.AZURE.value,
+ LlmProviders.AZURE_AI.value,
+ ]:
+ if isinstance(chunk, BaseModel) and hasattr(chunk, "model"):
+ # 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:
+ return _ProviderChunkEarlyReturn(None)
+ completion_obj["content"] = response_obj["text"]
+ self.intermittent_finish_reason = response_obj.get("finish_reason", None)
+ if response_obj["is_finished"]:
+ if response_obj["finish_reason"] == "error":
+ raise Exception(
+ "{} raised a streaming error - finish_reason: error, no content string given. Received Chunk={}".format(
+ self.custom_llm_provider, response_obj
+ )
+ )
+ self.received_finish_reason = response_obj["finish_reason"]
+ if response_obj.get("original_chunk", None) is not None:
+ if hasattr(response_obj["original_chunk"], "id"):
+ model_response = self.set_model_id(
+ response_obj["original_chunk"].id, model_response
+ )
+ if hasattr(response_obj["original_chunk"], "system_fingerprint"):
+ model_response.system_fingerprint = response_obj[
+ "original_chunk"
+ ].system_fingerprint
+ self.system_fingerprint = response_obj[
+ "original_chunk"
+ ].system_fingerprint
+ if response_obj["logprobs"] is not None:
+ model_response.choices[0].logprobs = response_obj["logprobs"]
+
+ if response_obj["usage"] is not None:
+ if isinstance(response_obj["usage"], dict):
+ setattr(
+ model_response,
+ "usage",
+ litellm.Usage(
+ prompt_tokens=response_obj["usage"].get(
+ "prompt_tokens", None
+ )
+ or None,
+ completion_tokens=response_obj["usage"].get(
+ "completion_tokens", None
+ )
+ or None,
+ total_tokens=response_obj["usage"].get("total_tokens", None)
+ or None,
+ ),
+ )
+ elif isinstance(response_obj["usage"], Usage):
+ setattr(
+ model_response,
+ "usage",
+ response_obj["usage"],
+ )
+ elif isinstance(response_obj["usage"], BaseModel):
+ setattr(
+ model_response,
+ "usage",
+ litellm.Usage(**response_obj["usage"].model_dump()),
+ )
+ return _ProviderChunkParsed(response_obj)
+
def chunk_creator(self, chunk: Any): # type: ignore
if hasattr(chunk, "id"):
self.response_id = chunk.id
model_response = self.model_response_creator()
- response_obj: Dict[str, Any] = {}
+ response_obj: dict[str, Any] = {}
try:
# return this for all models
- completion_obj: Dict[str, Any] = {"content": ""}
- from litellm.types.utils import GenericStreamingChunk as GChunk
-
- if (
- isinstance(chunk, ModelResponseStream)
- and self.custom_llm_provider is not None
- and self.custom_llm_provider in litellm._custom_providers
- ):
- _has_content = bool(
- chunk.choices
- and chunk.choices[0].delta is not None
- and (
- chunk.choices[0].delta.content
- or chunk.choices[0].delta.tool_calls
- )
- )
- if self.received_finish_reason is not None:
- if not _has_content:
- raise StopIteration
- if chunk.choices and chunk.choices[0].finish_reason:
- self.received_finish_reason = chunk.choices[0].finish_reason
- if not _has_content:
- return None
- # Strip finish_reason from the content chunk so it appears
- # only on the trailing empty-delta chunk (OpenAI spec).
- # finish_reason_handler() will emit the proper terminal chunk.
- chunk.choices[0].finish_reason = None # type: ignore[assignment]
- return chunk
-
- if (
- isinstance(chunk, dict)
- and generic_chunk_has_all_required_fields(
- chunk=chunk
- ) # check if chunk is a generic streaming chunk
- ) or (
- self.custom_llm_provider
- and self.custom_llm_provider in litellm._custom_providers
- ):
- if self.received_finish_reason is not None:
- _chunk_has_content = isinstance(chunk, dict) and (
- bool(chunk.get("text", ""))
- or chunk.get("tool_use") is not None
- # Usage-only final chunks are valid and needed to surface
- # finish_reason/usage to downstream translators.
- or chunk.get("usage") is not None
- )
- if not _chunk_has_content and (
- not isinstance(chunk, dict)
- or "provider_specific_fields" not in chunk
- ):
- raise StopIteration
- anthropic_response_obj: GChunk = cast(GChunk, chunk)
- completion_obj["content"] = anthropic_response_obj["text"]
- if anthropic_response_obj["is_finished"]:
- self.received_finish_reason = anthropic_response_obj[
- "finish_reason"
- ]
-
- if anthropic_response_obj["finish_reason"]:
- self.intermittent_finish_reason = anthropic_response_obj[
- "finish_reason"
- ]
-
- if anthropic_response_obj["usage"] is not None:
- setattr(
- model_response,
- "usage",
- litellm.Usage(**anthropic_response_obj["usage"]),
- )
-
- if (
- "tool_use" in anthropic_response_obj
- and anthropic_response_obj["tool_use"] is not None
- ):
- completion_obj["tool_calls"] = [anthropic_response_obj["tool_use"]]
-
- if (
- "provider_specific_fields" in anthropic_response_obj
- and anthropic_response_obj["provider_specific_fields"] is not None
- ):
- for key, value in anthropic_response_obj[
- "provider_specific_fields"
- ].items():
- setattr(model_response, key, value)
-
- response_obj = cast(Dict[str, Any], anthropic_response_obj)
- elif self.model == "replicate" or self.custom_llm_provider == "replicate":
- response_obj = self.handle_replicate_chunk(chunk)
- completion_obj["content"] = response_obj["text"]
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- elif self.custom_llm_provider and self.custom_llm_provider == "predibase":
- response_obj = self.handle_predibase_chunk(chunk)
- completion_obj["content"] = response_obj["text"]
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- elif (
- self.custom_llm_provider and self.custom_llm_provider == "baseten"
- ): # baseten doesn't provide streaming
- completion_obj["content"] = self.handle_baseten_chunk(chunk)
- elif (
- self.custom_llm_provider and self.custom_llm_provider == "ai21"
- ): # ai21 doesn't provide streaming
- response_obj = self.handle_ai21_chunk(chunk)
- completion_obj["content"] = response_obj["text"]
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- elif self.custom_llm_provider and self.custom_llm_provider == "maritalk":
- response_obj = self.handle_maritalk_chunk(chunk)
- completion_obj["content"] = response_obj["text"]
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- elif self.custom_llm_provider and self.custom_llm_provider == "vllm":
- completion_obj["content"] = chunk[0].outputs[0].text
- elif (
- self.custom_llm_provider and self.custom_llm_provider == "aleph_alpha"
- ): # aleph alpha doesn't provide streaming
- response_obj = self.handle_aleph_alpha_chunk(chunk)
- completion_obj["content"] = response_obj["text"]
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- elif self.custom_llm_provider == "nlp_cloud":
- try:
- response_obj = self.handle_nlp_cloud_chunk(chunk)
- completion_obj["content"] = response_obj["text"]
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- except Exception as e:
- if self.received_finish_reason:
- raise e
- else:
- if self.sent_first_chunk is False:
- raise Exception("An unknown error occurred with the stream")
- self.received_finish_reason = "stop"
- elif self.custom_llm_provider == "vertex_ai" and not isinstance(
- chunk, ModelResponseStream
- ):
- import proto # type: ignore
-
- if hasattr(chunk, "candidates") is True:
- try:
- try:
- completion_obj["content"] = chunk.text # type: ignore
- except Exception as e:
- original_exception = e
- if "Part has no text." in str(e):
- ## check for function calling
- function_call = (
- chunk.candidates[0].content.parts[0].function_call # type: ignore
- )
-
- args_dict = {}
-
- # Check if it's a RepeatedComposite instance
- for key, val in function_call.args.items():
- if isinstance(
- val,
- proto.marshal.collections.repeated.RepeatedComposite, # type: ignore
- ):
- # If so, convert to list
- args_dict[key] = [v for v in val]
- else:
- args_dict[key] = val
-
- try:
- args_str = json.dumps(args_dict)
- except Exception as e:
- raise e
- _delta_obj = litellm.utils.Delta(
- content=None,
- tool_calls=[
- {
- "id": f"call_{str(uuid.uuid4())}",
- "function": {
- "arguments": args_str,
- "name": function_call.name,
- },
- "type": "function",
- }
- ],
- )
- _streaming_response = StreamingChoices(delta=_delta_obj)
- _model_response = ModelResponseStream()
- _model_response.choices = [_streaming_response]
- response_obj = {"original_chunk": _model_response}
- else:
- raise original_exception
- if (
- hasattr(chunk.candidates[0], "finish_reason") # type: ignore
- and chunk.candidates[0].finish_reason.name # type: ignore
- != "FINISH_REASON_UNSPECIFIED"
- ): # every non-final chunk in vertex ai has this
- self.received_finish_reason = map_finish_reason( # type: ignore
- chunk.candidates[0].finish_reason.name
- )
- except Exception:
- if chunk.candidates[0].finish_reason.name == "SAFETY": # type: ignore
- raise Exception(
- f"The response was blocked by VertexAI. {str(chunk)}"
- )
- else:
- completion_obj["content"] = str(chunk)
- elif self.custom_llm_provider == "petals":
- if self.completion_stream is None or len(self.completion_stream) == 0:
- if self.received_finish_reason is not None:
- raise StopIteration
- else:
- self.received_finish_reason = "stop"
- chunk_size = 30
- new_chunk = self.completion_stream[:chunk_size] # type: ignore[index]
- completion_obj["content"] = new_chunk
- self.completion_stream = self.completion_stream[chunk_size:] # type: ignore[index]
- elif self.custom_llm_provider == "palm":
- # fake streaming
- response_obj = {}
- if self.completion_stream is None or len(self.completion_stream) == 0:
- if self.received_finish_reason is not None:
- raise StopIteration
- else:
- self.received_finish_reason = "stop"
- chunk_size = 30
- new_chunk = self.completion_stream[:chunk_size] # type: ignore[index]
- completion_obj["content"] = new_chunk
- self.completion_stream = self.completion_stream[chunk_size:] # type: ignore[index]
- elif self.custom_llm_provider == "triton":
- response_obj = self.handle_triton_stream(chunk)
- completion_obj["content"] = response_obj["text"]
- print_verbose(f"completion obj content: {completion_obj['content']}")
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- elif self.custom_llm_provider == "text-completion-openai":
- response_obj = self.handle_openai_text_completion_chunk(chunk)
- completion_obj["content"] = response_obj["text"]
- print_verbose(f"completion obj content: {completion_obj['content']}")
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- if response_obj["usage"] is not None:
- setattr(
- model_response,
- "usage",
- litellm.Usage(
- prompt_tokens=response_obj["usage"].prompt_tokens,
- completion_tokens=response_obj["usage"].completion_tokens,
- total_tokens=response_obj["usage"].total_tokens,
- ),
- )
- elif self.custom_llm_provider == "text-completion-codestral":
- if not isinstance(chunk, str):
- raise ValueError(f"chunk is not a string: {chunk}")
- response_obj = cast(
- Dict[str, Any],
- litellm.CodestralTextCompletionConfig()._chunk_parser(chunk),
- )
- completion_obj["content"] = response_obj["text"]
- print_verbose(f"completion obj content: {completion_obj['content']}")
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- if "usage" in response_obj is not None:
- setattr(
- model_response,
- "usage",
- litellm.Usage(
- prompt_tokens=response_obj["usage"].prompt_tokens,
- completion_tokens=response_obj["usage"].completion_tokens,
- total_tokens=response_obj["usage"].total_tokens,
- ),
- )
- elif self.custom_llm_provider == "azure_text":
- response_obj = self.handle_azure_text_completion_chunk(chunk)
- completion_obj["content"] = response_obj["text"]
- print_verbose(f"completion obj content: {completion_obj['content']}")
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- elif self.custom_llm_provider == "cached_response":
- chunk = cast(ModelResponseStream, chunk)
- response_obj = {
- "text": chunk.choices[0].delta.content,
- "is_finished": True,
- "finish_reason": chunk.choices[0].finish_reason,
- "original_chunk": chunk,
- "tool_calls": (
- chunk.choices[0].delta.tool_calls
- if hasattr(chunk.choices[0].delta, "tool_calls")
- else None
- ),
- }
-
- completion_obj["content"] = response_obj["text"]
- if response_obj["tool_calls"] is not None:
- completion_obj["tool_calls"] = response_obj["tool_calls"]
- print_verbose(f"completion obj content: {completion_obj['content']}")
- if hasattr(chunk, "id"):
- model_response.id = chunk.id
- self.response_id = chunk.id
- if hasattr(chunk, "system_fingerprint"):
- self.system_fingerprint = chunk.system_fingerprint
- if response_obj["is_finished"]:
- self.received_finish_reason = response_obj["finish_reason"]
- else: # openai / azure chat model
- if self.custom_llm_provider in [
- LlmProviders.AZURE.value,
- LlmProviders.AZURE_AI.value,
- ]:
- if isinstance(chunk, BaseModel) and hasattr(chunk, "model"):
- # 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:
- return
- completion_obj["content"] = response_obj["text"]
- self.intermittent_finish_reason = response_obj.get(
- "finish_reason", None
- )
- if response_obj["is_finished"]:
- if response_obj["finish_reason"] == "error":
- raise Exception(
- "{} raised a streaming error - finish_reason: error, no content string given. Received Chunk={}".format(
- self.custom_llm_provider, response_obj
- )
- )
- self.received_finish_reason = response_obj["finish_reason"]
- if response_obj.get("original_chunk", None) is not None:
- if hasattr(response_obj["original_chunk"], "id"):
- model_response = self.set_model_id(
- response_obj["original_chunk"].id, model_response
- )
- if hasattr(response_obj["original_chunk"], "system_fingerprint"):
- model_response.system_fingerprint = response_obj[
- "original_chunk"
- ].system_fingerprint
- self.system_fingerprint = response_obj[
- "original_chunk"
- ].system_fingerprint
- if response_obj["logprobs"] is not None:
- model_response.choices[0].logprobs = response_obj["logprobs"]
-
- if response_obj["usage"] is not None:
- if isinstance(response_obj["usage"], dict):
- setattr(
- model_response,
- "usage",
- litellm.Usage(
- prompt_tokens=response_obj["usage"].get(
- "prompt_tokens", None
- )
- or None,
- completion_tokens=response_obj["usage"].get(
- "completion_tokens", None
- )
- or None,
- total_tokens=response_obj["usage"].get(
- "total_tokens", None
- )
- or None,
- ),
- )
- elif isinstance(response_obj["usage"], Usage):
- setattr(
- model_response,
- "usage",
- response_obj["usage"],
- )
- elif isinstance(response_obj["usage"], BaseModel):
- setattr(
- model_response,
- "usage",
- litellm.Usage(**response_obj["usage"].model_dump()),
- )
+ completion_obj: dict[str, Any] = {"content": ""}
+ dispatch_result = self._dispatch_provider_chunk(
+ chunk=chunk,
+ model_response=model_response,
+ completion_obj=completion_obj,
+ )
+ if isinstance(dispatch_result, _ProviderChunkEarlyReturn):
+ return dispatch_result.value
+ response_obj = dispatch_result.response_obj
model_response.model = self.model
## FUNCTION CALL PARSING
@@ -1980,11 +2005,29 @@ class CustomStreamWrapper:
except StopIteration:
if self.sent_last_chunk is True:
- complete_streaming_response = litellm.stream_chunk_builder(
- chunks=self.chunks,
- messages=self.messages,
- logging_obj=self.logging_obj,
- )
+ try:
+ complete_streaming_response = litellm.stream_chunk_builder(
+ chunks=self.chunks,
+ messages=self.messages,
+ logging_obj=self.logging_obj,
+ )
+ except Exception as e:
+ # stream_chunk_builder can re-raise (as APIError) on large agentic
+ # streams. The raise originates inside this except-StopIteration block,
+ # so the sibling `except Exception` below does not catch it; it would
+ # escape __next__ and drop the request from SpendLogs. Recover
+ # best-effort usage from the raw chunks so cost is still tracked
+ verbose_logger.warning(
+ "stream_chunk_builder raised at end-of-stream (%s); logging "
+ "best-effort usage from chunks.",
+ str(e),
+ )
+ try:
+ complete_streaming_response = self.model_response_creator(
+ chunk={"usage": calculate_total_usage(chunks=self.chunks)}
+ )
+ except Exception:
+ complete_streaming_response = None
response = self.model_response_creator()
if complete_streaming_response is not None:
@@ -2209,11 +2252,27 @@ class CustomStreamWrapper:
except (StopAsyncIteration, StopIteration):
if self.sent_last_chunk is True:
# log the final chunk with accurate streaming values
- complete_streaming_response = litellm.stream_chunk_builder(
- chunks=self.chunks,
- messages=self.messages,
- logging_obj=self.logging_obj,
- )
+ try:
+ complete_streaming_response = litellm.stream_chunk_builder(
+ chunks=self.chunks,
+ messages=self.messages,
+ logging_obj=self.logging_obj,
+ )
+ except Exception as e:
+ # see sync __next__: a raise from stream_chunk_builder inside this
+ # except handler escapes __anext__ and drops the request from SpendLogs.
+ # Recover best-effort usage from the raw chunks so cost is still tracked
+ verbose_logger.warning(
+ "stream_chunk_builder raised at end-of-stream (%s); logging "
+ "best-effort usage from chunks.",
+ str(e),
+ )
+ try:
+ complete_streaming_response = self.model_response_creator(
+ chunk={"usage": calculate_total_usage(chunks=self.chunks)}
+ )
+ except Exception:
+ complete_streaming_response = None
response = self.model_response_creator()
if complete_streaming_response is not None:
diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py
index c24c990f356..822b75b37f4 100644
--- a/litellm/llms/anthropic/chat/transformation.py
+++ b/litellm/llms/anthropic/chat/transformation.py
@@ -229,6 +229,11 @@ DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING = (
"Sonnet 4.6+, and Mythos Preview."
)
+DROP_UNSUPPORTED_SPEED_WARNING = (
+ "Dropping unsupported `speed` for model=%s "
+ "(drop_params=True). Fast mode is only supported on select Opus models."
+)
+
class AnthropicConfig(AnthropicModelInfo, BaseConfig):
"""
@@ -374,6 +379,51 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
for level in ("low", "minimal", "medium", "high", "xhigh", "max")
)
+ @staticmethod
+ def _model_supports_speed_param(
+ model: str, custom_llm_provider: Optional[str] = None
+ ) -> bool:
+ """Whether the model accepts Anthropic's ``speed`` parameter (fast mode).
+
+ Fast mode is direct Anthropic API-only (not Bedrock, Vertex, or Azure).
+ Those providers strip their prefix before this shared transform runs, so a
+ bare ``claude-opus-4-8`` would otherwise resolve to the direct-API entry;
+ the routed provider is checked explicitly to keep them out.
+ """
+ if custom_llm_provider is not None and custom_llm_provider != "anthropic":
+ return False
+ return (
+ AnthropicModelInfo._get_exact_model_capability(model, "supports_speed")
+ is True
+ )
+
+ @staticmethod
+ def _maybe_drop_speed_param(
+ model: str,
+ optional_params: dict,
+ drop_params: bool,
+ custom_llm_provider: Optional[str] = None,
+ ) -> None:
+ if "speed" not in optional_params:
+ return
+ if AnthropicConfig._model_supports_speed_param(model, custom_llm_provider):
+ return
+ if not (litellm.drop_params or drop_params):
+ speed_value = optional_params.get("speed")
+ raise litellm.utils.UnsupportedParamsError(
+ message=(
+ f"{model} does not support speed={speed_value!r}. "
+ "To drop unsupported params, set "
+ "`litellm.drop_params = True`."
+ ),
+ status_code=400,
+ )
+ litellm.verbose_logger.warning(
+ DROP_UNSUPPORTED_SPEED_WARNING,
+ model,
+ )
+ optional_params.pop("speed", None)
+
@staticmethod
def _raise_invalid_reasoning_effort(
model: str, value: Any, llm_provider: str
@@ -1569,8 +1619,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
anthropic_context_management
)
elif param == "speed" and isinstance(value, str):
- # Pass through Anthropic-specific speed parameter for fast mode
optional_params["speed"] = value
+ AnthropicConfig._maybe_drop_speed_param(
+ model=model,
+ optional_params=optional_params,
+ drop_params=drop_params,
+ custom_llm_provider=self.custom_llm_provider,
+ )
elif param == "cache_control" and isinstance(value, dict):
# Pass through top-level cache_control for automatic prompt caching
optional_params["cache_control"] = value
@@ -1875,6 +1930,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
"has no thinking_blocks. The model won't use extended thinking for this turn."
)
+ AnthropicConfig._maybe_drop_speed_param(
+ model=model,
+ optional_params=optional_params,
+ drop_params=litellm.drop_params
+ or litellm_params.get("drop_params") is True,
+ custom_llm_provider=self.custom_llm_provider,
+ )
+
headers = self.update_headers_with_optional_anthropic_beta(
headers=headers, optional_params=optional_params
)
diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py
index 5741513903c..0e41ef619ba 100644
--- a/litellm/llms/anthropic/common_utils.py
+++ b/litellm/llms/anthropic/common_utils.py
@@ -367,6 +367,16 @@ class AnthropicModelInfo(BaseLLMModelInfo):
pass
return None
+ @staticmethod
+ def _get_exact_model_capability(model: str, key: str) -> Optional[bool]:
+ """Read boolean capability ``key`` from the exact model-map entry only.
+
+ Unlike ``_get_model_capability``, does not walk stripped provider aliases.
+ Use when a feature is tied to a specific host (e.g. Anthropic API fast mode).
+ """
+ value = litellm.model_cost.get(model, {}).get(key)
+ return value if isinstance(value, bool) else None
+
@staticmethod
def _supports_model_capability(model: str, key: str) -> bool:
"""Check a boolean capability ``key`` in the model map.
diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py
index a3ac465c463..7b10a447bc8 100644
--- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py
+++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py
@@ -507,7 +507,10 @@ def anthropic_messages_handler(
local_vars.update(kwargs)
anthropic_messages_optional_request_params = (
AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param(
- params=local_vars
+ params=local_vars,
+ model=model,
+ drop_params=litellm_params.get("drop_params") is True,
+ custom_llm_provider=custom_llm_provider,
)
)
if is_reasoning_auto_summary_enabled():
diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py b/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py
index c7c110ff3e3..8714939f025 100644
--- a/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py
+++ b/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py
@@ -84,8 +84,14 @@ class AdvisorOrchestrationHandler(MessagesInterceptor):
)
# Optional routing overrides for the advisor sub-call (e.g. proxy routing).
# If not set in the tool definition, litellm resolves from env vars.
- advisor_api_key: Optional[str] = advisor_tool.get("api_key")
- advisor_api_base: Optional[str] = advisor_tool.get("api_base")
+ # The advisor tool is caller-controlled; only honor a client-supplied
+ # api_base/api_key when the proxy has enabled clientside credentials,
+ # otherwise let litellm resolve from server config.
+ advisor_api_key: Optional[str] = None
+ advisor_api_base: Optional[str] = None
+ if _allow_client_side_advisor_credentials():
+ advisor_api_key = advisor_tool.get("api_key")
+ advisor_api_base = advisor_tool.get("api_base")
# Build the synthetic tool definition the provider will receive.
synthetic_advisor_tool = _make_synthetic_advisor_tool()
@@ -181,6 +187,20 @@ class AdvisorOrchestrationHandler(MessagesInterceptor):
# ---------------------------------------------------------------------------
+def _allow_client_side_advisor_credentials() -> bool:
+ """Whether a caller-supplied advisor api_base/api_key may be honored.
+
+ Gated on the proxy's ``allow_client_side_credentials`` opt-in. When the
+ interceptor runs outside the proxy (SDK use), there is no admin boundary
+ to protect, so client-supplied routing is allowed.
+ """
+ try:
+ from litellm.proxy.proxy_server import general_settings
+ except (ImportError, ModuleNotFoundError):
+ return True
+ return general_settings.get("allow_client_side_credentials") is True
+
+
def _make_synthetic_advisor_tool() -> Dict:
"""Build a regular tool definition the executor provider can understand."""
return {
diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/utils.py b/litellm/llms/anthropic/experimental_pass_through/messages/utils.py
index 88832fb3f63..42167e0fdaa 100644
--- a/litellm/llms/anthropic/experimental_pass_through/messages/utils.py
+++ b/litellm/llms/anthropic/experimental_pass_through/messages/utils.py
@@ -23,12 +23,19 @@ class AnthropicMessagesRequestUtils:
@staticmethod
def get_requested_anthropic_messages_optional_param(
params: Dict[str, Any],
+ *,
+ model: str | None = None,
+ drop_params: bool = False,
+ custom_llm_provider: str | None = None,
) -> AnthropicMessagesRequestOptionalParams:
"""
Filter parameters to only include those defined in AnthropicMessagesRequestOptionalParams.
Args:
params: Dictionary of parameters to filter
+ model: Resolved model id; when set, unsupported params may be dropped
+ drop_params: Per-request drop_params flag (also respects litellm.drop_params)
+ custom_llm_provider: Routed provider; fast mode is gated to direct Anthropic
Returns:
AnthropicMessagesRequestOptionalParams instance with only the valid parameters
@@ -37,6 +44,15 @@ class AnthropicMessagesRequestUtils:
filtered_params = {
k: v for k, v in params.items() if k in valid_keys and v is not None
}
+ if model is not None:
+ from litellm.llms.anthropic.chat.transformation import AnthropicConfig
+
+ AnthropicConfig._maybe_drop_speed_param(
+ model=model,
+ optional_params=filtered_params,
+ drop_params=drop_params,
+ custom_llm_provider=custom_llm_provider,
+ )
return cast(AnthropicMessagesRequestOptionalParams, filtered_params)
diff --git a/litellm/llms/apiserpent/search/transformation.py b/litellm/llms/apiserpent/search/transformation.py
index 1eb7d34c875..bc11875ba12 100644
--- a/litellm/llms/apiserpent/search/transformation.py
+++ b/litellm/llms/apiserpent/search/transformation.py
@@ -53,7 +53,13 @@ class APISerpentSearchConfig(BaseSearchConfig):
api_base: Optional[str] = None,
**kwargs,
) -> Dict:
- api_key = api_key or get_secret_str("APISERPENT_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("APISERPENT_API_KEY",),
+ base_env_var="APISERPENT_API_BASE",
+ default_api_base=APISERPENT_BASE,
+ )
if not api_key:
raise ValueError(
"APISERPENT_API_KEY is not set. Set `APISERPENT_API_KEY` environment variable."
diff --git a/litellm/llms/base_llm/sandbox/__init__.py b/litellm/llms/base_llm/sandbox/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/litellm/llms/base_llm/sandbox/transformation.py b/litellm/llms/base_llm/sandbox/transformation.py
new file mode 100644
index 00000000000..1c012a15fdb
--- /dev/null
+++ b/litellm/llms/base_llm/sandbox/transformation.py
@@ -0,0 +1,96 @@
+"""
+Base Sandbox transformation configuration.
+
+A sandbox provider runs an executable string inside an isolated container and
+returns whatever the sandbox produced. The lifecycle is create container ->
+run code -> delete container; `code_interpreter_tool` combines all three.
+"""
+
+from typing import Any, Union
+
+import httpx
+
+from pydantic import Field, PrivateAttr
+
+from litellm.types.llms.base import LiteLLMPydanticObjectBase
+
+SANDBOX_MAX_OUTPUT_BYTES = 10 * 1024 * 1024
+
+
+class ContainerHandle(LiteLLMPydanticObjectBase):
+ """A live sandbox container. Carries everything needed to reach it again."""
+
+ id: str
+ provider: str
+ domain: str | None = None
+
+ model_config = {"extra": "allow"}
+
+ _hidden_params: dict = PrivateAttr(default_factory=dict)
+
+
+class CodeExecutionResult(LiteLLMPydanticObjectBase):
+ """Passthrough of the sandbox's own execution output."""
+
+ stdout: str = ""
+ stderr: str = ""
+ results: list[dict[str, Any]] = Field(default_factory=list)
+ error: dict[str, Any] | None = None
+ execution_count: int | None = None
+ object: str = "code_execution"
+
+ model_config = {"extra": "allow"}
+
+ _hidden_params: dict = PrivateAttr(default_factory=dict)
+
+
+class BaseSandboxConfig:
+ """Provider-agnostic sandbox operations."""
+
+ def validate_environment(self, api_key: str | None = None, **kwargs) -> str:
+ raise NotImplementedError(
+ "validate_environment must be implemented by provider"
+ )
+
+ async def acreate_sandbox(
+ self,
+ *,
+ template: str | None = None,
+ timeout: int | None = None,
+ allow_internet_access: bool | None = None,
+ api_key: str | None = None,
+ **kwargs,
+ ) -> ContainerHandle:
+ raise NotImplementedError("acreate_sandbox must be implemented by provider")
+
+ async def arun_code(
+ self,
+ *,
+ container: Union[ContainerHandle, str],
+ code: str,
+ api_key: str | None = None,
+ **kwargs,
+ ) -> CodeExecutionResult:
+ raise NotImplementedError("arun_code must be implemented by provider")
+
+ async def adelete_sandbox(
+ self,
+ *,
+ container: Union[ContainerHandle, str],
+ api_key: str | None = None,
+ **kwargs,
+ ) -> bool:
+ raise NotImplementedError("adelete_sandbox must be implemented by provider")
+
+ async def _read_capped_lines(self, response: httpx.Response) -> list[str]:
+ lines: list[str] = []
+ total = 0
+ async for line in response.aiter_lines():
+ total += len(line.encode("utf-8"))
+ if total > SANDBOX_MAX_OUTPUT_BYTES:
+ raise ValueError(
+ f"Sandbox output exceeded {SANDBOX_MAX_OUTPUT_BYTES} bytes; aborting "
+ "to avoid unbounded memory use."
+ )
+ lines.append(line)
+ return lines
diff --git a/litellm/llms/base_llm/search/transformation.py b/litellm/llms/base_llm/search/transformation.py
index 4dfe86685fb..1581d8bb064 100644
--- a/litellm/llms/base_llm/search/transformation.py
+++ b/litellm/llms/base_llm/search/transformation.py
@@ -3,11 +3,13 @@ Base Search transformation configuration.
"""
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union
+from urllib.parse import urlsplit
import httpx
from pydantic import PrivateAttr
from litellm.llms.base_llm.chat.transformation import BaseLLMException
+from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.base import LiteLLMPydanticObjectBase
if TYPE_CHECKING:
@@ -16,6 +18,29 @@ else:
LiteLLMLoggingObj = Any
+def _search_host(url: str) -> str:
+ return urlsplit(url).netloc.lower()
+
+
+def _is_trusted_search_api_base(
+ caller_api_base: str,
+ default_api_base: str | None,
+ base_env_var: str | None,
+) -> bool:
+ candidate = _search_host(caller_api_base)
+ if not candidate:
+ return False
+ trusted = {
+ _search_host(base)
+ for base in (
+ default_api_base,
+ get_secret_str(base_env_var) if base_env_var else None,
+ )
+ if base
+ }
+ return candidate in trusted
+
+
class SearchResult(LiteLLMPydanticObjectBase):
"""Single search result."""
@@ -86,6 +111,60 @@ class BaseSearchConfig:
"max_tokens_per_page",
}
+ def _assert_trusted_api_base_for_server_credential(
+ self,
+ caller_api_base: str | None,
+ default_api_base: str | None,
+ base_env_var: str | None,
+ credential_name: str,
+ ) -> None:
+ """
+ Block sending a server-managed credential to a caller-chosen host.
+
+ A caller-supplied api_base is honored when constructing the request URL, so
+ falling back to a server-configured secret while the caller controls the host
+ leaks that secret. The provider default and the operator's own api_base
+ override are the only trusted destinations for a server-managed credential.
+ """
+ if not caller_api_base:
+ return
+ if _is_trusted_search_api_base(caller_api_base, default_api_base, base_env_var):
+ return
+ raise ValueError(
+ f"Refusing to send the server-configured {credential_name} to the "
+ f"caller-supplied api_base '{caller_api_base}'. Pass an explicit api_key "
+ f"when overriding api_base for this search provider."
+ )
+
+ def resolve_server_api_key(
+ self,
+ *,
+ caller_api_key: str | None,
+ caller_api_base: str | None,
+ key_env_vars: tuple[str, ...],
+ base_env_var: str | None,
+ default_api_base: str | None,
+ ) -> str | None:
+ """
+ Resolve a single-secret search API key, falling back to a server-managed
+ secret only when the request targets a trusted host.
+
+ Returns the caller's key when provided, otherwise the first set
+ server-managed secret (or None when none is set, for keyless providers).
+ """
+ if caller_api_key:
+ return caller_api_key
+ server_key = next(
+ (key for key in (get_secret_str(var) for var in key_env_vars) if key),
+ None,
+ )
+ if server_key is None:
+ return None
+ self._assert_trusted_api_base_for_server_credential(
+ caller_api_base, default_api_base, base_env_var, key_env_vars[0]
+ )
+ return server_key
+
def validate_environment(
self,
headers: Dict,
diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py
index 2c9ea187912..c31462a735b 100644
--- a/litellm/llms/bedrock/base_aws_llm.py
+++ b/litellm/llms/bedrock/base_aws_llm.py
@@ -10,7 +10,6 @@ from typing import (
Callable,
ClassVar,
Dict,
- List,
Literal,
Optional,
Tuple,
@@ -210,32 +209,11 @@ class BaseAWSLLM:
"""
Return a boto3.Credentials object
"""
- ## CHECK IS 'os.environ/' passed in
- params_to_check: List[Optional[str]] = [
- aws_access_key_id,
- aws_secret_access_key,
- aws_session_token,
- aws_region_name,
- aws_session_name,
- aws_profile_name,
- aws_role_name,
- aws_web_identity_token,
- aws_sts_endpoint,
- aws_external_id,
- ]
-
- # Iterate over parameters and update if needed
- for i, param in enumerate(params_to_check):
- if param and param.startswith("os.environ/"):
- _v = get_secret(param)
- if _v is not None and isinstance(_v, str):
- params_to_check[i] = _v
- elif param is None: # check if uppercase value in env
- key = self.aws_authentication_params[i]
- if key.upper() in os.environ:
- params_to_check[i] = os.getenv(key.upper())
-
- # Assign updated values back to parameters
+ # Only config-sourced credentials are expanded against the environment.
+ # os.environ/ references in the model config are resolved at load time,
+ # so any reference still present at this point is caller-supplied input and is
+ # left as-is rather than expanded into a process environment variable. Each
+ # unset param falls back to its matching fixed AWS_* ambient env var.
(
aws_access_key_id,
aws_secret_access_key,
@@ -247,7 +225,21 @@ class BaseAWSLLM:
aws_web_identity_token,
aws_sts_endpoint,
aws_external_id,
- ) = params_to_check
+ ) = tuple(
+ value if value is not None else os.getenv(env_var)
+ for value, env_var in (
+ (aws_access_key_id, "AWS_ACCESS_KEY_ID"),
+ (aws_secret_access_key, "AWS_SECRET_ACCESS_KEY"),
+ (aws_session_token, "AWS_SESSION_TOKEN"),
+ (aws_region_name, "AWS_REGION_NAME"),
+ (aws_session_name, "AWS_SESSION_NAME"),
+ (aws_profile_name, "AWS_PROFILE_NAME"),
+ (aws_role_name, "AWS_ROLE_NAME"),
+ (aws_web_identity_token, "AWS_WEB_IDENTITY_TOKEN"),
+ (aws_sts_endpoint, "AWS_STS_ENDPOINT"),
+ (aws_external_id, "AWS_EXTERNAL_ID"),
+ )
+ )
verbose_logger.debug(
"in get credentials\n"
@@ -845,6 +837,20 @@ class BaseAWSLLM:
f"IN Web Identity Token: {aws_web_identity_token} | Role Name: {aws_role_name} | Session Name: {aws_session_name}"
)
+ # get_secret() expands environment-variable references (an os.environ/
+ # prefix, or a bare name matching an environment variable). Config-sourced
+ # references are expanded at load time, so such a reference reaching here is
+ # caller-supplied input; reject it rather than expanding a process-environment
+ # value for use as the token.
+ if (
+ aws_web_identity_token.startswith("os.environ/")
+ or aws_web_identity_token in os.environ
+ ):
+ raise AwsAuthError(
+ message="Invalid web identity token reference.",
+ status_code=400,
+ )
+
oidc_token = get_secret(aws_web_identity_token)
if oidc_token is None:
diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py
index 75b560b4d6d..9fca7bc61af 100644
--- a/litellm/llms/bedrock/chat/invoke_handler.py
+++ b/litellm/llms/bedrock/chat/invoke_handler.py
@@ -70,6 +70,7 @@ from ..base_aws_llm import BaseAWSLLM
from ..common_utils import (
BedrockError,
ModelResponseIterator,
+ build_bedrock_stream_error,
get_bedrock_response_stream_shape,
get_bedrock_tool_name,
)
@@ -1841,23 +1842,7 @@ class AWSEventStreamDecoder:
parsed_response = self.parser.parse(response_dict, response_stream_shape)
if response_dict["status_code"] != 200:
- decoded_body = response_dict["body"].decode()
- if isinstance(decoded_body, dict):
- error_message = decoded_body.get("message")
- elif isinstance(decoded_body, str):
- error_message = decoded_body
- else:
- error_message = ""
- exception_status = response_dict["headers"].get(":exception-type")
- error_message = exception_status + " " + error_message
- raise BedrockError(
- status_code=response_dict["status_code"],
- message=(
- json.dumps(error_message)
- if isinstance(error_message, dict)
- else error_message
- ),
- )
+ raise build_bedrock_stream_error(response_dict, response_stream_shape)
if "chunk" in parsed_response:
chunk = parsed_response.get("chunk")
if not chunk:
diff --git a/litellm/llms/bedrock/chat/mantle/transformation.py b/litellm/llms/bedrock/chat/mantle/transformation.py
index cbed2232be5..93306025b02 100644
--- a/litellm/llms/bedrock/chat/mantle/transformation.py
+++ b/litellm/llms/bedrock/chat/mantle/transformation.py
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, List, Optional
from litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation import (
AmazonAnthropicClaudeConfig,
)
+from litellm.llms.bedrock.common_utils import build_mantle_messages_url
from litellm.types.llms.openai import AllMessageValues
if TYPE_CHECKING:
@@ -21,10 +22,6 @@ if TYPE_CHECKING:
else:
LiteLLMLoggingObj = Any
-MANTLE_ENDPOINT_TEMPLATE = (
- "https://bedrock-mantle.{region}.api.aws/anthropic/v1/messages"
-)
-
class AmazonMantleConfig(AmazonAnthropicClaudeConfig):
"""
@@ -46,7 +43,13 @@ class AmazonMantleConfig(AmazonAnthropicClaudeConfig):
stream: Optional[bool] = None,
) -> str:
region = self._get_aws_region_name(optional_params=optional_params, model=model)
- return MANTLE_ENDPOINT_TEMPLATE.format(region=region)
+ return build_mantle_messages_url(
+ api_base=api_base,
+ aws_bedrock_runtime_endpoint=optional_params.get(
+ "aws_bedrock_runtime_endpoint"
+ ),
+ region=region,
+ )
def validate_environment(
self,
diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py
index bdc5da321c6..5e97394f459 100644
--- a/litellm/llms/bedrock/common_utils.py
+++ b/litellm/llms/bedrock/common_utils.py
@@ -7,9 +7,21 @@ Common utilities used across bedrock chat/embedding/image generation
import functools
import json
import os
-from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Dict,
+ List,
+ Literal,
+ Mapping,
+ Optional,
+ TypedDict,
+ Union,
+)
if TYPE_CHECKING:
+ from botocore.model import Shape
+
from litellm.types.llms.bedrock import BedrockCreateBatchRequest
import httpx
@@ -610,6 +622,31 @@ def strip_bedrock_throughput_suffix(model: str) -> str:
return model
+MANTLE_MESSAGES_PATH = "/anthropic/v1/messages"
+
+
+def build_mantle_messages_url(
+ api_base: Optional[str],
+ aws_bedrock_runtime_endpoint: Optional[str],
+ region: str,
+) -> str:
+ """Build the bedrock-mantle Anthropic /messages URL.
+
+ Honors an explicit endpoint override (``api_base``, then
+ ``aws_bedrock_runtime_endpoint``) so private VPC / VPCE / GovCloud Mantle
+ endpoints are reachable; otherwise falls back to the public regional host.
+ The mantle messages path is appended unless the override already carries it,
+ so callers can pass either the host or the full messages URL.
+ """
+ override = api_base or aws_bedrock_runtime_endpoint
+ if override:
+ base = override.rstrip("/")
+ if base.endswith(MANTLE_MESSAGES_PATH):
+ return base
+ return f"{base}{MANTLE_MESSAGES_PATH}"
+ return f"https://bedrock-mantle.{region}.api.aws{MANTLE_MESSAGES_PATH}"
+
+
def get_bedrock_base_model(model: str) -> str:
"""
Get the base model from the given model name.
@@ -1132,6 +1169,39 @@ def get_bedrock_response_stream_shape():
return _load_bedrock_response_stream_shape()
+class BedrockEventStreamResponseDict(TypedDict):
+ status_code: int
+ headers: Mapping[str, str]
+ body: bytes
+
+
+def build_bedrock_stream_error(
+ response_dict: BedrockEventStreamResponseDict,
+ response_stream_shape: Shape | None,
+) -> BedrockError:
+ """Build a BedrockError for a non-200 event-stream error event.
+
+ botocore hard-codes HTTP 400 on every mid-stream error event, so the modeled
+ ResponseStream member's httpStatusCode is the real status. Resolve it from the
+ shape and fall back to the raw status when the type is not modeled.
+ """
+ exception_type = response_dict["headers"].get(":exception-type")
+ decoded_body = response_dict["body"].decode()
+ message = f"{exception_type} {decoded_body}" if exception_type else decoded_body
+
+ status_code = response_dict["status_code"]
+ if exception_type is not None and response_stream_shape is not None:
+ member = response_stream_shape.members.get(exception_type)
+ if member is not None:
+ modeled_status = (
+ (member.metadata or {}).get("error", {}).get("httpStatusCode")
+ )
+ if modeled_status is not None:
+ status_code = int(modeled_status)
+
+ return BedrockError(status_code=status_code, message=message)
+
+
class BedrockEventStreamDecoderBase:
"""
Base class for event stream decoding for Bedrock
@@ -1156,23 +1226,7 @@ class BedrockEventStreamDecoderBase:
parsed_response = self.parser.parse(response_dict, response_stream_shape)
if response_dict["status_code"] != 200:
- decoded_body = response_dict["body"].decode()
- if isinstance(decoded_body, dict):
- error_message = decoded_body.get("message")
- elif isinstance(decoded_body, str):
- error_message = decoded_body
- else:
- error_message = ""
- exception_status = response_dict["headers"].get(":exception-type")
- error_message = exception_status + " " + error_message
- raise BedrockError(
- status_code=response_dict["status_code"],
- message=(
- json.dumps(error_message)
- if isinstance(error_message, dict)
- else error_message
- ),
- )
+ raise build_bedrock_stream_error(response_dict, response_stream_shape)
if "chunk" in parsed_response:
chunk = parsed_response.get("chunk")
if not chunk:
diff --git a/litellm/llms/bedrock/messages/mantle_transformation.py b/litellm/llms/bedrock/messages/mantle_transformation.py
index 900d9aa97d8..94e7f90b719 100644
--- a/litellm/llms/bedrock/messages/mantle_transformation.py
+++ b/litellm/llms/bedrock/messages/mantle_transformation.py
@@ -8,6 +8,7 @@ stripping that are specific to the bedrock-mantle endpoint.
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from litellm.llms.bedrock.common_utils import build_mantle_messages_url
from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation import (
AmazonAnthropicClaudeMessagesConfig,
)
@@ -20,10 +21,6 @@ if TYPE_CHECKING:
else:
LiteLLMLoggingObj = Any
-MANTLE_ENDPOINT_TEMPLATE = (
- "https://bedrock-mantle.{region}.api.aws/anthropic/v1/messages"
-)
-
class AmazonMantleMessagesConfig(AmazonAnthropicClaudeMessagesConfig):
"""
@@ -43,7 +40,13 @@ class AmazonMantleMessagesConfig(AmazonAnthropicClaudeMessagesConfig):
stream: Optional[bool] = None,
) -> str:
region = self._get_aws_region_name(optional_params=optional_params, model=model)
- return MANTLE_ENDPOINT_TEMPLATE.format(region=region)
+ return build_mantle_messages_url(
+ api_base=api_base,
+ aws_bedrock_runtime_endpoint=optional_params.get(
+ "aws_bedrock_runtime_endpoint"
+ ),
+ region=region,
+ )
def validate_anthropic_messages_environment(
self,
diff --git a/litellm/llms/brave/search/transformation.py b/litellm/llms/brave/search/transformation.py
index 9dfcd6bc75a..8ffe7dcb126 100644
--- a/litellm/llms/brave/search/transformation.py
+++ b/litellm/llms/brave/search/transformation.py
@@ -115,7 +115,13 @@ class BraveSearchConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("BRAVE_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("BRAVE_API_KEY",),
+ base_env_var="BRAVE_API_BASE",
+ default_api_base=self.BRAVE_API_BASE,
+ )
if not api_key:
raise ValueError(
diff --git a/litellm/llms/cloudflare/chat/transformation.py b/litellm/llms/cloudflare/chat/transformation.py
index 66e253f304d..68f08741cc5 100644
--- a/litellm/llms/cloudflare/chat/transformation.py
+++ b/litellm/llms/cloudflare/chat/transformation.py
@@ -1,26 +1,15 @@
-import json
-import time
-from typing import AsyncIterator, Iterator, List, Optional, Union
+from typing import List, Optional, Union
import httpx
-import litellm
-from litellm.litellm_core_utils.url_utils import encode_url_path_segments
-from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
-from litellm.llms.base_llm.chat.transformation import (
- BaseConfig,
- BaseLLMException,
- LiteLLMLoggingObj,
+from litellm._logging import verbose_logger
+from litellm.llms.base_llm.chat.transformation import BaseLLMException
+from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
+from litellm.secret_managers.main import (
+ get_secret_str,
+ normalize_nonempty_secret_str,
)
-from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues
-from litellm.types.utils import (
- ChatCompletionToolCallChunk,
- ChatCompletionUsageBlock,
- GenericStreamingChunk,
- ModelResponse,
- Usage,
-)
class CloudflareError(BaseLLMException):
@@ -34,26 +23,46 @@ class CloudflareError(BaseLLMException):
message=message,
request=self.request,
response=self.response,
- ) # Call the base class constructor with the parameters it needs
+ )
-class CloudflareChatConfig(BaseConfig):
- max_tokens: Optional[int] = None
- stream: Optional[bool] = None
-
- def __init__(
+class CloudflareChatConfig(OpenAIGPTConfig):
+ def get_complete_url(
self,
- max_tokens: Optional[int] = None,
+ api_base: Optional[str],
+ api_key: Optional[str],
+ model: str,
+ optional_params: dict,
+ litellm_params: dict,
stream: Optional[bool] = None,
- ) -> None:
- locals_ = locals().copy()
- for key, value in locals_.items():
- if key != "self" and value is not None:
- setattr(self.__class__, key, value)
+ ) -> str:
+ return super().get_complete_url(
+ api_base=self._resolve_api_base(api_base),
+ api_key=api_key,
+ model=model,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ stream=stream,
+ )
- @classmethod
- def get_config(cls):
- return super().get_config()
+ @staticmethod
+ def _resolve_api_base(api_base: Optional[str]) -> str:
+ if not api_base:
+ account_id = normalize_nonempty_secret_str(
+ get_secret_str("CLOUDFLARE_ACCOUNT_ID")
+ )
+ if account_id is None:
+ raise ValueError(
+ "Missing CLOUDFLARE_ACCOUNT_ID - set CLOUDFLARE_ACCOUNT_ID in the environment or pass api_base explicitly"
+ )
+ return f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1"
+ trimmed = api_base.rstrip("/")
+ if trimmed.endswith("/ai/run"):
+ verbose_logger.warning(
+ "Cloudflare api_base ending in '/ai/run' is the legacy Workers AI path and no longer serves OpenAI-compatible requests; rewriting to the '/ai/v1' endpoint"
+ )
+ return f"{trimmed[: -len('/ai/run')]}/ai/v1"
+ return api_base
def validate_environment(
self,
@@ -67,107 +76,18 @@ class CloudflareChatConfig(BaseConfig):
) -> dict:
if api_key is None:
raise ValueError(
- "Missing CloudflareError API Key - A call is being made to cloudflare but no key is set either in the environment variables or via params"
+ "Missing Cloudflare API Key - A call is being made to cloudflare but no key is set either in the environment variables or via params"
)
- headers = {
- "accept": "application/json",
- "content-type": "apbplication/json",
- "Authorization": "Bearer " + api_key,
- }
- return headers
-
- 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:
- if api_base is None:
- account_id = get_secret_str("CLOUDFLARE_ACCOUNT_ID")
- api_base = (
- f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/"
- )
- encoded_model = encode_url_path_segments(model, field_name="model")
- return api_base + encoded_model
-
- def get_supported_openai_params(self, model: str) -> List[str]:
- return [
- "stream",
- "max_tokens",
- ]
-
- def map_openai_params(
- self,
- non_default_params: dict,
- optional_params: dict,
- model: str,
- drop_params: bool,
- ) -> dict:
- supported_openai_params = self.get_supported_openai_params(model=model)
- for param, value in non_default_params.items():
- if param == "max_completion_tokens":
- optional_params["max_tokens"] = value
- elif param in supported_openai_params:
- optional_params[param] = value
- return optional_params
-
- def transform_request(
- self,
- model: str,
- messages: List[AllMessageValues],
- optional_params: dict,
- litellm_params: dict,
- headers: dict,
- ) -> dict:
- config = litellm.CloudflareChatConfig.get_config()
- for k, v in config.items():
- if k not in optional_params:
- optional_params[k] = v
-
- data = {
- "messages": messages,
- **optional_params,
- }
- return data
-
- 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: str,
- api_key: Optional[str] = None,
- json_mode: Optional[bool] = None,
- ) -> ModelResponse:
- completion_response = raw_response.json()
-
- # Support both "response" and "response_text" keys (newer models like Nemotron use "response_text")
- result = completion_response["result"]
- model_response.choices[0].message.content = result.get("response") if result.get("response") is not None else result.get("response_text", "") # type: ignore
-
- prompt_tokens = litellm.utils.get_token_count(messages=messages, model=model)
- completion_tokens = len(
- encoding.encode(model_response["choices"][0]["message"].get("content", ""))
+ return super().validate_environment(
+ headers=headers,
+ model=model,
+ messages=messages,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ api_key=api_key,
+ api_base=api_base,
)
- model_response.created = int(time.time())
- model_response.model = "cloudflare/" + model
- usage = Usage(
- prompt_tokens=prompt_tokens,
- completion_tokens=completion_tokens,
- total_tokens=prompt_tokens + completion_tokens,
- )
- setattr(model_response, "usage", usage)
- return model_response
-
def get_error_class(
self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers]
) -> BaseLLMException:
@@ -175,48 +95,3 @@ class CloudflareChatConfig(BaseConfig):
status_code=status_code,
message=error_message,
)
-
- def get_model_response_iterator(
- self,
- streaming_response: Union[Iterator[str], AsyncIterator[str], ModelResponse],
- sync_stream: bool,
- json_mode: Optional[bool] = False,
- ):
- return CloudflareChatResponseIterator(
- streaming_response=streaming_response,
- sync_stream=sync_stream,
- json_mode=json_mode,
- )
-
-
-class CloudflareChatResponseIterator(BaseModelResponseIterator):
- def chunk_parser(self, chunk: dict) -> GenericStreamingChunk:
- try:
- text = ""
- tool_use: Optional[ChatCompletionToolCallChunk] = None
- is_finished = False
- finish_reason = ""
- usage: Optional[ChatCompletionUsageBlock] = None
- provider_specific_fields = None
-
- index = int(chunk.get("index", 0))
-
- if "response" in chunk and chunk["response"] is not None:
- text = chunk["response"]
- elif "response_text" in chunk and chunk["response_text"] is not None:
- text = chunk["response_text"]
-
- returned_chunk = GenericStreamingChunk(
- text=text,
- tool_use=tool_use,
- is_finished=is_finished,
- finish_reason=finish_reason,
- usage=usage,
- index=index,
- provider_specific_fields=provider_specific_fields,
- )
-
- return returned_chunk
-
- except json.JSONDecodeError:
- raise ValueError(f"Failed to decode JSON from chunk: {chunk}")
diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py
index 01c94476431..1000ab12803 100644
--- a/litellm/llms/custom_httpx/http_handler.py
+++ b/litellm/llms/custom_httpx/http_handler.py
@@ -42,6 +42,9 @@ from litellm.constants import (
HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS,
)
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
+from litellm.litellm_core_utils.request_timeout_resolver import (
+ get_configured_request_timeout,
+)
from litellm.types.llms.custom_http import *
if TYPE_CHECKING:
@@ -134,6 +137,18 @@ _DEFAULT_TIMEOUT = httpx.Timeout(
timeout=COMPLETION_HTTP_FALLBACK_SECONDS,
connect=HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS,
)
+
+
+def _default_cached_client_timeout() -> httpx.Timeout:
+ """Timeout for cached default httpx clients; honors an explicit litellm.request_timeout."""
+ configured = get_configured_request_timeout()
+ if configured is None:
+ return _DEFAULT_TIMEOUT
+ return httpx.Timeout(
+ timeout=configured, connect=HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS
+ )
+
+
_STREAMING_ERROR_BODY_READ_TIMEOUT_SECONDS = 5.0
_STREAMING_ERROR_BODY_READ_EXECUTOR = concurrent.futures.ThreadPoolExecutor(
max_workers=50,
@@ -1379,7 +1394,7 @@ def get_async_httpx_client(
_new_client = AsyncHTTPHandler(**handler_params)
else:
_new_client = AsyncHTTPHandler(
- timeout=_DEFAULT_TIMEOUT,
+ timeout=_default_cached_client_timeout(),
shared_session=shared_session,
)
@@ -1428,7 +1443,7 @@ def _get_httpx_client(params: Optional[dict] = None) -> HTTPHandler:
}
_new_client = HTTPHandler(**handler_params)
else:
- _new_client = HTTPHandler(timeout=_DEFAULT_TIMEOUT)
+ _new_client = HTTPHandler(timeout=_default_cached_client_timeout())
cache.set_cache(
key=_cache_key_name,
diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py
index 8ac5b47c6e7..948c90f9f99 100644
--- a/litellm/llms/custom_httpx/llm_http_handler.py
+++ b/litellm/llms/custom_httpx/llm_http_handler.py
@@ -1,5 +1,6 @@
import json
import ssl
+from functools import lru_cache
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from typing import (
TYPE_CHECKING,
@@ -13,6 +14,7 @@ from typing import (
Tuple,
Union,
cast,
+ get_type_hints,
)
import httpx # type: ignore
@@ -26,6 +28,7 @@ from litellm._logging import _redact_string, verbose_logger
from litellm.anthropic_beta_headers_manager import update_headers_with_filtered_beta
from litellm.constants import REALTIME_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES
from litellm.litellm_core_utils.realtime_streaming import RealTimeStreaming
+from litellm.litellm_core_utils.asyncify import run_async_function
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
from litellm.llms.base_llm.anthropic_messages.transformation import (
BaseAnthropicMessagesConfig,
@@ -101,6 +104,7 @@ from litellm.types.llms.openai import (
HttpxBinaryResponseContent,
OpenAIFileObject,
ResponseInputParam,
+ ResponsesAPIOptionalRequestParams,
ResponsesAPIResponse,
)
from litellm.types.rerank import RerankResponse
@@ -135,6 +139,7 @@ from litellm.utils import (
ImageResponse,
ModelResponse,
ProviderConfigManager,
+ async_pre_call_deployment_hook,
)
from .http_handler import get_shared_realtime_ssl_context
@@ -184,6 +189,47 @@ def _google_genai_streaming_hidden_params(
}
+@lru_cache(maxsize=None)
+def _responses_api_optional_request_param_names() -> frozenset[str]:
+ return frozenset(get_type_hints(ResponsesAPIOptionalRequestParams).keys())
+
+
+def _custom_logger_callbacks(logging_obj: Any) -> list[Any]:
+ from litellm.integrations.custom_logger import CustomLogger
+ from litellm.litellm_core_utils.litellm_logging import (
+ get_custom_logger_compatible_class,
+ )
+
+ dynamic_success_callbacks = getattr(logging_obj, "dynamic_success_callbacks", None)
+ callbacks = list(litellm.callbacks)
+ if isinstance(dynamic_success_callbacks, (list, tuple)):
+ callbacks.extend(dynamic_success_callbacks)
+
+ custom_loggers: list[Any] = []
+ for cb in callbacks:
+ if isinstance(cb, str):
+ resolved = get_custom_logger_compatible_class(cb) # type: ignore[arg-type]
+ if resolved is None:
+ continue
+ cb = resolved
+ if isinstance(cb, CustomLogger):
+ custom_loggers.append(cb)
+ return custom_loggers
+
+
+def _has_pre_call_deployment_hook(logging_obj: Any) -> bool:
+ from litellm.integrations.custom_logger import CustomLogger
+
+ base_func = CustomLogger.async_pre_call_deployment_hook
+ for cb in _custom_logger_callbacks(logging_obj):
+ cb_func = getattr(type(cb), "async_pre_call_deployment_hook", base_func)
+ if getattr(cb_func, "__func__", cb_func) is not getattr(
+ base_func, "__func__", base_func
+ ):
+ return True
+ return False
+
+
class BaseLLMHTTPHandler:
async def _make_common_async_call(
self,
@@ -1833,6 +1879,9 @@ class BaseLLMHTTPHandler:
data = provider_config.transform_search_request(
query=query,
optional_params=optional_params,
+ api_key=api_key,
+ api_base=api_base,
+ headers=headers or {},
)
# Get complete URL (pass data for providers that need request body for URL construction)
@@ -2224,12 +2273,92 @@ class BaseLLMHTTPHandler:
)
raise ValueError("anthropic_messages_handler is not implemented for sync calls")
+ def _run_sync_responses_pre_call_deployment_hook(
+ self,
+ *,
+ model: str,
+ input: Union[str, ResponseInputParam],
+ custom_llm_provider: str,
+ response_api_optional_request_params: dict[str, Any],
+ litellm_params: GenericLiteLLMParams,
+ logging_obj: LiteLLMLoggingObj,
+ ) -> tuple[
+ str,
+ Union[str, ResponseInputParam],
+ str,
+ dict[str, Any],
+ GenericLiteLLMParams,
+ ]:
+ if not _has_pre_call_deployment_hook(logging_obj):
+ return (
+ model,
+ input,
+ custom_llm_provider,
+ response_api_optional_request_params,
+ litellm_params,
+ )
+
+ modified_kwargs = run_async_function(
+ async_pre_call_deployment_hook,
+ {
+ **dict(litellm_params),
+ **response_api_optional_request_params,
+ "model": model,
+ "input": input,
+ "custom_llm_provider": custom_llm_provider,
+ },
+ CallTypes.responses.value,
+ )
+ if modified_kwargs is None:
+ return (
+ model,
+ input,
+ custom_llm_provider,
+ response_api_optional_request_params,
+ litellm_params,
+ )
+
+ optional_param_names = _responses_api_optional_request_param_names()
+ updated_response_params = {
+ **response_api_optional_request_params,
+ **{
+ key: value
+ for key, value in modified_kwargs.items()
+ if key in optional_param_names
+ },
+ }
+ updated_litellm_params = GenericLiteLLMParams(
+ **{
+ **dict(litellm_params),
+ **{
+ key: value
+ for key, value in modified_kwargs.items()
+ if key not in optional_param_names
+ and key not in {"model", "input", "custom_llm_provider"}
+ },
+ }
+ )
+ return (
+ str(modified_kwargs["model"]) if "model" in modified_kwargs else model,
+ cast(
+ Union[str, ResponseInputParam],
+ modified_kwargs["input"] if "input" in modified_kwargs else input,
+ ),
+ (
+ str(modified_kwargs["custom_llm_provider"])
+ if "custom_llm_provider" in modified_kwargs
+ else custom_llm_provider
+ ),
+ updated_response_params,
+ updated_litellm_params,
+ )
+
def response_api_handler(
self,
model: str,
input: Union[str, ResponseInputParam],
responses_api_provider_config: BaseResponsesAPIConfig,
- response_api_optional_request_params: Dict,
+ response_api_optional_request_params: dict[str, Any],
custom_llm_provider: str,
litellm_params: GenericLiteLLMParams,
logging_obj: LiteLLMLoggingObj,
@@ -2276,6 +2405,21 @@ class BaseLLMHTTPHandler:
shared_session=shared_session,
)
+ (
+ model,
+ input,
+ custom_llm_provider,
+ response_api_optional_request_params,
+ litellm_params,
+ ) = self._run_sync_responses_pre_call_deployment_hook(
+ model=model,
+ input=input,
+ custom_llm_provider=custom_llm_provider,
+ response_api_optional_request_params=response_api_optional_request_params,
+ litellm_params=litellm_params,
+ logging_obj=logging_obj,
+ )
+
if client is None or not isinstance(client, HTTPHandler):
sync_httpx_client = _get_httpx_client(
params={"ssl_verify": litellm_params.get("ssl_verify", None)}
@@ -2407,12 +2551,36 @@ class BaseLLMHTTPHandler:
provider_config=responses_api_provider_config,
)
- return responses_api_provider_config.transform_response_api_response(
- model=model,
- raw_response=response,
- logging_obj=logging_obj,
+ initial_response = (
+ responses_api_provider_config.transform_response_api_response(
+ model=model,
+ raw_response=response,
+ logging_obj=logging_obj,
+ )
)
+ if self._has_agentic_completion_hook(logging_obj):
+ final_response = run_async_function(
+ self._call_agentic_completion_hooks,
+ response=initial_response,
+ model=model,
+ messages=(
+ input
+ if isinstance(input, list)
+ else [{"role": "user", "content": input}]
+ ),
+ anthropic_messages_provider_config=responses_api_provider_config,
+ anthropic_messages_optional_request_params=response_api_optional_request_params,
+ logging_obj=logging_obj,
+ stream=False,
+ custom_llm_provider=custom_llm_provider,
+ kwargs=dict(litellm_params),
+ api_surface="responses",
+ )
+ return final_response if final_response is not None else initial_response
+
+ return initial_response
+
async def async_response_api_handler(
self,
model: str,
@@ -2570,12 +2738,44 @@ class BaseLLMHTTPHandler:
provider_config=responses_api_provider_config,
)
- return responses_api_provider_config.transform_response_api_response(
- model=model,
- raw_response=response,
- logging_obj=logging_obj,
+ initial_response = (
+ responses_api_provider_config.transform_response_api_response(
+ model=model,
+ raw_response=response,
+ logging_obj=logging_obj,
+ )
)
+ final_response = await self._call_agentic_completion_hooks(
+ response=initial_response,
+ model=model,
+ messages=(
+ input
+ if isinstance(input, list)
+ else [{"role": "user", "content": input}]
+ ),
+ anthropic_messages_provider_config=responses_api_provider_config,
+ anthropic_messages_optional_request_params=response_api_optional_request_params,
+ logging_obj=logging_obj,
+ stream=False,
+ custom_llm_provider=custom_llm_provider,
+ kwargs=dict(litellm_params),
+ api_surface="responses",
+ )
+
+ result = final_response if final_response is not None else initial_response
+ if litellm_params.get(
+ "_code_interpreter_interception_converted_stream"
+ ) and not litellm_params.get("_agentic_loop_depth"):
+ return self._wrap_responses_response_as_fake_stream(
+ result=result,
+ model=model,
+ responses_api_provider_config=responses_api_provider_config,
+ logging_obj=logging_obj,
+ custom_llm_provider=custom_llm_provider,
+ )
+ return result
+
async def async_delete_response_api_handler(
self,
response_id: str,
@@ -4734,22 +4934,9 @@ class BaseLLMHTTPHandler:
agentic callback is detected too.
"""
from litellm.integrations.custom_logger import CustomLogger
- from litellm.litellm_core_utils.litellm_logging import (
- get_custom_logger_compatible_class,
- )
base_func = CustomLogger.async_should_run_agentic_loop
- callbacks = litellm.callbacks + (
- getattr(logging_obj, "dynamic_success_callbacks", None) or []
- )
- for cb in callbacks:
- if isinstance(cb, str):
- resolved = get_custom_logger_compatible_class(cb) # type: ignore[arg-type]
- if resolved is None:
- continue
- cb = resolved
- if not isinstance(cb, CustomLogger):
- continue
+ for cb in _custom_logger_callbacks(logging_obj):
cb_func = getattr(type(cb), "async_should_run_agentic_loop", base_func)
if getattr(cb_func, "__func__", cb_func) is not getattr(
base_func, "__func__", base_func
@@ -4875,6 +5062,132 @@ class BaseLLMHTTPHandler:
return response
+ async def _execute_responses_agentic_plan(
+ self,
+ plan: AgenticLoopPlan,
+ model: str,
+ response_api_optional_request_params: dict,
+ logging_obj: "LiteLLMLoggingObj",
+ kwargs: dict,
+ depth: int,
+ max_loops: int,
+ fingerprints: list[str],
+ fingerprint: str,
+ callback: Any | None = None,
+ ) -> Any:
+ patch = plan.request_patch or AgenticLoopRequestPatch()
+ if patch.messages is None:
+ raise ValueError("Agentic loop plan missing patched responses input")
+
+ optional_params = dict(response_api_optional_request_params)
+ optional_params.update(patch.optional_params)
+ if patch.tools is not None:
+ optional_params["tools"] = patch.tools
+ optional_params = {
+ k: v
+ for k, v in optional_params.items()
+ if k != "stream" and k != "_code_interpreter_interception_converted_stream"
+ }
+
+ internal_keys = {"litellm_logging_obj"}
+ kwargs_for_followup = {
+ k: v
+ for k, v in kwargs.items()
+ if not k.startswith("_websearch_interception")
+ and not k.startswith("_compression_interception")
+ and k != "_code_interpreter_interception_converted_stream"
+ and k not in internal_keys
+ and k not in optional_params
+ }
+ kwargs_for_followup.update(patch.kwargs)
+ kwargs_for_followup["_agentic_loop_depth"] = depth + 1
+ kwargs_for_followup["max_agentic_loops"] = max_loops
+ kwargs_for_followup["_agentic_loop_fingerprints"] = fingerprints + [fingerprint]
+
+ try:
+ response = await litellm.aresponses(
+ model=patch.model or model,
+ input=patch.messages,
+ **optional_params,
+ **kwargs_for_followup,
+ )
+
+ if callback is not None:
+ try:
+ response = await callback.async_post_agentic_loop_response_hook(
+ response=response, plan=plan, kwargs=kwargs
+ )
+ except Exception as e:
+ _call_id = getattr(logging_obj, "litellm_call_id", "unknown")
+ verbose_logger.exception(
+ "LiteLLM.AgenticHookError: Exception in "
+ "async_post_agentic_loop_response_hook [call_id=%s model=%s]: %s",
+ _call_id,
+ model,
+ str(e),
+ )
+
+ return response
+ finally:
+ if callback is not None:
+ await self._run_agentic_loop_cleanup(
+ callback=callback,
+ plan=plan,
+ kwargs=kwargs,
+ logging_obj=logging_obj,
+ model=model,
+ )
+
+ @staticmethod
+ async def _run_agentic_loop_cleanup(
+ callback: Any,
+ plan: AgenticLoopPlan,
+ kwargs: dict,
+ logging_obj: "LiteLLMLoggingObj",
+ model: str,
+ ) -> None:
+ try:
+ await callback.async_agentic_loop_cleanup_hook(plan=plan, kwargs=kwargs)
+ except Exception as e:
+ _call_id = getattr(logging_obj, "litellm_call_id", "unknown")
+ verbose_logger.exception(
+ "LiteLLM.AgenticHookError: Exception in "
+ "async_agentic_loop_cleanup_hook [call_id=%s model=%s]: %s",
+ _call_id,
+ model,
+ str(e),
+ )
+
+ def _wrap_responses_response_as_fake_stream(
+ self,
+ result: Any,
+ model: str,
+ responses_api_provider_config: Any,
+ logging_obj: "LiteLLMLoggingObj",
+ custom_llm_provider: str,
+ ) -> Any:
+ """
+ Wrap a completed responses result as a synthetic stream.
+
+ Used when an interceptor forced stream=False to run the agentic loop on
+ the non-streaming path, but the caller originally asked for streaming.
+ """
+ import httpx
+
+ from litellm.responses.streaming_iterator import (
+ MockResponsesAPIStreamingIterator,
+ )
+
+ payload = result.model_dump() if hasattr(result, "model_dump") else result
+ raw_response = httpx.Response(status_code=200, json=payload)
+ return MockResponsesAPIStreamingIterator(
+ response=raw_response,
+ model=model,
+ responses_api_provider_config=responses_api_provider_config,
+ logging_obj=logging_obj,
+ custom_llm_provider=custom_llm_provider,
+ )
+
async def _execute_chat_completion_agentic_plan(
self,
plan: AgenticLoopPlan,
@@ -4940,6 +5253,7 @@ class BaseLLMHTTPHandler:
stream: bool,
custom_llm_provider: str,
kwargs: Dict,
+ api_surface: str = "anthropic_messages",
) -> Optional[Any]:
"""
Call agentic completion hooks for all custom loggers (Anthropic Messages API).
@@ -5046,6 +5360,20 @@ class BaseLLMHTTPHandler:
if not plan.run_agentic_loop:
continue
+ if api_surface == "responses":
+ return await self._execute_responses_agentic_plan(
+ plan=plan,
+ model=model,
+ response_api_optional_request_params=anthropic_messages_optional_request_params,
+ logging_obj=logging_obj,
+ kwargs=kwargs_with_provider,
+ depth=depth,
+ max_loops=max_loops,
+ fingerprints=fingerprints,
+ fingerprint=fingerprint,
+ callback=callback,
+ )
+
return await self._execute_anthropic_agentic_plan(
plan=plan,
model=model,
@@ -5083,7 +5411,7 @@ class BaseLLMHTTPHandler:
else False
)
- if websearch_converted_stream:
+ if api_surface == "anthropic_messages" and websearch_converted_stream:
from typing import cast
from litellm._logging import verbose_logger
@@ -5358,9 +5686,7 @@ class BaseLLMHTTPHandler:
import websockets
from websockets.asyncio.client import ClientConnection
- url = self._append_query_params(
- provider_config.get_complete_url(api_base, model, api_key), query_params
- )
+ url = provider_config.get_complete_url(api_base, model, api_key)
headers = provider_config.validate_environment(
headers=headers,
model=model,
diff --git a/litellm/llms/dataforseo/search/transformation.py b/litellm/llms/dataforseo/search/transformation.py
index 27c10d740b5..701db586b72 100644
--- a/litellm/llms/dataforseo/search/transformation.py
+++ b/litellm/llms/dataforseo/search/transformation.py
@@ -61,9 +61,18 @@ class DataForSEOSearchConfig(BaseSearchConfig):
password = get_secret_str("DATAFORSEO_PASSWORD")
# If api_key is provided in "login:password" format, use it
+ caller_supplied_credentials = bool(api_key and ":" in api_key)
if api_key and ":" in api_key:
login, password = api_key.split(":", 1)
+ if not caller_supplied_credentials and login and password:
+ self._assert_trusted_api_base_for_server_credential(
+ api_base,
+ self.DATAFORSEO_API_BASE,
+ "DATAFORSEO_API_BASE",
+ "DATAFORSEO_LOGIN",
+ )
+
if not login:
raise ValueError(
"DATAFORSEO_LOGIN is not set. Set `DATAFORSEO_LOGIN` environment variable or pass credentials in api_key parameter."
diff --git a/litellm/llms/e2b/__init__.py b/litellm/llms/e2b/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/litellm/llms/e2b/sandbox/__init__.py b/litellm/llms/e2b/sandbox/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/litellm/llms/e2b/sandbox/transformation.py b/litellm/llms/e2b/sandbox/transformation.py
new file mode 100644
index 00000000000..ecfc1642c97
--- /dev/null
+++ b/litellm/llms/e2b/sandbox/transformation.py
@@ -0,0 +1,212 @@
+"""
+e2b sandbox provider.
+
+Talks to e2b's REST API directly over httpx (no e2b SDK dependency):
+ - create: POST {api_base}/sandboxes
+ - run: POST https://{JUPYTER_PORT}-{sandboxID}.{domain}/execute (NDJSON stream)
+ - delete: DELETE {api_base}/sandboxes/{sandboxID}
+"""
+
+import json
+from typing import Union, cast
+
+import httpx
+
+from litellm.llms.base_llm.sandbox.transformation import (
+ BaseSandboxConfig,
+ CodeExecutionResult,
+ ContainerHandle,
+ SANDBOX_MAX_OUTPUT_BYTES,
+)
+from litellm.llms.custom_httpx.http_handler import (
+ AsyncHTTPHandler,
+ get_async_httpx_client,
+)
+from litellm.secret_managers.main import get_secret_str
+from litellm.types.llms.custom_http import httpxSpecialProvider
+
+E2B_API_BASE = "https://api.e2b.app"
+E2B_DEFAULT_TEMPLATE = "code-interpreter-v1"
+E2B_DEFAULT_DOMAIN = "e2b.app"
+JUPYTER_PORT = 49999
+DEFAULT_SANDBOX_TIMEOUT = 300
+MAX_OUTPUT_BYTES = SANDBOX_MAX_OUTPUT_BYTES
+
+
+class E2BSandboxConfig(BaseSandboxConfig):
+ def _http(self, client: AsyncHTTPHandler | None) -> AsyncHTTPHandler:
+ if client is not None:
+ return client
+ return get_async_httpx_client(llm_provider=httpxSpecialProvider.Sandbox)
+
+ def validate_environment(self, api_key: str | None = None, **kwargs) -> str:
+ key = api_key or get_secret_str("E2B_API_KEY")
+ if not key:
+ raise ValueError("E2B API key not set. Set E2B_API_KEY or pass api_key=...")
+ return key
+
+ async def acreate_sandbox(
+ self,
+ *,
+ template: str | None = None,
+ timeout: int | None = None,
+ allow_internet_access: bool | None = None,
+ api_key: str | None = None,
+ api_base: str | None = None,
+ metadata: dict | None = None,
+ client: AsyncHTTPHandler | None = None,
+ **kwargs,
+ ) -> ContainerHandle:
+ key = self.validate_environment(api_key=api_key)
+ base = api_base or E2B_API_BASE
+ body = {
+ "templateID": template or E2B_DEFAULT_TEMPLATE,
+ "timeout": timeout if timeout is not None else DEFAULT_SANDBOX_TIMEOUT,
+ "secure": True,
+ "allow_internet_access": (
+ True if allow_internet_access is None else allow_internet_access
+ ),
+ }
+ if metadata:
+ body["metadata"] = metadata
+
+ response = cast(
+ httpx.Response,
+ await self._http(client).post(
+ url=f"{base}/sandboxes",
+ headers={"X-API-Key": key, "Content-Type": "application/json"},
+ json=body,
+ ),
+ )
+ data = response.json()
+
+ handle = ContainerHandle(
+ id=data["sandboxID"],
+ provider="e2b",
+ domain=data.get("domain") or E2B_DEFAULT_DOMAIN,
+ )
+ handle._hidden_params = {
+ "envd_access_token": data.get("envdAccessToken"),
+ "traffic_access_token": data.get("trafficAccessToken"),
+ "api_key": key,
+ "api_base": base,
+ }
+ return handle
+
+ async def arun_code(
+ self,
+ *,
+ container: Union[ContainerHandle, str],
+ code: str,
+ api_key: str | None = None,
+ env_vars: dict | None = None,
+ client: AsyncHTTPHandler | None = None,
+ **kwargs,
+ ) -> CodeExecutionResult:
+ handle = self._as_handle(container)
+
+ token = handle._hidden_params.get("envd_access_token")
+ if not token:
+ raise ValueError(
+ "Cannot run code from a sandbox id alone. e2b secure sandboxes "
+ "require the access token returned by acreate_sandbox; pass the "
+ "ContainerHandle it returned instead of a bare sandbox id."
+ )
+
+ headers = {"Content-Type": "application/json", "X-Access-Token": token}
+ traffic_token = handle._hidden_params.get("traffic_access_token")
+ if traffic_token:
+ headers["E2B-Traffic-Access-Token"] = traffic_token
+
+ url = f"https://{JUPYTER_PORT}-{handle.id}.{handle.domain}/execute"
+ response = cast(
+ httpx.Response,
+ await self._http(client).post(
+ url=url,
+ headers=headers,
+ json={"code": code, "context_id": None, "env_vars": env_vars},
+ stream=True,
+ ),
+ )
+ lines = await self._read_capped_lines(response)
+ return self._parse_lines(lines)
+
+ async def adelete_sandbox(
+ self,
+ *,
+ container: Union[ContainerHandle, str],
+ api_key: str | None = None,
+ api_base: str | None = None,
+ client: AsyncHTTPHandler | None = None,
+ **kwargs,
+ ) -> bool:
+ handle = self._as_handle(container)
+ key = (
+ api_key
+ or handle._hidden_params.get("api_key")
+ or self.validate_environment()
+ )
+ base = api_base or handle._hidden_params.get("api_base") or E2B_API_BASE
+ try:
+ response = cast(
+ httpx.Response,
+ await self._http(client).delete(
+ url=f"{base}/sandboxes/{handle.id}",
+ headers={"X-API-Key": key},
+ ),
+ )
+ except httpx.HTTPStatusError as e:
+ if e.response.status_code == 404:
+ return False
+ raise
+ return 200 <= response.status_code < 300
+
+ @staticmethod
+ def _as_handle(container: Union[ContainerHandle, str]) -> ContainerHandle:
+ if isinstance(container, ContainerHandle):
+ return container
+ handle = ContainerHandle(
+ id=str(container), provider="e2b", domain=E2B_DEFAULT_DOMAIN
+ )
+ handle._hidden_params = {}
+ return handle
+
+ @staticmethod
+ def _parse_lines(lines: list[str]) -> CodeExecutionResult:
+ def _try_parse(stripped: str):
+ try:
+ return json.loads(stripped)
+ except json.JSONDecodeError:
+ return None
+
+ messages = tuple(
+ parsed
+ for line in lines
+ if (stripped := line.strip())
+ if (parsed := _try_parse(stripped)) is not None
+ )
+
+ def of_type(message_type: str):
+ return (m for m in messages if m.get("type") == message_type)
+
+ error = next(
+ (
+ {key: m.get(key) for key in ("name", "value", "traceback")}
+ for m in of_type("error")
+ ),
+ None,
+ )
+ execution_count = next(
+ (m.get("execution_count") for m in of_type("number_of_executions")),
+ None,
+ )
+
+ return CodeExecutionResult(
+ stdout="".join(m.get("text", "") for m in of_type("stdout")),
+ stderr="".join(m.get("text", "") for m in of_type("stderr")),
+ results=[
+ {k: v for k, v in m.items() if k != "type"} for m in of_type("result")
+ ],
+ error=error,
+ execution_count=execution_count,
+ )
diff --git a/litellm/llms/exa_ai/search/transformation.py b/litellm/llms/exa_ai/search/transformation.py
index 7a34ededa6b..5cfd14aeaa9 100644
--- a/litellm/llms/exa_ai/search/transformation.py
+++ b/litellm/llms/exa_ai/search/transformation.py
@@ -65,7 +65,13 @@ class ExaAISearchConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("EXA_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("EXA_API_KEY",),
+ base_env_var="EXA_API_BASE",
+ default_api_base=self.EXA_AI_API_BASE,
+ )
if not api_key:
raise ValueError(
"EXA_API_KEY is not set. Set `EXA_API_KEY` environment variable."
diff --git a/litellm/llms/fastcrw/search/transformation.py b/litellm/llms/fastcrw/search/transformation.py
index ce702266e7b..b571a659cac 100644
--- a/litellm/llms/fastcrw/search/transformation.py
+++ b/litellm/llms/fastcrw/search/transformation.py
@@ -57,7 +57,13 @@ class FastCRWSearchConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("CRW_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("CRW_API_KEY",),
+ base_env_var="CRW_API_BASE",
+ default_api_base=self.FASTCRW_API_BASE,
+ )
if not api_key:
raise ValueError(
"CRW_API_KEY is not set. Set `CRW_API_KEY` environment variable."
diff --git a/litellm/llms/firecrawl/search/transformation.py b/litellm/llms/firecrawl/search/transformation.py
index 18cf1d28c4d..7e01ba58706 100644
--- a/litellm/llms/firecrawl/search/transformation.py
+++ b/litellm/llms/firecrawl/search/transformation.py
@@ -61,7 +61,13 @@ class FirecrawlSearchConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("FIRECRAWL_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("FIRECRAWL_API_KEY",),
+ base_env_var="FIRECRAWL_API_BASE",
+ default_api_base=self.FIRECRAWL_API_BASE,
+ )
if not api_key:
raise ValueError(
"FIRECRAWL_API_KEY is not set. Set `FIRECRAWL_API_KEY` environment variable."
diff --git a/litellm/llms/fireworks_ai/audio_transcription/transformation.py b/litellm/llms/fireworks_ai/audio_transcription/transformation.py
deleted file mode 100644
index 00bb5f26797..00000000000
--- a/litellm/llms/fireworks_ai/audio_transcription/transformation.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from typing import List
-
-from litellm.types.llms.openai import OpenAIAudioTranscriptionOptionalParams
-
-from ...openai.transcriptions.whisper_transformation import (
- OpenAIWhisperAudioTranscriptionConfig,
-)
-from ..common_utils import FireworksAIMixin
-
-
-class FireworksAIAudioTranscriptionConfig(
- FireworksAIMixin, OpenAIWhisperAudioTranscriptionConfig
-):
- def get_supported_openai_params(
- self, model: str
- ) -> List[OpenAIAudioTranscriptionOptionalParams]:
- return ["language", "prompt", "response_format", "timestamp_granularities"]
diff --git a/litellm/llms/fireworks_ai/chat/transformation.py b/litellm/llms/fireworks_ai/chat/transformation.py
index 341c2fc7350..7e4395959b9 100644
--- a/litellm/llms/fireworks_ai/chat/transformation.py
+++ b/litellm/llms/fireworks_ai/chat/transformation.py
@@ -1,5 +1,15 @@
import json
-from typing import Any, List, Literal, Optional, Tuple, Union, cast
+from typing import (
+ Any,
+ AsyncIterator,
+ Iterator,
+ List,
+ Literal,
+ Optional,
+ Tuple,
+ Union,
+ cast,
+)
import httpx
@@ -15,7 +25,6 @@ from litellm.litellm_core_utils.llm_response_utils.get_headers import (
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
AllMessageValues,
- ChatCompletionImageObject,
ChatCompletionToolParam,
OpenAIChatCompletionToolParam,
)
@@ -25,6 +34,7 @@ from litellm.types.utils import (
Function,
Message,
ModelResponse,
+ ModelResponseStream,
ProviderSpecificModelInfo,
)
from litellm.utils import (
@@ -34,10 +44,34 @@ from litellm.utils import (
supports_tool_choice,
)
-from ...openai.chat.gpt_transformation import OpenAIGPTConfig
+from ...openai.chat.gpt_transformation import (
+ OpenAIChatCompletionStreamingHandler,
+ OpenAIGPTConfig,
+)
from ..common_utils import FireworksAIException
+def _extract_fireworks_hidden_params(payload: dict) -> dict:
+ """
+ Collect Fireworks-specific response fields (perf_metrics, prompt_token_ids,
+ per-choice raw_output and token_ids) from a non-streaming completion payload
+ or a single streaming chunk, so the same data lands in ``_hidden_params`` on
+ both response paths.
+ """
+ choices = [c for c in (payload.get("choices") or []) if isinstance(c, dict)]
+ top_level = {
+ f"fireworks_{field}": payload[field]
+ for field in ("perf_metrics", "prompt_token_ids")
+ if field in payload
+ }
+ per_choice = {
+ f"fireworks_{dest}": [c[field] for c in choices if field in c]
+ for field, dest in (("raw_output", "raw_outputs"), ("token_ids", "token_ids"))
+ if any(field in c for c in choices)
+ }
+ return {**top_level, **per_choice}
+
+
class FireworksAIConfig(OpenAIGPTConfig):
"""
Reference: https://docs.fireworks.ai/api-reference/post-chatcompletions
@@ -60,8 +94,7 @@ class FireworksAIConfig(OpenAIGPTConfig):
logprobs: Optional[int] = None
reasoning_effort: Optional[str] = None
- # Non OpenAI parameters - Fireworks AI only params
- prompt_truncate_length: Optional[int] = None
+ prompt_truncate_len: Optional[int] = None
context_length_exceeded_behavior: Optional[Literal["error", "truncate"]] = None
def __init__(
@@ -80,7 +113,7 @@ class FireworksAIConfig(OpenAIGPTConfig):
user: Optional[str] = None,
logprobs: Optional[int] = None,
reasoning_effort: Optional[str] = None,
- prompt_truncate_length: Optional[int] = None,
+ prompt_truncate_len: Optional[int] = None,
context_length_exceeded_behavior: Optional[Literal["error", "truncate"]] = None,
) -> None:
locals_ = locals().copy()
@@ -108,8 +141,30 @@ class FireworksAIConfig(OpenAIGPTConfig):
"response_format",
"user",
"logprobs",
- "prompt_truncate_length",
+ "prompt_truncate_len",
"context_length_exceeded_behavior",
+ "seed",
+ "top_logprobs",
+ "min_p",
+ "typical_p",
+ "repetition_penalty",
+ "mirostat_target",
+ "mirostat_lr",
+ "logit_bias",
+ "echo",
+ "echo_last",
+ "ignore_eos",
+ "prompt_cache_key",
+ "prompt_cache_isolation_key",
+ "raw_output",
+ "perf_metrics_in_response",
+ "return_token_ids",
+ "safe_tokenization",
+ "service_tier",
+ "speculation",
+ "prediction",
+ "stream_options",
+ "sampling_mask",
]
# Only add tools for models that support function calling
@@ -133,9 +188,11 @@ class FireworksAIConfig(OpenAIGPTConfig):
if supports_tool_choice(model=model, custom_llm_provider="fireworks_ai"):
supported_params.append("tool_choice")
- # Only add reasoning_effort for models that support it
+ # Only add reasoning params for models that support it
if supports_reasoning(model=model, custom_llm_provider="fireworks_ai"):
supported_params.append("reasoning_effort")
+ supported_params.append("reasoning_history")
+ supported_params.append("thinking")
return supported_params
@@ -151,6 +208,18 @@ class FireworksAIConfig(OpenAIGPTConfig):
param == "tools" and value is not None
for param, value in non_default_params.items()
)
+ if (
+ non_default_params.get("thinking") is not None
+ and non_default_params.get("reasoning_effort") is not None
+ ):
+ raise litellm.BadRequestError(
+ message=(
+ "Fireworks AI chat completions does not support specifying both "
+ "`thinking` and `reasoning_effort` in the same request."
+ ),
+ model=model,
+ llm_provider="fireworks_ai",
+ )
for param, value in non_default_params.items():
if param == "tool_choice":
@@ -174,40 +243,19 @@ class FireworksAIConfig(OpenAIGPTConfig):
optional_params["response_format"] = value
elif param == "max_completion_tokens":
optional_params["max_tokens"] = value
+ elif param == "reasoning_effort":
+ if value is True:
+ optional_params["reasoning_effort"] = "medium"
+ elif value is False:
+ optional_params["reasoning_effort"] = "none"
+ else:
+ optional_params["reasoning_effort"] = value
elif param in supported_openai_params:
if value is not None:
optional_params[param] = value
return optional_params
- def _add_transform_inline_image_block(
- self,
- content: ChatCompletionImageObject,
- model: str,
- disable_add_transform_inline_image_block: Optional[bool],
- ) -> ChatCompletionImageObject:
- """
- Add transform_inline to the image_url (allows non-vision models to parse documents/images/etc.)
- - ignore if model is a vision model
- - ignore if user has disabled this feature
- """
- if (
- "vision" in model or disable_add_transform_inline_image_block
- ): # allow user to toggle this feature.
- return content
- if isinstance(content["image_url"], str):
- # Skip base64 data URLs — appending #transform=inline corrupts the
- # base64 payload and causes an "Incorrect padding" decode error on
- # the Fireworks side. Data URLs are already inlined by definition.
- # Lower-case before checking: URI schemes are case-insensitive (RFC 3986).
- if not content["image_url"].lower().startswith("data:"):
- content["image_url"] = f"{content['image_url']}#transform=inline"
- elif isinstance(content["image_url"], dict):
- url = content["image_url"]["url"]
- if not url.lower().startswith("data:"):
- content["image_url"]["url"] = f"{url}#transform=inline"
- return content
-
def _transform_tools(
self, tools: List[OpenAIChatCompletionToolParam]
) -> List[OpenAIChatCompletionToolParam]:
@@ -225,36 +273,46 @@ class FireworksAIConfig(OpenAIGPTConfig):
self, messages: List[AllMessageValues], model: str, litellm_params: dict
) -> List[AllMessageValues]:
"""
- Add 'transform=inline' to the url of the image_url
+ Strip fields not permitted by FireworksAI from messages.
"""
from litellm.litellm_core_utils.prompt_templates.common_utils import (
filter_value_from_dict,
- migrate_file_to_image_url,
)
- disable_add_transform_inline_image_block = cast(
- Optional[bool],
- litellm_params.get("disable_add_transform_inline_image_block")
- or litellm.disable_add_transform_inline_image_block,
+ supports_vision_value = self._get_model_cost_capability_exact(
+ model=model, capability="supports_vision"
)
- ## For any 'file' message type with pdf content, move to 'image_url' message type
- for message in messages:
- if message["role"] == "user":
- _message_content = message.get("content")
- if _message_content is not None and isinstance(_message_content, list):
- for idx, content in enumerate(_message_content):
- if content["type"] == "file":
- _message_content[idx] = migrate_file_to_image_url(content)
for message in messages:
if message["role"] == "user":
_message_content = message.get("content")
if _message_content is not None and isinstance(_message_content, list):
for content in _message_content:
- if content["type"] == "image_url":
- content = self._add_transform_inline_image_block(
- content=content,
+ if not isinstance(content, dict):
+ continue
+ if content.get("type") == "file":
+ raise litellm.BadRequestError(
+ message=(
+ "Fireworks AI chat completions does not support "
+ "file content blocks. For PDFs, convert pages to "
+ "images and send image_url blocks to a Fireworks "
+ "vision model, or extract text before calling a "
+ "text-only model."
+ ),
model=model,
- disable_add_transform_inline_image_block=disable_add_transform_inline_image_block,
+ llm_provider="fireworks_ai",
+ )
+ if (
+ content.get("type") == "image_url"
+ and supports_vision_value is False
+ ):
+ raise litellm.BadRequestError(
+ message=(
+ f"Fireworks AI model {model} does not support "
+ "image inputs. Use a Fireworks vision model or "
+ "remove image_url content blocks."
+ ),
+ model=model,
+ llm_provider="fireworks_ai",
)
filter_value_from_dict(cast(dict, message), "cache_control")
# Remove fields not permitted by FireworksAI (additionalProperties: false
@@ -317,43 +375,55 @@ class FireworksAIConfig(OpenAIGPTConfig):
return True
return ("-" + key_short + "-") in short_name
- def _get_model_cost_capability(self, model: str, capability: str) -> Optional[bool]:
+ @staticmethod
+ def _short_model_name(model: str) -> str:
short_name = model
if short_name.startswith("fireworks_ai/"):
short_name = short_name[len("fireworks_ai/") :]
if short_name.startswith("accounts/fireworks/models/"):
short_name = short_name[len("accounts/fireworks/models/") :]
+ return short_name
- candidate_keys = [
+ def _get_model_cost_capability_exact(
+ self, model: str, capability: str
+ ) -> Optional[bool]:
+ short_name = self._short_model_name(model)
+ candidate_keys = (
model,
f"fireworks_ai/{short_name}",
f"fireworks_ai/accounts/fireworks/models/{short_name}",
- ]
-
+ )
for candidate_key in candidate_keys:
model_info = litellm.model_cost.get(candidate_key)
if model_info is not None and model_info.get(capability) is not None:
return cast(Optional[bool], model_info.get(capability))
+ return None
- # Fallback: preserve historical substring matching for model name
- # variants (e.g. fine-tuned or regionally-suffixed versions of a
- # known model). Pick the *longest* matching entry so a more specific
- # known model (e.g. "qwen3-8b-instruct") wins over a less specific
- # one (e.g. "qwen3-8b") when the query model is more specific still.
- # Use hyphen-aligned matching to avoid false positives where a short
- # known model name is an unrelated substring of a longer one.
- best_match_short: Optional[str] = None
- best_match_value: Optional[bool] = None
- for key_short, model_info in self._get_fireworks_index():
- if model_info.get(capability) is None:
- continue
- if not self._matches_on_hyphen_boundary(short_name, key_short):
- continue
- if best_match_short is None or len(key_short) > len(best_match_short):
- best_match_short = key_short
- best_match_value = cast(Optional[bool], model_info.get(capability))
+ def _get_model_cost_capability(self, model: str, capability: str) -> Optional[bool]:
+ exact = self._get_model_cost_capability_exact(
+ model=model, capability=capability
+ )
+ if exact is not None:
+ return exact
- return best_match_value
+ # Fallback: substring matching for model name variants (e.g. fine-tuned
+ # or regionally-suffixed versions of a known model). Pick the *longest*
+ # matching entry so a more specific known model (e.g. "qwen3-8b-instruct")
+ # wins over a less specific one (e.g. "qwen3-8b"). Hyphen-aligned matching
+ # avoids false positives where a short known name is an unrelated
+ # substring of a longer one. This stays a soft signal: capability-gated
+ # hard rejections use the exact lookup so a fuzzy match never blocks a
+ # custom deployment.
+ short_name = self._short_model_name(model)
+ matches = [
+ (key_short, cast(Optional[bool], model_info.get(capability)))
+ for key_short, model_info in self._get_fireworks_index()
+ if model_info.get(capability) is not None
+ and self._matches_on_hyphen_boundary(short_name, key_short)
+ ]
+ if not matches:
+ return None
+ return max(matches, key=lambda match: len(match[0]))[1]
def get_provider_info(self, model: str) -> ProviderSpecificModelInfo:
supports_function_calling_value = self._get_model_cost_capability(
@@ -362,12 +432,16 @@ class FireworksAIConfig(OpenAIGPTConfig):
supports_reasoning_value = self._get_model_cost_capability(
model=model, capability="supports_reasoning"
)
+ supports_vision_value = self._get_model_cost_capability(
+ model=model, capability="supports_vision"
+ )
+ supports_pdf_input_value = self._get_model_cost_capability(
+ model=model, capability="supports_pdf_input"
+ )
provider_specific_model_info: ProviderSpecificModelInfo = {
"supports_function_calling": True,
"supports_prompt_caching": True, # https://docs.fireworks.ai/guides/prompt-caching
- "supports_pdf_input": True, # via document inlining
- "supports_vision": True, # via document inlining
}
if supports_function_calling_value is not None:
@@ -381,6 +455,14 @@ class FireworksAIConfig(OpenAIGPTConfig):
supports_reasoning_value
)
+ if supports_vision_value is not None:
+ provider_specific_model_info["supports_vision"] = supports_vision_value
+
+ if supports_pdf_input_value is not None:
+ provider_specific_model_info["supports_pdf_input"] = (
+ supports_pdf_input_value
+ )
+
return provider_specific_model_info
def transform_request(
@@ -402,6 +484,15 @@ class FireworksAIConfig(OpenAIGPTConfig):
if "tools" in optional_params and optional_params["tools"] is not None:
tools = self._transform_tools(tools=optional_params["tools"])
optional_params["tools"] = tools
+ if optional_params.get("stream"):
+ stream_options = optional_params.get("stream_options")
+ if stream_options is None:
+ optional_params["stream_options"] = {"include_usage": True}
+ elif stream_options.get("include_usage") is not False:
+ optional_params["stream_options"] = {
+ **stream_options,
+ "include_usage": True,
+ }
return super().transform_request(
model=model,
messages=messages,
@@ -494,10 +585,25 @@ class FireworksAIConfig(OpenAIGPTConfig):
)
)
- response._hidden_params = {"additional_headers": additional_headers}
+ response._hidden_params = {
+ "additional_headers": additional_headers,
+ **_extract_fireworks_hidden_params(completion_response),
+ }
return response
+ def get_model_response_iterator(
+ self,
+ streaming_response: Union[Iterator[str], AsyncIterator[str], ModelResponse],
+ sync_stream: bool,
+ json_mode: Optional[bool] = False,
+ ) -> Any:
+ return FireworksAIChatCompletionStreamingHandler(
+ streaming_response=streaming_response,
+ sync_stream=sync_stream,
+ json_mode=json_mode,
+ )
+
def _get_openai_compatible_provider_info(
self, api_base: Optional[str], api_key: Optional[str]
) -> Tuple[Optional[str], Optional[str]]:
@@ -554,3 +660,15 @@ class FireworksAIConfig(OpenAIGPTConfig):
or get_secret_str("FIREWORKSAI_API_KEY")
or get_secret_str("FIREWORKS_AI_TOKEN")
)
+
+
+class FireworksAIChatCompletionStreamingHandler(OpenAIChatCompletionStreamingHandler):
+ def chunk_parser(self, chunk: dict) -> ModelResponseStream:
+ parsed = super().chunk_parser(chunk)
+ fireworks_fields = _extract_fireworks_hidden_params(chunk)
+ if fireworks_fields:
+ parsed.provider_specific_fields = {
+ **(getattr(parsed, "provider_specific_fields", None) or {}),
+ **fireworks_fields,
+ }
+ return parsed
diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py
index 74f6cd4d831..e153d00e6ab 100644
--- a/litellm/llms/gemini/realtime/transformation.py
+++ b/litellm/llms/gemini/realtime/transformation.py
@@ -103,6 +103,10 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
# bypassing spend and budget accounting.
self._pending_usage_metadata: Optional[dict] = None
+ def _include_function_response_id(self) -> bool:
+ """Google AI Studio Gemini 3.5+ accepts ``id`` on functionResponses; Vertex AI rejects it."""
+ return True
+
@staticmethod
def _usage_detail_alias(details: Any, defaults: Dict[str, int]) -> Dict[str, Any]:
if not isinstance(details, dict):
@@ -604,10 +608,9 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
)
# Build Gemini toolResponse format
- function_response = {
- "id": call_id,
- "response": output_dict,
- }
+ function_response: dict[str, Any] = {"response": output_dict}
+ if self._include_function_response_id() and call_id:
+ function_response["id"] = call_id
if function_name:
function_response["name"] = function_name
diff --git a/litellm/llms/google_pse/search/transformation.py b/litellm/llms/google_pse/search/transformation.py
index a8aa109cbf0..5cd3f2085a8 100644
--- a/litellm/llms/google_pse/search/transformation.py
+++ b/litellm/llms/google_pse/search/transformation.py
@@ -85,7 +85,13 @@ class GooglePSESearchConfig(BaseSearchConfig):
Google PSE uses API key as a query parameter, not in headers.
This method is called but headers are not used for authentication.
"""
- api_key = api_key or get_secret_str("GOOGLE_PSE_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("GOOGLE_PSE_API_KEY",),
+ base_env_var="GOOGLE_PSE_API_BASE",
+ default_api_base=self.GOOGLE_PSE_API_BASE,
+ )
if not api_key:
raise ValueError(
"GOOGLE_PSE_API_KEY is not set. Set `GOOGLE_PSE_API_KEY` environment variable."
@@ -137,6 +143,7 @@ class GooglePSESearchConfig(BaseSearchConfig):
query: Union[str, List[str]],
optional_params: dict,
api_key: Optional[str] = None,
+ api_base: str | None = None,
search_engine_id: Optional[str] = None,
**kwargs,
) -> Dict:
@@ -165,8 +172,16 @@ class GooglePSESearchConfig(BaseSearchConfig):
# Google PSE only supports single string queries
query = " ".join(query)
- # Get API credentials
- api_key = api_key or get_secret_str("GOOGLE_PSE_API_KEY")
+ # Get API credentials. The key is sent as a query param to api_base, so
+ # resolve it host-aware to avoid leaking a server-managed key to a
+ # caller-supplied host.
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("GOOGLE_PSE_API_KEY",),
+ base_env_var="GOOGLE_PSE_API_BASE",
+ default_api_base=self.GOOGLE_PSE_API_BASE,
+ )
search_engine_id = search_engine_id or get_secret_str("GOOGLE_PSE_ENGINE_ID")
if not api_key:
diff --git a/litellm/llms/linkup/search/transformation.py b/litellm/llms/linkup/search/transformation.py
index 2b17d5642ac..d27ae038f9e 100644
--- a/litellm/llms/linkup/search/transformation.py
+++ b/litellm/llms/linkup/search/transformation.py
@@ -61,7 +61,13 @@ class LinkupSearchConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("LINKUP_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("LINKUP_API_KEY",),
+ base_env_var="LINKUP_API_BASE",
+ default_api_base=self.LINKUP_API_BASE,
+ )
if not api_key:
raise ValueError(
"LINKUP_API_KEY is not set. Set `LINKUP_API_KEY` environment variable."
diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py
index f1ad3708236..8d0cf993814 100644
--- a/litellm/llms/mistral/chat/transformation.py
+++ b/litellm/llms/mistral/chat/transformation.py
@@ -247,6 +247,8 @@ class MistralConfig(OpenAIGPTConfig):
The above statement is not valid now. Need to plan to remove all the #1,2,3
Mistral API supports content as a list.
"""
+ messages = [self._strip_output_only_fields(m) for m in messages]
+
## 1. If 'image_url' or 'file' in content, then transform with base class and mistral-specific handling
for m in messages:
_content_block = m.get("content")
@@ -409,6 +411,25 @@ class MistralConfig(OpenAIGPTConfig):
return cleaned_tools
+ @classmethod
+ def _strip_output_only_fields(cls, message: AllMessageValues) -> AllMessageValues:
+ """
+ ``reasoning_content`` and ``thinking_blocks`` are output-only fields that
+ LiteLLM attaches to assistant responses. Mistral's input schema forbids
+ unknown fields, so replaying them verbatim in a follow-up turn triggers a
+ 422 ``extra_forbidden``. Drop them before the request is sent.
+ """
+ if message["role"] != "assistant":
+ return message
+ return cast(
+ AllMessageValues,
+ {
+ k: v
+ for k, v in message.items()
+ if k not in ("reasoning_content", "thinking_blocks")
+ },
+ )
+
@classmethod
def _handle_name_in_message(cls, message: AllMessageValues) -> AllMessageValues:
"""
diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json
index 24943563937..d87346fea70 100644
--- a/litellm/llms/openai_like/providers.json
+++ b/litellm/llms/openai_like/providers.json
@@ -115,6 +115,14 @@
"max_completion_tokens": "max_tokens"
}
},
+ "darkbloom": {
+ "base_url": "https://api.darkbloom.dev/v1",
+ "api_key_env": "DARKBLOOM_API_KEY",
+ "api_base_env": "DARKBLOOM_API_BASE",
+ "param_mappings": {
+ "max_completion_tokens": "max_tokens"
+ }
+ },
"neosantara": {
"base_url": "https://api.neosantara.xyz/v1",
"api_key_env": "NEOSANTARA_API_KEY",
diff --git a/litellm/llms/opensandbox/__init__.py b/litellm/llms/opensandbox/__init__.py
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/litellm/llms/opensandbox/__init__.py
@@ -0,0 +1 @@
+
diff --git a/litellm/llms/opensandbox/sandbox/__init__.py b/litellm/llms/opensandbox/sandbox/__init__.py
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/litellm/llms/opensandbox/sandbox/__init__.py
@@ -0,0 +1 @@
+
diff --git a/litellm/llms/opensandbox/sandbox/transformation.py b/litellm/llms/opensandbox/sandbox/transformation.py
new file mode 100644
index 00000000000..dc9f8440d30
--- /dev/null
+++ b/litellm/llms/opensandbox/sandbox/transformation.py
@@ -0,0 +1,598 @@
+import asyncio
+import json
+import time
+from typing import Union, cast
+
+import httpx
+
+from litellm.constants import (
+ OPEN_SANDBOX_API_BASE_ENV_VAR,
+ OPEN_SANDBOX_API_KEY_ENV_VAR,
+ OPEN_SANDBOX_DEFAULT_CPU_LIMIT,
+ OPEN_SANDBOX_DEFAULT_ENTRYPOINT,
+ OPEN_SANDBOX_DEFAULT_LANGUAGE,
+ OPEN_SANDBOX_DEFAULT_MEMORY_LIMIT,
+ OPEN_SANDBOX_DEFAULT_TEMPLATE,
+ OPEN_SANDBOX_DEFAULT_TIMEOUT,
+ OPEN_SANDBOX_EXECD_PORT,
+ OPEN_SANDBOX_POLL_INTERVAL,
+ OPEN_SANDBOX_READY_TIMEOUT,
+)
+from litellm.llms.base_llm.sandbox.transformation import (
+ BaseSandboxConfig,
+ CodeExecutionResult,
+ ContainerHandle,
+ SANDBOX_MAX_OUTPUT_BYTES,
+)
+from litellm.llms.custom_httpx.http_handler import (
+ AsyncHTTPHandler,
+ get_async_httpx_client,
+)
+from litellm.secret_managers.main import get_secret_str
+from litellm.types.llms.custom_http import httpxSpecialProvider
+
+DEFAULT_SANDBOX_TIMEOUT = OPEN_SANDBOX_DEFAULT_TIMEOUT
+DEFAULT_READY_TIMEOUT = OPEN_SANDBOX_READY_TIMEOUT
+DEFAULT_POLL_INTERVAL = OPEN_SANDBOX_POLL_INTERVAL
+MAX_OUTPUT_BYTES = SANDBOX_MAX_OUTPUT_BYTES
+
+
+class OpenSandboxSandboxConfig(BaseSandboxConfig):
+ def _http(self, client: AsyncHTTPHandler | None) -> AsyncHTTPHandler:
+ if client is not None:
+ return client
+ return get_async_httpx_client(llm_provider=httpxSpecialProvider.Sandbox)
+
+ def validate_environment(self, api_key: str | None = None, **kwargs) -> str:
+ if api_key is not None:
+ return api_key
+ return get_secret_str(OPEN_SANDBOX_API_KEY_ENV_VAR) or ""
+
+ async def acreate_sandbox(
+ self,
+ *,
+ template: str | None = None,
+ timeout: int | None = None,
+ allow_internet_access: bool | None = None,
+ api_key: str | None = None,
+ api_base: str | None = None,
+ metadata: dict[str, str] | None = None,
+ env_vars: dict[str, str] | None = None,
+ resource_limits: dict[str, str] | None = None,
+ resource_requests: dict[str, str] | None = None,
+ entrypoint: list[str] | tuple[str, ...] | None = None,
+ network_policy: dict[str, object] | None = None,
+ secure_access: bool = False,
+ use_server_proxy: bool = False,
+ ready_timeout: float | None = None,
+ poll_interval: float | None = None,
+ client: AsyncHTTPHandler | None = None,
+ **kwargs,
+ ) -> ContainerHandle:
+ key = self.validate_environment(api_key=api_key)
+ base = self._api_base(api_base)
+ ready_timeout_seconds = (
+ float(ready_timeout) if ready_timeout is not None else DEFAULT_READY_TIMEOUT
+ )
+ poll_interval_seconds = (
+ float(poll_interval) if poll_interval is not None else DEFAULT_POLL_INTERVAL
+ )
+ body = self._create_body(
+ template=template,
+ timeout=timeout,
+ allow_internet_access=allow_internet_access,
+ metadata=metadata,
+ env_vars=env_vars,
+ resource_limits=resource_limits,
+ resource_requests=resource_requests,
+ entrypoint=entrypoint,
+ network_policy=network_policy,
+ secure_access=secure_access,
+ )
+
+ response = cast(
+ httpx.Response,
+ await self._http(client).post(
+ url=f"{base}/sandboxes",
+ headers=self._lifecycle_headers(key),
+ json=body,
+ ),
+ )
+ data = response.json()
+ sandbox_id = str(data["id"])
+
+ if self._sandbox_state(data) != "Running":
+ await self._wait_until_running(
+ sandbox_id=sandbox_id,
+ api_base=base,
+ headers=self._lifecycle_headers(key),
+ client=client,
+ ready_timeout=ready_timeout_seconds,
+ poll_interval=poll_interval_seconds,
+ )
+
+ endpoint, endpoint_headers = await self._wait_for_execd_endpoint(
+ sandbox_id=sandbox_id,
+ api_base=base,
+ headers=self._lifecycle_headers(key),
+ use_server_proxy=use_server_proxy,
+ client=client,
+ ready_timeout=ready_timeout_seconds,
+ poll_interval=poll_interval_seconds,
+ )
+
+ handle = ContainerHandle(id=sandbox_id, provider="opensandbox", domain=base)
+ handle._hidden_params = {
+ "api_base": base,
+ "api_key": key,
+ "execd_endpoint": endpoint,
+ "execd_headers": endpoint_headers,
+ "use_server_proxy": use_server_proxy,
+ }
+ return handle
+
+ async def arun_code(
+ self,
+ *,
+ container: Union[ContainerHandle, str],
+ code: str,
+ api_key: str | None = None,
+ api_base: str | None = None,
+ language: str = OPEN_SANDBOX_DEFAULT_LANGUAGE,
+ use_server_proxy: bool = False,
+ ready_timeout: float | None = None,
+ poll_interval: float | None = None,
+ client: AsyncHTTPHandler | None = None,
+ **kwargs,
+ ) -> CodeExecutionResult:
+ handle = await self._ensure_handle(
+ container=container,
+ api_key=api_key,
+ api_base=api_base,
+ use_server_proxy=use_server_proxy,
+ ready_timeout=(
+ float(ready_timeout)
+ if ready_timeout is not None
+ else DEFAULT_READY_TIMEOUT
+ ),
+ poll_interval=(
+ float(poll_interval)
+ if poll_interval is not None
+ else DEFAULT_POLL_INTERVAL
+ ),
+ client=client,
+ )
+ endpoint = str(handle._hidden_params["execd_endpoint"])
+ endpoint_headers = self._as_str_dict(handle._hidden_params.get("execd_headers"))
+ base = str(
+ handle._hidden_params.get("api_base")
+ or handle.domain
+ or self._api_base(api_base)
+ )
+ lines = await self._post_code(
+ url=f"{self._endpoint_base_url(endpoint, base)}/code",
+ headers={
+ "Content-Type": "application/json",
+ "Accept": "text/event-stream",
+ "Cache-Control": "no-cache",
+ **endpoint_headers,
+ },
+ body={
+ "code": code,
+ "context": {"language": language},
+ },
+ client=client,
+ )
+ return self._parse_lines(lines)
+
+ async def adelete_sandbox(
+ self,
+ *,
+ container: Union[ContainerHandle, str],
+ api_key: str | None = None,
+ api_base: str | None = None,
+ client: AsyncHTTPHandler | None = None,
+ **kwargs,
+ ) -> bool:
+ handle = self._as_handle(container, api_base=api_base)
+ base = str(handle._hidden_params.get("api_base") or self._api_base(api_base))
+ key = self._api_key(api_key=api_key, handle=handle)
+ try:
+ response = cast(
+ httpx.Response,
+ await self._http(client).delete(
+ url=f"{base}/sandboxes/{handle.id}",
+ headers=self._lifecycle_headers(key),
+ ),
+ )
+ except httpx.HTTPStatusError as e:
+ if e.response.status_code == 404:
+ return False
+ raise
+ return 200 <= response.status_code < 300
+
+ async def _ensure_handle(
+ self,
+ *,
+ container: Union[ContainerHandle, str],
+ api_key: str | None,
+ api_base: str | None,
+ use_server_proxy: bool,
+ ready_timeout: float,
+ poll_interval: float,
+ client: AsyncHTTPHandler | None,
+ ) -> ContainerHandle:
+ handle = self._as_handle(container, api_base=api_base)
+ if handle._hidden_params.get("execd_endpoint"):
+ return handle
+
+ base = str(handle._hidden_params.get("api_base") or self._api_base(api_base))
+ key = self._api_key(api_key=api_key, handle=handle)
+ resolved_use_server_proxy = bool(
+ handle._hidden_params.get("use_server_proxy", use_server_proxy)
+ )
+ endpoint, endpoint_headers = await self._wait_for_execd_endpoint(
+ sandbox_id=handle.id,
+ api_base=base,
+ headers=self._lifecycle_headers(key),
+ use_server_proxy=resolved_use_server_proxy,
+ client=client,
+ ready_timeout=ready_timeout,
+ poll_interval=poll_interval,
+ )
+ handle.domain = base
+ handle._hidden_params = {
+ **handle._hidden_params,
+ "api_base": base,
+ "api_key": key,
+ "execd_endpoint": endpoint,
+ "execd_headers": endpoint_headers,
+ "use_server_proxy": resolved_use_server_proxy,
+ }
+ return handle
+
+ async def _wait_until_running(
+ self,
+ *,
+ sandbox_id: str,
+ api_base: str,
+ headers: dict[str, str],
+ client: AsyncHTTPHandler | None,
+ ready_timeout: float,
+ poll_interval: float,
+ ) -> None:
+ deadline = time.monotonic() + ready_timeout
+ while True:
+ response = cast(
+ httpx.Response,
+ await self._http(client).get(
+ url=f"{api_base}/sandboxes/{sandbox_id}",
+ headers=headers,
+ ),
+ )
+ data = response.json()
+ state = self._sandbox_state(data)
+ if state == "Running":
+ return
+ if state in {"Failed", "Stopping", "Terminated"}:
+ raise ValueError(f"OpenSandbox sandbox {sandbox_id} entered {state}")
+ if time.monotonic() >= deadline:
+ raise TimeoutError(
+ f"OpenSandbox sandbox {sandbox_id} was not Running within "
+ f"{ready_timeout} seconds"
+ )
+ await asyncio.sleep(poll_interval)
+
+ async def _wait_for_execd_endpoint(
+ self,
+ *,
+ sandbox_id: str,
+ api_base: str,
+ headers: dict[str, str],
+ use_server_proxy: bool,
+ client: AsyncHTTPHandler | None,
+ ready_timeout: float,
+ poll_interval: float,
+ ) -> tuple[str, dict[str, str]]:
+ deadline = time.monotonic() + ready_timeout
+ last_error: Exception | None = None
+ while True:
+ try:
+ return await self._get_execd_endpoint(
+ sandbox_id=sandbox_id,
+ api_base=api_base,
+ headers=headers,
+ use_server_proxy=use_server_proxy,
+ client=client,
+ )
+ except httpx.HTTPStatusError as e:
+ if e.response.status_code != 404:
+ raise
+ last_error = e
+ except ValueError as e:
+ last_error = e
+
+ if time.monotonic() >= deadline:
+ raise TimeoutError(
+ f"OpenSandbox execd endpoint for {sandbox_id} was not ready within "
+ f"{ready_timeout} seconds"
+ ) from last_error
+ await asyncio.sleep(poll_interval)
+
+ async def _get_execd_endpoint(
+ self,
+ *,
+ sandbox_id: str,
+ api_base: str,
+ headers: dict[str, str],
+ use_server_proxy: bool,
+ client: AsyncHTTPHandler | None,
+ ) -> tuple[str, dict[str, str]]:
+ response = cast(
+ httpx.Response,
+ await self._http(client).get(
+ url=f"{api_base}/sandboxes/{sandbox_id}/endpoints/{OPEN_SANDBOX_EXECD_PORT}",
+ headers=headers,
+ params={"use_server_proxy": use_server_proxy},
+ ),
+ )
+ data = response.json()
+ endpoint = data.get("endpoint")
+ if not endpoint:
+ raise ValueError(
+ f"OpenSandbox did not return an execd endpoint for {sandbox_id}"
+ )
+ return str(endpoint), self._as_str_dict(data.get("headers"))
+
+ async def _post_code(
+ self,
+ *,
+ url: str,
+ headers: dict[str, str],
+ body: dict[str, object],
+ client: AsyncHTTPHandler | None,
+ ) -> list[str]:
+ timeout = httpx.Timeout(connect=30.0, read=None, write=30.0, pool=None)
+ response = cast(
+ httpx.Response,
+ await self._http(client).post(
+ url=url,
+ headers=headers,
+ timeout=timeout,
+ json=body,
+ stream=True,
+ ),
+ )
+ return await self._read_capped_lines(response)
+
+ def _api_key(self, *, api_key: str | None, handle: ContainerHandle) -> str:
+ if api_key is not None:
+ return api_key
+ if "api_key" in handle._hidden_params:
+ return str(handle._hidden_params["api_key"])
+ return self.validate_environment()
+
+ @staticmethod
+ def _create_body(
+ *,
+ template: str | None,
+ timeout: int | None,
+ allow_internet_access: bool | None,
+ metadata: dict[str, str] | None,
+ env_vars: dict[str, str] | None,
+ resource_limits: dict[str, str] | None,
+ resource_requests: dict[str, str] | None,
+ entrypoint: list[str] | tuple[str, ...] | None,
+ network_policy: dict[str, object] | None,
+ secure_access: bool,
+ ) -> dict[str, object]:
+ body: dict[str, object] = {
+ "image": {"uri": template or OPEN_SANDBOX_DEFAULT_TEMPLATE},
+ "entrypoint": list(entrypoint or OPEN_SANDBOX_DEFAULT_ENTRYPOINT),
+ "timeout": timeout if timeout is not None else DEFAULT_SANDBOX_TIMEOUT,
+ "resourceLimits": resource_limits
+ or OpenSandboxSandboxConfig._default_resource_limits(),
+ }
+ if metadata:
+ body["metadata"] = metadata
+ if env_vars:
+ body["env"] = env_vars
+ if resource_requests:
+ body["resourceRequests"] = resource_requests
+ if network_policy is not None:
+ body["networkPolicy"] = network_policy
+ elif allow_internet_access is not True:
+ body["networkPolicy"] = {"defaultAction": "deny", "egress": []}
+ if secure_access:
+ body["secureAccess"] = True
+ return body
+
+ @staticmethod
+ def _default_resource_limits() -> dict[str, str]:
+ return {
+ "cpu": OPEN_SANDBOX_DEFAULT_CPU_LIMIT,
+ "memory": OPEN_SANDBOX_DEFAULT_MEMORY_LIMIT,
+ }
+
+ @staticmethod
+ def _sandbox_state(data: object) -> str | None:
+ if not isinstance(data, dict):
+ return None
+ status = data.get("status")
+ if not isinstance(status, dict):
+ return None
+ state = status.get("state")
+ return str(state) if state is not None else None
+
+ @staticmethod
+ def _as_str_dict(value: object) -> dict[str, str]:
+ if not isinstance(value, dict):
+ return {}
+ return {str(k): str(v) for k, v in value.items()}
+
+ @staticmethod
+ def _api_base(api_base: str | None) -> str:
+ base = api_base or get_secret_str(OPEN_SANDBOX_API_BASE_ENV_VAR)
+ if not base:
+ raise ValueError(
+ "OpenSandbox api_base is required. Pass api_base or set "
+ f"{OPEN_SANDBOX_API_BASE_ENV_VAR}."
+ )
+ return str(base).rstrip("/")
+
+ @staticmethod
+ def _lifecycle_headers(api_key: str) -> dict[str, str]:
+ headers = {"Content-Type": "application/json"}
+ if api_key:
+ headers["OPEN-SANDBOX-API-KEY"] = api_key
+ return headers
+
+ @staticmethod
+ def _endpoint_base_url(endpoint: str, api_base: str) -> str:
+ normalized_endpoint = endpoint.rstrip("/")
+ if normalized_endpoint.startswith(("http://", "https://")):
+ return normalized_endpoint
+ protocol = api_base.split("://", 1)[0] if "://" in api_base else "http"
+ return f"{protocol}://{normalized_endpoint}"
+
+ @staticmethod
+ def _as_handle(
+ container: Union[ContainerHandle, str], *, api_base: str | None
+ ) -> ContainerHandle:
+ if isinstance(container, ContainerHandle):
+ return container
+ handle = ContainerHandle(
+ id=str(container),
+ provider="opensandbox",
+ domain=OpenSandboxSandboxConfig._api_base(api_base),
+ )
+ handle._hidden_params = {}
+ return handle
+
+ @staticmethod
+ def _parse_lines(lines: list[str]) -> CodeExecutionResult:
+ messages = tuple(
+ event
+ for line in lines
+ if (event := OpenSandboxSandboxConfig._parse_sse_line(line)) is not None
+ )
+
+ def of_type(message_type: str):
+ return (m for m in messages if m.get("type") == message_type)
+
+ error = next(
+ (OpenSandboxSandboxConfig._normalize_error(m) for m in of_type("error")),
+ None,
+ )
+ execution_count = next(
+ (
+ OpenSandboxSandboxConfig._as_int(m.get("execution_count"))
+ for m in of_type("execution_count")
+ if OpenSandboxSandboxConfig._as_int(m.get("execution_count"))
+ is not None
+ ),
+ None,
+ )
+
+ return CodeExecutionResult(
+ stdout="".join(str(m.get("text", "")) for m in of_type("stdout")),
+ stderr="".join(str(m.get("text", "")) for m in of_type("stderr")),
+ results=[
+ OpenSandboxSandboxConfig._normalize_result(m) for m in of_type("result")
+ ],
+ error=error,
+ execution_count=execution_count,
+ )
+
+ @staticmethod
+ def _parse_sse_line(line: str) -> dict[str, object] | None:
+ stripped = line.strip()
+ if not stripped or stripped.startswith(
+ (
+ ":",
+ "event:",
+ "id:",
+ "retry:",
+ )
+ ):
+ return None
+ data = stripped[5:].strip() if stripped.startswith("data:") else stripped
+ if not data:
+ return None
+ try:
+ parsed = json.loads(data)
+ except json.JSONDecodeError:
+ return None
+ if not isinstance(parsed, dict):
+ return None
+ if "type" not in parsed and "code" in parsed and "message" in parsed:
+ return {
+ "type": "error",
+ "error": {
+ "ename": str(parsed["code"]),
+ "evalue": str(parsed["message"]),
+ "traceback": [],
+ },
+ }
+ return parsed
+
+ @staticmethod
+ def _normalize_result(message: dict[str, object]) -> dict[str, object]:
+ results = message.get("results")
+ if isinstance(results, dict):
+ return {str(k): v for k, v in results.items()}
+ return {
+ str(k): v
+ for k, v in message.items()
+ if k not in {"type", "timestamp", "execution_count"}
+ }
+
+ @staticmethod
+ def _normalize_error(message: dict[str, object]) -> dict[str, object]:
+ raw_error = message.get("error")
+ if isinstance(raw_error, dict):
+ name = OpenSandboxSandboxConfig._first_non_none_value(
+ raw_error, "ename", "name", default=""
+ )
+ value = OpenSandboxSandboxConfig._first_non_none_value(
+ raw_error, "evalue", "value", default=""
+ )
+ traceback = OpenSandboxSandboxConfig._first_non_none_value(
+ raw_error, "traceback", default=[]
+ )
+ return {
+ "name": name,
+ "value": value,
+ "traceback": traceback,
+ }
+ return {
+ "name": OpenSandboxSandboxConfig._first_non_none_value(
+ message, "name", default=""
+ ),
+ "value": OpenSandboxSandboxConfig._first_non_none_value(
+ message, "value", "text", default=""
+ ),
+ "traceback": OpenSandboxSandboxConfig._first_non_none_value(
+ message, "traceback", default=[]
+ ),
+ }
+
+ @staticmethod
+ def _as_int(value: object) -> int | None:
+ if isinstance(value, int):
+ return value
+ if isinstance(value, str):
+ try:
+ return int(value)
+ except ValueError:
+ return None
+ return None
+
+ @staticmethod
+ def _first_non_none_value(
+ values: dict[str, object], *keys: str, default: object
+ ) -> object:
+ return next(
+ (values[key] for key in keys if key in values and values[key] is not None),
+ default,
+ )
diff --git a/litellm/llms/parallel_ai/search/transformation.py b/litellm/llms/parallel_ai/search/transformation.py
index 85602bf1d86..35a0d84df40 100644
--- a/litellm/llms/parallel_ai/search/transformation.py
+++ b/litellm/llms/parallel_ai/search/transformation.py
@@ -67,10 +67,12 @@ class ParallelAISearchConfig(BaseSearchConfig):
api_base: Optional[str] = None,
**kwargs,
) -> Dict:
- api_key = (
- api_key
- or get_secret_str("PARALLEL_AI_API_KEY")
- or get_secret_str("PARALLEL_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("PARALLEL_AI_API_KEY", "PARALLEL_API_KEY"),
+ base_env_var="PARALLEL_AI_API_BASE",
+ default_api_base=self.PARALLEL_AI_API_BASE,
)
if not api_key:
raise ValueError(
diff --git a/litellm/llms/perplexity/cost_calculator.py b/litellm/llms/perplexity/cost_calculator.py
index bf055f91aa0..ec7ec397ea6 100644
--- a/litellm/llms/perplexity/cost_calculator.py
+++ b/litellm/llms/perplexity/cost_calculator.py
@@ -98,10 +98,11 @@ def cost_per_token(model: str, usage: Usage) -> Tuple[float, float]:
if num_search_queries > 0 and search_cost_value is not None:
# Handle both dict and float formats
if isinstance(search_cost_value, dict):
- # Use the "low" size as default - tests expect 0.005 / 1000
- search_cost_per_query = (
- _safe_float_cast(search_cost_value.get("search_context_size_low", 0))
- / 1000
+ # search_context_cost_per_query stores the per-request price in USD
+ # (e.g. sonar low = $0.005/request). Use it directly, matching the
+ # gemini cost calculator which reads the same field per request.
+ search_cost_per_query = _safe_float_cast(
+ search_cost_value.get("search_context_size_low", 0)
)
else:
search_cost_per_query = _safe_float_cast(search_cost_value)
diff --git a/litellm/llms/perplexity/search/transformation.py b/litellm/llms/perplexity/search/transformation.py
index ea96f87957c..55de52c5384 100644
--- a/litellm/llms/perplexity/search/transformation.py
+++ b/litellm/llms/perplexity/search/transformation.py
@@ -50,7 +50,13 @@ class PerplexitySearchConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("PERPLEXITYAI_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("PERPLEXITYAI_API_KEY",),
+ base_env_var="PERPLEXITY_API_BASE",
+ default_api_base=self.PERPLEXITY_API_BASE,
+ )
if not api_key:
raise ValueError(
"PERPLEXITYAI_API_KEY is not set. Set `PERPLEXITYAI_API_KEY` environment variable."
diff --git a/litellm/llms/searchapi/search/transformation.py b/litellm/llms/searchapi/search/transformation.py
index c04e1377f9c..ae8413684cc 100644
--- a/litellm/llms/searchapi/search/transformation.py
+++ b/litellm/llms/searchapi/search/transformation.py
@@ -74,7 +74,13 @@ class SearchAPIConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("SEARCHAPI_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("SEARCHAPI_API_KEY",),
+ base_env_var="SEARCHAPI_API_BASE",
+ default_api_base=self.SEARCHAPI_API_BASE,
+ )
if not api_key:
raise ValueError(
@@ -114,6 +120,7 @@ class SearchAPIConfig(BaseSearchConfig):
query: Union[str, List[str]],
optional_params: dict,
api_key: Optional[str] = None,
+ api_base: str | None = None,
search_engine_id: Optional[str] = None,
**kwargs,
) -> Dict:
@@ -137,8 +144,16 @@ class SearchAPIConfig(BaseSearchConfig):
if isinstance(query, list):
query = " ".join(query)
- # Get API key from parameter or environment
- api_key = api_key or get_secret_str("SEARCHAPI_API_KEY")
+ # Get API key from parameter or environment. The key is sent as a query
+ # param to api_base, so resolve it host-aware to avoid leaking a
+ # server-managed key to a caller-supplied host.
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("SEARCHAPI_API_KEY",),
+ base_env_var="SEARCHAPI_API_BASE",
+ default_api_base=self.SEARCHAPI_API_BASE,
+ )
if not api_key:
raise ValueError(
"SEARCHAPI_API_KEY is not set. Set `SEARCHAPI_API_KEY` environment variable."
diff --git a/litellm/llms/searxng/search/transformation.py b/litellm/llms/searxng/search/transformation.py
index ee6f3895721..ff68be5709e 100644
--- a/litellm/llms/searxng/search/transformation.py
+++ b/litellm/llms/searxng/search/transformation.py
@@ -61,7 +61,13 @@ class SearXNGSearchConfig(BaseSearchConfig):
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")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("SEARXNG_API_KEY",),
+ base_env_var="SEARXNG_API_BASE",
+ default_api_base=None,
+ )
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
headers["Content-Type"] = "application/json"
diff --git a/litellm/llms/serper/search/transformation.py b/litellm/llms/serper/search/transformation.py
index 0daccbe652b..dd43f2d2dc9 100644
--- a/litellm/llms/serper/search/transformation.py
+++ b/litellm/llms/serper/search/transformation.py
@@ -55,7 +55,13 @@ class SerperSearchConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("SERPER_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("SERPER_API_KEY",),
+ base_env_var="SERPER_API_BASE",
+ default_api_base=self.SERPER_API_BASE,
+ )
if not api_key:
raise ValueError(
"SERPER_API_KEY is not set. Set `SERPER_API_KEY` environment variable."
diff --git a/litellm/llms/tavily/search/transformation.py b/litellm/llms/tavily/search/transformation.py
index ec96db96f36..647cfb5fa84 100644
--- a/litellm/llms/tavily/search/transformation.py
+++ b/litellm/llms/tavily/search/transformation.py
@@ -64,7 +64,13 @@ class TavilySearchConfig(BaseSearchConfig):
"""
Validate environment and return headers.
"""
- api_key = api_key or get_secret_str("TAVILY_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("TAVILY_API_KEY",),
+ base_env_var="TAVILY_API_BASE",
+ default_api_base=self.TAVILY_API_BASE,
+ )
if not api_key:
raise ValueError(
"TAVILY_API_KEY is not set. Set `TAVILY_API_KEY` environment variable."
diff --git a/litellm/llms/tinyfish/search/transformation.py b/litellm/llms/tinyfish/search/transformation.py
index c4949380e3a..b92f7ca1aff 100644
--- a/litellm/llms/tinyfish/search/transformation.py
+++ b/litellm/llms/tinyfish/search/transformation.py
@@ -67,7 +67,13 @@ class TinyfishSearchConfig(BaseSearchConfig):
api_base: str | None = None,
**kwargs: object,
) -> dict[str, str]:
- resolved_key = api_key or get_secret_str("TINYFISH_API_KEY")
+ resolved_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("TINYFISH_API_KEY",),
+ base_env_var="TINYFISH_API_BASE",
+ default_api_base=self.TINYFISH_API_BASE,
+ )
if not resolved_key:
raise ValueError(
"TINYFISH_API_KEY is not set. Set `TINYFISH_API_KEY` environment variable."
diff --git a/litellm/llms/vertex_ai/realtime/transformation.py b/litellm/llms/vertex_ai/realtime/transformation.py
index d6441db7856..1fe9f15c9f0 100644
--- a/litellm/llms/vertex_ai/realtime/transformation.py
+++ b/litellm/llms/vertex_ai/realtime/transformation.py
@@ -32,6 +32,9 @@ class VertexAIRealtimeConfig(GeminiRealtimeConfig):
self._project = project
self._location = location
+ def _include_function_response_id(self) -> bool:
+ return False
+
# ------------------------------------------------------------------
# URL
# ------------------------------------------------------------------
diff --git a/litellm/llms/watsonx/embed/transformation.py b/litellm/llms/watsonx/embed/transformation.py
index 930212e3ef3..ae873d63fe9 100644
--- a/litellm/llms/watsonx/embed/transformation.py
+++ b/litellm/llms/watsonx/embed/transformation.py
@@ -43,8 +43,19 @@ class IBMWatsonXEmbeddingConfig(IBMWatsonXMixin, BaseEmbeddingConfig):
api_params=watsonx_api_params,
)
+ if isinstance(input, str):
+ inputs: list[str] = [input]
+ elif isinstance(input, list):
+ if len(input) > 0 and isinstance(input[0], (list, int)):
+ raise ValueError(
+ "WatsonX embeddings require a string or list of strings"
+ )
+ inputs = input
+ else:
+ inputs = [input]
+
return {
- "inputs": input,
+ "inputs": inputs,
"parameters": optional_params,
**watsonx_auth_payload,
}
diff --git a/litellm/llms/you_com/search/transformation.py b/litellm/llms/you_com/search/transformation.py
index 3c94b991735..0c7916e4c05 100644
--- a/litellm/llms/you_com/search/transformation.py
+++ b/litellm/llms/you_com/search/transformation.py
@@ -64,7 +64,13 @@ class YouComSearchConfig(BaseSearchConfig):
endpoint with the `X-API-Key` header. Otherwise fall through to the
keyless free tier; no auth header is required.
"""
- api_key = api_key or get_secret_str("YOUCOM_API_KEY")
+ api_key = self.resolve_server_api_key(
+ caller_api_key=api_key,
+ caller_api_base=api_base,
+ key_env_vars=("YOUCOM_API_KEY",),
+ base_env_var="YOUCOM_API_BASE",
+ default_api_base=self.YOU_COM_API_BASE,
+ )
headers["Content-Type"] = "application/json"
# Pin Accept-Encoding to identity: the keyless `api.you.com/v1/agents/search`
# endpoint advertises gzip content-encoding but returns body bytes the
diff --git a/litellm/main.py b/litellm/main.py
index 5becb807092..cabb070dbb1 100644
--- a/litellm/main.py
+++ b/litellm/main.py
@@ -81,11 +81,17 @@ from litellm.constants import (
from litellm.exceptions import LiteLLMUnknownProvider
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.asyncify import run_async_function
+from litellm.litellm_core_utils.chat_completion_agentic_loop import (
+ maybe_run_chat_completion_agentic_loop,
+)
from litellm.litellm_core_utils.audio_utils.utils import (
calculate_request_duration,
get_audio_file_for_health_check,
)
from litellm.litellm_core_utils.completion_timeout import CompletionTimeout
+from litellm.litellm_core_utils.request_timeout_resolver import (
+ get_configured_request_timeout,
+)
from litellm.litellm_core_utils.get_litellm_params import OPTIONAL_KWARGS_KEYS
from litellm.litellm_core_utils.dd_tracing import tracer
from litellm.litellm_core_utils.get_provider_specific_headers import (
@@ -118,6 +124,10 @@ from litellm.llms.vertex_ai.common_utils import (
)
from litellm.realtime_api.main import _realtime_health_check
from litellm.secret_managers.main import get_secret_bool, get_secret_str
+from litellm.types.completion import (
+ _CompletionDispatchContext,
+ _CompletionDispatchResult,
+)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import (
CustomPricingLiteLLMParams,
@@ -650,6 +660,39 @@ async def acompletion(
response_object=response,
model_response_object=litellm.ModelResponse(),
)
+ # Provider-agnostic dispatch point for the chat-completions agentic loop
+ # (code-interpreter interception, etc). Chat routing forks per provider
+ # before this (OpenAI goes through the OpenAI SDK in openai.py, others
+ # through the shared httpx handler), so a dispatch inside any single
+ # provider handler would miss the others. Here is where every fork
+ # reconverges, so the loop runs once for all providers. Responses needs
+ # no equivalent: every provider already funnels through one shared
+ # handler where the loop is dispatched.
+ if isinstance(response, litellm.ModelResponse):
+ looped = await maybe_run_chat_completion_agentic_loop(
+ response=response,
+ model=model,
+ messages=messages,
+ optional_params={
+ k: v
+ for k, v in completion_kwargs.items()
+ if v is not None
+ and k
+ not in (
+ "model",
+ "messages",
+ "stream",
+ "acompletion",
+ "deployment_id",
+ )
+ },
+ kwargs=kwargs,
+ logging_obj=kwargs.get("litellm_logging_obj"),
+ custom_llm_provider=custom_llm_provider,
+ stream=bool(stream),
+ )
+ if looped is not None:
+ response = looped
if isinstance(response, CustomStreamWrapper):
response.set_logging_event_loop(
loop=loop
@@ -1084,6 +1127,3825 @@ def _build_custom_pricing_entry(
return entry
+def _complete_azure(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ _azure_detection_model = ctx._azure_detection_model
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ api_version = ctx.api_version
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ extra_headers = ctx.extra_headers
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ max_retries = ctx.max_retries
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ timeout = ctx.timeout
+
+ dynamic_params = False
+ if client is not None and (
+ isinstance(client, openai.AzureOpenAI)
+ or isinstance(client, openai.AsyncAzureOpenAI)
+ ):
+ dynamic_params = _check_dynamic_azure_params(
+ azure_client_params={"api_version": api_version},
+ azure_client=client,
+ )
+
+ api_type = get_secret("AZURE_API_TYPE") or "azure"
+
+ api_base = api_base or litellm.api_base or get_secret("AZURE_API_BASE")
+
+ api_version = (
+ api_version
+ or litellm.api_version
+ or get_secret_str("AZURE_API_VERSION")
+ or litellm.AZURE_DEFAULT_API_VERSION
+ )
+
+ api_key = (
+ api_key
+ or litellm.api_key
+ or litellm.azure_key
+ or get_secret_str("AZURE_OPENAI_API_KEY")
+ or get_secret_str("AZURE_API_KEY")
+ )
+
+ azure_ad_token = optional_params.get("extra_body", {}).pop(
+ "azure_ad_token", None
+ ) or get_secret_str("AZURE_AD_TOKEN")
+
+ azure_ad_token_provider = litellm_params.get("azure_ad_token_provider", None)
+
+ headers = headers or litellm.headers
+
+ if extra_headers is not None:
+ optional_params["extra_headers"] = extra_headers
+ if max_retries is not None:
+ optional_params["max_retries"] = max_retries
+
+ if litellm.AzureOpenAIO1Config().is_o_series_model(model=_azure_detection_model):
+ ## LOAD CONFIG - if set
+ config = litellm.AzureOpenAIO1Config.get_config()
+ for k, v in config.items():
+ if (
+ k not in optional_params
+ ): # completion(top_k=3) > azure_config(top_k=3) <- allows for dynamic variables to be passed in
+ optional_params[k] = v
+
+ response = azure_o1_chat_completions.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ api_key=api_key,
+ api_base=api_base,
+ api_version=api_version,
+ dynamic_params=dynamic_params,
+ azure_ad_token=azure_ad_token,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ logging_obj=logging,
+ acompletion=acompletion,
+ timeout=timeout, # type: ignore
+ client=client, # pass AsyncAzureOpenAI, AzureOpenAI client
+ custom_llm_provider=custom_llm_provider,
+ )
+ else:
+ ## LOAD CONFIG - if set
+ config = litellm.AzureOpenAIConfig.get_config()
+ for k, v in config.items():
+ if (
+ k not in optional_params
+ ): # completion(top_k=3) > azure_config(top_k=3) <- allows for dynamic variables to be passed in
+ optional_params[k] = v
+
+ ## COMPLETION CALL
+ response = azure_chat_completions.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ api_key=api_key,
+ api_base=api_base,
+ api_version=api_version,
+ api_type=api_type,
+ dynamic_params=dynamic_params,
+ azure_ad_token=azure_ad_token,
+ azure_ad_token_provider=azure_ad_token_provider,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ logging_obj=logging,
+ acompletion=acompletion,
+ timeout=timeout, # type: ignore
+ client=client, # pass AsyncAzureOpenAI, AzureOpenAI client
+ )
+
+ if optional_params.get("stream", False):
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=response,
+ additional_args={
+ "headers": headers,
+ "api_version": api_version,
+ "api_base": api_base,
+ },
+ )
+
+ return response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_azure_text(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ api_version = ctx.api_version
+ client = ctx.client
+ extra_headers = ctx.extra_headers
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ timeout = ctx.timeout
+
+ api_type = get_secret_str("AZURE_API_TYPE") or "azure"
+
+ api_base = api_base or litellm.api_base or get_secret_str("AZURE_API_BASE")
+
+ if api_base is None:
+ raise ValueError(
+ "api_base is required for Azure OpenAI LLM provider. Either set it dynamically or set the AZURE_API_BASE environment variable."
+ )
+
+ api_version = (
+ api_version or litellm.api_version or get_secret_str("AZURE_API_VERSION")
+ )
+
+ api_key = (
+ api_key
+ or litellm.api_key
+ or litellm.azure_key
+ or get_secret_str("AZURE_OPENAI_API_KEY")
+ or get_secret_str("AZURE_API_KEY")
+ )
+
+ azure_ad_token = optional_params.get("extra_body", {}).pop(
+ "azure_ad_token", None
+ ) or get_secret_str("AZURE_AD_TOKEN")
+
+ azure_ad_token_provider = litellm_params.get("azure_ad_token_provider", None)
+
+ headers = headers or litellm.headers
+
+ if extra_headers is not None:
+ optional_params["extra_headers"] = extra_headers
+
+ ## LOAD CONFIG - if set
+ config = litellm.AzureOpenAIConfig.get_config()
+ for k, v in config.items():
+ if (
+ k not in optional_params
+ ): # completion(top_k=3) > azure_config(top_k=3) <- allows for dynamic variables to be passed in
+ optional_params[k] = v
+
+ ## COMPLETION CALL
+ response = azure_text_completions.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ api_key=api_key,
+ api_base=api_base,
+ api_version=cast(str, api_version),
+ api_type=api_type,
+ azure_ad_token=azure_ad_token,
+ azure_ad_token_provider=azure_ad_token_provider,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ logging_obj=logging,
+ acompletion=acompletion,
+ timeout=timeout,
+ client=client, # pass AsyncAzureOpenAI, AzureOpenAI client
+ )
+
+ if optional_params.get("stream", False) or acompletion is True:
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=response,
+ additional_args={
+ "headers": headers,
+ "api_version": api_version,
+ "api_base": api_base,
+ },
+ )
+
+ return response
+
+
+def _complete_deepseek(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ try:
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+ except Exception as e:
+ ## LOGGING - log the original exception returned
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ return response
+
+
+def _complete_azure_ai(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ extra_headers = ctx.extra_headers
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ from litellm.llms.azure_ai.common_utils import AzureFoundryModelInfo
+
+ azure_ai_route = AzureFoundryModelInfo.get_azure_ai_route(model)
+
+ # Check if this is an agents route - model format: azure_ai/agents/
+ if azure_ai_route == "agents":
+ from litellm.llms.azure_ai.agents import AzureAIAgentsConfig
+
+ api_base = AzureFoundryModelInfo.get_api_base(api_base)
+ if api_base is None:
+ raise ValueError(
+ "Azure AI Agents requests require an api_base. "
+ "Set `api_base` or the AZURE_AI_API_BASE env var."
+ )
+ api_key = AzureFoundryModelInfo.get_api_key(api_key)
+
+ response = AzureAIAgentsConfig.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ api_key=api_key,
+ model_response=model_response,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout,
+ acompletion=acompletion,
+ stream=stream,
+ headers=headers or litellm.headers,
+ )
+
+ # Check if this is a Claude model - route to Azure Anthropic handler
+ elif "claude" in model.lower():
+ # Use Azure Anthropic handler for Claude models
+ api_base = AzureFoundryModelInfo.get_api_base(api_base)
+ if api_base is None:
+ raise ValueError(
+ "Azure Anthropic requests require an api_base. "
+ "Set `api_base` or the AZURE_AI_API_BASE env var."
+ )
+ api_key = AzureFoundryModelInfo.get_api_key(api_key)
+
+ # Ensure the URL ends with /v1/messages for Anthropic
+ if api_base:
+ api_base = api_base.rstrip("/")
+ if not api_base.endswith("/v1/messages"):
+ if "/anthropic" in api_base:
+ parts = api_base.split("/anthropic", 1)
+ api_base = parts[0] + "/anthropic"
+ else:
+ api_base = api_base + "/anthropic"
+ api_base = api_base + "/v1/messages"
+
+ response = azure_anthropic_chat_completions.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ acompletion=acompletion,
+ custom_prompt_dict=litellm.custom_prompt_dict,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ headers=headers,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ )
+ if optional_params.get("stream", False) or acompletion is True:
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=response,
+ )
+ response = response
+ else:
+ # Non-Claude models use standard Azure AI flow
+ api_base = AzureFoundryModelInfo.get_api_base(api_base)
+ # set API KEY
+ api_key = AzureFoundryModelInfo.get_api_key(api_key)
+
+ headers = headers or litellm.headers
+
+ if extra_headers is not None:
+ optional_params["extra_headers"] = extra_headers
+
+ ## FOR COHERE
+ if "command-r" in model: # make sure tool call in messages are str
+ messages = stringify_json_tool_call_content(messages=messages)
+
+ ## COMPLETION CALL
+ try:
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout, # type: ignore
+ client=client, # pass AsyncOpenAI, OpenAI client
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ )
+ except Exception as e:
+ ## LOGGING - log the original exception returned
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ if optional_params.get("stream", False):
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=response,
+ additional_args={"headers": headers},
+ )
+
+ return response
+
+
+def _complete_text_completion_openai(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ text_completion = ctx.text_completion
+ timeout = ctx.timeout
+
+ openai.api_type = "openai"
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret("OPENAI_BASE_URL")
+ or get_secret("OPENAI_API_BASE")
+ or "https://api.openai.com/v1"
+ )
+
+ openai.api_version = None
+ # set API KEY
+
+ api_key = (
+ api_key or litellm.api_key or litellm.openai_key or get_secret("OPENAI_API_KEY")
+ )
+
+ headers = headers or litellm.headers
+
+ ## LOAD CONFIG - if set
+ config = litellm.OpenAITextCompletionConfig.get_config()
+ for k, v in config.items():
+ if (
+ k not in optional_params
+ ): # completion(top_k=3) > openai_text_config(top_k=3) <- allows for dynamic variables to be passed in
+ optional_params[k] = v
+ if litellm.organization:
+ openai.organization = litellm.organization
+
+ ## COMPLETION CALL
+ _response = openai_text_completions.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ api_key=api_key,
+ custom_llm_provider=custom_llm_provider,
+ api_base=api_base,
+ acompletion=acompletion,
+ client=client, # pass AsyncOpenAI, OpenAI client
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ timeout=timeout, # type: ignore
+ )
+
+ if (
+ optional_params.get("stream", False) is False
+ and acompletion is False
+ and text_completion is False
+ ):
+ # convert to chat completion response
+ _response = (
+ litellm.OpenAITextCompletionConfig().convert_to_chat_model_response_object(
+ response_object=_response, model_response_object=model_response
+ )
+ )
+
+ if optional_params.get("stream", False) or acompletion is True:
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=_response,
+ additional_args={"headers": headers},
+ )
+ return _response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_fireworks_ai(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ try:
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+ except Exception as e:
+ ## LOGGING - log the original exception returned
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ return response
+
+
+def _complete_heroku(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ try:
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+ except Exception as e:
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ return response
+
+
+def _complete_ragflow(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ try:
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+ except Exception as e:
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ return response
+
+
+def _complete_xai(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ try:
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+ except Exception as e:
+ ## LOGGING - log the original exception returned
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ return response
+
+
+def _complete_groq(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
+ or litellm.api_base
+ or get_secret("GROQ_API_BASE")
+ or "https://api.groq.com/openai/v1"
+ )
+
+ # set API KEY
+ api_key = (
+ api_key
+ or litellm.api_key # for deepinfra/perplexity/anyscale/friendliai we check in get_llm_provider and pass in the api key from there
+ or litellm.groq_key
+ or get_secret("GROQ_API_KEY")
+ )
+
+ headers = headers or litellm.headers
+
+ ## LOAD CONFIG - if set
+ config = litellm.GroqChatConfig.get_config()
+ for k, v in config.items():
+ if (
+ k not in optional_params
+ ): # completion(top_k=3) > openai_config(top_k=3) <- allows for dynamic variables to be passed in
+ optional_params[k] = v
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider=custom_llm_provider,
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+
+
+def _complete_bedrock_mantle(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = api_base or litellm.api_base or get_secret("BEDROCK_MANTLE_API_BASE")
+ api_key = api_key or litellm.api_key or get_secret("BEDROCK_MANTLE_API_KEY")
+ headers = headers or litellm.headers
+ config = litellm.BedrockMantleChatConfig.get_config()
+ for k, v in config.items():
+ if k not in optional_params:
+ optional_params[k] = v
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider=custom_llm_provider,
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ client=client,
+ )
+
+
+def _complete_a2a(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ (
+ api_base,
+ api_key,
+ headers,
+ ) = litellm.A2AConfig.resolve_agent_config_from_registry(
+ model=model,
+ api_base=api_base,
+ api_key=api_key,
+ headers=headers,
+ optional_params=optional_params,
+ )
+
+ # Fall back to environment variables and defaults
+ api_base = api_base or litellm.api_base or get_secret_str("A2A_API_BASE")
+
+ if api_base is None:
+ raise Exception(
+ "api_base is required for A2A provider. "
+ "Either provide api_base parameter, set A2A_API_BASE environment variable, "
+ "or register the agent in the proxy with model='a2a/'."
+ )
+
+ headers = headers or litellm.headers
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider=custom_llm_provider,
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ client=client,
+ provider_config=provider_config,
+ )
+
+
+def _complete_gigachat(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.api_key
+ or litellm.gigachat_key
+ or get_secret("GIGACHAT_API_KEY")
+ or get_secret("GIGACHAT_CREDENTIALS")
+ )
+
+ headers = headers or litellm.headers or {}
+
+ ## COMPLETION CALL
+ try:
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+ except Exception as e:
+ ## LOGGING - log the original exception returned
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ return response
+
+
+def _complete_sap(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ headers = headers or litellm.headers
+ ## LOAD CONFIG - if set
+ config = litellm.GenAIHubOrchestrationConfig.get_config()
+ for k, v in config.items():
+ if (
+ k not in optional_params
+ ): # completion(top_k=3) > openai_config(top_k=3) <- allows for dynamic variables to be passed in
+ optional_params[k] = v
+
+ return sap_gen_ai_hub_chat_completions.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout, # type: ignore
+ shared_session=shared_session,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ api_base=api_base,
+ stream=stream,
+ )
+
+
+def _complete_aiohttp_openai(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ extra_headers = ctx.extra_headers
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
+ or litellm.api_base
+ or get_secret("OPENAI_BASE_URL")
+ or get_secret("OPENAI_API_BASE")
+ or "https://api.openai.com/v1"
+ )
+ # set API KEY
+ api_key = (
+ api_key
+ or litellm.api_key # for deepinfra/perplexity/anyscale/friendliai we check in get_llm_provider and pass in the api key from there
+ or litellm.openai_key
+ or get_secret("OPENAI_API_KEY")
+ )
+
+ headers = headers or litellm.headers
+
+ if extra_headers is not None:
+ optional_params["extra_headers"] = extra_headers
+ return base_llm_aiohttp_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ )
+
+
+def _complete_cometapi(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.cometapi_key
+ or get_secret_str("COMETAPI_KEY")
+ or litellm.api_key
+ )
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret_str("COMETAPI_API_BASE")
+ or "https://api.cometapi.com/v1"
+ )
+
+ ## COMPLETION CALL
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+
+ ## LOGGING
+ logging.post_call(input=messages, api_key=api_key, original_response=response)
+
+ return response
+
+
+def _complete_minimax(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = api_key or get_secret_str("MINIMAX_API_KEY") or litellm.api_key
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret_str("MINIMAX_API_BASE")
+ or "https://api.minimax.io/v1"
+ )
+
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ custom_llm_provider=custom_llm_provider,
+ model_response=model_response,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ optional_params=optional_params,
+ timeout=timeout,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ acompletion=acompletion,
+ stream=stream,
+ api_key=api_key,
+ headers=headers,
+ client=client,
+ provider_config=provider_config,
+ )
+ logging.post_call(input=messages, api_key=api_key, original_response=response)
+
+ return response
+
+
+def _complete_hosted_vllm(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = api_base or litellm.api_base or get_secret_str("HOSTED_VLLM_API_BASE")
+
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ custom_llm_provider=custom_llm_provider,
+ model_response=model_response,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ optional_params=optional_params,
+ timeout=timeout,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ acompletion=acompletion,
+ stream=stream,
+ api_key=api_key,
+ headers=headers,
+ client=client,
+ provider_config=provider_config,
+ )
+ logging.post_call(input=messages, api_key=api_key, original_response=response)
+
+ return response
+
+
+def _complete_custom_openai(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ custom_prompt_dict = ctx.custom_prompt_dict
+ extra_headers = ctx.extra_headers
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ metadata = ctx.metadata
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ organization = ctx.organization
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
+ or litellm.api_base
+ or get_secret("OPENAI_BASE_URL")
+ or get_secret("OPENAI_API_BASE")
+ or "https://api.openai.com/v1"
+ )
+ organization = (
+ organization
+ or litellm.organization
+ or get_secret("OPENAI_ORGANIZATION")
+ or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105
+ )
+ openai.organization = organization
+ # set API KEY
+ api_key = (
+ api_key
+ or litellm.api_key # for deepinfra/perplexity/anyscale/friendliai we check in get_llm_provider and pass in the api key from there
+ or litellm.openai_key
+ or get_secret("OPENAI_API_KEY")
+ )
+
+ headers = headers or litellm.headers
+
+ # Add GitHub Copilot headers (same as /responses endpoint does)
+ if custom_llm_provider == "github_copilot":
+ from litellm.llms.github_copilot.authenticator import Authenticator
+ from litellm.llms.github_copilot.common_utils import (
+ get_copilot_default_headers,
+ )
+
+ copilot_auth = Authenticator()
+ copilot_api_key = copilot_auth.get_api_key()
+ copilot_headers = get_copilot_default_headers(copilot_api_key)
+ if extra_headers:
+ copilot_headers.update(extra_headers)
+ extra_headers = copilot_headers
+
+ if extra_headers is not None:
+ optional_params["extra_headers"] = extra_headers
+
+ if (
+ litellm.enable_preview_features and metadata is not None
+ ): # [PREVIEW] allow metadata to be passed to OPENAI
+ openai_metadata = get_requester_metadata(metadata)
+ if openai_metadata is not None:
+ optional_params["metadata"] = openai_metadata
+
+ ## LOAD CONFIG - if set
+ config = litellm.OpenAIConfig.get_config()
+ for k, v in config.items():
+ if (
+ k not in optional_params
+ ): # completion(top_k=3) > openai_config(top_k=3) <- allows for dynamic variables to be passed in
+ optional_params[k] = v
+
+ ## COMPLETION CALL
+ use_base_llm_http_handler = get_secret_bool(
+ "EXPERIMENTAL_OPENAI_BASE_LLM_HTTP_HANDLER"
+ )
+
+ try:
+ if use_base_llm_http_handler:
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ custom_llm_provider=custom_llm_provider,
+ model_response=model_response,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ optional_params=optional_params,
+ timeout=timeout,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ acompletion=acompletion,
+ stream=stream,
+ api_key=api_key,
+ headers=headers,
+ client=client,
+ provider_config=provider_config,
+ )
+ else:
+ response = openai_chat_completions.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ timeout=timeout, # type: ignore
+ custom_prompt_dict=custom_prompt_dict,
+ client=client, # pass AsyncOpenAI, OpenAI client
+ organization=organization,
+ custom_llm_provider=custom_llm_provider,
+ shared_session=shared_session,
+ )
+ except Exception as e:
+ ## LOGGING - log the original exception returned
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ if optional_params.get("stream", False):
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=response,
+ additional_args={"headers": headers},
+ )
+
+ return response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_mistral(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = api_key or litellm.api_key or get_secret("MISTRAL_API_KEY")
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret("MISTRAL_API_BASE")
+ or "https://api.mistral.ai/v1"
+ )
+
+ return base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ custom_llm_provider=custom_llm_provider,
+ model_response=model_response,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ optional_params=optional_params,
+ timeout=timeout,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ acompletion=acompletion,
+ stream=stream,
+ api_key=api_key,
+ headers=headers,
+ client=client,
+ provider_config=provider_config,
+ )
+
+
+def _complete_replicate(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ custom_prompt_dict = ctx.custom_prompt_dict
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+
+ replicate_key = (
+ api_key
+ or litellm.replicate_key
+ or litellm.api_key
+ or get_secret("REPLICATE_API_KEY")
+ or get_secret("REPLICATE_API_TOKEN")
+ )
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret("REPLICATE_API_BASE")
+ or "https://api.replicate.com/v1"
+ )
+
+ custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
+
+ model_response = replicate_chat_completion( # type: ignore
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(), # for calculating input/output tokens
+ api_key=replicate_key,
+ logging_obj=logging,
+ custom_prompt_dict=custom_prompt_dict,
+ acompletion=acompletion,
+ headers=headers,
+ )
+
+ if optional_params.get("stream", False) is True:
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=replicate_key,
+ original_response=model_response,
+ )
+
+ return model_response
+
+
+def _complete_anthropic_text(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ custom_prompt_dict = ctx.custom_prompt_dict
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.anthropic_key
+ or litellm.api_key
+ or os.environ.get("ANTHROPIC_API_KEY")
+ )
+ custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
+ api_base = cast(
+ Optional[str],
+ api_base
+ or litellm.api_base
+ or get_secret("ANTHROPIC_API_BASE")
+ or get_secret("ANTHROPIC_BASE_URL")
+ or "https://api.anthropic.com/v1/complete",
+ )
+
+ # Check if we should disable automatic URL suffix appending
+ disable_url_suffix = get_secret_bool("LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX")
+ if (
+ api_base is not None
+ and not disable_url_suffix
+ and not api_base.endswith("/v1/complete")
+ ):
+ api_base += "/v1/complete"
+ elif disable_url_suffix:
+ verbose_logger.debug(
+ "LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX is set, skipping /v1/complete suffix"
+ )
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="anthropic_text",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ )
+
+
+def _complete_anthropic(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ custom_prompt_dict = ctx.custom_prompt_dict
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.anthropic_key
+ or litellm.api_key
+ or os.environ.get("ANTHROPIC_API_KEY")
+ )
+ custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
+ # call /messages
+ # default route for all anthropic models
+ api_base = cast(
+ Optional[str],
+ api_base
+ or litellm.api_base
+ or get_secret("ANTHROPIC_API_BASE")
+ or get_secret("ANTHROPIC_BASE_URL")
+ or "https://api.anthropic.com/v1/messages",
+ )
+
+ # Check if we should disable automatic URL suffix appending
+ disable_url_suffix = get_secret_bool("LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX")
+ if (
+ api_base is not None
+ and not disable_url_suffix
+ and not api_base.endswith("/v1/messages")
+ ):
+ api_base += "/v1/messages"
+ elif disable_url_suffix:
+ verbose_logger.debug(
+ "LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX is set, skipping /v1/messages suffix"
+ )
+
+ response = anthropic_chat_completions.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ acompletion=acompletion,
+ custom_prompt_dict=litellm.custom_prompt_dict,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(), # for calculating input/output tokens
+ api_key=api_key,
+ logging_obj=logging,
+ headers=headers,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ )
+ if optional_params.get("stream", False) or acompletion is True:
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=response,
+ )
+ return response
+
+
+def _complete_nlp_cloud(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+
+ nlp_cloud_key = (
+ api_key
+ or litellm.nlp_cloud_key
+ or get_secret("NLP_CLOUD_API_KEY")
+ or litellm.api_key
+ )
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret("NLP_CLOUD_API_BASE")
+ or "https://api.nlpcloud.io/v1/gpu/"
+ )
+
+ response = nlp_cloud_chat_completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ api_key=nlp_cloud_key,
+ logging_obj=logging,
+ )
+
+ if "stream" in optional_params and optional_params["stream"] is True:
+ # don't try to access stream object,
+ response = CustomStreamWrapper(
+ response,
+ model,
+ custom_llm_provider="nlp_cloud",
+ logging_obj=logging,
+ )
+
+ if optional_params.get("stream", False) or acompletion is True:
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=response,
+ )
+
+ return response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_aleph_alpha(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+
+ aleph_alpha_key = (
+ api_key
+ or litellm.aleph_alpha_key
+ or get_secret("ALEPH_ALPHA_API_KEY")
+ or get_secret("ALEPHALPHA_API_KEY")
+ or litellm.api_key
+ )
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret("ALEPH_ALPHA_API_BASE")
+ or "https://api.aleph-alpha.com/complete"
+ )
+
+ model_response = aleph_alpha.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ default_max_tokens_to_sample=litellm.max_tokens,
+ api_key=aleph_alpha_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ )
+
+ if "stream" in optional_params and optional_params["stream"] is True:
+ # don't try to access stream object,
+ return CustomStreamWrapper(
+ model_response,
+ model,
+ custom_llm_provider="aleph_alpha",
+ logging_obj=logging,
+ )
+ return model_response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_cohere_chat(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ extra_headers = ctx.extra_headers
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ cohere_key = (
+ api_key
+ or litellm.cohere_key
+ or get_secret_str("COHERE_API_KEY")
+ or get_secret_str("CO_API_KEY")
+ or litellm.api_key
+ )
+
+ cohere_route = CohereModelInfo.get_cohere_route(model)
+ verbose_logger.debug(f"Cohere route: {cohere_route}")
+ # Set API base based on route
+ if cohere_route == "v2":
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret_str("COHERE_API_BASE")
+ or "https://api.cohere.com/v2/chat"
+ )
+ # Remove v2/ prefix from model name for the actual API call
+ if "v2/" in model:
+ model = model.replace("v2/", "")
+ else:
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret_str("COHERE_API_BASE")
+ or "https://api.cohere.ai/v1/chat"
+ )
+
+ headers = headers or litellm.headers or {}
+ if headers is None:
+ headers = {}
+
+ if extra_headers is not None:
+ headers.update(extra_headers)
+
+ verbose_logger.debug(f"Model: {model}, API Base: {api_base}")
+ verbose_logger.debug(f"Provider Config: {provider_config}")
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="cohere_chat",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=cohere_key,
+ provider_config=provider_config,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ )
+
+
+def _complete_maritalk(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ custom_prompt_dict = ctx.custom_prompt_dict
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+
+ maritalk_key = (
+ api_key
+ or litellm.maritalk_key
+ or get_secret("MARITALK_API_KEY")
+ or litellm.api_key
+ )
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret("MARITALK_API_BASE")
+ or "https://chat.maritaca.ai/api"
+ )
+
+ return openai_like_chat_completion.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ api_key=maritalk_key,
+ logging_obj=logging,
+ custom_llm_provider="maritalk",
+ custom_prompt_dict=custom_prompt_dict,
+ )
+
+
+def _complete_amazon_nova(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ custom_llm_provider = ctx.custom_llm_provider
+ custom_prompt_dict = ctx.custom_prompt_dict
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.amazon_nova_api_key
+ or get_secret_str("AMAZON_NOVA_API_KEY")
+ or litellm.api_key
+ )
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret_str("AMAZON_NOVA_API_BASE")
+ or "https://api.nova.amazon.com/v1"
+ )
+ return openai_like_chat_completion.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ timeout=timeout,
+ custom_llm_provider=custom_llm_provider,
+ custom_prompt_dict=custom_prompt_dict,
+ )
+
+
+def _complete_huggingface(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ huggingface_key = (
+ api_key
+ or litellm.huggingface_key
+ or os.environ.get("HF_TOKEN")
+ or os.environ.get("HUGGINGFACE_API_KEY")
+ or litellm.api_key
+ )
+ hf_headers = headers or litellm.headers
+ return base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=hf_headers,
+ model_response=model_response,
+ api_key=huggingface_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ )
+
+
+def _complete_oci(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ return base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ )
+
+
+def _complete_compactifai(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = api_key or get_secret_str("COMPACTIFAI_API_KEY") or litellm.api_key
+
+ api_base = api_base or "https://api.compactif.ai/v1"
+
+ ## COMPLETION CALL
+ return base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+
+
+def _complete_oobabooga(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ api_base = ctx.api_base
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+
+ model_response = oobabooga.completion(
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ api_base=api_base, # type: ignore
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ api_key=None,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ )
+ if "stream" in optional_params and optional_params["stream"] is True:
+ # don't try to access stream object,
+ return CustomStreamWrapper(
+ model_response,
+ model,
+ custom_llm_provider="oobabooga",
+ logging_obj=logging,
+ )
+ return model_response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_databricks(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ api_base # for databricks we check in get_llm_provider and pass in the api base from there
+ or litellm.api_base
+ or os.getenv("DATABRICKS_API_BASE")
+ )
+
+ # set API KEY
+ api_key = (
+ api_key
+ or litellm.api_key # for databricks we check in get_llm_provider and pass in the api key from there
+ or litellm.databricks_key
+ or get_secret("DATABRICKS_API_KEY")
+ )
+
+ headers = headers or litellm.headers
+
+ ## COMPLETION CALL
+ try:
+ response = base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ custom_llm_provider="databricks",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+ except Exception as e:
+ ## LOGGING - log the original exception returned
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ if optional_params.get("stream", False):
+ ## LOGGING
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=response,
+ additional_args={"headers": headers},
+ )
+
+ return response
+
+
+def _complete_datarobot(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ return base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=provider_config,
+ )
+
+
+def _complete_openrouter(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret_str("OPENROUTER_API_BASE")
+ or "https://openrouter.ai/api/v1"
+ )
+
+ api_key = (
+ api_key
+ or litellm.api_key
+ or litellm.openrouter_key
+ or get_secret_str("OPENROUTER_API_KEY")
+ or get_secret_str("OR_API_KEY")
+ )
+
+ openrouter_site_url = get_secret("OR_SITE_URL") or "https://litellm.ai"
+ openrouter_app_name = get_secret("OR_APP_NAME") or "liteLLM"
+
+ openrouter_headers = {
+ "HTTP-Referer": openrouter_site_url,
+ "X-Title": openrouter_app_name,
+ }
+
+ _headers = headers or litellm.headers
+ if _headers:
+ openrouter_headers.update(_headers)
+
+ headers = openrouter_headers
+
+ ## Load Config
+ config = litellm.OpenrouterConfig.get_config()
+ for k, v in config.items():
+ if k == "extra_body":
+ # we use openai 'extra_body' to pass openrouter specific params - transforms, route, models
+ if "extra_body" in optional_params:
+ optional_params[k].update(v)
+ else:
+ optional_params[k] = v
+ elif k not in optional_params:
+ optional_params[k] = v
+
+ ## COMPLETION CALL
+ response = base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="openrouter",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+ ## LOGGING
+ logging.post_call(
+ input=messages, api_key=openai.api_key, original_response=response
+ )
+
+ return response
+
+
+def _complete_vercel_ai_gateway(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret_str("VERCEL_AI_GATEWAY_API_BASE")
+ or "https://ai-gateway.vercel.sh/v1"
+ )
+
+ api_key = api_key or litellm.api_key or get_secret("VERCEL_AI_GATEWAY_API_KEY")
+
+ vercel_site_url = get_secret("VERCEL_SITE_URL") or "https://litellm.ai"
+ vercel_app_name = get_secret("VERCEL_APP_NAME") or "liteLLM"
+
+ vercel_headers = {
+ "http-referer": vercel_site_url,
+ "x-title": vercel_app_name,
+ }
+
+ _headers = headers or litellm.headers
+ if _headers:
+ vercel_headers.update(_headers)
+
+ headers = vercel_headers
+
+ ## Load Config
+ config = litellm.VercelAIGatewayConfig.get_config()
+ for k, v in config.items():
+ if k == "extra_body":
+ # we use openai 'extra_body' to pass vercel specific params - providerOptions
+ if "extra_body" in optional_params:
+ optional_params[k].update(v)
+ else:
+ optional_params[k] = v
+ elif k not in optional_params:
+ optional_params[k] = v
+
+ ## COMPLETION CALL
+ response = base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="vercel_ai_gateway",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+ ## LOGGING
+ logging.post_call(
+ input=messages, api_key=openai.api_key, original_response=response
+ )
+
+ return response
+
+
+def _complete_vertex_ai_beta(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ timeout = ctx.timeout
+
+ vertex_ai_project = (
+ optional_params.pop("vertex_project", None)
+ or optional_params.pop("vertex_ai_project", None)
+ or litellm.vertex_project
+ or get_secret("VERTEXAI_PROJECT")
+ )
+ vertex_ai_location = (
+ optional_params.pop("vertex_location", None)
+ or optional_params.pop("vertex_ai_location", None)
+ or litellm.vertex_location
+ or get_secret("VERTEXAI_LOCATION")
+ )
+ vertex_credentials = (
+ optional_params.pop("vertex_credentials", None)
+ or optional_params.pop("vertex_ai_credentials", None)
+ or get_secret("VERTEXAI_CREDENTIALS")
+ )
+
+ gemini_api_key = (
+ api_key
+ or get_api_key_from_env()
+ or get_secret("PALM_API_KEY") # older palm api key should also work
+ or litellm.api_key
+ )
+
+ api_base = api_base or litellm.api_base or get_secret("GEMINI_API_BASE")
+ new_params = safe_deep_copy(optional_params or {})
+ return vertex_chat_completion.completion( # type: ignore
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=new_params,
+ litellm_params=litellm_params, # type: ignore
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ vertex_location=vertex_ai_location,
+ vertex_project=vertex_ai_project,
+ vertex_credentials=vertex_credentials,
+ gemini_api_key=gemini_api_key,
+ logging_obj=logging,
+ acompletion=acompletion,
+ timeout=timeout,
+ custom_llm_provider=custom_llm_provider, # type: ignore
+ client=client,
+ api_base=api_base,
+ extra_headers=headers,
+ )
+
+
+def _complete_vertex_ai(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ custom_prompt_dict = ctx.custom_prompt_dict
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ vertex_ai_project = (
+ optional_params.pop("vertex_project", None)
+ or optional_params.pop("vertex_ai_project", None)
+ or litellm.vertex_project
+ or get_secret("VERTEXAI_PROJECT")
+ )
+ vertex_ai_location = (
+ optional_params.pop("vertex_location", None)
+ or optional_params.pop("vertex_ai_location", None)
+ or litellm.vertex_location
+ or get_secret("VERTEXAI_LOCATION")
+ )
+ vertex_credentials = (
+ optional_params.pop("vertex_credentials", None)
+ or optional_params.pop("vertex_ai_credentials", None)
+ or get_secret("VERTEXAI_CREDENTIALS")
+ )
+
+ api_base = api_base or litellm.api_base or get_secret("VERTEXAI_API_BASE")
+
+ new_params = safe_deep_copy(optional_params or {})
+ model_route = get_vertex_ai_model_route(model=model, litellm_params=litellm_params)
+
+ if model_route == VertexAIModelRoute.PARTNER_MODELS:
+ model_response = vertex_partner_models_chat_completion.completion(
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=new_params,
+ litellm_params=litellm_params, # type: ignore
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ api_base=api_base,
+ vertex_location=vertex_ai_location,
+ vertex_project=vertex_ai_project,
+ vertex_credentials=vertex_credentials,
+ logging_obj=logging,
+ acompletion=acompletion,
+ headers=headers,
+ custom_prompt_dict=custom_prompt_dict,
+ timeout=timeout,
+ client=client,
+ )
+ elif model_route == VertexAIModelRoute.GEMINI:
+ model_response = vertex_chat_completion.completion( # type: ignore
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=new_params,
+ litellm_params=litellm_params, # type: ignore
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ vertex_location=vertex_ai_location,
+ vertex_project=vertex_ai_project,
+ vertex_credentials=vertex_credentials,
+ gemini_api_key=None,
+ logging_obj=logging,
+ acompletion=acompletion,
+ timeout=timeout,
+ custom_llm_provider=custom_llm_provider, # type: ignore
+ client=client,
+ api_base=api_base,
+ extra_headers=headers,
+ )
+ elif model_route == VertexAIModelRoute.GEMMA:
+ # Vertex Gemma Models with custom prediction endpoint
+ model_response = vertex_gemma_chat_completion.completion(
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=new_params,
+ litellm_params=litellm_params, # type: ignore
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ api_base=api_base,
+ vertex_location=vertex_ai_location,
+ vertex_project=vertex_ai_project,
+ vertex_credentials=vertex_credentials,
+ logging_obj=logging,
+ acompletion=acompletion,
+ headers=headers,
+ custom_prompt_dict=custom_prompt_dict,
+ timeout=timeout,
+ client=client,
+ )
+ elif model_route == VertexAIModelRoute.MODEL_GARDEN:
+ # Vertex Model Garden - OpenAI compatible models
+ model_response = vertex_model_garden_chat_completion.completion(
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=new_params,
+ litellm_params=litellm_params, # type: ignore
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ api_base=api_base,
+ vertex_location=vertex_ai_location,
+ vertex_project=vertex_ai_project,
+ vertex_credentials=vertex_credentials,
+ logging_obj=logging,
+ acompletion=acompletion,
+ headers=headers,
+ custom_prompt_dict=custom_prompt_dict,
+ timeout=timeout,
+ client=client,
+ )
+ elif model_route == VertexAIModelRoute.AGENT_ENGINE:
+ # Vertex AI Agent Engine (Reasoning Engines)
+ from litellm.llms.vertex_ai.agent_engine.transformation import (
+ VertexAgentEngineConfig,
+ )
+
+ vertex_agent_engine_config = VertexAgentEngineConfig()
+
+ # Update litellm_params with vertex credentials
+ litellm_params["vertex_project"] = vertex_ai_project
+ litellm_params["vertex_location"] = vertex_ai_location
+ litellm_params["vertex_credentials"] = vertex_credentials
+
+ model_response = base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ model_response=model_response,
+ optional_params=new_params,
+ litellm_params=litellm_params, # type: ignore
+ encoding=_get_encoding(),
+ api_key=None,
+ api_base=api_base,
+ logging_obj=logging,
+ acompletion=acompletion,
+ timeout=timeout,
+ client=client,
+ custom_llm_provider="vertex_ai",
+ provider_config=vertex_agent_engine_config,
+ headers=headers or {},
+ )
+ else: # VertexAIModelRoute.NON_GEMINI
+ model_response = vertex_ai_non_gemini.completion(
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=new_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ vertex_location=vertex_ai_location,
+ vertex_project=vertex_ai_project,
+ vertex_credentials=vertex_credentials,
+ logging_obj=logging,
+ acompletion=acompletion,
+ )
+
+ if (
+ "stream" in optional_params
+ and optional_params["stream"] is True
+ and acompletion is False
+ ):
+ return CustomStreamWrapper(
+ model_response,
+ model,
+ custom_llm_provider="vertex_ai",
+ logging_obj=logging,
+ )
+ return model_response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_predibase(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ custom_prompt_dict = ctx.custom_prompt_dict
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ timeout = ctx.timeout
+
+ tenant_id = (
+ optional_params.pop("tenant_id", None)
+ or optional_params.pop("predibase_tenant_id", None)
+ or litellm.predibase_tenant_id
+ or get_secret("PREDIBASE_TENANT_ID")
+ )
+
+ if tenant_id is None:
+ raise ValueError(
+ "Missing Predibase Tenant ID - Required for making the request. Set dynamically (e.g. `completion(..tenant_id=)`) or in env - `PREDIBASE_TENANT_ID`."
+ )
+
+ api_base = (
+ api_base
+ or optional_params.pop("api_base", None)
+ or optional_params.pop("base_url", None)
+ or litellm.api_base
+ or get_secret("PREDIBASE_API_BASE")
+ )
+
+ api_key = (
+ api_key
+ or litellm.api_key
+ or litellm.predibase_key
+ or get_secret("PREDIBASE_API_KEY")
+ )
+
+ _model_response = predibase_chat_completions.completion(
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ acompletion=acompletion,
+ api_base=api_base,
+ custom_prompt_dict=custom_prompt_dict,
+ api_key=api_key,
+ tenant_id=tenant_id,
+ timeout=timeout,
+ )
+
+ if (
+ "stream" in optional_params
+ and optional_params["stream"] is True
+ and acompletion is False
+ ):
+ return _model_response
+ return _model_response
+
+
+def _complete_text_completion_codestral(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ custom_prompt_dict = ctx.custom_prompt_dict
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ api_base
+ or optional_params.pop("api_base", None)
+ or optional_params.pop("base_url", None)
+ or litellm.api_base
+ or "https://codestral.mistral.ai/v1/fim/completions"
+ )
+
+ api_key = api_key or litellm.api_key or get_secret("CODESTRAL_API_KEY")
+
+ text_completion_model_response = litellm.TextCompletionResponse(stream=stream)
+
+ _model_response = codestral_text_completions.completion( # type: ignore
+ model=model,
+ messages=messages,
+ model_response=text_completion_model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ acompletion=acompletion,
+ api_base=api_base,
+ custom_prompt_dict=custom_prompt_dict,
+ api_key=api_key,
+ timeout=timeout,
+ )
+
+ if (
+ "stream" in optional_params
+ and optional_params["stream"] is True
+ and acompletion is False
+ ):
+ return _model_response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+ return _model_response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_text_completion_inception(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ text_completion = ctx.text_completion
+ timeout = ctx.timeout
+
+ passed_api_base = (
+ api_base
+ or optional_params.pop("api_base", None)
+ or optional_params.pop("base_url", None)
+ )
+ api_base = (
+ passed_api_base
+ or get_secret_str("INCEPTION_API_BASE")
+ or "https://api.inceptionlabs.ai/v1"
+ )
+ # FIM is served at `/v1/fim/completions`; the OpenAI client appends
+ # `/completions`, so point it at the `/v1/fim` base.
+ api_base = api_base.rstrip("/")
+ if not api_base.endswith("/fim"):
+ api_base += "/fim"
+
+ # Don't forward the server-managed Inception key to a caller-supplied
+ # api_base; only resolve it for the default/server base, or when the
+ # caller passes their own key.
+ if passed_api_base is None or api_key:
+ api_key = (
+ api_key or litellm.inception_key or get_secret_str("INCEPTION_API_KEY")
+ )
+
+ _response = openai_text_completions.completion(
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ api_key=api_key, # type: ignore[arg-type]
+ custom_llm_provider="text-completion-inception",
+ api_base=api_base,
+ acompletion=acompletion,
+ client=client,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ timeout=timeout, # type: ignore
+ )
+
+ if (
+ optional_params.get("stream", False) is False
+ and acompletion is False
+ and text_completion is False
+ ):
+ _response = (
+ litellm.OpenAITextCompletionConfig().convert_to_chat_model_response_object(
+ response_object=_response, model_response_object=model_response
+ )
+ )
+
+ if optional_params.get("stream", False) or acompletion is True:
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=_response,
+ additional_args={"headers": headers},
+ )
+ return _response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_sagemaker_chat(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ custom_llm_provider=custom_llm_provider,
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+
+
+def _complete_sagemaker(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ custom_prompt_dict = ctx.custom_prompt_dict
+ hf_model_name = ctx.hf_model_name
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+
+ return sagemaker_llm.completion(
+ model=model,
+ messages=messages,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ custom_prompt_dict=custom_prompt_dict,
+ hf_model_name=hf_model_name,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ acompletion=acompletion,
+ )
+
+
+def _complete_bedrock(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_prompt_dict = ctx.custom_prompt_dict
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ provider_config = ctx.provider_config
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
+
+ if "aws_bedrock_client" in optional_params:
+ verbose_logger.warning(
+ "'aws_bedrock_client' is a deprecated param. Please move to another auth method - https://docs.litellm.ai/docs/providers/bedrock#boto3---authentication."
+ )
+ # Extract credentials for legacy boto3 client and pass thru to httpx
+ aws_bedrock_client = optional_params.pop("aws_bedrock_client")
+ creds = aws_bedrock_client._get_credentials().get_frozen_credentials()
+
+ if creds.access_key:
+ optional_params["aws_access_key_id"] = creds.access_key
+ if creds.secret_key:
+ optional_params["aws_secret_access_key"] = creds.secret_key
+ if creds.token:
+ optional_params["aws_session_token"] = creds.token
+ if (
+ "aws_region_name" not in optional_params
+ or optional_params["aws_region_name"] is None
+ ):
+ optional_params["aws_region_name"] = aws_bedrock_client.meta.region_name
+
+ bedrock_route = BedrockModelInfo.get_bedrock_route(model)
+ if bedrock_route == "claude_platform":
+ provider_config = ProviderConfigManager.get_provider_chat_config(
+ model=model,
+ provider=LlmProviders.BEDROCK,
+ )
+ model = BedrockModelInfo.get_claude_platform_model(model)
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="bedrock",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ client=client,
+ provider_config=provider_config,
+ )
+ elif bedrock_route == "converse":
+ model = model.replace("converse/", "")
+ response = bedrock_converse_chat_completion.completion(
+ model=model,
+ messages=messages,
+ custom_prompt_dict=custom_prompt_dict,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params, # type: ignore
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ extra_headers=headers, # Use merged headers instead of original extra_headers
+ timeout=timeout,
+ acompletion=acompletion,
+ client=client,
+ api_base=api_base,
+ api_key=api_key,
+ )
+ elif bedrock_route == "converse_like":
+ model = model.replace("converse_like/", "")
+ response = base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ custom_llm_provider="bedrock",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+ else:
+ response = base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ custom_llm_provider="bedrock",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ client=client,
+ )
+
+ return response
+
+
+def _complete_watsonx(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_prompt_dict = ctx.custom_prompt_dict
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ timeout = ctx.timeout
+
+ return watsonx_chat_completion.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ timeout=timeout, # type: ignore
+ custom_prompt_dict=custom_prompt_dict,
+ client=client, # pass AsyncOpenAI, OpenAI client
+ encoding=_get_encoding(),
+ custom_llm_provider="watsonx",
+ )
+
+
+def _complete_watsonx_text(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or optional_params.pop("apikey", None)
+ or get_secret_str("WATSONX_APIKEY")
+ or get_secret_str("WATSONX_API_KEY")
+ or get_secret_str("WX_API_KEY")
+ )
+
+ api_base = (
+ api_base
+ or optional_params.pop(
+ "url",
+ optional_params.pop("api_base", optional_params.pop("base_url", None)),
+ )
+ or get_secret_str("WATSONX_API_BASE")
+ or get_secret_str("WATSONX_URL")
+ or get_secret_str("WX_URL")
+ or get_secret_str("WML_URL")
+ )
+
+ wx_credentials = optional_params.pop(
+ "wx_credentials",
+ optional_params.pop(
+ "watsonx_credentials", None
+ ), # follow {provider}_credentials, same as vertex ai
+ )
+
+ token: Optional[str] = None
+ if wx_credentials is not None:
+ api_base = wx_credentials.get("url", api_base)
+ api_key = wx_credentials.get("apikey", wx_credentials.get("api_key", api_key))
+ token = wx_credentials.get(
+ "token",
+ wx_credentials.get(
+ "watsonx_token", None
+ ), # follow format of {provider}_token, same as azure - e.g. 'azure_ad_token=..'
+ )
+
+ if token is not None:
+ optional_params["token"] = token
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="watsonx_text",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+
+
+def _complete_vllm(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ custom_prompt_dict = ctx.custom_prompt_dict
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+
+ custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
+ model_response = vllm_handler.completion(
+ model=model,
+ messages=messages,
+ custom_prompt_dict=custom_prompt_dict,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ )
+
+ if "stream" in optional_params and optional_params["stream"] is True: ## [BETA]
+ # don't try to access stream object,
+ return CustomStreamWrapper(
+ model_response,
+ model,
+ custom_llm_provider="vllm",
+ logging_obj=logging,
+ )
+
+ ## RESPONSE OBJECT
+ return model_response
+
+
+def _complete_ollama(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ litellm.api_base
+ or api_base
+ or get_secret("OLLAMA_API_BASE")
+ or "http://localhost:11434"
+ )
+ if api_key is not None and "Authorization" not in headers:
+ headers["Authorization"] = f"Bearer {api_key}"
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="ollama",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+
+
+def _complete_ollama_chat(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = (
+ litellm.api_base
+ or api_base
+ or get_secret("OLLAMA_API_BASE")
+ or "http://localhost:11434"
+ )
+
+ api_key = (
+ api_key
+ or litellm.ollama_key
+ or os.environ.get("OLLAMA_API_KEY")
+ or litellm.api_key
+ )
+ if api_key is not None and "Authorization" not in headers:
+ headers["Authorization"] = f"Bearer {api_key}"
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="ollama_chat",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ client=client,
+ )
+
+
+def _complete_triton(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = litellm.api_base or api_base
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider=custom_llm_provider,
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ )
+
+
+def _complete_cloudflare(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ custom_prompt_dict = ctx.custom_prompt_dict
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.cloudflare_api_key
+ or litellm.api_key
+ or get_secret("CLOUDFLARE_API_KEY")
+ )
+ api_base = api_base or litellm.api_base or get_secret("CLOUDFLARE_API_BASE")
+
+ custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="cloudflare",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
+ )
+
+
+def _complete_petals(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ api_base = ctx.api_base
+ client = ctx.client
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+
+ api_base = api_base or litellm.api_base
+
+ stream = optional_params.pop("stream", False)
+ model_response = petals_handler.completion(
+ model=model,
+ messages=messages,
+ api_base=api_base,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ encoding=_get_encoding(),
+ logging_obj=logging,
+ client=client,
+ )
+ if stream is True: ## [BETA]
+ # Fake streaming for petals
+ resp_string = model_response["choices"][0]["message"]["content"]
+ return CustomStreamWrapper(
+ resp_string,
+ model,
+ custom_llm_provider="petals",
+ logging_obj=logging,
+ )
+ return model_response
+
+
+def _complete_snowflake(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ try:
+ client = (
+ HTTPHandler(timeout=timeout) if stream is False else None
+ ) # Keep this here, otherwise, the httpx.client closes and streaming is impossible
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ )
+
+ except Exception as e:
+ ## LOGGING - log the original exception returned
+ logging.post_call(
+ input=messages,
+ api_key=api_key,
+ original_response=str(e),
+ additional_args={"headers": headers},
+ )
+ raise e
+
+ return response
+
+
+def _complete_gradient_ai(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_base = litellm.api_base or api_base
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider="gradient_ai",
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ )
+
+
+def _complete_bytez(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.bytez_key
+ or get_secret_str("BYTEZ_API_KEY")
+ or litellm.api_key
+ )
+
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=bytez_transformation,
+ )
+
+ pass
+
+ return response
+
+
+def _complete_lemonade(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.lemonade_key
+ or get_secret_str("LEMONADE_API_KEY")
+ or litellm.api_key
+ )
+
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=lemonade_transformation,
+ )
+
+ pass
+
+ return response
+
+
+def _complete_ovhcloud(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ api_key = (
+ api_key
+ or litellm.ovhcloud_key
+ or get_secret_str("OVHCLOUD_API_KEY")
+ or litellm.api_key
+ )
+
+ api_base = (
+ api_base
+ or litellm.api_base
+ or get_secret_str("OVHCLOUD_API_BASE")
+ or "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1"
+ )
+
+ response = base_llm_http_handler.completion(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ timeout=timeout, # type: ignore
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ encoding=_get_encoding(),
+ stream=stream,
+ provider_config=ovhcloud_transformation,
+ )
+
+ pass
+
+ return response
+
+
+def _complete_custom(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ api_base = ctx.api_base
+ headers = ctx.headers
+ kwargs = ctx.kwargs
+ max_tokens = ctx.max_tokens
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ temperature = ctx.temperature
+ top_p = ctx.top_p
+
+ url = litellm.api_base or api_base or ""
+ if url is None or url == "":
+ raise ValueError(
+ "api_base not set. Set api_base or litellm.api_base for custom endpoints"
+ )
+
+ """
+ assume input to custom LLM api bases follow this format:
+ resp = litellm.module_level_client.post(
+ 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,
+ }
+ }
+ )
+
+ """
+ prompt = " ".join([message["content"] for message in messages]) # type: ignore
+ resp = litellm.module_level_client.post(
+ url,
+ headers=headers,
+ json={
+ "model": model,
+ "params": {
+ "prompt": [prompt],
+ "max_tokens": max_tokens,
+ "temperature": temperature,
+ "top_p": top_p,
+ "top_k": kwargs.get("top_k"),
+ },
+ **kwargs.get("extra_body", {}),
+ },
+ )
+ response_json = resp.json()
+ """
+ assume all responses from custom api_bases of this format:
+ {
+ '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'
+ }
+ ]
+ }
+ """
+ string_response = response_json["data"][0]["output"][0]
+ ## RESPONSE OBJECT
+ model_response.choices[0].message.content = string_response # type: ignore
+ model_response.created = int(time.time())
+ model_response.model = model
+ return model_response
+
+
+def _complete_custom_providers(
+ ctx: _CompletionDispatchContext,
+) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ custom_prompt_dict = ctx.custom_prompt_dict
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logger_fn = ctx.logger_fn
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ custom_handler: Optional[CustomLLM] = None
+ for item in litellm.custom_provider_map:
+ if item["provider"] == custom_llm_provider:
+ custom_handler = item["custom_handler"]
+
+ if custom_handler is None:
+ raise LiteLLMUnknownProvider(
+ model=model, custom_llm_provider=custom_llm_provider
+ )
+
+ ## ROUTE LLM CALL ##
+ handler_fn = custom_chat_llm_router(
+ async_fn=acompletion, stream=stream, custom_llm=custom_handler
+ )
+
+ headers = headers or litellm.headers or {}
+
+ ## CALL FUNCTION
+ response = handler_fn(
+ model=model,
+ messages=messages,
+ headers=headers,
+ model_response=model_response,
+ print_verbose=print_verbose,
+ api_key=api_key,
+ api_base=api_base,
+ acompletion=acompletion,
+ logging_obj=logging,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ timeout=timeout, # type: ignore
+ custom_prompt_dict=custom_prompt_dict,
+ client=client, # pass AsyncOpenAI, OpenAI client
+ encoding=_get_encoding(),
+ )
+ if stream is True:
+ return CustomStreamWrapper(
+ completion_stream=response,
+ model=model,
+ custom_llm_provider=custom_llm_provider,
+ logging_obj=logging,
+ )
+
+ return response # pyright: ignore[reportReturnType] # provider SDK return type is broader than the dispatch contract
+
+
+def _complete_langgraph(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ from litellm.llms.langgraph.chat.transformation import LangGraphConfig
+
+ (
+ api_base,
+ api_key,
+ ) = LangGraphConfig()._get_openai_compatible_provider_info(
+ api_base=api_base or litellm.api_base,
+ api_key=api_key or litellm.api_key,
+ )
+
+ headers = headers or litellm.headers
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider=custom_llm_provider,
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ client=client,
+ )
+
+
+def _complete_langflow(ctx: _CompletionDispatchContext) -> _CompletionDispatchResult:
+ acompletion = ctx.acompletion
+ api_base = ctx.api_base
+ api_key = ctx.api_key
+ client = ctx.client
+ custom_llm_provider = ctx.custom_llm_provider
+ headers = ctx.headers
+ litellm_params = ctx.litellm_params
+ logging = ctx.logging
+ messages = ctx.messages
+ model = ctx.model
+ model_response = ctx.model_response
+ optional_params = ctx.optional_params
+ shared_session = ctx.shared_session
+ stream = ctx.stream
+ timeout = ctx.timeout
+
+ from litellm.llms.langflow.chat.transformation import LangFlowConfig
+
+ (
+ api_base,
+ api_key,
+ ) = LangFlowConfig()._get_openai_compatible_provider_info(
+ api_base=api_base or litellm.api_base,
+ api_key=api_key or litellm.api_key,
+ )
+
+ headers = headers or litellm.headers
+
+ return base_llm_http_handler.completion(
+ model=model,
+ stream=stream,
+ messages=messages,
+ acompletion=acompletion,
+ api_base=api_base,
+ model_response=model_response,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ shared_session=shared_session,
+ custom_llm_provider=custom_llm_provider,
+ timeout=timeout,
+ headers=headers,
+ encoding=_get_encoding(),
+ api_key=api_key,
+ logging_obj=logging,
+ client=client,
+ )
+
+
@tracer.wrap()
@client
def completion( # type: ignore
@@ -1215,9 +5077,7 @@ def completion( # type: ignore
if LiteLLM_Proxy_MCP_Handler._should_use_litellm_mcp_gateway(
tools=tools_for_mcp
):
- # Return coroutine - acompletion will await it
- # completion() can return a coroutine when MCP tools are present, which acompletion() awaits
- return acompletion_with_mcp( # type: ignore[return-value]
+ return acompletion_with_mcp( # pyright: ignore[reportReturnType] # MCP path returns a coroutine that acompletion() awaits; completion()'s sync return type omits it
model=model,
messages=messages,
functions=functions,
@@ -1389,12 +5249,16 @@ def completion( # type: ignore
logging: LiteLLMLoggingObj = cast(LiteLLMLoggingObj, litellm_logging_obj)
fallbacks = fallbacks or litellm.model_fallbacks
if fallbacks is not None:
- return completion_with_fallbacks(**args)
+ return completion_with_fallbacks( # pyright: ignore[reportReturnType] # fallback runner is untyped; resolves to ModelResponse|CustomStreamWrapper at runtime
+ **args
+ )
if model_list is not None:
deployments = [
m["litellm_params"] for m in model_list if m["model_name"] == model
]
- return litellm.batch_completion_models(deployments=deployments, **args)
+ return litellm.batch_completion_models( # pyright: ignore[reportReturnType] # batch path returns a list of responses, outside completion()'s single-response return type
+ deployments=deployments, **args
+ )
if litellm.model_alias_map and model in litellm.model_alias_map:
model = litellm.model_alias_map[
model
@@ -1454,7 +5318,7 @@ def completion( # type: ignore
timeout,
kwargs,
custom_llm_provider,
- global_timeout=getattr(litellm, "request_timeout", None),
+ global_timeout=get_configured_request_timeout(),
supports_httpx_timeout=supports_httpx_timeout,
)
@@ -1719,7 +5583,7 @@ def completion( # type: ignore
else:
optional_params["reasoning_effort"] = {"summary": rs_val}
- return responses_api_bridge.completion(
+ return responses_api_bridge.completion( # pyright: ignore[reportReturnType] # bridge returns a coroutine on the acompletion path; awaited by the async caller
model=model,
messages=messages,
headers=headers,
@@ -1749,375 +5613,52 @@ def completion( # type: ignore
optional_params
)
+ _dispatch_ctx = _CompletionDispatchContext(
+ _azure_detection_model=_azure_detection_model,
+ acompletion=acompletion,
+ api_base=api_base,
+ api_key=api_key,
+ api_version=api_version,
+ client=client,
+ custom_llm_provider=custom_llm_provider,
+ custom_prompt_dict=custom_prompt_dict,
+ extra_headers=extra_headers,
+ headers=headers,
+ hf_model_name=hf_model_name,
+ kwargs=kwargs,
+ litellm_params=litellm_params,
+ logger_fn=logger_fn,
+ logging=logging,
+ max_retries=max_retries,
+ max_tokens=max_tokens,
+ messages=messages,
+ metadata=metadata,
+ model=model,
+ model_response=model_response,
+ optional_params=optional_params,
+ organization=organization,
+ provider_config=provider_config,
+ shared_session=shared_session,
+ stream=stream,
+ temperature=temperature,
+ text_completion=text_completion,
+ timeout=timeout,
+ top_p=top_p,
+ )
if custom_llm_provider == "azure":
# azure configs
## check dynamic params ##
- dynamic_params = False
- if client is not None and (
- isinstance(client, openai.AzureOpenAI)
- or isinstance(client, openai.AsyncAzureOpenAI)
- ):
- dynamic_params = _check_dynamic_azure_params(
- azure_client_params={"api_version": api_version},
- azure_client=client,
- )
-
- api_type = get_secret("AZURE_API_TYPE") or "azure"
-
- api_base = api_base or litellm.api_base or get_secret("AZURE_API_BASE")
-
- api_version = (
- api_version
- or litellm.api_version
- or get_secret_str("AZURE_API_VERSION")
- or litellm.AZURE_DEFAULT_API_VERSION
- )
-
- api_key = (
- api_key
- or litellm.api_key
- or litellm.azure_key
- or get_secret_str("AZURE_OPENAI_API_KEY")
- or get_secret_str("AZURE_API_KEY")
- )
-
- azure_ad_token = optional_params.get("extra_body", {}).pop(
- "azure_ad_token", None
- ) or get_secret_str("AZURE_AD_TOKEN")
-
- azure_ad_token_provider = litellm_params.get(
- "azure_ad_token_provider", None
- )
-
- headers = headers or litellm.headers
-
- if extra_headers is not None:
- optional_params["extra_headers"] = extra_headers
- if max_retries is not None:
- optional_params["max_retries"] = max_retries
-
- if litellm.AzureOpenAIO1Config().is_o_series_model(
- model=_azure_detection_model
- ):
- ## LOAD CONFIG - if set
- config = litellm.AzureOpenAIO1Config.get_config()
- for k, v in config.items():
- if (
- k not in optional_params
- ): # completion(top_k=3) > azure_config(top_k=3) <- allows for dynamic variables to be passed in
- optional_params[k] = v
-
- response = azure_o1_chat_completions.completion(
- model=model,
- messages=messages,
- headers=headers,
- api_key=api_key,
- api_base=api_base,
- api_version=api_version,
- dynamic_params=dynamic_params,
- azure_ad_token=azure_ad_token,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- logging_obj=logging,
- acompletion=acompletion,
- timeout=timeout, # type: ignore
- client=client, # pass AsyncAzureOpenAI, AzureOpenAI client
- custom_llm_provider=custom_llm_provider,
- )
- else:
- ## LOAD CONFIG - if set
- config = litellm.AzureOpenAIConfig.get_config()
- for k, v in config.items():
- if (
- k not in optional_params
- ): # completion(top_k=3) > azure_config(top_k=3) <- allows for dynamic variables to be passed in
- optional_params[k] = v
-
- ## COMPLETION CALL
- response = azure_chat_completions.completion(
- model=model,
- messages=messages,
- headers=headers,
- api_key=api_key,
- api_base=api_base,
- api_version=api_version,
- api_type=api_type,
- dynamic_params=dynamic_params,
- azure_ad_token=azure_ad_token,
- azure_ad_token_provider=azure_ad_token_provider,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- logging_obj=logging,
- acompletion=acompletion,
- timeout=timeout, # type: ignore
- client=client, # pass AsyncAzureOpenAI, AzureOpenAI client
- )
-
- if optional_params.get("stream", False):
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=response,
- additional_args={
- "headers": headers,
- "api_version": api_version,
- "api_base": api_base,
- },
- )
+ response = _complete_azure(_dispatch_ctx)
elif custom_llm_provider == "azure_text":
# azure configs
- api_type = get_secret_str("AZURE_API_TYPE") or "azure"
-
- api_base = api_base or litellm.api_base or get_secret_str("AZURE_API_BASE")
-
- if api_base is None:
- raise ValueError(
- "api_base is required for Azure OpenAI LLM provider. Either set it dynamically or set the AZURE_API_BASE environment variable."
- )
-
- api_version = (
- api_version
- or litellm.api_version
- or get_secret_str("AZURE_API_VERSION")
- )
-
- api_key = (
- api_key
- or litellm.api_key
- or litellm.azure_key
- or get_secret_str("AZURE_OPENAI_API_KEY")
- or get_secret_str("AZURE_API_KEY")
- )
-
- azure_ad_token = optional_params.get("extra_body", {}).pop(
- "azure_ad_token", None
- ) or get_secret_str("AZURE_AD_TOKEN")
-
- azure_ad_token_provider = litellm_params.get(
- "azure_ad_token_provider", None
- )
-
- headers = headers or litellm.headers
-
- if extra_headers is not None:
- optional_params["extra_headers"] = extra_headers
-
- ## LOAD CONFIG - if set
- config = litellm.AzureOpenAIConfig.get_config()
- for k, v in config.items():
- if (
- k not in optional_params
- ): # completion(top_k=3) > azure_config(top_k=3) <- allows for dynamic variables to be passed in
- optional_params[k] = v
-
- ## COMPLETION CALL
- response = azure_text_completions.completion(
- model=model,
- messages=messages,
- headers=headers,
- api_key=api_key,
- api_base=api_base,
- api_version=cast(str, api_version),
- api_type=api_type,
- azure_ad_token=azure_ad_token,
- azure_ad_token_provider=azure_ad_token_provider,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- logging_obj=logging,
- acompletion=acompletion,
- timeout=timeout,
- client=client, # pass AsyncAzureOpenAI, AzureOpenAI client
- )
-
- if optional_params.get("stream", False) or acompletion is True:
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=response,
- additional_args={
- "headers": headers,
- "api_version": api_version,
- "api_base": api_base,
- },
- )
+ response = _complete_azure_text(_dispatch_ctx)
elif custom_llm_provider == "deepseek":
## COMPLETION CALL
- try:
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
- except Exception as e:
- ## LOGGING - log the original exception returned
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
+ response = _complete_deepseek(_dispatch_ctx)
elif custom_llm_provider == "azure_ai":
- from litellm.llms.azure_ai.common_utils import AzureFoundryModelInfo
-
- azure_ai_route = AzureFoundryModelInfo.get_azure_ai_route(model)
-
- # Check if this is an agents route - model format: azure_ai/agents/
- if azure_ai_route == "agents":
- from litellm.llms.azure_ai.agents import AzureAIAgentsConfig
-
- api_base = AzureFoundryModelInfo.get_api_base(api_base)
- if api_base is None:
- raise ValueError(
- "Azure AI Agents requests require an api_base. "
- "Set `api_base` or the AZURE_AI_API_BASE env var."
- )
- api_key = AzureFoundryModelInfo.get_api_key(api_key)
-
- response = AzureAIAgentsConfig.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- api_key=api_key,
- model_response=model_response,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout,
- acompletion=acompletion,
- stream=stream,
- headers=headers or litellm.headers,
- )
-
- # Check if this is a Claude model - route to Azure Anthropic handler
- elif "claude" in model.lower():
- # Use Azure Anthropic handler for Claude models
- api_base = AzureFoundryModelInfo.get_api_base(api_base)
- if api_base is None:
- raise ValueError(
- "Azure Anthropic requests require an api_base. "
- "Set `api_base` or the AZURE_AI_API_BASE env var."
- )
- api_key = AzureFoundryModelInfo.get_api_key(api_key)
-
- # Ensure the URL ends with /v1/messages for Anthropic
- if api_base:
- api_base = api_base.rstrip("/")
- if not api_base.endswith("/v1/messages"):
- if "/anthropic" in api_base:
- parts = api_base.split("/anthropic", 1)
- api_base = parts[0] + "/anthropic"
- else:
- api_base = api_base + "/anthropic"
- api_base = api_base + "/v1/messages"
-
- response = azure_anthropic_chat_completions.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- acompletion=acompletion,
- custom_prompt_dict=litellm.custom_prompt_dict,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- headers=headers,
- timeout=timeout,
- client=client,
- custom_llm_provider=custom_llm_provider,
- )
- if optional_params.get("stream", False) or acompletion is True:
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=response,
- )
- response = response
- else:
- # Non-Claude models use standard Azure AI flow
- api_base = AzureFoundryModelInfo.get_api_base(api_base)
- # set API KEY
- api_key = AzureFoundryModelInfo.get_api_key(api_key)
-
- headers = headers or litellm.headers
-
- if extra_headers is not None:
- optional_params["extra_headers"] = extra_headers
-
- ## FOR COHERE
- if "command-r" in model: # make sure tool call in messages are str
- messages = stringify_json_tool_call_content(messages=messages)
-
- ## COMPLETION CALL
- try:
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout, # type: ignore
- client=client, # pass AsyncOpenAI, OpenAI client
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- )
- except Exception as e:
- ## LOGGING - log the original exception returned
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
-
- if optional_params.get("stream", False):
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=response,
- additional_args={"headers": headers},
- )
+ response = _complete_azure_ai(_dispatch_ctx)
elif (
custom_llm_provider == "text-completion-openai"
or "ft:babbage-002" in model
@@ -2126,535 +5667,42 @@ def completion( # type: ignore
in litellm.openai_text_completion_compatible_providers
and kwargs.get("text_completion") is True
):
- openai.api_type = "openai"
-
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("OPENAI_BASE_URL")
- or get_secret("OPENAI_API_BASE")
- or "https://api.openai.com/v1"
- )
-
- openai.api_version = None
- # set API KEY
-
- api_key = (
- api_key
- or litellm.api_key
- or litellm.openai_key
- or get_secret("OPENAI_API_KEY")
- )
-
- headers = headers or litellm.headers
-
- ## LOAD CONFIG - if set
- config = litellm.OpenAITextCompletionConfig.get_config()
- for k, v in config.items():
- if (
- k not in optional_params
- ): # completion(top_k=3) > openai_text_config(top_k=3) <- allows for dynamic variables to be passed in
- optional_params[k] = v
- if litellm.organization:
- openai.organization = litellm.organization
-
- if (
- len(messages) > 0
- and "content" in messages[0]
- and isinstance(messages[0]["content"], list)
- ):
- # text-davinci-003 can accept a string or array, if it's an array, assume the array is set in messages[0]['content']
- # https://platform.openai.com/docs/api-reference/completions/create
- prompt = messages[0]["content"]
- else:
- prompt = " ".join([message["content"] for message in messages]) # type: ignore
-
- ## COMPLETION CALL
- _response = openai_text_completions.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- print_verbose=print_verbose,
- api_key=api_key,
- custom_llm_provider=custom_llm_provider,
- api_base=api_base,
- acompletion=acompletion,
- client=client, # pass AsyncOpenAI, OpenAI client
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- timeout=timeout, # type: ignore
- )
-
- if (
- optional_params.get("stream", False) is False
- and acompletion is False
- and text_completion is False
- ):
- # convert to chat completion response
- _response = litellm.OpenAITextCompletionConfig().convert_to_chat_model_response_object(
- response_object=_response, model_response_object=model_response
- )
-
- if optional_params.get("stream", False) or acompletion is True:
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=_response,
- additional_args={"headers": headers},
- )
- response = _response
+ response = _complete_text_completion_openai(_dispatch_ctx)
elif custom_llm_provider == "fireworks_ai":
## COMPLETION CALL
- try:
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
- except Exception as e:
- ## LOGGING - log the original exception returned
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
+ response = _complete_fireworks_ai(_dispatch_ctx)
elif custom_llm_provider == "heroku":
- try:
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout,
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
- except Exception as e:
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
+ response = _complete_heroku(_dispatch_ctx)
elif custom_llm_provider == "ragflow":
## COMPLETION CALL - RAGFlow uses HTTP handler to support custom URL paths
- try:
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout,
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
- except Exception as e:
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
+ response = _complete_ragflow(_dispatch_ctx)
elif custom_llm_provider == "xai":
## COMPLETION CALL
- try:
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
- except Exception as e:
- ## LOGGING - log the original exception returned
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
+ response = _complete_xai(_dispatch_ctx)
elif custom_llm_provider == "groq":
- api_base = (
- api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
- or litellm.api_base
- or get_secret("GROQ_API_BASE")
- or "https://api.groq.com/openai/v1"
- )
-
- # set API KEY
- api_key = (
- api_key
- or litellm.api_key # for deepinfra/perplexity/anyscale/friendliai we check in get_llm_provider and pass in the api key from there
- or litellm.groq_key
- or get_secret("GROQ_API_KEY")
- )
-
- headers = headers or litellm.headers
-
- ## LOAD CONFIG - if set
- config = litellm.GroqChatConfig.get_config()
- for k, v in config.items():
- if (
- k not in optional_params
- ): # completion(top_k=3) > openai_config(top_k=3) <- allows for dynamic variables to be passed in
- optional_params[k] = v
-
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider=custom_llm_provider,
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
+ response = _complete_groq(_dispatch_ctx)
elif custom_llm_provider == "bedrock_mantle":
- api_base = (
- api_base or litellm.api_base or get_secret("BEDROCK_MANTLE_API_BASE")
- )
- api_key = api_key or litellm.api_key or get_secret("BEDROCK_MANTLE_API_KEY")
- headers = headers or litellm.headers
- config = litellm.BedrockMantleChatConfig.get_config()
- for k, v in config.items():
- if k not in optional_params:
- optional_params[k] = v
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider=custom_llm_provider,
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- client=client,
- )
+ response = _complete_bedrock_mantle(_dispatch_ctx)
elif custom_llm_provider == "a2a":
# A2A (Agent-to-Agent) Protocol
# Resolve agent configuration from registry if model format is "a2a/"
- (
- api_base,
- api_key,
- headers,
- ) = litellm.A2AConfig.resolve_agent_config_from_registry(
- model=model,
- api_base=api_base,
- api_key=api_key,
- headers=headers,
- optional_params=optional_params,
- )
-
- # Fall back to environment variables and defaults
- api_base = api_base or litellm.api_base or get_secret_str("A2A_API_BASE")
-
- if api_base is None:
- raise Exception(
- "api_base is required for A2A provider. "
- "Either provide api_base parameter, set A2A_API_BASE environment variable, "
- "or register the agent in the proxy with model='a2a/'."
- )
-
- headers = headers or litellm.headers
-
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider=custom_llm_provider,
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- client=client,
- provider_config=provider_config,
- )
+ response = _complete_a2a(_dispatch_ctx)
elif custom_llm_provider == "gigachat":
# GigaChat - Sber AI's LLM (Russia)
- api_key = (
- api_key
- or litellm.api_key
- or litellm.gigachat_key
- or get_secret("GIGACHAT_API_KEY")
- or get_secret("GIGACHAT_CREDENTIALS")
- )
-
- headers = headers or litellm.headers or {}
-
- ## COMPLETION CALL
- try:
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout,
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
- except Exception as e:
- ## LOGGING - log the original exception returned
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
+ response = _complete_gigachat(_dispatch_ctx)
elif custom_llm_provider == "sap":
- headers = headers or litellm.headers
- ## LOAD CONFIG - if set
- config = litellm.GenAIHubOrchestrationConfig.get_config()
- for k, v in config.items():
- if (
- k not in optional_params
- ): # completion(top_k=3) > openai_config(top_k=3) <- allows for dynamic variables to be passed in
- optional_params[k] = v
-
- response = sap_gen_ai_hub_chat_completions.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout, # type: ignore
- shared_session=shared_session,
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- api_key=api_key,
- api_base=api_base,
- stream=stream,
- )
+ response = _complete_sap(_dispatch_ctx)
elif custom_llm_provider == "aiohttp_openai":
# NEW aiohttp provider for 10-100x higher RPS
- api_base = (
- api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
- or litellm.api_base
- or get_secret("OPENAI_BASE_URL")
- or get_secret("OPENAI_API_BASE")
- or "https://api.openai.com/v1"
- )
- # set API KEY
- api_key = (
- api_key
- or litellm.api_key # for deepinfra/perplexity/anyscale/friendliai we check in get_llm_provider and pass in the api key from there
- or litellm.openai_key
- or get_secret("OPENAI_API_KEY")
- )
-
- headers = headers or litellm.headers
-
- if extra_headers is not None:
- optional_params["extra_headers"] = extra_headers
- response = base_llm_aiohttp_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout,
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- )
+ response = _complete_aiohttp_openai(_dispatch_ctx)
elif custom_llm_provider == "cometapi":
- api_key = (
- api_key
- or litellm.cometapi_key
- or get_secret_str("COMETAPI_KEY")
- or litellm.api_key
- )
-
- api_base = (
- api_base
- or litellm.api_base
- or get_secret_str("COMETAPI_API_BASE")
- or "https://api.cometapi.com/v1"
- )
-
- ## COMPLETION CALL
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout,
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
-
- ## LOGGING
- logging.post_call(
- input=messages, api_key=api_key, original_response=response
- )
+ response = _complete_cometapi(_dispatch_ctx)
elif custom_llm_provider == "minimax":
- api_key = api_key or get_secret_str("MINIMAX_API_KEY") or litellm.api_key
-
- api_base = (
- api_base
- or litellm.api_base
- or get_secret_str("MINIMAX_API_BASE")
- or "https://api.minimax.io/v1"
- )
-
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- custom_llm_provider=custom_llm_provider,
- model_response=model_response,
- encoding=_get_encoding(),
- logging_obj=logging,
- optional_params=optional_params,
- timeout=timeout,
- litellm_params=litellm_params,
- shared_session=shared_session,
- acompletion=acompletion,
- stream=stream,
- api_key=api_key,
- headers=headers,
- client=client,
- provider_config=provider_config,
- )
- logging.post_call(
- input=messages, api_key=api_key, original_response=response
- )
+ response = _complete_minimax(_dispatch_ctx)
elif custom_llm_provider == "hosted_vllm":
- api_base = (
- api_base or litellm.api_base or get_secret_str("HOSTED_VLLM_API_BASE")
- )
-
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- custom_llm_provider=custom_llm_provider,
- model_response=model_response,
- encoding=_get_encoding(),
- logging_obj=logging,
- optional_params=optional_params,
- timeout=timeout,
- litellm_params=litellm_params,
- shared_session=shared_session,
- acompletion=acompletion,
- stream=stream,
- api_key=api_key,
- headers=headers,
- client=client,
- provider_config=provider_config,
- )
- logging.post_call(
- input=messages, api_key=api_key, original_response=response
- )
+ response = _complete_hosted_vllm(_dispatch_ctx)
elif (
model in litellm.open_ai_chat_completion_models
or custom_llm_provider == "custom_openai"
@@ -2679,205 +5727,17 @@ def completion( # type: ignore
): # allow user to make an openai call with a custom base
# note: if a user sets a custom base - we should ensure this works
# allow for the setting of dynamic and stateful api-bases
- api_base = (
- api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
- or litellm.api_base
- or get_secret("OPENAI_BASE_URL")
- or get_secret("OPENAI_API_BASE")
- or "https://api.openai.com/v1"
- )
- organization = (
- organization
- or litellm.organization
- or get_secret("OPENAI_ORGANIZATION")
- or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105
- )
- openai.organization = organization
- # set API KEY
- api_key = (
- api_key
- or litellm.api_key # for deepinfra/perplexity/anyscale/friendliai we check in get_llm_provider and pass in the api key from there
- or litellm.openai_key
- or get_secret("OPENAI_API_KEY")
- )
-
- headers = headers or litellm.headers
-
- # Add GitHub Copilot headers (same as /responses endpoint does)
- if custom_llm_provider == "github_copilot":
- from litellm.llms.github_copilot.authenticator import Authenticator
- from litellm.llms.github_copilot.common_utils import (
- get_copilot_default_headers,
- )
-
- copilot_auth = Authenticator()
- copilot_api_key = copilot_auth.get_api_key()
- copilot_headers = get_copilot_default_headers(copilot_api_key)
- if extra_headers:
- copilot_headers.update(extra_headers)
- extra_headers = copilot_headers
-
- if extra_headers is not None:
- optional_params["extra_headers"] = extra_headers
-
- if (
- litellm.enable_preview_features and metadata is not None
- ): # [PREVIEW] allow metadata to be passed to OPENAI
- openai_metadata = get_requester_metadata(metadata)
- if openai_metadata is not None:
- optional_params["metadata"] = openai_metadata
-
- ## LOAD CONFIG - if set
- config = litellm.OpenAIConfig.get_config()
- for k, v in config.items():
- if (
- k not in optional_params
- ): # completion(top_k=3) > openai_config(top_k=3) <- allows for dynamic variables to be passed in
- optional_params[k] = v
-
- ## COMPLETION CALL
- use_base_llm_http_handler = get_secret_bool(
- "EXPERIMENTAL_OPENAI_BASE_LLM_HTTP_HANDLER"
- )
-
- try:
- if use_base_llm_http_handler:
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- custom_llm_provider=custom_llm_provider,
- model_response=model_response,
- encoding=_get_encoding(),
- logging_obj=logging,
- optional_params=optional_params,
- timeout=timeout,
- litellm_params=litellm_params,
- shared_session=shared_session,
- acompletion=acompletion,
- stream=stream,
- api_key=api_key,
- headers=headers,
- client=client,
- provider_config=provider_config,
- )
- else:
- response = openai_chat_completions.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- print_verbose=print_verbose,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- timeout=timeout, # type: ignore
- custom_prompt_dict=custom_prompt_dict,
- client=client, # pass AsyncOpenAI, OpenAI client
- organization=organization,
- custom_llm_provider=custom_llm_provider,
- shared_session=shared_session,
- )
- except Exception as e:
- ## LOGGING - log the original exception returned
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
-
- if optional_params.get("stream", False):
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=response,
- additional_args={"headers": headers},
- )
+ response = _complete_custom_openai(_dispatch_ctx)
elif custom_llm_provider == "mistral":
- api_key = api_key or litellm.api_key or get_secret("MISTRAL_API_KEY")
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("MISTRAL_API_BASE")
- or "https://api.mistral.ai/v1"
- )
-
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- custom_llm_provider=custom_llm_provider,
- model_response=model_response,
- encoding=_get_encoding(),
- logging_obj=logging,
- optional_params=optional_params,
- timeout=timeout,
- litellm_params=litellm_params,
- shared_session=shared_session,
- acompletion=acompletion,
- stream=stream,
- api_key=api_key,
- headers=headers,
- client=client,
- provider_config=provider_config,
- )
+ response = _complete_mistral(_dispatch_ctx)
elif (
"replicate" in model
or custom_llm_provider == "replicate"
or model in litellm.replicate_models
):
# Setting the relevant API KEY for replicate, replicate defaults to using os.environ.get("REPLICATE_API_TOKEN")
- replicate_key = (
- api_key
- or litellm.replicate_key
- or litellm.api_key
- or get_secret("REPLICATE_API_KEY")
- or get_secret("REPLICATE_API_TOKEN")
- )
-
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("REPLICATE_API_BASE")
- or "https://api.replicate.com/v1"
- )
-
- custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
-
- model_response = replicate_chat_completion( # type: ignore
- model=model,
- messages=messages,
- api_base=api_base,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(), # for calculating input/output tokens
- api_key=replicate_key,
- logging_obj=logging,
- custom_prompt_dict=custom_prompt_dict,
- acompletion=acompletion,
- headers=headers,
- )
-
- if optional_params.get("stream", False) is True:
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=replicate_key,
- original_response=model_response,
- )
-
- response = model_response
+ response = _complete_replicate(_dispatch_ctx)
elif (
"clarifai" in model
or custom_llm_provider == "clarifai"
@@ -2885,614 +5745,36 @@ def completion( # type: ignore
):
pass # Deprecated - handled in the openai compatible provider section above
elif custom_llm_provider == "anthropic_text":
- api_key = (
- api_key
- or litellm.anthropic_key
- or litellm.api_key
- or os.environ.get("ANTHROPIC_API_KEY")
- )
- custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("ANTHROPIC_API_BASE")
- or get_secret("ANTHROPIC_BASE_URL")
- or "https://api.anthropic.com/v1/complete"
- )
-
- # Check if we should disable automatic URL suffix appending
- disable_url_suffix = get_secret_bool("LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX")
- if (
- api_base is not None
- and not disable_url_suffix
- and not api_base.endswith("/v1/complete")
- ):
- api_base += "/v1/complete"
- elif disable_url_suffix:
- verbose_logger.debug(
- "LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX is set, skipping /v1/complete suffix"
- )
-
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="anthropic_text",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- )
+ response = _complete_anthropic_text(_dispatch_ctx)
elif custom_llm_provider == "anthropic":
- api_key = (
- api_key
- or litellm.anthropic_key
- or litellm.api_key
- or os.environ.get("ANTHROPIC_API_KEY")
- )
- custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
- # call /messages
- # default route for all anthropic models
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("ANTHROPIC_API_BASE")
- or get_secret("ANTHROPIC_BASE_URL")
- or "https://api.anthropic.com/v1/messages"
- )
-
- # Check if we should disable automatic URL suffix appending
- disable_url_suffix = get_secret_bool("LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX")
- if (
- api_base is not None
- and not disable_url_suffix
- and not api_base.endswith("/v1/messages")
- ):
- api_base += "/v1/messages"
- elif disable_url_suffix:
- verbose_logger.debug(
- "LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX is set, skipping /v1/messages suffix"
- )
-
- response = anthropic_chat_completions.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- acompletion=acompletion,
- custom_prompt_dict=litellm.custom_prompt_dict,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(), # for calculating input/output tokens
- api_key=api_key,
- logging_obj=logging,
- headers=headers,
- timeout=timeout,
- client=client,
- custom_llm_provider=custom_llm_provider,
- )
- if optional_params.get("stream", False) or acompletion is True:
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=response,
- )
- response = response
+ response = _complete_anthropic(_dispatch_ctx)
elif custom_llm_provider == "nlp_cloud":
- nlp_cloud_key = (
- api_key
- or litellm.nlp_cloud_key
- or get_secret("NLP_CLOUD_API_KEY")
- or litellm.api_key
- )
-
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("NLP_CLOUD_API_BASE")
- or "https://api.nlpcloud.io/v1/gpu/"
- )
-
- response = nlp_cloud_chat_completion(
- model=model,
- messages=messages,
- api_base=api_base,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- api_key=nlp_cloud_key,
- logging_obj=logging,
- )
-
- if "stream" in optional_params and optional_params["stream"] is True:
- # don't try to access stream object,
- response = CustomStreamWrapper(
- response,
- model,
- custom_llm_provider="nlp_cloud",
- logging_obj=logging,
- )
-
- if optional_params.get("stream", False) or acompletion is True:
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=response,
- )
-
- response = response
+ response = _complete_nlp_cloud(_dispatch_ctx)
elif custom_llm_provider == "aleph_alpha":
- aleph_alpha_key = (
- api_key
- or litellm.aleph_alpha_key
- or get_secret("ALEPH_ALPHA_API_KEY")
- or get_secret("ALEPHALPHA_API_KEY")
- or litellm.api_key
- )
-
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("ALEPH_ALPHA_API_BASE")
- or "https://api.aleph-alpha.com/complete"
- )
-
- model_response = aleph_alpha.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- default_max_tokens_to_sample=litellm.max_tokens,
- api_key=aleph_alpha_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- )
-
- if "stream" in optional_params and optional_params["stream"] is True:
- # don't try to access stream object,
- response = CustomStreamWrapper(
- model_response,
- model,
- custom_llm_provider="aleph_alpha",
- logging_obj=logging,
- )
- return response
- response = model_response
+ response = _complete_aleph_alpha(_dispatch_ctx)
elif custom_llm_provider == "cohere_chat" or custom_llm_provider == "cohere":
- cohere_key = (
- api_key
- or litellm.cohere_key
- or get_secret_str("COHERE_API_KEY")
- or get_secret_str("CO_API_KEY")
- or litellm.api_key
- )
-
- cohere_route = CohereModelInfo.get_cohere_route(model)
- verbose_logger.debug(f"Cohere route: {cohere_route}")
- # Set API base based on route
- if cohere_route == "v2":
- api_base = (
- api_base
- or litellm.api_base
- or get_secret_str("COHERE_API_BASE")
- or "https://api.cohere.com/v2/chat"
- )
- # Remove v2/ prefix from model name for the actual API call
- if "v2/" in model:
- model = model.replace("v2/", "")
- else:
- api_base = (
- api_base
- or litellm.api_base
- or get_secret_str("COHERE_API_BASE")
- or "https://api.cohere.ai/v1/chat"
- )
-
- headers = headers or litellm.headers or {}
- if headers is None:
- headers = {}
-
- if extra_headers is not None:
- headers.update(extra_headers)
-
- verbose_logger.debug(f"Model: {model}, API Base: {api_base}")
- verbose_logger.debug(f"Provider Config: {provider_config}")
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="cohere_chat",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=cohere_key,
- provider_config=provider_config,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- )
+ response = _complete_cohere_chat(_dispatch_ctx)
elif custom_llm_provider == "maritalk":
- maritalk_key = (
- api_key
- or litellm.maritalk_key
- or get_secret("MARITALK_API_KEY")
- or litellm.api_key
- )
-
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("MARITALK_API_BASE")
- or "https://chat.maritaca.ai/api"
- )
-
- model_response = openai_like_chat_completion.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- api_key=maritalk_key,
- logging_obj=logging,
- custom_llm_provider="maritalk",
- custom_prompt_dict=custom_prompt_dict,
- )
-
- response = model_response
+ response = _complete_maritalk(_dispatch_ctx)
elif custom_llm_provider == "amazon_nova":
- api_key = (
- api_key
- or litellm.amazon_nova_api_key
- or get_secret_str("AMAZON_NOVA_API_KEY")
- or litellm.api_key
- )
- api_base = (
- api_base
- or litellm.api_base
- or get_secret_str("AMAZON_NOVA_API_BASE")
- or "https://api.nova.amazon.com/v1"
- )
- response = openai_like_chat_completion.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- timeout=timeout,
- custom_llm_provider=custom_llm_provider,
- custom_prompt_dict=custom_prompt_dict,
- )
+ response = _complete_amazon_nova(_dispatch_ctx)
elif custom_llm_provider == "huggingface":
- huggingface_key = (
- api_key
- or litellm.huggingface_key
- or os.environ.get("HF_TOKEN")
- or os.environ.get("HUGGINGFACE_API_KEY")
- or litellm.api_key
- )
- hf_headers = headers or litellm.headers
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=hf_headers,
- model_response=model_response,
- api_key=huggingface_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- )
+ response = _complete_huggingface(_dispatch_ctx)
elif custom_llm_provider == "oci":
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- )
+ response = _complete_oci(_dispatch_ctx)
elif custom_llm_provider == "compactifai":
- api_key = (
- api_key or get_secret_str("COMPACTIFAI_API_KEY") or litellm.api_key
- )
-
- api_base = api_base or "https://api.compactif.ai/v1"
-
- ## COMPLETION CALL
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout,
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
+ response = _complete_compactifai(_dispatch_ctx)
elif custom_llm_provider == "oobabooga":
- custom_llm_provider = "oobabooga"
- model_response = oobabooga.completion(
- model=model,
- messages=messages,
- model_response=model_response,
- api_base=api_base, # type: ignore
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- api_key=None,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- logging_obj=logging,
- )
- if "stream" in optional_params and optional_params["stream"] is True:
- # don't try to access stream object,
- response = CustomStreamWrapper(
- model_response,
- model,
- custom_llm_provider="oobabooga",
- logging_obj=logging,
- )
- return response
- response = model_response
+ response = _complete_oobabooga(_dispatch_ctx)
elif custom_llm_provider == "databricks":
- api_base = (
- api_base # for databricks we check in get_llm_provider and pass in the api base from there
- or litellm.api_base
- or os.getenv("DATABRICKS_API_BASE")
- )
-
- # set API KEY
- api_key = (
- api_key
- or litellm.api_key # for databricks we check in get_llm_provider and pass in the api key from there
- or litellm.databricks_key
- or get_secret("DATABRICKS_API_KEY")
- )
-
- headers = headers or litellm.headers
-
- ## COMPLETION CALL
- try:
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- custom_llm_provider="databricks",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
- except Exception as e:
- ## LOGGING - log the original exception returned
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
-
- if optional_params.get("stream", False):
- ## LOGGING
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=response,
- additional_args={"headers": headers},
- )
+ response = _complete_databricks(_dispatch_ctx)
elif custom_llm_provider == "datarobot":
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=provider_config,
- )
+ response = _complete_datarobot(_dispatch_ctx)
elif custom_llm_provider == "openrouter":
- api_base = (
- api_base
- or litellm.api_base
- or get_secret_str("OPENROUTER_API_BASE")
- or "https://openrouter.ai/api/v1"
- )
-
- api_key = (
- api_key
- or litellm.api_key
- or litellm.openrouter_key
- or get_secret_str("OPENROUTER_API_KEY")
- or get_secret_str("OR_API_KEY")
- )
-
- openrouter_site_url = get_secret("OR_SITE_URL") or "https://litellm.ai"
- openrouter_app_name = get_secret("OR_APP_NAME") or "liteLLM"
-
- openrouter_headers = {
- "HTTP-Referer": openrouter_site_url,
- "X-Title": openrouter_app_name,
- }
-
- _headers = headers or litellm.headers
- if _headers:
- openrouter_headers.update(_headers)
-
- headers = openrouter_headers
-
- ## Load Config
- config = litellm.OpenrouterConfig.get_config()
- for k, v in config.items():
- if k == "extra_body":
- # we use openai 'extra_body' to pass openrouter specific params - transforms, route, models
- if "extra_body" in optional_params:
- optional_params[k].update(v)
- else:
- optional_params[k] = v
- elif k not in optional_params:
- optional_params[k] = v
-
- data = {"model": model, "messages": messages, **optional_params}
-
- ## COMPLETION CALL
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="openrouter",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
- ## LOGGING
- logging.post_call(
- input=messages, api_key=openai.api_key, original_response=response
- )
+ response = _complete_openrouter(_dispatch_ctx)
elif custom_llm_provider == "vercel_ai_gateway":
- api_base = (
- api_base
- or litellm.api_base
- or get_secret_str("VERCEL_AI_GATEWAY_API_BASE")
- or "https://ai-gateway.vercel.sh/v1"
- )
-
- api_key = (
- api_key or litellm.api_key or get_secret("VERCEL_AI_GATEWAY_API_KEY")
- )
-
- vercel_site_url = get_secret("VERCEL_SITE_URL") or "https://litellm.ai"
- vercel_app_name = get_secret("VERCEL_APP_NAME") or "liteLLM"
-
- vercel_headers = {
- "http-referer": vercel_site_url,
- "x-title": vercel_app_name,
- }
-
- _headers = headers or litellm.headers
- if _headers:
- vercel_headers.update(_headers)
-
- headers = vercel_headers
-
- ## Load Config
- config = litellm.VercelAIGatewayConfig.get_config()
- for k, v in config.items():
- if k == "extra_body":
- # we use openai 'extra_body' to pass vercel specific params - providerOptions
- if "extra_body" in optional_params:
- optional_params[k].update(v)
- else:
- optional_params[k] = v
- elif k not in optional_params:
- optional_params[k] = v
-
- data = {"model": model, "messages": messages, **optional_params}
-
- ## COMPLETION CALL
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="vercel_ai_gateway",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
- ## LOGGING
- logging.post_call(
- input=messages, api_key=openai.api_key, original_response=response
- )
+ response = _complete_vercel_ai_gateway(_dispatch_ctx)
elif (
custom_llm_provider == "together_ai"
or ("togethercomputer" in model)
@@ -3507,1114 +5789,75 @@ def completion( # type: ignore
"Palm was decommisioned on October 2024. Please use the `gemini/` route for Gemini Google AI Studio Models. Announcement: https://ai.google.dev/palm_docs/palm?hl=en"
)
elif custom_llm_provider == "vertex_ai_beta" or custom_llm_provider == "gemini":
- vertex_ai_project = (
- optional_params.pop("vertex_project", None)
- or optional_params.pop("vertex_ai_project", None)
- or litellm.vertex_project
- or get_secret("VERTEXAI_PROJECT")
- )
- vertex_ai_location = (
- optional_params.pop("vertex_location", None)
- or optional_params.pop("vertex_ai_location", None)
- or litellm.vertex_location
- or get_secret("VERTEXAI_LOCATION")
- )
- vertex_credentials = (
- optional_params.pop("vertex_credentials", None)
- or optional_params.pop("vertex_ai_credentials", None)
- or get_secret("VERTEXAI_CREDENTIALS")
- )
-
- gemini_api_key = (
- api_key
- or get_api_key_from_env()
- or get_secret("PALM_API_KEY") # older palm api key should also work
- or litellm.api_key
- )
-
- api_base = api_base or litellm.api_base or get_secret("GEMINI_API_BASE")
- new_params = safe_deep_copy(optional_params or {})
- response = vertex_chat_completion.completion( # type: ignore
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=new_params,
- litellm_params=litellm_params, # type: ignore
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- vertex_location=vertex_ai_location,
- vertex_project=vertex_ai_project,
- vertex_credentials=vertex_credentials,
- gemini_api_key=gemini_api_key,
- logging_obj=logging,
- acompletion=acompletion,
- timeout=timeout,
- custom_llm_provider=custom_llm_provider, # type: ignore
- client=client,
- api_base=api_base,
- extra_headers=headers,
- )
+ response = _complete_vertex_ai_beta(_dispatch_ctx)
elif custom_llm_provider == "vertex_ai":
- vertex_ai_project = (
- optional_params.pop("vertex_project", None)
- or optional_params.pop("vertex_ai_project", None)
- or litellm.vertex_project
- or get_secret("VERTEXAI_PROJECT")
- )
- vertex_ai_location = (
- optional_params.pop("vertex_location", None)
- or optional_params.pop("vertex_ai_location", None)
- or litellm.vertex_location
- or get_secret("VERTEXAI_LOCATION")
- )
- vertex_credentials = (
- optional_params.pop("vertex_credentials", None)
- or optional_params.pop("vertex_ai_credentials", None)
- or get_secret("VERTEXAI_CREDENTIALS")
- )
-
- api_base = api_base or litellm.api_base or get_secret("VERTEXAI_API_BASE")
-
- new_params = safe_deep_copy(optional_params or {})
- model_route = get_vertex_ai_model_route(
- model=model, litellm_params=litellm_params
- )
-
- if model_route == VertexAIModelRoute.PARTNER_MODELS:
- model_response = vertex_partner_models_chat_completion.completion(
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=new_params,
- litellm_params=litellm_params, # type: ignore
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- api_base=api_base,
- vertex_location=vertex_ai_location,
- vertex_project=vertex_ai_project,
- vertex_credentials=vertex_credentials,
- logging_obj=logging,
- acompletion=acompletion,
- headers=headers,
- custom_prompt_dict=custom_prompt_dict,
- timeout=timeout,
- client=client,
- )
- elif model_route == VertexAIModelRoute.GEMINI:
- model_response = vertex_chat_completion.completion( # type: ignore
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=new_params,
- litellm_params=litellm_params, # type: ignore
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- vertex_location=vertex_ai_location,
- vertex_project=vertex_ai_project,
- vertex_credentials=vertex_credentials,
- gemini_api_key=None,
- logging_obj=logging,
- acompletion=acompletion,
- timeout=timeout,
- custom_llm_provider=custom_llm_provider, # type: ignore
- client=client,
- api_base=api_base,
- extra_headers=headers,
- )
- elif model_route == VertexAIModelRoute.GEMMA:
- # Vertex Gemma Models with custom prediction endpoint
- model_response = vertex_gemma_chat_completion.completion(
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=new_params,
- litellm_params=litellm_params, # type: ignore
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- api_base=api_base,
- vertex_location=vertex_ai_location,
- vertex_project=vertex_ai_project,
- vertex_credentials=vertex_credentials,
- logging_obj=logging,
- acompletion=acompletion,
- headers=headers,
- custom_prompt_dict=custom_prompt_dict,
- timeout=timeout,
- client=client,
- )
- elif model_route == VertexAIModelRoute.MODEL_GARDEN:
- # Vertex Model Garden - OpenAI compatible models
- model_response = vertex_model_garden_chat_completion.completion(
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=new_params,
- litellm_params=litellm_params, # type: ignore
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- api_base=api_base,
- vertex_location=vertex_ai_location,
- vertex_project=vertex_ai_project,
- vertex_credentials=vertex_credentials,
- logging_obj=logging,
- acompletion=acompletion,
- headers=headers,
- custom_prompt_dict=custom_prompt_dict,
- timeout=timeout,
- client=client,
- )
- elif model_route == VertexAIModelRoute.AGENT_ENGINE:
- # Vertex AI Agent Engine (Reasoning Engines)
- from litellm.llms.vertex_ai.agent_engine.transformation import (
- VertexAgentEngineConfig,
- )
-
- vertex_agent_engine_config = VertexAgentEngineConfig()
-
- # Update litellm_params with vertex credentials
- litellm_params["vertex_project"] = vertex_ai_project
- litellm_params["vertex_location"] = vertex_ai_location
- litellm_params["vertex_credentials"] = vertex_credentials
-
- model_response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- model_response=model_response,
- optional_params=new_params,
- litellm_params=litellm_params, # type: ignore
- encoding=_get_encoding(),
- api_key=None,
- api_base=api_base,
- logging_obj=logging,
- acompletion=acompletion,
- timeout=timeout,
- client=client,
- custom_llm_provider="vertex_ai",
- provider_config=vertex_agent_engine_config,
- headers=headers or {},
- )
- else: # VertexAIModelRoute.NON_GEMINI
- model_response = vertex_ai_non_gemini.completion(
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=new_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- vertex_location=vertex_ai_location,
- vertex_project=vertex_ai_project,
- vertex_credentials=vertex_credentials,
- logging_obj=logging,
- acompletion=acompletion,
- )
-
- if (
- "stream" in optional_params
- and optional_params["stream"] is True
- and acompletion is False
- ):
- response = CustomStreamWrapper(
- model_response,
- model,
- custom_llm_provider="vertex_ai",
- logging_obj=logging,
- )
- return response
- response = model_response
+ response = _complete_vertex_ai(_dispatch_ctx)
elif custom_llm_provider == "predibase":
- tenant_id = (
- optional_params.pop("tenant_id", None)
- or optional_params.pop("predibase_tenant_id", None)
- or litellm.predibase_tenant_id
- or get_secret("PREDIBASE_TENANT_ID")
- )
-
- if tenant_id is None:
- raise ValueError(
- "Missing Predibase Tenant ID - Required for making the request. Set dynamically (e.g. `completion(..tenant_id=)`) or in env - `PREDIBASE_TENANT_ID`."
- )
-
- api_base = (
- api_base
- or optional_params.pop("api_base", None)
- or optional_params.pop("base_url", None)
- or litellm.api_base
- or get_secret("PREDIBASE_API_BASE")
- )
-
- api_key = (
- api_key
- or litellm.api_key
- or litellm.predibase_key
- or get_secret("PREDIBASE_API_KEY")
- )
-
- _model_response = predibase_chat_completions.completion(
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- logging_obj=logging,
- acompletion=acompletion,
- api_base=api_base,
- custom_prompt_dict=custom_prompt_dict,
- api_key=api_key,
- tenant_id=tenant_id,
- timeout=timeout,
- )
-
- if (
- "stream" in optional_params
- and optional_params["stream"] is True
- and acompletion is False
- ):
- return _model_response
- response = _model_response
+ response = _complete_predibase(_dispatch_ctx)
elif custom_llm_provider == "text-completion-codestral":
- api_base = (
- api_base
- or optional_params.pop("api_base", None)
- or optional_params.pop("base_url", None)
- or litellm.api_base
- or "https://codestral.mistral.ai/v1/fim/completions"
- )
-
- api_key = api_key or litellm.api_key or get_secret("CODESTRAL_API_KEY")
-
- text_completion_model_response = litellm.TextCompletionResponse(
- stream=stream
- )
-
- _model_response = codestral_text_completions.completion( # type: ignore
- model=model,
- messages=messages,
- model_response=text_completion_model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- logging_obj=logging,
- acompletion=acompletion,
- api_base=api_base,
- custom_prompt_dict=custom_prompt_dict,
- api_key=api_key,
- timeout=timeout,
- )
-
- if (
- "stream" in optional_params
- and optional_params["stream"] is True
- and acompletion is False
- ):
- return _model_response
- response = _model_response
+ response = _complete_text_completion_codestral(_dispatch_ctx)
elif custom_llm_provider == "text-completion-inception":
- passed_api_base = (
- api_base
- or optional_params.pop("api_base", None)
- or optional_params.pop("base_url", None)
- )
- api_base = (
- passed_api_base
- or get_secret_str("INCEPTION_API_BASE")
- or "https://api.inceptionlabs.ai/v1"
- )
- # FIM is served at `/v1/fim/completions`; the OpenAI client appends
- # `/completions`, so point it at the `/v1/fim` base.
- api_base = api_base.rstrip("/")
- if not api_base.endswith("/fim"):
- api_base += "/fim"
-
- # Don't forward the server-managed Inception key to a caller-supplied
- # api_base; only resolve it for the default/server base, or when the
- # caller passes their own key.
- if passed_api_base is None or api_key:
- api_key = (
- api_key
- or litellm.inception_key
- or get_secret_str("INCEPTION_API_KEY")
- )
-
- _response = openai_text_completions.completion(
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- api_key=api_key, # type: ignore[arg-type]
- custom_llm_provider="text-completion-inception",
- api_base=api_base,
- acompletion=acompletion,
- client=client,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- timeout=timeout, # type: ignore
- )
-
- if (
- optional_params.get("stream", False) is False
- and acompletion is False
- and text_completion is False
- ):
- _response = litellm.OpenAITextCompletionConfig().convert_to_chat_model_response_object(
- response_object=_response, model_response_object=model_response
- )
-
- if optional_params.get("stream", False) or acompletion is True:
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=_response,
- additional_args={"headers": headers},
- )
- response = _response
+ response = _complete_text_completion_inception(_dispatch_ctx)
elif custom_llm_provider in ("sagemaker_chat", "sagemaker_nova"):
# boto3 reads keys from .env
# sagemaker_chat: HF Messages API endpoints
# sagemaker_nova: Nova models on SageMaker (OpenAI-compatible)
- model_response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- custom_llm_provider=custom_llm_provider,
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
-
- ## RESPONSE OBJECT
- response = model_response
+ response = _complete_sagemaker_chat(_dispatch_ctx)
elif custom_llm_provider == "sagemaker":
# boto3 reads keys from .env
- model_response = sagemaker_llm.completion(
- model=model,
- messages=messages,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- custom_prompt_dict=custom_prompt_dict,
- hf_model_name=hf_model_name,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- logging_obj=logging,
- acompletion=acompletion,
- )
-
- ## RESPONSE OBJECT
- response = model_response
+ response = _complete_sagemaker(_dispatch_ctx)
elif custom_llm_provider == "bedrock":
# boto3 reads keys from .env
- custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
-
- if "aws_bedrock_client" in optional_params:
- verbose_logger.warning(
- "'aws_bedrock_client' is a deprecated param. Please move to another auth method - https://docs.litellm.ai/docs/providers/bedrock#boto3---authentication."
- )
- # Extract credentials for legacy boto3 client and pass thru to httpx
- aws_bedrock_client = optional_params.pop("aws_bedrock_client")
- creds = aws_bedrock_client._get_credentials().get_frozen_credentials()
-
- if creds.access_key:
- optional_params["aws_access_key_id"] = creds.access_key
- if creds.secret_key:
- optional_params["aws_secret_access_key"] = creds.secret_key
- if creds.token:
- optional_params["aws_session_token"] = creds.token
- if (
- "aws_region_name" not in optional_params
- or optional_params["aws_region_name"] is None
- ):
- optional_params["aws_region_name"] = (
- aws_bedrock_client.meta.region_name
- )
-
- bedrock_route = BedrockModelInfo.get_bedrock_route(model)
- if bedrock_route == "claude_platform":
- provider_config = ProviderConfigManager.get_provider_chat_config(
- model=model,
- provider=LlmProviders.BEDROCK,
- )
- model = BedrockModelInfo.get_claude_platform_model(model)
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="bedrock",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- client=client,
- provider_config=provider_config,
- )
- return response
- elif bedrock_route == "converse":
- model = model.replace("converse/", "")
- response = bedrock_converse_chat_completion.completion(
- model=model,
- messages=messages,
- custom_prompt_dict=custom_prompt_dict,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params, # type: ignore
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- logging_obj=logging,
- extra_headers=headers, # Use merged headers instead of original extra_headers
- timeout=timeout,
- acompletion=acompletion,
- client=client,
- api_base=api_base,
- api_key=api_key,
- )
- elif bedrock_route == "converse_like":
- model = model.replace("converse_like/", "")
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- custom_llm_provider="bedrock",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
- else:
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- custom_llm_provider="bedrock",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- client=client,
- )
+ response = _complete_bedrock(_dispatch_ctx)
elif custom_llm_provider == "watsonx":
- response = watsonx_chat_completion.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- print_verbose=print_verbose,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- timeout=timeout, # type: ignore
- custom_prompt_dict=custom_prompt_dict,
- client=client, # pass AsyncOpenAI, OpenAI client
- encoding=_get_encoding(),
- custom_llm_provider="watsonx",
- )
+ response = _complete_watsonx(_dispatch_ctx)
elif custom_llm_provider == "watsonx_text":
- api_key = (
- api_key
- or optional_params.pop("apikey", None)
- or get_secret_str("WATSONX_APIKEY")
- or get_secret_str("WATSONX_API_KEY")
- or get_secret_str("WX_API_KEY")
- )
-
- api_base = (
- api_base
- or optional_params.pop(
- "url",
- optional_params.pop(
- "api_base", optional_params.pop("base_url", None)
- ),
- )
- or get_secret_str("WATSONX_API_BASE")
- or get_secret_str("WATSONX_URL")
- or get_secret_str("WX_URL")
- or get_secret_str("WML_URL")
- )
-
- wx_credentials = optional_params.pop(
- "wx_credentials",
- optional_params.pop(
- "watsonx_credentials", None
- ), # follow {provider}_credentials, same as vertex ai
- )
-
- token: Optional[str] = None
- if wx_credentials is not None:
- api_base = wx_credentials.get("url", api_base)
- api_key = wx_credentials.get(
- "apikey", wx_credentials.get("api_key", api_key)
- )
- token = wx_credentials.get(
- "token",
- wx_credentials.get(
- "watsonx_token", None
- ), # follow format of {provider}_token, same as azure - e.g. 'azure_ad_token=..'
- )
-
- if token is not None:
- optional_params["token"] = token
-
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="watsonx_text",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
+ response = _complete_watsonx_text(_dispatch_ctx)
elif custom_llm_provider == "vllm":
- custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
- model_response = vllm_handler.completion(
- model=model,
- messages=messages,
- custom_prompt_dict=custom_prompt_dict,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- logging_obj=logging,
- )
-
- if (
- "stream" in optional_params and optional_params["stream"] is True
- ): ## [BETA]
- # don't try to access stream object,
- response = CustomStreamWrapper(
- model_response,
- model,
- custom_llm_provider="vllm",
- logging_obj=logging,
- )
- return response
-
- ## RESPONSE OBJECT
- response = model_response
+ response = _complete_vllm(_dispatch_ctx)
elif custom_llm_provider == "ollama":
- api_base = (
- litellm.api_base
- or api_base
- or get_secret("OLLAMA_API_BASE")
- or "http://localhost:11434"
- )
- if api_key is not None and "Authorization" not in headers:
- headers["Authorization"] = f"Bearer {api_key}"
-
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="ollama",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
+ response = _complete_ollama(_dispatch_ctx)
elif custom_llm_provider == "ollama_chat":
- api_base = (
- litellm.api_base
- or api_base
- or get_secret("OLLAMA_API_BASE")
- or "http://localhost:11434"
- )
-
- api_key = (
- api_key
- or litellm.ollama_key
- or os.environ.get("OLLAMA_API_KEY")
- or litellm.api_key
- )
- if api_key is not None and "Authorization" not in headers:
- headers["Authorization"] = f"Bearer {api_key}"
-
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="ollama_chat",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- client=client,
- )
+ response = _complete_ollama_chat(_dispatch_ctx)
elif custom_llm_provider == "triton":
- api_base = litellm.api_base or api_base
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider=custom_llm_provider,
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- )
+ response = _complete_triton(_dispatch_ctx)
elif custom_llm_provider == "cloudflare":
- api_key = (
- api_key
- or litellm.cloudflare_api_key
- or litellm.api_key
- or get_secret("CLOUDFLARE_API_KEY")
- )
- account_id = get_secret("CLOUDFLARE_ACCOUNT_ID")
- api_base = (
- api_base
- or litellm.api_base
- or get_secret("CLOUDFLARE_API_BASE")
- or f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/"
- )
-
- custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="cloudflare",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging, # model call logging done inside the class as we make need to modify I/O to fit aleph alpha's requirements
- )
+ response = _complete_cloudflare(_dispatch_ctx)
elif custom_llm_provider == "petals" or model in litellm.petals_models:
- api_base = api_base or litellm.api_base
-
- custom_llm_provider = "petals"
- stream = optional_params.pop("stream", False)
- model_response = petals_handler.completion(
- model=model,
- messages=messages,
- api_base=api_base,
- model_response=model_response,
- print_verbose=print_verbose,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- encoding=_get_encoding(),
- logging_obj=logging,
- client=client,
- )
- if stream is True: ## [BETA]
- # Fake streaming for petals
- resp_string = model_response["choices"][0]["message"]["content"]
- response = CustomStreamWrapper(
- resp_string,
- model,
- custom_llm_provider="petals",
- logging_obj=logging,
- )
- return response
- response = model_response
+ response = _complete_petals(_dispatch_ctx)
elif custom_llm_provider == "snowflake" or model in litellm.snowflake_models:
- try:
- client = (
- HTTPHandler(timeout=timeout) if stream is False else None
- ) # Keep this here, otherwise, the httpx.client closes and streaming is impossible
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- )
-
- except Exception as e:
- ## LOGGING - log the original exception returned
- logging.post_call(
- input=messages,
- api_key=api_key,
- original_response=str(e),
- additional_args={"headers": headers},
- )
- raise e
+ response = _complete_snowflake(_dispatch_ctx)
elif custom_llm_provider == "gradient_ai":
- api_base = litellm.api_base or api_base
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider="gradient_ai",
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- )
+ response = _complete_gradient_ai(_dispatch_ctx)
elif custom_llm_provider == "bytez":
- api_key = (
- api_key
- or litellm.bytez_key
- or get_secret_str("BYTEZ_API_KEY")
- or litellm.api_key
- )
-
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=bytez_transformation,
- )
-
- pass
+ response = _complete_bytez(_dispatch_ctx)
elif custom_llm_provider == "lemonade":
- api_key = (
- api_key
- or litellm.lemonade_key
- or get_secret_str("LEMONADE_API_KEY")
- or litellm.api_key
- )
-
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=lemonade_transformation,
- )
-
- pass
+ response = _complete_lemonade(_dispatch_ctx)
elif custom_llm_provider == "ovhcloud" or model in litellm.ovhcloud_models:
- api_key = (
- api_key
- or litellm.ovhcloud_key
- or get_secret_str("OVHCLOUD_API_KEY")
- or litellm.api_key
- )
-
- api_base = (
- api_base
- or litellm.api_base
- or get_secret_str("OVHCLOUD_API_BASE")
- or "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1"
- )
-
- response = base_llm_http_handler.completion(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- timeout=timeout, # type: ignore
- client=client,
- custom_llm_provider=custom_llm_provider,
- encoding=_get_encoding(),
- stream=stream,
- provider_config=ovhcloud_transformation,
- )
-
- pass
+ response = _complete_ovhcloud(_dispatch_ctx)
elif custom_llm_provider == "custom":
- url = litellm.api_base or api_base or ""
- if url is None or url == "":
- raise ValueError(
- "api_base not set. Set api_base or litellm.api_base for custom endpoints"
- )
-
- """
- assume input to custom LLM api bases follow this format:
- resp = litellm.module_level_client.post(
- 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,
- }
- }
- )
-
- """
- prompt = " ".join([message["content"] for message in messages]) # type: ignore
- resp = litellm.module_level_client.post(
- url,
- headers=headers,
- json={
- "model": model,
- "params": {
- "prompt": [prompt],
- "max_tokens": max_tokens,
- "temperature": temperature,
- "top_p": top_p,
- "top_k": kwargs.get("top_k"),
- },
- **kwargs.get("extra_body", {}),
- },
- )
- response_json = resp.json()
- """
- assume all responses from custom api_bases of this format:
- {
- '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'
- }
- ]
- }
- """
- string_response = response_json["data"][0]["output"][0]
- ## RESPONSE OBJECT
- model_response.choices[0].message.content = string_response # type: ignore
- model_response.created = int(time.time())
- model_response.model = model
- response = model_response
+ response = _complete_custom(_dispatch_ctx)
elif (
custom_llm_provider in litellm._custom_providers
): # Assume custom LLM provider
# Get the Custom Handler
- custom_handler: Optional[CustomLLM] = None
- for item in litellm.custom_provider_map:
- if item["provider"] == custom_llm_provider:
- custom_handler = item["custom_handler"]
-
- if custom_handler is None:
- raise LiteLLMUnknownProvider(
- model=model, custom_llm_provider=custom_llm_provider
- )
-
- ## ROUTE LLM CALL ##
- handler_fn = custom_chat_llm_router(
- async_fn=acompletion, stream=stream, custom_llm=custom_handler
- )
-
- headers = headers or litellm.headers or {}
-
- ## CALL FUNCTION
- response = handler_fn(
- model=model,
- messages=messages,
- headers=headers,
- model_response=model_response,
- print_verbose=print_verbose,
- api_key=api_key,
- api_base=api_base,
- acompletion=acompletion,
- logging_obj=logging,
- optional_params=optional_params,
- litellm_params=litellm_params,
- logger_fn=logger_fn,
- timeout=timeout, # type: ignore
- custom_prompt_dict=custom_prompt_dict,
- client=client, # pass AsyncOpenAI, OpenAI client
- encoding=_get_encoding(),
- )
- if stream is True:
- return CustomStreamWrapper(
- completion_stream=response,
- model=model,
- custom_llm_provider=custom_llm_provider,
- logging_obj=logging,
- )
+ response = _complete_custom_providers(_dispatch_ctx)
elif custom_llm_provider == "langgraph":
# LangGraph - Agent Runtime Provider
- from litellm.llms.langgraph.chat.transformation import LangGraphConfig
-
- (
- api_base,
- api_key,
- ) = LangGraphConfig()._get_openai_compatible_provider_info(
- api_base=api_base or litellm.api_base,
- api_key=api_key or litellm.api_key,
- )
-
- headers = headers or litellm.headers
-
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider=custom_llm_provider,
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- client=client,
- )
+ response = _complete_langgraph(_dispatch_ctx)
elif custom_llm_provider == "langflow":
# LangFlow - Visual AI Agent Platform
- from litellm.llms.langflow.chat.transformation import LangFlowConfig
-
- (
- api_base,
- api_key,
- ) = LangFlowConfig()._get_openai_compatible_provider_info(
- api_base=api_base or litellm.api_base,
- api_key=api_key or litellm.api_key,
- )
-
- headers = headers or litellm.headers
-
- response = base_llm_http_handler.completion(
- model=model,
- stream=stream,
- messages=messages,
- acompletion=acompletion,
- api_base=api_base,
- model_response=model_response,
- optional_params=optional_params,
- litellm_params=litellm_params,
- shared_session=shared_session,
- custom_llm_provider=custom_llm_provider,
- timeout=timeout,
- headers=headers,
- encoding=_get_encoding(),
- api_key=api_key,
- logging_obj=logging,
- client=client,
- )
+ response = _complete_langflow(_dispatch_ctx)
else:
raise LiteLLMUnknownProvider(
diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json
index 7a5f8b9e1e3..6ebac7efc8d 100644
--- a/litellm/model_prices_and_context_window_backup.json
+++ b/litellm/model_prices_and_context_window_backup.json
@@ -571,7 +571,7 @@
"output_vector_size": 1536
},
"amazon.titan-embed-text-v2:0": {
- "input_cost_per_token": 2e-07,
+ "input_cost_per_token": 2e-08,
"litellm_provider": "bedrock",
"max_input_tokens": 8192,
"max_tokens": 8192,
@@ -10443,7 +10443,8 @@
"fast": 6.0
},
"supports_output_config": true,
- "supports_max_reasoning_effort": true
+ "supports_max_reasoning_effort": true,
+ "supports_speed": true
},
"claude-opus-4-6-20260205": {
"cache_creation_input_token_cost": 6.25e-06,
@@ -10476,7 +10477,8 @@
"fast": 6.0
},
"supports_max_reasoning_effort": true,
- "supports_output_config": true
+ "supports_output_config": true,
+ "supports_speed": true
},
"claude-opus-4-7": {
"cache_creation_input_token_cost": 6.25e-06,
@@ -10511,7 +10513,8 @@
"us": 1.1,
"fast": 6.0
},
- "supports_output_config": true
+ "supports_output_config": true,
+ "supports_speed": true
},
"claude-opus-4-7-20260416": {
"cache_creation_input_token_cost": 6.25e-06,
@@ -10546,7 +10549,8 @@
"us": 1.1,
"fast": 6.0
},
- "supports_output_config": true
+ "supports_output_config": true,
+ "supports_speed": true
},
"claude-fable-5": {
"cache_creation_input_token_cost": 1.25e-05,
@@ -10615,7 +10619,8 @@
"us": 1.1,
"fast": 2.0
},
- "supports_output_config": true
+ "supports_output_config": true,
+ "supports_speed": true
},
"claude-sonnet-4-20250514": {
"deprecation_date": "2026-05-14",
@@ -10684,6 +10689,268 @@
"mode": "chat",
"output_cost_per_token": 1.923e-06
},
+ "cloudflare/@cf/openai/gpt-oss-120b": {
+ "input_cost_per_token": 3.5e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 128000,
+ "max_output_tokens": 128000,
+ "max_tokens": 128000,
+ "mode": "chat",
+ "output_cost_per_token": 7.5e-07,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/google/gemma-2b-it-lora": {
+ "input_cost_per_token": 0.0,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 8192,
+ "max_output_tokens": 8192,
+ "max_tokens": 8192,
+ "mode": "chat",
+ "output_cost_per_token": 0.0
+ },
+ "cloudflare/@cf/meta/llama-3.2-3b-instruct": {
+ "input_cost_per_token": 5.09e-08,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 80000,
+ "max_output_tokens": 80000,
+ "max_tokens": 80000,
+ "mode": "chat",
+ "output_cost_per_token": 3.35e-07
+ },
+ "cloudflare/@cf/meta/llama-guard-3-8b": {
+ "input_cost_per_token": 4.84e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 131072,
+ "max_output_tokens": 131072,
+ "max_tokens": 131072,
+ "mode": "chat",
+ "output_cost_per_token": 3e-08
+ },
+ "cloudflare/@cf/mistral/mistral-7b-instruct-v0.2-lora": {
+ "input_cost_per_token": 0.0,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 15000,
+ "max_output_tokens": 15000,
+ "max_tokens": 15000,
+ "mode": "chat",
+ "output_cost_per_token": 0.0
+ },
+ "cloudflare/@cf/moonshotai/kimi-k2.7-code": {
+ "cache_read_input_token_cost": 1.9e-07,
+ "input_cost_per_token": 9.5e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 262144,
+ "max_output_tokens": 262144,
+ "max_tokens": 262144,
+ "mode": "chat",
+ "output_cost_per_token": 4e-06,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b": {
+ "input_cost_per_token": 4.97e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 80000,
+ "max_output_tokens": 80000,
+ "max_tokens": 80000,
+ "mode": "chat",
+ "output_cost_per_token": 4.881e-06,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/meta/llama-3.1-8b-instruct-fp8": {
+ "input_cost_per_token": 1.52e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 32000,
+ "max_output_tokens": 32000,
+ "max_tokens": 32000,
+ "mode": "chat",
+ "output_cost_per_token": 2.87e-07
+ },
+ "cloudflare/@cf/meta/llama-3.2-1b-instruct": {
+ "input_cost_per_token": 2.7e-08,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 60000,
+ "max_output_tokens": 60000,
+ "max_tokens": 60000,
+ "mode": "chat",
+ "output_cost_per_token": 2.01e-07
+ },
+ "cloudflare/@cf/moonshotai/kimi-k2.6": {
+ "cache_read_input_token_cost": 1.6e-07,
+ "input_cost_per_token": 9.5e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 262144,
+ "max_output_tokens": 262144,
+ "max_tokens": 262144,
+ "mode": "chat",
+ "output_cost_per_token": 4e-06,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/zai-org/glm-4.7-flash": {
+ "input_cost_per_token": 6.05e-08,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 131072,
+ "max_output_tokens": 131072,
+ "max_tokens": 131072,
+ "mode": "chat",
+ "output_cost_per_token": 4e-07,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/meta-llama/llama-2-7b-chat-hf-lora": {
+ "input_cost_per_token": 0.0,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 8192,
+ "max_output_tokens": 8192,
+ "max_tokens": 8192,
+ "mode": "chat",
+ "output_cost_per_token": 0.0
+ },
+ "cloudflare/@cf/meta/llama-3.3-70b-instruct-fp8-fast": {
+ "input_cost_per_token": 2.93e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 24000,
+ "max_output_tokens": 24000,
+ "max_tokens": 24000,
+ "mode": "chat",
+ "output_cost_per_token": 2.253e-06,
+ "supports_function_calling": true
+ },
+ "cloudflare/@cf/ibm-granite/granite-4.0-h-micro": {
+ "input_cost_per_token": 1.7e-08,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 131000,
+ "max_output_tokens": 131000,
+ "max_tokens": 131000,
+ "mode": "chat",
+ "output_cost_per_token": 1.12e-07,
+ "supports_function_calling": true
+ },
+ "cloudflare/@cf/qwen/qwen2.5-coder-32b-instruct": {
+ "input_cost_per_token": 6.6e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 32768,
+ "max_output_tokens": 32768,
+ "max_tokens": 32768,
+ "mode": "chat",
+ "output_cost_per_token": 1e-06
+ },
+ "cloudflare/@cf/zai-org/glm-5.2": {
+ "cache_read_input_token_cost": 2.6e-07,
+ "input_cost_per_token": 1.4e-06,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 262144,
+ "max_output_tokens": 262144,
+ "max_tokens": 262144,
+ "mode": "chat",
+ "output_cost_per_token": 4.4e-06,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/nvidia/nemotron-3-120b-a12b": {
+ "input_cost_per_token": 5e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 256000,
+ "max_output_tokens": 256000,
+ "max_tokens": 256000,
+ "mode": "chat",
+ "output_cost_per_token": 1.5e-06,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/aisingapore/gemma-sea-lion-v4-27b-it": {
+ "input_cost_per_token": 3.51e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 128000,
+ "max_output_tokens": 128000,
+ "max_tokens": 128000,
+ "mode": "chat",
+ "output_cost_per_token": 5.55e-07
+ },
+ "cloudflare/@cf/qwen/qwen3-30b-a3b-fp8": {
+ "input_cost_per_token": 5.09e-08,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 32768,
+ "max_output_tokens": 32768,
+ "max_tokens": 32768,
+ "mode": "chat",
+ "output_cost_per_token": 3.35e-07,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/google/gemma-7b-it-lora": {
+ "input_cost_per_token": 0.0,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 3500,
+ "max_output_tokens": 3500,
+ "max_tokens": 3500,
+ "mode": "chat",
+ "output_cost_per_token": 0.0
+ },
+ "cloudflare/@cf/google/gemma-4-26b-a4b-it": {
+ "input_cost_per_token": 1e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 256000,
+ "max_output_tokens": 256000,
+ "max_tokens": 256000,
+ "mode": "chat",
+ "output_cost_per_token": 3e-07,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/mistralai/mistral-small-3.1-24b-instruct": {
+ "input_cost_per_token": 3.51e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 128000,
+ "max_output_tokens": 128000,
+ "max_tokens": 128000,
+ "mode": "chat",
+ "output_cost_per_token": 5.55e-07,
+ "supports_function_calling": true
+ },
+ "cloudflare/@cf/meta/llama-3.2-11b-vision-instruct": {
+ "input_cost_per_token": 4.85e-08,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 128000,
+ "max_output_tokens": 128000,
+ "max_tokens": 128000,
+ "mode": "chat",
+ "output_cost_per_token": 6.76e-07,
+ "supports_vision": true
+ },
+ "cloudflare/@cf/openai/gpt-oss-20b": {
+ "input_cost_per_token": 2e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 128000,
+ "max_output_tokens": 128000,
+ "max_tokens": 128000,
+ "mode": "chat",
+ "output_cost_per_token": 3e-07,
+ "supports_function_calling": true,
+ "supports_reasoning": true
+ },
+ "cloudflare/@cf/meta/llama-4-scout-17b-16e-instruct": {
+ "input_cost_per_token": 2.7e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 131000,
+ "max_output_tokens": 131000,
+ "max_tokens": 131000,
+ "mode": "chat",
+ "output_cost_per_token": 8.5e-07,
+ "supports_function_calling": true
+ },
+ "cloudflare/@cf/qwen/qwq-32b": {
+ "input_cost_per_token": 6.6e-07,
+ "litellm_provider": "cloudflare",
+ "max_input_tokens": 24000,
+ "max_output_tokens": 24000,
+ "max_tokens": 24000,
+ "mode": "chat",
+ "output_cost_per_token": 1e-06,
+ "supports_reasoning": true
+ },
"codestral/codestral-2405": {
"input_cost_per_token": 0.0,
"litellm_provider": "codestral",
@@ -15011,7 +15278,7 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
- "supports_vision": false
+ "supports_vision": true
},
"fireworks_ai/accounts/fireworks/models/mixtral-8x22b-instruct-hf": {
"input_cost_per_token": 1.2e-06,
@@ -15314,7 +15581,7 @@
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
- "supports_vision": false
+ "supports_vision": true
},
"fireworks_ai/qwen3p7-plus": {
"cache_read_input_token_cost": 8e-08,
@@ -20088,8 +20355,6 @@
"output_cost_per_token": 8e-06,
"output_cost_per_token_batches": 4e-06,
"output_cost_per_token_priority": 1.4e-05,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -20163,8 +20428,6 @@
"output_cost_per_token": 1.6e-06,
"output_cost_per_token_batches": 8e-07,
"output_cost_per_token_priority": 2.8e-06,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -20238,8 +20501,6 @@
"output_cost_per_token": 4e-07,
"output_cost_per_token_batches": 2e-07,
"output_cost_per_token_priority": 8e-07,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -20311,8 +20572,6 @@
"output_cost_per_token": 1e-05,
"output_cost_per_token_batches": 5e-06,
"output_cost_per_token_priority": 1.7e-05,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
@@ -20354,8 +20613,6 @@
"mode": "chat",
"output_cost_per_token": 1e-05,
"output_cost_per_token_batches": 5e-06,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
@@ -20377,8 +20634,6 @@
"mode": "chat",
"output_cost_per_token": 1e-05,
"output_cost_per_token_batches": 5e-06,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
@@ -20667,8 +20922,6 @@
"output_cost_per_token": 6e-07,
"output_cost_per_token_batches": 3e-07,
"output_cost_per_token_priority": 1e-06,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
@@ -21372,8 +21625,6 @@
"output_cost_per_token": 1e-05,
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -21767,6 +22018,8 @@
"output_cost_per_token_flex": 1.5e-05,
"output_cost_per_token_batches": 1.5e-05,
"output_cost_per_token_priority": 6e-05,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -21815,6 +22068,8 @@
"output_cost_per_token_flex": 1.5e-05,
"output_cost_per_token_batches": 1.5e-05,
"output_cost_per_token_priority": 6e-05,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -21859,6 +22114,8 @@
"output_cost_per_token_above_272k_tokens": 0.00027,
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/responses",
"/v1/batch"
@@ -21903,6 +22160,8 @@
"output_cost_per_token_above_272k_tokens": 0.00027,
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/responses",
"/v1/batch"
@@ -21951,6 +22210,8 @@
"output_cost_per_token_flex": 7.5e-06,
"output_cost_per_token_batches": 7.5e-06,
"output_cost_per_token_priority": 3e-05,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -21998,6 +22259,8 @@
"output_cost_per_token_flex": 7.5e-06,
"output_cost_per_token_batches": 7.5e-06,
"output_cost_per_token_priority": 3e-05,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -22038,6 +22301,8 @@
"output_cost_per_token_above_272k_tokens": 0.00027,
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/responses",
"/v1/batch"
@@ -22081,6 +22346,8 @@
"output_cost_per_token_above_272k_tokens": 0.00027,
"output_cost_per_token_flex": 9e-05,
"output_cost_per_token_batches": 9e-05,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/responses",
"/v1/batch"
@@ -22126,6 +22393,8 @@
"output_cost_per_token_flex": 2.25e-06,
"output_cost_per_token_batches": 2.25e-06,
"output_cost_per_token_priority": 9e-06,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -22172,6 +22441,8 @@
"output_cost_per_token_flex": 2.25e-06,
"output_cost_per_token_batches": 2.25e-06,
"output_cost_per_token_priority": 9e-06,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -22215,6 +22486,8 @@
"output_cost_per_token": 1.25e-06,
"output_cost_per_token_flex": 6.25e-07,
"output_cost_per_token_batches": 6.25e-07,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -22258,6 +22531,8 @@
"output_cost_per_token": 1.25e-06,
"output_cost_per_token_flex": 6.25e-07,
"output_cost_per_token_batches": 6.25e-07,
+ "regional_processing_uplift_multiplier_eu": 1.10,
+ "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -22296,8 +22571,6 @@
"mode": "responses",
"output_cost_per_token": 0.00012,
"output_cost_per_token_batches": 6e-05,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/batch",
"/v1/responses"
@@ -22704,8 +22977,6 @@
"output_cost_per_token": 2e-06,
"output_cost_per_token_flex": 1e-06,
"output_cost_per_token_priority": 3.6e-06,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
@@ -22787,8 +23058,6 @@
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"max_tokens": 128000,
- "regional_processing_uplift_multiplier_eu": 1.10,
- "regional_processing_uplift_multiplier_us": 1.10,
"mode": "chat",
"output_cost_per_token": 4e-07,
"output_cost_per_token_flex": 2e-07,
@@ -39908,24 +40177,6 @@
"litellm_provider": "fireworks_ai",
"mode": "chat"
},
- "fireworks_ai/accounts/fireworks/models/whisper-v3": {
- "max_tokens": 4096,
- "max_input_tokens": 4096,
- "max_output_tokens": 4096,
- "input_cost_per_token": 0.0,
- "output_cost_per_token": 0.0,
- "litellm_provider": "fireworks_ai",
- "mode": "audio_transcription"
- },
- "fireworks_ai/accounts/fireworks/models/whisper-v3-turbo": {
- "max_tokens": 4096,
- "max_input_tokens": 4096,
- "max_output_tokens": 4096,
- "input_cost_per_token": 0.0,
- "output_cost_per_token": 0.0,
- "litellm_provider": "fireworks_ai",
- "mode": "audio_transcription"
- },
"fireworks_ai/accounts/fireworks/models/yi-34b": {
"max_tokens": 4096,
"max_input_tokens": 4096,
@@ -43061,6 +43312,40 @@
"supports_tool_choice": true,
"supports_vision": false
},
+ "darkbloom/gemma-4-26b": {
+ "input_cost_per_token": 3e-08,
+ "litellm_provider": "darkbloom",
+ "max_input_tokens": 131072,
+ "max_output_tokens": 32768,
+ "max_tokens": 32768,
+ "mode": "chat",
+ "output_cost_per_token": 1.65e-07,
+ "source": "https://www.darkbloom.dev/",
+ "supported_endpoints": [
+ "/v1/chat/completions"
+ ],
+ "supports_function_calling": true,
+ "supports_native_streaming": true,
+ "supports_system_messages": true,
+ "supports_tool_choice": true
+ },
+ "darkbloom/gpt-oss-20b": {
+ "input_cost_per_token": 1.45e-08,
+ "litellm_provider": "darkbloom",
+ "max_input_tokens": 131072,
+ "max_output_tokens": 32768,
+ "max_tokens": 32768,
+ "mode": "chat",
+ "output_cost_per_token": 7e-08,
+ "source": "https://www.darkbloom.dev/",
+ "supported_endpoints": [
+ "/v1/chat/completions"
+ ],
+ "supports_function_calling": true,
+ "supports_native_streaming": true,
+ "supports_system_messages": true,
+ "supports_tool_choice": true
+ },
"deepseek/deepseek-v4-pro": {
"cache_creation_input_token_cost": 0.0,
"cache_read_input_token_cost": 3.625e-09,
diff --git a/litellm/ocr/main.py b/litellm/ocr/main.py
index b27082c361a..3a9ef8db804 100644
--- a/litellm/ocr/main.py
+++ b/litellm/ocr/main.py
@@ -10,7 +10,7 @@ import os
import re
from functools import partial
from io import IOBase
-from typing import Any, Coroutine, Dict, Optional, Union
+from typing import Any, Callable, Coroutine, Dict, Optional, Union, cast
import httpx
@@ -20,6 +20,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.ocr.rust_bridge import RustOcr, load_rust_ocr, rust_ocr_enabled
from litellm.types.router import GenericLiteLLMParams
from litellm.utils import ProviderConfigManager, client
@@ -28,6 +29,82 @@ base_llm_http_handler = BaseLLMHTTPHandler()
#################################################
+def _timeout_to_seconds(
+ timeout: Optional[Union[float, httpx.Timeout]],
+) -> Optional[float]:
+ """Convert the Python OCR timeout to a single seconds value for the Rust bridge.
+
+ The Rust HTTP client takes one duration; ``httpx.Timeout`` carries separate
+ connect/read/write/pool values, so pick the read deadline as the closest
+ analog to a total-request timeout.
+ """
+ if timeout is None:
+ return None
+ if isinstance(timeout, httpx.Timeout):
+ return timeout.read
+ return float(timeout)
+
+
+def _run_rust_ocr(
+ rust_ocr: RustOcr,
+ logging_obj: LiteLLMLoggingObj,
+ provider_config: BaseOCRConfig,
+ resolve_api_key: Callable[[str], Optional[str]],
+ model: str,
+ document: dict[str, object],
+ api_key: Optional[str],
+ api_base: Optional[str],
+ optional_params: dict[str, object],
+ litellm_params: dict[str, object],
+ timeout_seconds: Optional[float],
+) -> OCRResponse:
+ """Run the Mistral OCR call through the Rust bridge and wrap the result.
+
+ Resolves the key the same way the Python path does so secret-manager backends
+ (AWS/Azure/GCP/Vault) work; the Rust bridge's own fallback only reads the
+ process environment. The request that Rust actually sends (resolved URL and
+ headers) is mirrored into pre_call so logs match the wire. Dependencies are
+ injected so this stays unit-testable without patching module globals.
+ """
+ resolved_api_key = api_key or resolve_api_key("MISTRAL_API_KEY")
+ resolved_headers = provider_config.validate_environment(
+ headers={},
+ model=model,
+ api_key=resolved_api_key,
+ api_base=api_base,
+ litellm_params=litellm_params,
+ )
+ resolved_complete_url = provider_config.get_complete_url(
+ api_base=api_base,
+ model=model,
+ optional_params=optional_params,
+ litellm_params=litellm_params,
+ )
+ logging_obj.pre_call(
+ input="OCR document processing",
+ api_key=resolved_api_key,
+ additional_args={
+ "complete_input_dict": {
+ "model": model,
+ "document": document,
+ **optional_params,
+ },
+ "api_base": resolved_complete_url,
+ "headers": resolved_headers,
+ },
+ )
+ return OCRResponse.model_validate(
+ rust_ocr(
+ model=model,
+ document=document,
+ api_key=resolved_api_key,
+ api_base=api_base,
+ optional_params=optional_params,
+ timeout_seconds=timeout_seconds,
+ )
+ )
+
+
@client
async def aocr(
model: str,
@@ -220,7 +297,7 @@ def ocr(
"""
local_vars = locals()
try:
- litellm_logging_obj: LiteLLMLoggingObj = kwargs.pop("litellm_logging_obj") # type: ignore
+ litellm_logging_obj = cast(LiteLLMLoggingObj, kwargs.pop("litellm_logging_obj"))
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None)
_is_async = kwargs.pop("aocr", False) is True
@@ -261,7 +338,6 @@ def ocr(
if dynamic_api_base:
api_base = dynamic_api_base
- # Get provider config
ocr_provider_config: Optional[BaseOCRConfig] = (
ProviderConfigManager.get_provider_ocr_config(
model=model,
@@ -278,17 +354,14 @@ 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 = {}
for param in supported_params:
if param in kwargs:
non_default_params[param] = kwargs.pop(param)
- # Map parameters to provider-specific format
optional_params = ocr_provider_config.map_ocr_params(
non_default_params=non_default_params,
optional_params={},
@@ -297,7 +370,8 @@ def ocr(
verbose_logger.debug(f"OCR optional_params after mapping: {optional_params}")
- # Pre Call logging
+ effective_timeout = timeout or request_timeout
+
litellm_logging_obj.update_from_kwargs(
kwargs=kwargs,
model=model,
@@ -309,12 +383,35 @@ def ocr(
custom_llm_provider=custom_llm_provider,
)
- # Call the handler - pass document dict directly
+ # Optional Rust path: hand the whole Mistral OCR call to the Rust bridge.
+ if custom_llm_provider == "mistral" and rust_ocr_enabled():
+ rust_ocr = load_rust_ocr()
+ if rust_ocr is None:
+ verbose_logger.debug(
+ "Rust OCR bridge unavailable; falling back to Python path"
+ )
+ else:
+ from litellm.secret_managers.main import get_secret_str
+
+ return _run_rust_ocr(
+ rust_ocr=rust_ocr,
+ logging_obj=litellm_logging_obj,
+ provider_config=ocr_provider_config,
+ resolve_api_key=get_secret_str,
+ model=model,
+ document=document,
+ api_key=api_key,
+ api_base=api_base,
+ optional_params=optional_params,
+ litellm_params=dict(litellm_params),
+ timeout_seconds=_timeout_to_seconds(effective_timeout),
+ )
+
response = base_llm_http_handler.ocr(
model=model,
- document=document, # Pass the entire document dict
+ document=document,
optional_params=optional_params,
- timeout=timeout or request_timeout,
+ timeout=effective_timeout,
logging_obj=litellm_logging_obj,
api_key=api_key,
api_base=api_base,
diff --git a/litellm/ocr/rust_bridge.py b/litellm/ocr/rust_bridge.py
new file mode 100644
index 00000000000..61f9e8ca69a
--- /dev/null
+++ b/litellm/ocr/rust_bridge.py
@@ -0,0 +1,74 @@
+"""
+Optional Rust-backed OCR path.
+
+Enable with ``litellm.use_litellm_rust()``; the sync ``litellm.ocr()`` entrypoint
+then routes supported Mistral calls through the compiled ``litellm_python_bridge``
+extension, which performs the whole OCR call (URL, headers, HTTP, parse) in Rust.
+
+No module-level ``litellm`` imports keep this a leaf so ``litellm/ocr/main.py``
+can import it statically without forming an import cycle.
+"""
+
+from __future__ import annotations
+
+from typing import Final, Protocol, cast
+
+
+class RustOcr(Protocol):
+ """Signature of the compiled ``litellm_python_bridge.ocr`` entrypoint."""
+
+ def __call__(
+ self,
+ model: str,
+ document: dict[str, object],
+ api_key: str | None,
+ api_base: str | None,
+ optional_params: dict[str, object],
+ timeout_seconds: float | None,
+ ) -> dict[str, object]: ...
+
+
+class _Unset:
+ """Sentinel type so ``ocr=None`` can clear a prior injection while omission preserves it."""
+
+
+_UNSET: Final[_Unset] = _Unset()
+
+_rust_ocr_enabled = False
+_rust_ocr_impl: RustOcr | None = None
+
+
+def use_litellm_rust(
+ enabled: bool = True, *, ocr: RustOcr | None | _Unset = _UNSET
+) -> None:
+ """Route supported OCR calls through the Rust ``litellm_python_bridge`` extension.
+
+ ``ocr`` injects the bridge callable; when omitted the compiled extension is
+ loaded on demand and any previously injected bridge is preserved. Pass
+ ``ocr=None`` explicitly to clear a prior injection.
+ """
+ global _rust_ocr_enabled, _rust_ocr_impl
+ _rust_ocr_enabled = enabled
+ if not isinstance(ocr, _Unset):
+ _rust_ocr_impl = ocr
+
+
+def rust_ocr_enabled() -> bool:
+ """Whether the Rust OCR path has been turned on via ``use_litellm_rust()``."""
+ return _rust_ocr_enabled
+
+
+def load_rust_ocr() -> RustOcr | None:
+ """Return the Rust OCR callable, or ``None`` when no bridge is available.
+
+ Prefers an injected implementation, otherwise loads the compiled
+ ``litellm_python_bridge`` extension; a missing extension yields ``None`` so
+ the caller can fall back to the Python path instead of hard-failing.
+ """
+ if _rust_ocr_impl is not None:
+ return _rust_ocr_impl
+ try:
+ import litellm_python_bridge
+ except ImportError:
+ return None
+ return cast(RustOcr, litellm_python_bridge.ocr)
diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json
index db6183edaa0..dd7712aabca 100644
--- a/litellm/provider_endpoints_support_backup.json
+++ b/litellm/provider_endpoints_support_backup.json
@@ -1835,6 +1835,23 @@
"interactions": true
}
},
+ "darkbloom": {
+ "display_name": "Darkbloom (`darkbloom`)",
+ "url": "https://docs.litellm.ai/docs/providers/darkbloom",
+ "endpoints": {
+ "chat_completions": true,
+ "messages": false,
+ "responses": false,
+ "embeddings": false,
+ "image_generations": false,
+ "audio_transcriptions": false,
+ "audio_speech": false,
+ "moderations": false,
+ "batches": false,
+ "rerank": false,
+ "a2a": false
+ }
+ },
"predibase": {
"display_name": "Predibase (`predibase`)",
"url": "https://docs.litellm.ai/docs/providers/predibase",
diff --git a/litellm/proxy/_experimental/mcp_server/AGENTS.md b/litellm/proxy/_experimental/mcp_server/AGENTS.md
new file mode 100644
index 00000000000..8eebc3ea3b3
--- /dev/null
+++ b/litellm/proxy/_experimental/mcp_server/AGENTS.md
@@ -0,0 +1,95 @@
+# Experimental MCP Server Change Guidelines
+
+Read @../../../../CLAUDE.md and @CLAUDE.md before changing this package.
+
+This directory owns the proxy-hosted MCP server implementation. Keep changes
+inside the module that owns the behavior, and only reach outside this package
+when the public type contract, database schema, dashboard, or cross-proxy route
+wiring must change with it.
+
+## File Structure
+
+Respect the current package boundaries:
+
+```text
+litellm/proxy/_experimental/mcp_server/
+ AGENTS.md
+ CLAUDE.md
+ server.py # ASGI/MCP route handling, sessions, tool calls [PR7: 7-arm only — move BYOK/OAuth pre-fetch into resolver]
+ mcp_server_manager.py # upstream server registry, clients, tool routing [PR7: _create_mcp_client swaps resolve_mcp_auth -> resolve_credentials]
+ auth/
+ user_api_key_auth_mcp.py # LiteLLM admission auth and MCP request headers
+ token_exchange.py # OAuth token exchange handling [unchanged; V1TokenExchangeAdapter delegates here]
+ litellm_auth_handler.py # authenticated-user adapter for MCP sessions
+ outbound_credentials/ # NEW — typed upstream-credential resolution (resolve_credentials + arms)
+ __init__.py # public surface: resolve_credentials, the configs, CredError
+ result.py # Ok | Error union (pure stdlib)
+ types.py # AuthConfig union, CredError, Subject, ServerSpec
+ httpx_auth.py # NoOpAuth, StaticHeaderAuth (every mode -> one httpx.Auth)
+ resolver.py # resolve_credentials(): exhaustive per-mode match + assert_never
+ seams.py # injected Protocols (one per cache-touching mode)
+ v1_adapters.py # v1-backed seam bodies; delegate to auth/oauth2/db owners
+ adapter.py # to_subject / to_server_spec / raise_public (v1 <-> v2 boundary)
+ discoverable_endpoints.py # MCP OAuth metadata, authorize, token, callback
+ byok_oauth_endpoints.py # BYOK OAuth UI/API flow
+ oauth_utils.py # redirect URI and proxy base URL validation
+ oauth2_token_cache.py # OAuth2 and per-user token resolution/cache [PR7: resolve_mcp_auth removed; cache class stays, V1OAuth2CacheAdapter delegates to async_get_token]
+ db.py # MCP server, credential, env var, submission DB access [unchanged; V1ByokStore delegates to _get_byok_credential / get_user_credential]
+ toolset_db.py # MCP toolset DB access
+ rest_endpoints.py # proxy REST facade for listing/calling MCP tools [PR7: 7-arm only — pass identity + inbound token down instead of mcp_auth_header]
+ openapi_to_mcp_generator.py# OpenAPI spec to MCP tool generation
+ sampling_handler.py # MCP sampling to LiteLLM completion flow
+ elicitation_handler.py # MCP elicitation relay flow
+ semantic_tool_filter.py # semantic filtering of available MCP tools
+ guardrail_translation/
+ handler.py # MCP guardrail result translation
+ sse_transport.py # SSE transport implementation
+ mcp_context.py # contextvars for MCP request/session metadata
+ mcp_debug.py # debug helpers
+ tool_registry.py # in-memory MCP tool registry helpers
+ cost_calculator.py # MCP tool cost calculation
+ ui_session_utils.py # dashboard session auth context helpers
+ utils.py # shared primitives used by several modules
+```
+
+Do not add broad catch-all modules. Prefer the existing owner above, and add a
+new file only for a distinct capability that would otherwise make an existing
+module materially harder to understand.
+
+## Implementation Rules
+
+- Preserve the boundary between LiteLLM admission auth and upstream MCP auth.
+ Admission belongs in `auth/user_api_key_auth_mcp.py`; upstream token exchange,
+ delegated auth, per-user OAuth, BYOK, and raw header forwarding belong in the
+ dedicated OAuth/header modules.
+- Treat `none`, bearer/API key, OAuth, OAuth token exchange, delegated upstream
+ auth, SSE, streamable HTTP, and stdio as separate flows. Do not collapse them
+ behind a single generic branch unless tests prove every mode still behaves
+ correctly.
+- Be especially careful with `available_on_public_internet: false` combined with
+ `delegate_auth_to_upstream: true`. The local `CLAUDE.md` explains the anonymous
+ upstream PKCE path that must remain intentional.
+- Keep database-backed fields in sync across migrations, typed models under
+ `litellm/types/mcp.py` or `litellm/types/mcp_server/`, config loading, this
+ package, and dashboard state when the field is user-visible.
+- Use the official MCP SDK types and established LiteLLM Pydantic models where
+ they exist. Avoid untyped protocol dictionaries at package boundaries.
+- Keep security-sensitive logic easy to audit. Header forwarding, IP filtering,
+ public internet checks, token storage, env var interpolation, and credential
+ encryption need focused tests for both allowed and rejected paths.
+- Avoid adding comments to new code unless they explain non-obvious security or
+ protocol behavior. Prefer clear names and small functions.
+
+## Tests
+
+Mirror this package under `tests/test_litellm/proxy/_experimental/mcp_server/`.
+For regressions, extend the existing mapped test file instead of creating a new
+one. Use subdirectories that match the implementation path, such as
+`auth/test_token_exchange.py` for `auth/token_exchange.py` and
+`guardrail_translation/test_mcp_guardrail_handler.py` for
+`guardrail_translation/handler.py`.
+
+Use `tests/mcp_tests/` only when extending an existing broader MCP integration
+scenario that already lives there. Route, auth, tool listing, tool execution,
+OAuth, sampling, elicitation, DB, and dashboard-session changes should have
+focused coverage in the mirrored `tests/test_litellm/...` path first.
diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py
index e47fc84b533..90108de25c3 100644
--- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py
+++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py
@@ -12,6 +12,7 @@ from litellm.proxy._types import (
LiteLLM_TeamTable,
ProxyException,
SpecialHeaders,
+ SpecialMCPServerNames,
UserAPIKeyAuth,
)
from litellm.proxy.auth.ip_address_utils import IPAddressUtils
@@ -642,6 +643,15 @@ class MCPRequestHandler:
user_api_key_auth
)
)
+
+ # The key explicitly opted out of every MCP server. This overrides
+ # team inheritance and additive grants (mirrors no-default-models).
+ if (
+ SpecialMCPServerNames.no_mcp_servers.value
+ in allowed_mcp_servers_for_key
+ ):
+ return []
+
allowed_mcp_servers_for_team = (
await MCPRequestHandler._get_allowed_mcp_servers_for_team(
user_api_key_auth
@@ -1058,6 +1068,13 @@ class MCPRequestHandler:
if key_object_permission is None:
return []
+ # Sentinel opt-out: surface it unexpanded so the caller can short-circuit
+ # to zero servers instead of inheriting the team.
+ if SpecialMCPServerNames.no_mcp_servers.value in (
+ key_object_permission.mcp_servers or []
+ ):
+ return [SpecialMCPServerNames.no_mcp_servers.value]
+
# Permission entries may be server_ids OR names/aliases — expand to ids.
direct_mcp_servers = global_mcp_server_manager.expand_permission_list(
key_object_permission.mcp_servers or []
diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
index afec884cd96..5e704b889ae 100644
--- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
+++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
@@ -80,6 +80,7 @@ from litellm.proxy._types import (
MCPEnvVar,
MCPTransport,
MCPTransportType,
+ SpecialMCPServerNames,
UserAPIKeyAuth,
)
from litellm.proxy.auth.ip_address_utils import IPAddressUtils
@@ -1349,6 +1350,17 @@ class MCPServerManager:
allow_all_server_ids = self.get_allow_all_keys_server_ids()
try:
+ # The key explicitly opted out of every MCP server. Return zero before
+ # layering on allow_all_keys servers so the opt-out is absolute.
+ key_object_permission = (
+ user_api_key_auth.object_permission if user_api_key_auth else None
+ )
+ if key_object_permission is not None and (
+ SpecialMCPServerNames.no_mcp_servers.value
+ in (key_object_permission.mcp_servers or [])
+ ):
+ return []
+
# Check if object_permission.mcp_servers is explicitly set
has_explicit_object_permission = False
if user_api_key_auth and user_api_key_auth.object_permission:
diff --git a/litellm/proxy/_experimental/mcp_server/outbound_credentials/__init__.py b/litellm/proxy/_experimental/mcp_server/outbound_credentials/__init__.py
new file mode 100644
index 00000000000..73166a45d6e
--- /dev/null
+++ b/litellm/proxy/_experimental/mcp_server/outbound_credentials/__init__.py
@@ -0,0 +1,73 @@
+"""Typed upstream-credential resolution for MCP servers.
+
+This subpackage houses the typed credential vocabulary and the ``resolve_credentials``
+dispatch. A server declares one per-mode config from the ``AuthConfig`` discriminated union;
+``UpstreamCredentialProvider.resolve_credentials`` selects one arm and returns an ``httpx.Auth``
+or a typed ``CredError``. Failures are modeled as values via :mod:`.result` (``Result[T,
+CredError]``) rather than raised, so every seam is total. Nothing here is wired onto a live
+request path yet.
+"""
+
+from litellm.proxy._experimental.mcp_server.outbound_credentials.httpx_auth import (
+ NoOpAuth,
+ StaticHeaderAuth,
+)
+from litellm.proxy._experimental.mcp_server.outbound_credentials.resolver import (
+ UpstreamCredentialProvider,
+)
+from litellm.proxy._experimental.mcp_server.outbound_credentials.result import (
+ Error,
+ Ok,
+ Result,
+)
+from litellm.proxy._experimental.mcp_server.outbound_credentials.types import (
+ Ambient,
+ ApiKeyConfig,
+ ApiKeySource,
+ AssumeRole,
+ AuthConfig,
+ AuthorizationCodeConfig,
+ AuthSpecKind,
+ AwsCredentialSource,
+ AwsSigV4Config,
+ Byok,
+ ClientCredentialsConfig,
+ CredError,
+ NoneConfig,
+ PassthroughConfig,
+ ServerSpec,
+ SharedKey,
+ StaticKeys,
+ Subject,
+ TokenExchangeConfig,
+ parse_auth_spec_kind,
+)
+
+__all__ = [
+ "Ok",
+ "Error",
+ "Result",
+ "NoOpAuth",
+ "StaticHeaderAuth",
+ "UpstreamCredentialProvider",
+ "AuthSpecKind",
+ "CredError",
+ "Subject",
+ "ServerSpec",
+ "AuthConfig",
+ "parse_auth_spec_kind",
+ "AuthorizationCodeConfig",
+ "ClientCredentialsConfig",
+ "TokenExchangeConfig",
+ "ApiKeyConfig",
+ "ApiKeySource",
+ "SharedKey",
+ "Byok",
+ "PassthroughConfig",
+ "NoneConfig",
+ "AwsSigV4Config",
+ "AwsCredentialSource",
+ "StaticKeys",
+ "AssumeRole",
+ "Ambient",
+]
diff --git a/litellm/proxy/_experimental/mcp_server/outbound_credentials/httpx_auth.py b/litellm/proxy/_experimental/mcp_server/outbound_credentials/httpx_auth.py
new file mode 100644
index 00000000000..2345fa98123
--- /dev/null
+++ b/litellm/proxy/_experimental/mcp_server/outbound_credentials/httpx_auth.py
@@ -0,0 +1,45 @@
+"""Concrete `httpx.Auth` objects the resolver returns for the self-contained modes.
+
+These are the egress credential as the SDK consumes it: an `httpx.Auth` attached to the
+upstream `AsyncClient`. The OAuth-flow modes (`authorization_code`, `client_credentials`,
+`token_exchange`) return SDK-provided auth objects instead and land later.
+
+`auth_flow` mutating the outbound request is the `httpx.Auth` contract, not a house-style
+violation: the request is httpx's object, and these carry no state of their own.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Generator
+
+import httpx
+from pydantic import SecretStr
+
+
+class NoOpAuth(httpx.Auth):
+ """Attaches nothing — the `none` mode (and the seam-level default)."""
+
+ def auth_flow(
+ self, request: httpx.Request
+ ) -> Generator[httpx.Request, httpx.Response, None]:
+ yield request
+
+
+class StaticHeaderAuth(httpx.Auth):
+ """Sets one fixed header on every request — the `api_key` family and `passthrough`.
+
+ The header value is a live credential (a bearer token, an API key, a forwarded user
+ token), so it is held as a `SecretStr` and unwrapped only when written onto the request.
+ That keeps it masked in reprs, `vars()`, tracebacks, and structured logs, matching the
+ `SecretStr` discipline the config models use.
+ """
+
+ def __init__(self, header_value: str, header_name: str = "Authorization") -> None:
+ self.header_name = header_name
+ self._header_value = SecretStr(header_value)
+
+ def auth_flow(
+ self, request: httpx.Request
+ ) -> Generator[httpx.Request, httpx.Response, None]:
+ request.headers[self.header_name] = self._header_value.get_secret_value()
+ yield request
diff --git a/litellm/proxy/_experimental/mcp_server/outbound_credentials/resolver.py b/litellm/proxy/_experimental/mcp_server/outbound_credentials/resolver.py
new file mode 100644
index 00000000000..7bcdb3e6529
--- /dev/null
+++ b/litellm/proxy/_experimental/mcp_server/outbound_credentials/resolver.py
@@ -0,0 +1,70 @@
+"""The one credential resolver: dispatch on the declared mode, fail closed.
+
+`resolve_credentials` selects exactly one arm off the server's typed `config` and either
+produces an `httpx.Auth` or returns a typed `CredError`. The `match` is over the `AuthConfig`
+variant, so each arm receives its own fully-typed config with no field-presence inference and
+no precedence cascade. It is wildcard-free with an `assert_never` tail, so adding a mode without
+an arm fails the type gate (basedpyright `reportMatchNotExhaustive`); a bypassed gate fails loudly
+at runtime instead of returning `None`.
+
+This skeleton ships every arm as a `not_implemented` stub. Each mode's real body, with its
+injected seam, lands in its own follow-up PR; until then the arm returns a typed error rather
+than silently producing no credential. Pure v2: no imports from v1.
+"""
+
+from __future__ import annotations
+
+import httpx
+from typing_extensions import assert_never
+
+from litellm.proxy._experimental.mcp_server.outbound_credentials.result import (
+ Error,
+ Result,
+)
+from litellm.proxy._experimental.mcp_server.outbound_credentials.types import (
+ ApiKeyConfig,
+ AuthorizationCodeConfig,
+ AuthSpecKind,
+ AwsSigV4Config,
+ ClientCredentialsConfig,
+ CredError,
+ NoneConfig,
+ PassthroughConfig,
+ ServerSpec,
+ Subject,
+ TokenExchangeConfig,
+)
+
+
+class UpstreamCredentialProvider:
+ """Produces the one `httpx.Auth` for a `(subject, upstream)` pair, per declared mode.
+
+ Collaborators (the per-mode credential stores and token fetchers) are injected as each arm
+ is built; the skeleton needs none, since every arm is a stub.
+ """
+
+ async def resolve_credentials(
+ self, subject: Subject, server: ServerSpec
+ ) -> Result[httpx.Auth, CredError]:
+ match server.config:
+ case NoneConfig():
+ return _not_implemented(AuthSpecKind.none)
+ case ApiKeyConfig():
+ return _not_implemented(AuthSpecKind.api_key)
+ case PassthroughConfig():
+ return _not_implemented(AuthSpecKind.passthrough)
+ case ClientCredentialsConfig():
+ return _not_implemented(AuthSpecKind.client_credentials)
+ case TokenExchangeConfig():
+ return _not_implemented(AuthSpecKind.token_exchange)
+ case AuthorizationCodeConfig():
+ return _not_implemented(AuthSpecKind.authorization_code)
+ case AwsSigV4Config():
+ return _not_implemented(AuthSpecKind.aws_sigv4)
+ assert_never(server.config)
+
+
+def _not_implemented(kind: AuthSpecKind) -> Result[httpx.Auth, CredError]:
+ return Error(
+ CredError.of_not_implemented(f"{kind.value}: resolver arm not implemented yet")
+ )
diff --git a/litellm/proxy/_experimental/mcp_server/outbound_credentials/result.py b/litellm/proxy/_experimental/mcp_server/outbound_credentials/result.py
new file mode 100644
index 00000000000..a612e8510f5
--- /dev/null
+++ b/litellm/proxy/_experimental/mcp_server/outbound_credentials/result.py
@@ -0,0 +1,54 @@
+"""A tagged-union ``Result`` the type checker can actually narrow.
+
+``Ok`` and ``Error`` are separate frozen classes joined by a ``Union`` alias, so
+reaching for ``result.ok`` before eliminating the ``Error`` arm (via ``isinstance``
+or a ``match`` pattern) is a type error rather than a runtime ``AttributeError``. A
+single class carrying both payload fields would make that unguarded access invisible
+to the type checker.
+
+Both variants are covariant and frozen; the absent side defaults to ``Never`` so a
+bare ``Ok(value)`` or ``Error(err)`` infers fully and is assignable to any ``Result``
+whose matching side fits.
+
+``is_ok`` / ``is_error`` are runtime predicates that also narrow via their ``Literal``
+returns; inside strictly typed code, discriminate with ``match`` or ``isinstance``.
+
+This is the shared ``Result`` shape for the ``outbound_credentials`` resolver: every
+seam returns ``Result[T, CredError]`` instead of raising, so each failure is a value
+the caller must handle rather than an exception that can slip past the type checker.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Generic, Literal, TypeAlias
+
+from typing_extensions import Never, TypeVar
+
+_TOk_co = TypeVar("_TOk_co", covariant=True, default=Never)
+_TError_co = TypeVar("_TError_co", covariant=True, default=Never)
+
+
+@dataclass(frozen=True)
+class Ok(Generic[_TOk_co, _TError_co]):
+ ok: _TOk_co
+
+ def is_ok(self) -> Literal[True]:
+ return True
+
+ def is_error(self) -> Literal[False]:
+ return False
+
+
+@dataclass(frozen=True)
+class Error(Generic[_TOk_co, _TError_co]):
+ error: _TError_co
+
+ def is_ok(self) -> Literal[False]:
+ return False
+
+ def is_error(self) -> Literal[True]:
+ return True
+
+
+Result: TypeAlias = Ok[_TOk_co, _TError_co] | Error[_TOk_co, _TError_co]
diff --git a/litellm/proxy/_experimental/mcp_server/outbound_credentials/types.py b/litellm/proxy/_experimental/mcp_server/outbound_credentials/types.py
new file mode 100644
index 00000000000..2088dc77252
--- /dev/null
+++ b/litellm/proxy/_experimental/mcp_server/outbound_credentials/types.py
@@ -0,0 +1,334 @@
+"""The upstream-credential vocabulary — the typed seam the resolver dispatches on.
+
+This module ships the data types only; the resolver lands in a later PR. It is the contract
+the credential build implements and the spec tests assert against.
+
+Design invariants encoded here:
+
+- **Mode is the single source of truth.** A server declares exactly one per-mode `config`
+ (the `AuthConfig` discriminated union); `auth_spec_kind` is *derived* from it, never a
+ second field that can drift. The resolver dispatches on the config variant, one arm per
+ mode. No field-presence inference, no precedence cascade.
+- **Illegal states unrepresentable.** Each mode's config is its own frozen model holding
+ only that mode's fields — an `aws_sigv4` server cannot hold OAuth fields, and a config
+ missing a required field is rejected at construction, not at call time.
+- **Fail-closed at the boundary.** A raw mode string can only enter through
+ `parse_auth_spec_kind()`, which returns a typed `CredError`.
+- **Errors as values.** Every seam returns `Result[_, CredError]`; only edge adapters raise.
+- **No v1 imports.** This vocabulary stays free of `MCPServer` and the rest of v1; the
+ v1 -> v2 adapter maps onto these types in a later PR.
+
+Sum types are Expression `@tagged_union`s discriminated on a `Literal` `tag`, matched via
+`self.tag` with an `assert_never` tail; `Result` is this package's vendored `Ok | Error`
+union (see `result.py`), not `expression.Result`.
+"""
+
+from __future__ import annotations
+
+from enum import Enum
+from typing import Annotated, Literal
+
+from expression import case, tag, tagged_union
+from pydantic import BaseModel, ConfigDict, Field, SecretStr
+from typing_extensions import assert_never
+
+from litellm.proxy._experimental.mcp_server.outbound_credentials.result import (
+ Error,
+ Ok,
+ Result,
+)
+
+
+class AuthSpecKind(str, Enum):
+ """The server's statically-declared upstream-auth mode — derived from its `config`.
+
+ Covers v1's full `MCPAuth` surface, not only OAuth grants: the three grant modes, the
+ collapsed static-header family, client passthrough, no-auth, and AWS request signing.
+ BYOK is *not* a member: it is the `api_key` mode seeded per-user, a source selector
+ inside that arm. The static-header schemes v1 splits into separate `MCPAuth` values
+ (`bearer_token`/`api_key`/`basic`/`token`/`authorization`) collapse into `api_key`; the
+ scheme is a parameter the arm carries, not its own mode.
+ """
+
+ authorization_code = "authorization_code" # per-user 3LO; gateway-stored token
+ client_credentials = "client_credentials" # gateway service account (M2M)
+ token_exchange = "token_exchange" # RFC 8693: token endpoint + subject_token (OBO)
+ api_key = "api_key" # static header, any scheme (BYOK = per-user-seeded source)
+ passthrough = "passthrough" # client forwards an upstream-audience token
+ none = "none" # no upstream credential; resolve yields a no-op auth, never an error
+ aws_sigv4 = "aws_sigv4" # AWS SigV4 per-request signing (e.g. Bedrock AgentCore)
+
+
+@tagged_union(frozen=True)
+class CredError:
+ """Why a credential could not be produced. Fail-closed: an arm yields this or an `httpx.Auth`.
+
+ Discriminated on the `Literal` `tag`; consumers `match self.tag` (see `summary`) so the
+ type checker can prove exhaustiveness. Construct via the `of_*` factories.
+ """
+
+ tag: Literal[
+ "unauthorized",
+ "misconfigured",
+ "upstream_unavailable",
+ "unsupported_mode",
+ "precondition_required",
+ "not_implemented",
+ ] = tag()
+
+ unauthorized: str = (
+ case()
+ ) # no usable credential for this (subject, server) -> 401 challenge
+ misconfigured: str = (
+ case()
+ ) # the declared mode is missing required config -> 5xx (operator)
+ upstream_unavailable: str = (
+ case()
+ ) # the IdP / token endpoint could not be reached -> 503
+ unsupported_mode: str = (
+ case()
+ ) # a raw mode string did not parse into AuthSpecKind (boundary)
+ precondition_required: str = (
+ case()
+ ) # a required per-user value (e.g. an env var) has not been provided -> 412
+ not_implemented: str = (
+ case()
+ ) # the declared mode's resolver arm is not built yet -> 501 (not operator error)
+
+ @staticmethod
+ def of_unauthorized(detail: str) -> CredError:
+ return CredError(unauthorized=detail)
+
+ @staticmethod
+ def of_misconfigured(detail: str) -> CredError:
+ return CredError(misconfigured=detail)
+
+ @staticmethod
+ def of_upstream_unavailable(detail: str) -> CredError:
+ return CredError(upstream_unavailable=detail)
+
+ @staticmethod
+ def of_unsupported_mode(detail: str) -> CredError:
+ return CredError(unsupported_mode=detail)
+
+ @staticmethod
+ def of_precondition_required(detail: str) -> CredError:
+ return CredError(precondition_required=detail)
+
+ @staticmethod
+ def of_not_implemented(detail: str) -> CredError:
+ return CredError(not_implemented=detail)
+
+ @property
+ def summary(self) -> str:
+ # Exhaustiveness: every Literal tag has an arm; the trailing assert_never typechecks
+ # only while that stays true (a `case _` would defeat reportMatchNotExhaustive).
+ match self.tag:
+ case "unauthorized":
+ return f"unauthorized: {self.unauthorized}"
+ case "misconfigured":
+ return f"misconfigured: {self.misconfigured}"
+ case "upstream_unavailable":
+ return f"upstream unavailable: {self.upstream_unavailable}"
+ case "unsupported_mode":
+ return self.unsupported_mode
+ case "precondition_required":
+ return f"precondition required: {self.precondition_required}"
+ case "not_implemented":
+ return f"not implemented: {self.not_implemented}"
+ assert_never(self.tag)
+
+
+class AuthorizationCodeConfig(BaseModel):
+ """Per-user 3LO; the gateway is the OAuth client and stores the user's token.
+
+ Endpoints are discovered (RFC 9728 -> RFC 8414) and the client is registered via DCR
+ (RFC 7591), so the common case carries none of the fields below; they are optional manual
+ overrides for IdPs without discovery / DCR. The per-user token is read from the token store
+ at resolve time, not held here.
+ """
+
+ model_config = ConfigDict(frozen=True)
+ kind: Literal[AuthSpecKind.authorization_code] = AuthSpecKind.authorization_code
+ scopes: tuple[str, ...] = ()
+ client_id: str | None = None
+ client_secret: SecretStr | None = None
+ authorization_url: str | None = None
+ token_url: str | None = None
+
+
+class ClientCredentialsConfig(BaseModel):
+ """M2M service account; one upstream identity for every user.
+
+ Fields are optional so the config can be built incomplete: a value may be supplied at
+ runtime (`token_url` via RFC 8414 discovery, `client_id`/`secret` via DCR), and the
+ resolver arm raises `CredError.misconfigured` when a needed field is still absent.
+ """
+
+ model_config = ConfigDict(frozen=True)
+ kind: Literal[AuthSpecKind.client_credentials] = AuthSpecKind.client_credentials
+ client_id: str | None = None
+ client_secret: SecretStr | None = None
+ token_url: str | None = None
+ scopes: tuple[str, ...] = ()
+
+
+class TokenExchangeConfig(BaseModel):
+ """RFC 8693 OBO; swap the caller's live subject_token for a token bound to the upstream's
+ audience (`server.resource`, RFC 8707). The gateway authenticates to the exchange endpoint
+ as an OAuth client (`client_id`/`client_secret`); the inbound token is sent only to that
+ endpoint, never to the upstream.
+ """
+
+ model_config = ConfigDict(frozen=True)
+ kind: Literal[AuthSpecKind.token_exchange] = AuthSpecKind.token_exchange
+ subject_token_type: str = "urn:ietf:params:oauth:token-type:access_token"
+ token_exchange_endpoint: str | None = None
+ client_id: str | None = None
+ client_secret: SecretStr | None = None
+ scopes: tuple[str, ...] = ()
+
+
+class SharedKey(BaseModel):
+ """A fixed key configured on the server, identical for every caller."""
+
+ model_config = ConfigDict(frozen=True)
+ source: Literal["shared"] = "shared"
+ value: SecretStr
+
+
+class Byok(BaseModel):
+ """A key the user brings via the entry flow, stored per-user and pulled from the credential
+ store at resolve time. Missing means the user must provide it, a 401 + WWW-Authenticate
+ challenge."""
+
+ model_config = ConfigDict(frozen=True)
+ source: Literal["byok"] = "byok"
+
+
+ApiKeySource = Annotated[SharedKey | Byok, Field(discriminator="source")]
+
+
+class ApiKeyConfig(BaseModel):
+ """A fixed credential injected as a header. The value is shared (in config) or seeded
+ per-user (pulled from the store); `header_name` and `value_prefix` say where and how it is
+ written, modeled like OpenAPI's apiKey scheme so any upstream convention is expressible
+ (Authorization + Bearer, a raw value on X-API-Key, Ocp-Apim-Subscription-Key, etc.).
+ """
+
+ model_config = ConfigDict(frozen=True)
+ kind: Literal[AuthSpecKind.api_key] = AuthSpecKind.api_key
+ header_name: str = "Authorization"
+ value_prefix: str = "Bearer"
+ key_source: ApiKeySource
+
+ def header(self, value: str) -> tuple[str, str]:
+ formatted = f"{self.value_prefix} {value}" if self.value_prefix else value
+ return self.header_name, formatted
+
+
+class PassthroughConfig(BaseModel):
+ """Client-driven upstream OAuth; the gateway forwards the client's upstream token."""
+
+ model_config = ConfigDict(frozen=True)
+ kind: Literal[AuthSpecKind.passthrough] = AuthSpecKind.passthrough
+
+
+class NoneConfig(BaseModel):
+ """No upstream credential; the request is sent unauthenticated."""
+
+ model_config = ConfigDict(frozen=True)
+ kind: Literal[AuthSpecKind.none] = AuthSpecKind.none
+
+
+class StaticKeys(BaseModel):
+ """Long-lived AWS access keys configured on the server."""
+
+ model_config = ConfigDict(frozen=True)
+ source: Literal["static_keys"] = "static_keys"
+ access_key_id: str
+ secret_access_key: SecretStr
+ session_token: SecretStr | None = None
+
+
+class AssumeRole(BaseModel):
+ """An IAM role the gateway assumes via STS for short-lived, auto-refreshed credentials."""
+
+ model_config = ConfigDict(frozen=True)
+ source: Literal["assume_role"] = "assume_role"
+ role_arn: str
+ session_name: str | None = None
+ external_id: str | None = None
+
+
+class Ambient(BaseModel):
+ """The environment's default AWS credential chain (instance profile, IRSA, env vars)."""
+
+ model_config = ConfigDict(frozen=True)
+ source: Literal["ambient"] = "ambient"
+
+
+AwsCredentialSource = Annotated[
+ StaticKeys | AssumeRole | Ambient, Field(discriminator="source")
+]
+
+
+class AwsSigV4Config(BaseModel):
+ """AWS SigV4 per-request signing for an AWS-hosted upstream (e.g. Bedrock AgentCore). The
+ gateway signs with its own AWS identity, never the caller's; `credentials` selects how that
+ identity is obtained, defaulting to the ambient credential chain."""
+
+ model_config = ConfigDict(frozen=True)
+ kind: Literal[AuthSpecKind.aws_sigv4] = AuthSpecKind.aws_sigv4
+ region: str
+ service: str = "bedrock-agentcore"
+ credentials: AwsCredentialSource = Ambient()
+
+
+AuthConfig = Annotated[
+ AuthorizationCodeConfig
+ | ClientCredentialsConfig
+ | TokenExchangeConfig
+ | ApiKeyConfig
+ | PassthroughConfig
+ | NoneConfig
+ | AwsSigV4Config,
+ Field(discriminator="kind"),
+]
+
+
+class Subject(BaseModel):
+ """The validated inbound principal. NOT the v1 request object and NOT the LiteLLM key."""
+
+ model_config = ConfigDict(frozen=True)
+
+ tenant_id: str
+ subject_id: str
+ # Opaque, already-validated inbound identity. Only `token_exchange` / `passthrough` read it.
+ inbound_token: SecretStr | None = None
+
+
+class ServerSpec(BaseModel):
+ """The declared upstream. A v2-native type; the v1 -> v2 adapter maps onto this."""
+
+ model_config = ConfigDict(frozen=True)
+
+ server_id: str
+ resource: str # RFC 8707 audience URI this upstream's tokens are bound to
+ config: AuthConfig
+
+ @property
+ def auth_spec_kind(self) -> AuthSpecKind:
+ return self.config.kind
+
+
+def parse_auth_spec_kind(raw: str) -> Result[AuthSpecKind, CredError]:
+ """Boundary parser — the *only* place an unknown mode is handled, and it fails closed.
+
+ Inside the core the mode is always a valid `AuthSpecKind`, so the resolver never needs a
+ wildcard arm and basedpyright can prove its `match` exhaustive.
+ """
+ try:
+ return Ok(AuthSpecKind(raw))
+ except ValueError:
+ return Error(CredError.of_unsupported_mode(f"unknown auth_spec_kind: {raw!r}"))
diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py
index 08e42e918e9..e891425274f 100644
--- a/litellm/proxy/_experimental/mcp_server/server.py
+++ b/litellm/proxy/_experimental/mcp_server/server.py
@@ -63,7 +63,11 @@ from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client,
httpxSpecialProvider,
)
-from litellm.proxy._types import UserAPIKeyAuth
+from litellm.proxy._types import (
+ ProxyException,
+ SpecialMCPServerNames,
+ UserAPIKeyAuth,
+)
from litellm.proxy.auth.ip_address_utils import IPAddressUtils
from litellm.proxy.litellm_pre_call_utils import (
LiteLLMProxyRequestSetup,
@@ -229,6 +233,28 @@ def _jsonrpc_text_has_top_level_method(text: str) -> bool:
return False
+def _proxy_exception_to_http_exception(exc: ProxyException) -> HTTPException:
+ """Map a ``ProxyException`` to an ``HTTPException`` that preserves its real
+ status code and headers.
+
+ ``user_api_key_auth`` raises ``ProxyException`` (not ``HTTPException``) on
+ auth failures. The MCP ASGI handlers re-raise ``HTTPException`` to keep the
+ status and any ``WWW-Authenticate`` challenge, but a ``ProxyException`` would
+ otherwise fall through to their generic handler and be flattened to a 500 —
+ dropping the 401 + challenge an OAuth client needs to re-authenticate, so the
+ tool call surfaces as a cancelled/terminated session instead.
+ """
+ try:
+ status_code = int(exc.code)
+ except (TypeError, ValueError):
+ status_code = 500
+ return HTTPException(
+ status_code=status_code,
+ detail=exc.message,
+ headers=exc.headers or None,
+ )
+
+
if MCP_AVAILABLE:
from mcp.server import Server
from mcp.server.lowlevel.server import NotificationOptions
@@ -3352,6 +3378,19 @@ if MCP_AVAILABLE:
from litellm.proxy._types import LiteLLM_ObjectPermissionTable
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
+ # A key scoped to no MCP servers opts out of every MCP path. Enforce it
+ # here too, since toolset scoping replaces mcp_servers and would otherwise
+ # drop the sentinel. Checked before the admin branch, mirroring
+ # get_allowed_mcp_servers.
+ original_op = user_api_key_auth.object_permission
+ if original_op is not None and SpecialMCPServerNames.no_mcp_servers.value in (
+ original_op.mcp_servers or []
+ ):
+ raise HTTPException(
+ status_code=403,
+ detail="API key is scoped to no MCP servers; toolset access is denied.",
+ )
+
# Access control: non-admin keys must have this toolset in their grant list.
# Use _user_has_admin_view so that PROXY_ADMIN_VIEW_ONLY is also treated as admin.
is_admin = _user_has_admin_view(user_api_key_auth)
@@ -4006,6 +4045,12 @@ if MCP_AVAILABLE:
except HTTPException:
# Re-raise HTTP exceptions to preserve status codes and details
raise
+ except ProxyException as e:
+ # Auth failures from user_api_key_auth arrive as ProxyException, not
+ # HTTPException. Preserve the real status (e.g. 401 + WWW-Authenticate)
+ # so OAuth clients can re-authenticate instead of receiving a generic
+ # 500 that surfaces as a cancelled tool call.
+ raise _proxy_exception_to_http_exception(e)
except Exception as e:
verbose_logger.exception(f"Error handling MCP request: {e}")
# Try to send a graceful error response for non-HTTP exceptions
@@ -4123,6 +4168,12 @@ if MCP_AVAILABLE:
# Re-raise HTTP exceptions to preserve status codes and details
# (e.g. 401 + WWW-Authenticate challenges from OAuth pass-through).
raise
+ except ProxyException as e:
+ # Auth failures from user_api_key_auth arrive as ProxyException, not
+ # HTTPException. Preserve the real status (e.g. 401 + WWW-Authenticate)
+ # so OAuth clients can re-authenticate instead of receiving a generic
+ # 500 that surfaces as a cancelled tool call.
+ raise _proxy_exception_to_http_exception(e)
except Exception as e:
verbose_logger.exception(f"Error handling MCP request: {e}")
# Try to send a graceful error response for non-HTTP exceptions
diff --git a/litellm/proxy/_experimental/out/404.html b/litellm/proxy/_experimental/out/404.html
index 45de348c4d5..6d246c81652 100644
--- a/litellm/proxy/_experimental/out/404.html
+++ b/litellm/proxy/_experimental/out/404.html
@@ -1 +1 @@
-404: This page could not be found.LiteLLM Dashboard
404
This page could not be found.
\ No newline at end of file
+404: This page could not be found.LiteLLM Dashboard
404
This page could not be found.
\ No newline at end of file
diff --git a/litellm/proxy/_experimental/out/404/index.html b/litellm/proxy/_experimental/out/404/index.html
index 45de348c4d5..6d246c81652 100644
--- a/litellm/proxy/_experimental/out/404/index.html
+++ b/litellm/proxy/_experimental/out/404/index.html
@@ -1 +1 @@
-404: This page could not be found.LiteLLM Dashboard
404
This page could not be found.
\ No newline at end of file
+404: This page could not be found.LiteLLM Dashboard