Merge branch 'main' into litellm_claude_code_max

This commit is contained in:
Ishaan Jaffer 2026-01-20 17:04:28 -08:00
commit 14593a5b3c
50 changed files with 626 additions and 316 deletions

View file

@ -320,72 +320,36 @@ jobs:
run: |
echo "REPO_OWNER=`echo ${{github.repository_owner}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV}
- name: Get LiteLLM Latest Tag
id: current_app_tag
shell: bash
run: |
LATEST_TAG=$(git describe --tags --exclude "*dev*" --abbrev=0)
if [ -z "${LATEST_TAG}" ]; then
echo "latest_tag=latest" | tee -a $GITHUB_OUTPUT
else
echo "latest_tag=${LATEST_TAG}" | tee -a $GITHUB_OUTPUT
fi
- name: Get last published chart version
id: current_version
shell: bash
run: |
CHART_LIST=$(helm show chart oci://${{ env.REGISTRY }}/${{ env.REPO_OWNER }}/${{ env.CHART_NAME }} 2>/dev/null || true)
if [ -z "${CHART_LIST}" ]; then
echo "current-version=1.0.0" | tee -a $GITHUB_OUTPUT
else
# Extract version and strip any prerelease suffix (e.g., 1.0.5-latest -> 1.0.5)
VERSION=$(printf '%s' "${CHART_LIST}" | grep '^version:' | awk 'BEGIN{FS=":"}{print $2}' | tr -d " " | cut -d'-' -f1)
echo "current-version=${VERSION}" | tee -a $GITHUB_OUTPUT
fi
env:
HELM_EXPERIMENTAL_OCI: '1'
# Automatically update the helm chart version one "patch" level
- name: Bump release version
id: bump_version
uses: christian-draeger/increment-semantic-version@1.1.0
with:
current-version: ${{ steps.current_version.outputs.current-version || '1.0.0' }}
version-fragment: 'bug'
# Add suffix for non-stable releases (semantic versioning)
# Sync Helm chart version with LiteLLM release version (1-1 versioning)
# This allows users to easily map Helm chart versions to LiteLLM versions
# See: https://codefresh.io/docs/docs/ci-cd-guides/helm-best-practices/
- name: Calculate chart and app versions
id: chart_version
shell: bash
run: |
BASE_VERSION="${{ steps.bump_version.outputs.next-version || '1.0.0' }}"
RELEASE_TYPE="${{ github.event.inputs.release_type }}"
INPUT_TAG="${{ github.event.inputs.tag }}"
RELEASE_TYPE="${{ github.event.inputs.release_type }}"
# Chart version (independent Helm chart versioning with release type suffix)
if [ "$RELEASE_TYPE" = "stable" ]; then
echo "version=${BASE_VERSION}" | tee -a $GITHUB_OUTPUT
else
echo "version=${BASE_VERSION}-${RELEASE_TYPE}" | tee -a $GITHUB_OUTPUT
# Chart version = LiteLLM version without 'v' prefix (Helm semver convention)
# v1.81.0 -> 1.81.0, v1.81.0.rc.1 -> 1.81.0.rc.1
CHART_VERSION="${INPUT_TAG#v}"
# Add suffix for 'latest' releases (rc already has suffix in tag)
if [ "$RELEASE_TYPE" = "latest" ]; then
CHART_VERSION="${CHART_VERSION}-latest"
fi
# App version (must match Docker tags)
# stable/rc releases: Docker creates main-{tag}, so use the tag
# latest/dev releases: Docker only creates main-{release_type}, so use release_type
if [ "$RELEASE_TYPE" = "stable" ] || [ "$RELEASE_TYPE" = "rc" ]; then
APP_VERSION="${INPUT_TAG}"
else
APP_VERSION="${RELEASE_TYPE}"
fi
# App version = Docker tag (keeps 'v' prefix to match Docker image tags)
APP_VERSION="${INPUT_TAG}"
echo "version=${CHART_VERSION}" | tee -a $GITHUB_OUTPUT
echo "app_version=${APP_VERSION}" | tee -a $GITHUB_OUTPUT
- uses: ./.github/actions/helm-oci-chart-releaser
with:
name: ${{ env.CHART_NAME }}
repository: ${{ env.REPO_OWNER }}
tag: ${{ github.event.inputs.chartVersion || steps.chart_version.outputs.version || '1.0.0' }}
tag: ${{ steps.chart_version.outputs.version }}
app_version: ${{ steps.chart_version.outputs.app_version }}
path: deploy/charts/${{ env.CHART_NAME }}
registry: ${{ env.REGISTRY }}

View file

@ -1,10 +1,12 @@
# this workflow is triggered by an API call when there is a new PyPI release of LiteLLM
# Standalone workflow to publish LiteLLM Helm Chart
# Note: The main ghcr_deploy.yml workflow also publishes the Helm chart as part of a full release
name: Build, Publish LiteLLM Helm Chart. New Release
on:
workflow_dispatch:
inputs:
chartVersion:
description: "Update the helm chart's version to this"
tag:
description: "LiteLLM version tag (e.g., v1.81.0)"
required: true
# Defines two custom environment variables for the workflow. Used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
@ -31,24 +33,22 @@ jobs:
run: |
echo "REPO_OWNER=`echo ${{github.repository_owner}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV}
- name: Get LiteLLM Latest Tag
id: current_app_tag
uses: WyriHaximus/github-action-get-previous-tag@v1.3.0
- name: Get last published chart version
id: current_version
# Sync Helm chart version with LiteLLM release version (1-1 versioning)
- name: Calculate chart and app versions
id: chart_version
shell: bash
run: helm show chart oci://${{ env.REGISTRY }}/${{ env.REPO_OWNER }}/litellm-helm | grep '^version:' | awk 'BEGIN{FS=":"}{print "current-version="$2}' | tr -d " " | tee -a $GITHUB_OUTPUT
env:
HELM_EXPERIMENTAL_OCI: '1'
run: |
INPUT_TAG="${{ github.event.inputs.tag }}"
# Automatically update the helm chart version one "patch" level
- name: Bump release version
id: bump_version
uses: christian-draeger/increment-semantic-version@1.1.0
with:
current-version: ${{ steps.current_version.outputs.current-version || '0.1.0' }}
version-fragment: 'bug'
# Chart version = LiteLLM version without 'v' prefix
# v1.81.0 -> 1.81.0
CHART_VERSION="${INPUT_TAG#v}"
# App version = Docker tag (keeps 'v' prefix)
APP_VERSION="${INPUT_TAG}"
echo "version=${CHART_VERSION}" | tee -a $GITHUB_OUTPUT
echo "app_version=${APP_VERSION}" | tee -a $GITHUB_OUTPUT
- name: Lint helm chart
run: helm lint deploy/charts/litellm-helm
@ -57,8 +57,8 @@ jobs:
with:
name: litellm-helm
repository: ${{ env.REPO_OWNER }}
tag: ${{ github.event.inputs.chartVersion || steps.bump_version.outputs.next-version || '0.1.0' }}
app_version: ${{ steps.current_app_tag.outputs.tag || 'latest' }}
tag: ${{ steps.chart_version.outputs.version }}
app_version: ${{ steps.chart_version.outputs.app_version }}
path: deploy/charts/litellm-helm
registry: ${{ env.REGISTRY }}
registry_username: ${{ github.actor }}

View file

@ -2,6 +2,7 @@
if [ "$SEPARATE_HEALTH_APP" = "1" ]; then
export LITELLM_ARGS="$@"
export SUPERVISORD_STOPWAITSECS="${SUPERVISORD_STOPWAITSECS:-3600}"
exec supervisord -c /etc/supervisord.conf
fi

View file

@ -16,6 +16,7 @@ priority=1
exitcodes=0
stopasgroup=true
killasgroup=true
stopwaitsecs=%(ENV_SUPERVISORD_STOPWAITSECS)s
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes = 0
@ -31,6 +32,7 @@ priority=2
exitcodes=0
stopasgroup=true
killasgroup=true
stopwaitsecs=%(ENV_SUPERVISORD_STOPWAITSECS)s
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
stdout_logfile_maxbytes = 0

View file

@ -15,6 +15,17 @@ import TabItem from '@theme/TabItem';
<br />
:::tip Gemini API vs Vertex AI
| Model Format | Provider | Auth Required |
|-------------|----------|---------------|
| `gemini/gemini-2.0-flash` | Gemini API | `GEMINI_API_KEY` (simple API key) |
| `vertex_ai/gemini-2.0-flash` | Vertex AI | GCP credentials + project |
| `gemini-2.0-flash` (no prefix) | Vertex AI | GCP credentials + project |
**If you just want to use an API key** (like OpenAI), use the `gemini/` prefix.
Models without a prefix default to Vertex AI which requires full GCP authentication.
:::
## API Keys

View file

@ -14,6 +14,17 @@ import TabItem from '@theme/TabItem';
| Base URL | 1. Regional endpoints<br/>`https://{vertex_location}-aiplatform.googleapis.com/`<br/>2. Global endpoints (limited availability)<br/>`https://aiplatform.googleapis.com/`|
| Supported Operations | [`/chat/completions`](#sample-usage), `/completions`, [`/embeddings`](#embedding-models), [`/audio/speech`](#text-to-speech-apis), [`/fine_tuning`](#fine-tuning-apis), [`/batches`](#batch-apis), [`/files`](#batch-apis), [`/images`](#image-generation-models), [`/rerank`](#rerank-api) |
:::tip Vertex AI vs Gemini API
| Model Format | Provider | Auth Required |
|-------------|----------|---------------|
| `vertex_ai/gemini-2.0-flash` | Vertex AI | GCP credentials + project |
| `gemini-2.0-flash` (no prefix) | Vertex AI | GCP credentials + project |
| `gemini/gemini-2.0-flash` | Gemini API | `GEMINI_API_KEY` (simple API key) |
**If you just want to use an API key** (like OpenAI), use the `gemini/` prefix instead. See [Gemini - Google AI Studio](./gemini.md).
Models without a prefix default to Vertex AI which requires GCP authentication.
:::
<br />
<br />

View file

@ -875,6 +875,7 @@ router_settings:
| SECRET_MANAGER_REFRESH_INTERVAL | Refresh interval in seconds for secret manager. Default is 86400 (24 hours)
| SEPARATE_HEALTH_APP | If set to '1', runs health endpoints on a separate ASGI app and port. Default: '0'.
| SEPARATE_HEALTH_PORT | Port for the separate health endpoints app. Only used if SEPARATE_HEALTH_APP=1. Default: 4001.
| SUPERVISORD_STOPWAITSECS | Upper bound timeout in seconds for graceful shutdown when SEPARATE_HEALTH_APP=1. Default: 3600 (1 hour).
| SERVER_ROOT_PATH | Root path for the server application
| SEND_USER_API_KEY_ALIAS | Flag to send user API key alias to Zscaler AI Guard. Default is False
| SEND_USER_API_KEY_TEAM_ID | Flag to send user API key team ID to Zscaler AI Guard. Default is False

View file

@ -277,8 +277,13 @@ Set the following environment variable(s):
```bash
SEPARATE_HEALTH_APP="1" # Default "0"
SEPARATE_HEALTH_PORT="8001" # Default "4001", Works only if `SEPARATE_HEALTH_APP` is "1"
SUPERVISORD_STOPWAITSECS="3600" # Optional: Upper bound timeout in seconds for graceful shutdown. Default: 3600 (1 hour). Only used when SEPARATE_HEALTH_APP=1.
```
**Graceful Shutdown:**
Previously, `stopwaitsecs` was not set, defaulting to 10 seconds and causing in-flight requests to fail. `SUPERVISORD_STOPWAITSECS` (default: 3600) provides an upper bound for graceful shutdown, allowing uvicorn to wait for all in-flight requests to complete.
<video controls width="100%" style={{ borderRadius: '8px', marginBottom: '1em' }}>
<source src="https://cdn.loom.com/sessions/thumbnails/b08be303331246b88fdc053940d03281-1718990992822.mp4" type="video/mp4" />
Your browser does not support the video tag.

Binary file not shown.

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "litellm-proxy-extras"
version = "0.4.23"
version = "0.4.25"
description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package."
authors = ["BerriAI"]
readme = "README.md"
@ -22,7 +22,7 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.commitizen]
version = "0.4.23"
version = "0.4.25"
version_files = [
"pyproject.toml:version",
"../requirements.txt:litellm-proxy-extras==",

View file

@ -23,6 +23,11 @@ from .common_utils import (
is_global_only_vertex_model,
)
GOOGLE_IMPORT_ERROR_MESSAGE = (
"Google Cloud SDK not found. Install it with: pip install 'litellm[google]' "
"or pip install google-cloud-aiplatform"
)
if TYPE_CHECKING:
from google.auth.credentials import Credentials as GoogleCredentialsObject
else:
@ -138,7 +143,10 @@ class VertexBase:
# Google Auth Helpers -- extracted for mocking purposes in tests
def _credentials_from_identity_pool(self, json_obj, scopes):
from google.auth import identity_pool
try:
from google.auth import identity_pool
except ImportError:
raise ImportError(GOOGLE_IMPORT_ERROR_MESSAGE)
creds = identity_pool.Credentials.from_info(json_obj)
if scopes and hasattr(creds, "requires_scopes") and creds.requires_scopes:
@ -146,7 +154,10 @@ class VertexBase:
return creds
def _credentials_from_identity_pool_with_aws(self, json_obj, scopes):
from google.auth import aws
try:
from google.auth import aws
except ImportError:
raise ImportError(GOOGLE_IMPORT_ERROR_MESSAGE)
creds = aws.Credentials.from_info(json_obj)
if scopes and hasattr(creds, "requires_scopes") and creds.requires_scopes:
@ -154,22 +165,30 @@ class VertexBase:
return creds
def _credentials_from_authorized_user(self, json_obj, scopes):
import google.oauth2.credentials
try:
import google.oauth2.credentials
except ImportError:
raise ImportError(GOOGLE_IMPORT_ERROR_MESSAGE)
return google.oauth2.credentials.Credentials.from_authorized_user_info(
json_obj, scopes=scopes
)
def _credentials_from_service_account(self, json_obj, scopes):
import google.oauth2.service_account
try:
import google.oauth2.service_account
except ImportError:
raise ImportError(GOOGLE_IMPORT_ERROR_MESSAGE)
return google.oauth2.service_account.Credentials.from_service_account_info(
json_obj, scopes=scopes
)
def _credentials_from_default_auth(self, scopes):
import google.auth as google_auth
try:
import google.auth as google_auth
except ImportError:
raise ImportError(GOOGLE_IMPORT_ERROR_MESSAGE)
return google_auth.default(scopes=scopes)
@ -261,9 +280,12 @@ class VertexBase:
return api_base
def refresh_auth(self, credentials: Any) -> None:
from google.auth.transport.requests import (
Request, # type: ignore[import-untyped]
)
try:
from google.auth.transport.requests import (
Request, # type: ignore[import-untyped]
)
except ImportError:
raise ImportError(GOOGLE_IMPORT_ERROR_MESSAGE)
credentials.refresh(Request())

View file

@ -4681,26 +4681,35 @@ class Router:
parent_otel_span=parent_otel_span,
)
# raises an exception if this error should not be retries
self.should_retry_this_error(
error=e,
healthy_deployments=_healthy_deployments,
all_deployments=_all_deployments,
context_window_fallbacks=context_window_fallbacks,
regular_fallbacks=fallbacks,
content_policy_fallbacks=content_policy_fallbacks,
)
# Check retry policy FIRST, before should_retry_this_error
# This allows retry policies to override the healthy deployments check
_retry_policy_applies = False
if (
self.retry_policy is not None
or self.model_group_retry_policy is not None
):
# get num_retries from retry policy
# Use the model_group captured at the start of the function, or get it from metadata
# kwargs.get("model") at this point is the deployment model, not the model_group
_model_group_for_retry_policy = model_group or _metadata.get("model_group") or kwargs.get("model")
_retry_policy_retries = self.get_num_retries_from_retry_policy(
exception=original_exception, model_group=kwargs.get("model")
exception=original_exception, model_group=_model_group_for_retry_policy
)
if _retry_policy_retries is not None:
num_retries = _retry_policy_retries
_retry_policy_applies = True
# raises an exception if this error should not be retries
# Skip this check if retry policy applies (retry policy takes precedence)
if not _retry_policy_applies:
self.should_retry_this_error(
error=e,
healthy_deployments=_healthy_deployments,
all_deployments=_all_deployments,
context_window_fallbacks=context_window_fallbacks,
regular_fallbacks=fallbacks,
content_policy_fallbacks=content_policy_fallbacks,
)
## LOGGING
if num_retries > 0:
kwargs = self.log_retry(kwargs=kwargs, e=original_exception)

View file

@ -53,6 +53,7 @@ azure-keyvault-secrets = {version = "^4.8.0", optional = true}
azure-storage-blob = {version="^12.25.1", optional=true}
google-cloud-kms = {version = "^2.21.3", optional = true}
google-cloud-iam = {version = "^2.19.1", optional = true}
google-cloud-aiplatform = {version = ">=1.38.0", optional = true}
resend = {version = ">=0.8.0", optional = true}
pynacl = {version = "^1.5.0", optional = true}
websockets = {version = "^15.0.1", optional = true}
@ -60,7 +61,7 @@ boto3 = { version = "1.40.76", optional = true }
redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"}
mcp = {version = ">=1.25.0,<2.0.0", optional = true, python = ">=3.10"}
a2a-sdk = {version = "^0.3.22", optional = true, python = ">=3.10"}
litellm-proxy-extras = {version = "0.4.23", optional = true}
litellm-proxy-extras = {version = "0.4.25", optional = true}
rich = {version = "13.7.1", optional = true}
litellm-enterprise = {version = "0.1.27", optional = true}
diskcache = {version = "^5.6.1", optional = true}
@ -126,6 +127,7 @@ semantic-router = ["semantic-router"]
mlflow = ["mlflow"]
google = ["google-cloud-aiplatform"]
[tool.isort]
profile = "black"

View file

@ -49,7 +49,7 @@ sentry_sdk==2.21.0 # for sentry error handling
detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests
cryptography==44.0.1
tzdata==2025.1 # IANA time zone database
litellm-proxy-extras==0.4.23 # for proxy extras - e.g. prisma migrations
litellm-proxy-extras==0.4.25 # for proxy extras - e.g. prisma migrations
llm-sandbox==0.3.31 # for skill execution in sandbox
### LITELLM PACKAGE DEPENDENCIES
python-dotenv==1.0.1 # for env

View file

@ -20,6 +20,17 @@ from typing import Dict, List, Optional, Tuple
import httpx
import yaml
# Default prompt for health checks - exactly 100k characters
# Generate a repeating pattern to reach exactly 100,000 characters
_base_text = "This is a health check test prompt for LiteLLM proxy. "
_repeat_count = (100000 // len(_base_text)) + 1
_DEFAULT_COMPLETION_PROMPT = (_base_text * _repeat_count)[:100000]
# Default embedding text - also exactly 100k characters
_embedding_base_text = "This is a test for vectorization. "
_embedding_repeat_count = (100000 // len(_embedding_base_text)) + 1
_DEFAULT_EMBEDDING_TEXT = (_embedding_base_text * _embedding_repeat_count)[:100000]
class LiteLLMHealthCheckClient:
"""Client for health checking LiteLLM proxy models."""
@ -29,8 +40,9 @@ class LiteLLMHealthCheckClient:
base_url: str,
api_key: str,
timeout: int = 120, # Match Go implementation's 120s timeout
completion_prompt: str = "Say this is a test", # Match Go implementation
embedding_text: str = "This is a test for vectorization.", # Match Go implementation
completion_prompt: str = _DEFAULT_COMPLETION_PROMPT, # Default ~100k chars
embedding_text: str = _DEFAULT_EMBEDDING_TEXT, # Default ~100k chars
custom_auth_header: Optional[str] = None,
):
"""
Initialize the health check client.
@ -41,16 +53,34 @@ class LiteLLMHealthCheckClient:
timeout: Request timeout in seconds (default: 120, matching Go implementation)
completion_prompt: Test prompt for chat/completion models
embedding_text: Test text for embedding models
custom_auth_header: Optional custom header name for authentication (e.g., "x-ifood-requester-service").
If provided, uses this header instead of standard "Authorization" header.
"""
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout = timeout
self.completion_prompt = completion_prompt
self.embedding_text = embedding_text
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
# Debug: Print prompt/text lengths
print(f"DEBUG: Completion prompt length: {len(self.completion_prompt)} characters", file=sys.stderr)
print(f"DEBUG: Embedding text length: {len(self.embedding_text)} characters", file=sys.stderr)
# Support custom auth header for proxies with custom authentication
# Handle both None and empty string
if custom_auth_header and custom_auth_header.strip():
custom_auth_header = custom_auth_header.strip()
self.headers = {
custom_auth_header: f"Bearer {api_key}",
"Content-Type": "application/json",
}
print(f"Using custom auth header: {custom_auth_header}", file=sys.stderr)
else:
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
print("Using standard Authorization header", file=sys.stderr)
def load_models_from_yaml(self, yaml_path: str) -> List[Dict]:
"""
@ -182,6 +212,8 @@ class LiteLLMHealthCheckClient:
if is_embedding:
# Test embedding endpoint (matching Go implementation)
embedding_text_length = len(self.embedding_text)
print(f"DEBUG: Sending embedding text of length {embedding_text_length} chars to model {model_id}", file=sys.stderr)
embedding_response = await client.post(
f"{self.base_url}/v1/embeddings",
headers=self.headers,
@ -202,6 +234,8 @@ class LiteLLMHealthCheckClient:
result["dimensions"] = dimensions
else:
# Test chat completion endpoint (matching Go implementation)
prompt_length = len(self.completion_prompt)
print(f"DEBUG: Sending prompt of length {prompt_length} chars to model {model_id}", file=sys.stderr)
completion_response = await client.post(
f"{self.base_url}/v1/chat/completions",
headers=self.headers,
@ -358,6 +392,11 @@ async def main():
base_url = os.environ.get("LITELLM_BASE_URL", "http://localhost:4000")
api_key = os.environ.get("LITELLM_API_KEY", "sk-1234")
yaml_path = os.environ.get("LITELLM_MODELS_YAML")
custom_auth_header = os.environ.get("LITELLM_CUSTOM_AUTH_HEADER") # e.g., "x-ifood-requester-service"
# Debug: Print custom auth header value if set
if custom_auth_header:
print(f"Custom auth header from env: '{custom_auth_header}'", file=sys.stderr)
if not base_url:
print("Error: LITELLM_BASE_URL environment variable not set", file=sys.stderr)
@ -369,10 +408,10 @@ async def main():
timeout = int(os.environ.get("LITELLM_TIMEOUT", "120")) # Match Go's 120s default
completion_prompt = os.environ.get(
"LITELLM_COMPLETION_PROMPT", "Say this is a test"
"LITELLM_COMPLETION_PROMPT", _DEFAULT_COMPLETION_PROMPT
)
embedding_text = os.environ.get(
"LITELLM_EMBEDDING_TEXT", "This is a test for vectorization."
"LITELLM_EMBEDDING_TEXT", _DEFAULT_EMBEDDING_TEXT
)
json_output = os.environ.get("LITELLM_JSON_OUTPUT", "").lower() == "true"
# Optional: only health-check these model IDs (comma-separated). E.g.:
@ -386,6 +425,7 @@ async def main():
timeout=timeout,
completion_prompt=completion_prompt,
embedding_text=embedding_text,
custom_auth_header=custom_auth_header,
)
# Load models from YAML if provided, otherwise fetch from API

View file

@ -30,6 +30,14 @@ export LITELLM_MODELS_YAML="/path/to/config.yaml"
python scripts/health_check/health_check_client.py
```
**Option 3: Use custom authentication header**
```bash
export LITELLM_BASE_URL="https://litellm.example.com"
export LITELLM_API_KEY="your-api-key"
export LITELLM_CUSTOM_AUTH_HEADER="x-custom-auth-header"
python scripts/health_check/health_check_client.py
```
### As a Docker Container
1. Build the Docker image:
@ -47,6 +55,16 @@ docker run --rm \
litellm/litellm-health-check:latest
```
3. Run with custom authentication header:
```bash
docker run --rm \
-e LITELLM_BASE_URL="https://litellm.example.com" \
-e LITELLM_API_KEY="your-api-key" \
-e LITELLM_CUSTOM_AUTH_HEADER="x-custom-auth-header" \
litellm/litellm-health-check:latest
```
### Parallel Execution (Stress Testing)
Run multiple health check containers in parallel:
@ -65,6 +83,30 @@ export LITELLM_API_KEY="your-api-key"
./scripts/health_check/run_parallel_health_checks.sh 16
```
**With Custom Auth Header:**
```powershell
$env:LITELLM_BASE_URL="https://litellm.example.com"
$env:LITELLM_API_KEY="your-api-key"
$env:LITELLM_CUSTOM_AUTH_HEADER="x-custom-auth-header"
.\scripts\health_check\run_parallel_health_checks.ps1 16
```
**With Custom Docker Image:**
```powershell
$env:LITELLM_BASE_URL="https://litellm.example.com"
$env:LITELLM_API_KEY="your-api-key"
$env:LITELLM_CUSTOM_AUTH_HEADER="x-custom-auth-header"
.\scripts\health_check\run_parallel_health_checks.ps1 -NumParallelJobs 16 -ImageName "your-registry/your-image:tag"
```
**Bash with Custom Image:**
```bash
export LITELLM_BASE_URL="https://litellm.example.com"
export LITELLM_API_KEY="your-api-key"
export LITELLM_CUSTOM_AUTH_HEADER="x-custom-auth-header"
./scripts/health_check/run_parallel_health_checks.sh 16 "your-registry/your-image:tag"
```
## Configuration
@ -73,14 +115,29 @@ export LITELLM_API_KEY="your-api-key"
- `LITELLM_BASE_URL` (required): Base URL of the LiteLLM proxy
- Example: `https://litellm.example.com`
- `LITELLM_API_KEY` (required): API key for authentication
- `LITELLM_CUSTOM_AUTH_HEADER` (optional): Custom header name for authentication
- Use this when your LiteLLM proxy uses a custom authentication header instead of the standard `Authorization` header
- Example: `x-custom-auth-header` (the API key will be sent as `Bearer <api_key>` in this header)
- `LITELLM_MODELS_YAML` (optional): Path to YAML config file with model_list
- If provided, reads models from YAML instead of fetching from API
- Example: `/path/to/config.yaml`
- `LITELLM_TIMEOUT` (optional): Request timeout in seconds (default: 120)
- `LITELLM_COMPLETION_PROMPT` (optional): Test prompt for chat/completion models (default: "Say this is a test")
- `LITELLM_EMBEDDING_TEXT` (optional): Test text for embedding models (default: "This is a test for vectorization.")
- `LITELLM_COMPLETION_PROMPT` (optional): Test prompt for chat/completion models (default: ~100k characters)
- `LITELLM_EMBEDDING_TEXT` (optional): Test text for embedding models (default: ~100k characters)
- `LITELLM_JSON_OUTPUT` (optional): Output results as JSON (default: false)
### Parallel Script Parameters
**PowerShell (`run_parallel_health_checks.ps1`):**
- `-NumParallelJobs` (optional): Number of parallel containers to run (default: 16)
- `-ImageName` (optional): Docker image to use (default: `litellm/litellm-health-check:latest`)
- `-ContainerRuntime` (optional): Container runtime to use (default: `docker`)
**Bash (`run_parallel_health_checks.sh`):**
- `[num_parallel_jobs]` (optional): Number of parallel containers to run (default: 16)
- `[image_name]` (optional): Docker image to use (default: `litellm/litellm-health-check:latest`)
- `[container_runtime]` (optional): Container runtime to use (default: `docker`)
## Output
### Standard Output (Human-Readable)
@ -166,7 +223,20 @@ Run multiple health checks in parallel:
**PowerShell:**
```powershell
# Using default image
.\scripts\health_check\run_parallel_health_checks.ps1 16
# Using custom image
.\scripts\health_check\run_parallel_health_checks.ps1 -NumParallelJobs 16 -ImageName "your-registry/your-image:tag"
```
**Bash:**
```bash
# Using default image
./scripts/health_check/run_parallel_health_checks.sh 16
# Using custom image
./scripts/health_check/run_parallel_health_checks.sh 16 "your-registry/your-image:tag"
```
### 3. CI/CD Integration

View file

@ -49,6 +49,11 @@ Write-Host " 2. Set LITELLM_BASE_URL to the correct URL (e.g., http://host.do
Write-Host " 3. On Linux, you may need to use the host IP instead of host.docker.internal" -ForegroundColor Yellow
Write-Host ""
# Capture environment variables in parent scope for use in parallel block
$baseUrl = $env:LITELLM_BASE_URL
$apiKey = $env:LITELLM_API_KEY
$customAuthHeader = $env:LITELLM_CUSTOM_AUTH_HEADER
# Run parallel health checks
# This creates an infinite loop that keeps spawning containers
# Each container tests all models, then exits, and a new one starts
@ -57,13 +62,20 @@ while ($true) {
1..$NumParallelJobs | ForEach-Object -Parallel {
$runtime = $using:ContainerRuntime
$imageName = $using:ImageName
$baseUrl = $env:LITELLM_BASE_URL
$apiKey = $env:LITELLM_API_KEY
$baseUrl = $using:baseUrl
$apiKey = $using:apiKey
$customAuthHeader = $using:customAuthHeader
& $runtime run --rm `
-e LITELLM_BASE_URL="$baseUrl" `
-e LITELLM_API_KEY="$apiKey" `
-e LITELLM_JSON_OUTPUT="true" `
$imageName
$envVars = @(
"-e", "LITELLM_BASE_URL=$baseUrl",
"-e", "LITELLM_API_KEY=$apiKey",
"-e", "LITELLM_JSON_OUTPUT=true"
)
if ($customAuthHeader) {
$envVars += "-e", "LITELLM_CUSTOM_AUTH_HEADER=$customAuthHeader"
}
& $runtime run --rm $envVars $imageName
} -ThrottleLimit $NumParallelJobs
}

View file

@ -54,11 +54,18 @@ echo ""
# Function to run a single health check container
run_health_check() {
"$CONTAINER_RUNTIME" run --rm \
-e LITELLM_BASE_URL="$LITELLM_BASE_URL" \
-e LITELLM_API_KEY="$LITELLM_API_KEY" \
-e LITELLM_JSON_OUTPUT="true" \
"$IMAGE_NAME"
local env_vars=(
-e "LITELLM_BASE_URL=$LITELLM_BASE_URL"
-e "LITELLM_API_KEY=$LITELLM_API_KEY"
-e "LITELLM_JSON_OUTPUT=true"
)
# Pass through custom auth header if set
if [ -n "$LITELLM_CUSTOM_AUTH_HEADER" ]; then
env_vars+=(-e "LITELLM_CUSTOM_AUTH_HEADER=$LITELLM_CUSTOM_AUTH_HEADER")
fi
"$CONTAINER_RUNTIME" run --rm "${env_vars[@]}" "$IMAGE_NAME"
}
# Run parallel health checks

View file

@ -6,9 +6,12 @@ Tests:
2. Get marketplace.json (list enabled plugins)
"""
import json
import os
import sys
import time
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock
import pytest
@ -17,7 +20,6 @@ sys.path.insert(0, os.path.abspath("../.."))
import litellm
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.proxy_server import LitellmUserRoles
from litellm.proxy.utils import PrismaClient, ProxyLogging
from litellm.caching.caching import DualCache
from litellm.types.proxy.claude_code_endpoints import RegisterPluginRequest
@ -27,33 +29,118 @@ from litellm.proxy.anthropic_endpoints.claude_code_endpoints.claude_code_marketp
get_marketplace,
)
proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache())
class MockPluginRecord:
"""Mock plugin record that mimics Prisma model behavior."""
def __init__(self, name, version, description, manifest_json, enabled=True, created_by=None):
self.id = f"plugin-{name}-{int(time.time())}"
self.name = name
self.version = version
self.description = description
self.manifest_json = manifest_json
self.files_json = "{}"
self.enabled = enabled
self.created_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(timezone.utc)
self.created_by = created_by
@pytest.fixture
def prisma_client():
from litellm.proxy.proxy_cli import append_query_params
def mock_prisma_client():
"""Create a mock PrismaClient that doesn't require Prisma binaries."""
# In-memory storage for plugins
plugins_store = {}
params = {"connection_limit": 100, "pool_timeout": 60}
database_url = os.getenv("DATABASE_URL")
modified_url = append_query_params(database_url, params)
os.environ["DATABASE_URL"] = modified_url
# Create mock client
mock_client = MagicMock()
mock_client.proxy_logging_obj = MagicMock()
prisma_client = PrismaClient(
database_url=os.environ["DATABASE_URL"], proxy_logging_obj=proxy_logging_obj
)
# Mock the db attribute
mock_client.db = MagicMock()
litellm.proxy.proxy_server.litellm_proxy_budget_name = (
f"litellm-proxy-budget-{time.time()}"
)
# Mock the plugin table with async methods
mock_table = MagicMock()
return prisma_client
async def find_unique(where):
"""Mock find_unique - returns plugin if exists, None otherwise."""
plugin_name = where.get("name")
return plugins_store.get(plugin_name)
async def find_many(where=None):
"""Mock find_many - returns list of plugins matching where clause."""
if where is None or where == {}:
return list(plugins_store.values())
enabled = where.get("enabled")
if enabled is not None:
return [p for p in plugins_store.values() if p.enabled == enabled]
return list(plugins_store.values())
async def create(data):
"""Mock create - creates a new plugin."""
plugin_name = data["name"]
manifest = data.get("manifest_json", "{}")
plugin = MockPluginRecord(
name=plugin_name,
version=data.get("version"),
description=data.get("description"),
manifest_json=manifest,
enabled=data.get("enabled", True),
created_by=data.get("created_by"),
)
plugins_store[plugin_name] = plugin
return plugin
async def update(where, data):
"""Mock update - updates an existing plugin."""
plugin_name = where.get("name")
if plugin_name not in plugins_store:
raise ValueError(f"Plugin {plugin_name} not found")
plugin = plugins_store[plugin_name]
# Update fields
if "version" in data:
plugin.version = data["version"]
if "description" in data:
plugin.description = data["description"]
if "manifest_json" in data:
plugin.manifest_json = data["manifest_json"]
if "enabled" in data:
plugin.enabled = data["enabled"]
if "updated_at" in data:
plugin.updated_at = data["updated_at"]
return plugin
async def delete(where):
"""Mock delete - deletes a plugin."""
plugin_name = where.get("name")
if plugin_name in plugins_store:
del plugins_store[plugin_name]
return None
async def connect():
"""Mock connect - no-op."""
pass
# Set up async mocks
mock_table.find_unique = AsyncMock(side_effect=find_unique)
mock_table.find_many = AsyncMock(side_effect=find_many)
mock_table.create = AsyncMock(side_effect=create)
mock_table.update = AsyncMock(side_effect=update)
mock_table.delete = AsyncMock(side_effect=delete)
mock_client.db.litellm_claudecodeplugintable = mock_table
mock_client.connect = AsyncMock(side_effect=connect)
# Store plugins_store on the mock for cleanup if needed
mock_client._plugins_store = plugins_store
return mock_client
@pytest.mark.asyncio
async def test_register_plugin(prisma_client):
async def test_register_plugin(mock_prisma_client):
"""Test registering a plugin in the marketplace."""
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
setattr(litellm.proxy.proxy_server, "prisma_client", mock_prisma_client)
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
await litellm.proxy.proxy_server.prisma_client.connect()
@ -85,16 +172,23 @@ async def test_register_plugin(prisma_client):
assert response["plugin"]["version"] == "1.0.0"
assert response["plugin"]["enabled"] is True
# Verify the plugin was stored in the mock
stored_plugin = await mock_prisma_client.db.litellm_claudecodeplugintable.find_unique(
where={"name": plugin_name}
)
assert stored_plugin is not None
assert stored_plugin.name == plugin_name
# Cleanup - delete the plugin
await prisma_client.db.litellm_claudecodeplugintable.delete(
await mock_prisma_client.db.litellm_claudecodeplugintable.delete(
where={"name": plugin_name}
)
@pytest.mark.asyncio
async def test_get_marketplace(prisma_client):
async def test_get_marketplace(mock_prisma_client):
"""Test getting marketplace.json with registered plugins."""
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
setattr(litellm.proxy.proxy_server, "prisma_client", mock_prisma_client)
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
await litellm.proxy.proxy_server.prisma_client.connect()
@ -124,7 +218,6 @@ async def test_get_marketplace(prisma_client):
response = await get_marketplace()
# Response is a JSONResponse, get the body
import json
body = json.loads(response.body.decode())
assert body["name"] == "litellm"
@ -140,6 +233,6 @@ async def test_get_marketplace(prisma_client):
assert our_plugin["version"] == "2.0.0"
# Cleanup
await prisma_client.db.litellm_claudecodeplugintable.delete(
await mock_prisma_client.db.litellm_claudecodeplugintable.delete(
where={"name": plugin_name}
)

View file

@ -156,8 +156,8 @@ async def test_basic_spend_accuracy():
response = await chat_completion(session, key)
print("response: ", response)
# wait 15 seconds for spend to be updated
await asyncio.sleep(15)
# wait 25 seconds for spend to be updated
await asyncio.sleep(25)
# Get spend information for each entity
key_info = await get_spend_info(session, "key", key)

View file

@ -371,6 +371,9 @@ def test_virtual_key_llm_api_routes_allows_registered_pass_through_endpoints():
with patch(
"litellm.proxy.pass_through_endpoints.pass_through_endpoints._registered_pass_through_routes",
mock_registered_routes,
), patch(
"litellm.proxy.pass_through_endpoints.pass_through_endpoints.get_server_root_path",
return_value="/",
):
# Create a virtual key with llm_api_routes permission
valid_token = UserAPIKeyAuth(
@ -417,6 +420,9 @@ def test_virtual_key_without_llm_api_routes_cannot_access_pass_through():
with patch(
"litellm.proxy.pass_through_endpoints.pass_through_endpoints._registered_pass_through_routes",
mock_registered_routes,
), patch(
"litellm.proxy.pass_through_endpoints.pass_through_endpoints.get_server_root_path",
return_value="/",
):
# Create a virtual key without llm_api_routes permission
valid_token = UserAPIKeyAuth(

View file

@ -1,4 +1,4 @@
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import { useMutation } from "@tanstack/react-query";
interface CreateParams {
@ -18,7 +18,7 @@ const performCloudZeroCreate = async (accessToken: string, params: CreateParams)
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({

View file

@ -1,4 +1,4 @@
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import { useMutation } from "@tanstack/react-query";
interface DryRunParams {
@ -16,7 +16,7 @@ const performCloudZeroDryRun = async (accessToken: string, params: DryRunParams
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({

View file

@ -1,4 +1,4 @@
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import { useMutation } from "@tanstack/react-query";
interface ExportParams {
@ -16,7 +16,7 @@ const performCloudZeroExport = async (accessToken: string, params: ExportParams
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({

View file

@ -1,5 +1,5 @@
import { CloudZeroSettings } from "@/components/CloudZeroCostTracking/types";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createQueryKeys } from "../common/queryKeysFactory";
@ -12,7 +12,7 @@ const getCloudZeroSettings = async (accessToken: string): Promise<CloudZeroSetti
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
@ -82,7 +82,7 @@ const updateCloudZeroSettings = async (accessToken: string, params: UpdateParams
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
@ -140,7 +140,7 @@ const deleteCloudZeroSettings = async (accessToken: string): Promise<DeleteRespo
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});

View file

@ -7,6 +7,7 @@ import { RouterFieldsResponse, useRouterFields } from "./useRouterFields";
// Mock the networking module
vi.mock("@/components/networking", () => ({
proxyBaseUrl: null,
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
}));
// Mock useAuthorized hook

View file

@ -1,7 +1,7 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { createQueryKeys } from "../common/queryKeysFactory";
import { proxyBaseUrl } from "@/components/networking";
import { proxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
export interface RouterSettingsField {
field_name: string;
@ -39,7 +39,7 @@ const getRouterFields = async (accessToken: string): Promise<RouterFieldsRespons
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});

View file

@ -1,20 +1,19 @@
import React, { useState, useEffect, useMemo } from "react";
import { Input } from "antd";
import { Card, TabGroup, TabList, Tab, TabPanels, TabPanel, Text } from "@tremor/react";
import { SearchOutlined } from "@ant-design/icons";
import { getClaudeCodeMarketplace } from "../networking";
import { ModelDataTable } from "../model_dashboard/table";
import { getMarketplaceTableColumns } from "./marketplace_table_columns";
import NotificationsManager from "../molecules/notifications_manager";
import {
MarketplaceResponse,
MarketplacePluginEntry,
} from "../claude_code_plugins/types";
import { Card, Tab, TabGroup, TabList, TabPanel, TabPanels, Text } from "@tremor/react";
import { Input } from "antd";
import React, { useEffect, useMemo, useState } from "react";
import {
extractCategories,
filterPluginsBySearch,
filterPluginsByCategory,
filterPluginsBySearch,
} from "../claude_code_plugins/helpers";
import {
MarketplaceResponse
} from "../claude_code_plugins/types";
import { ModelDataTable } from "../model_dashboard/table";
import NotificationsManager from "../molecules/notifications_manager";
import { getClaudeCodeMarketplace } from "../networking";
import { getMarketplaceTableColumns } from "./marketplace_table_columns";
interface ClaudeCodeMarketplaceTabProps {
publicPage?: boolean;

View file

@ -1,15 +1,15 @@
import React from "react";
import { Card, Badge, Button, Text } from "@tremor/react";
import { Tooltip } from "antd";
import { CopyOutlined, ExternalLinkIcon } from "@heroicons/react/outline";
import { MarketplacePluginEntry } from "@/components/claude_code_plugins/types";
import {
formatInstallCommand,
getCategoryBadgeColor,
getSourceLink,
truncateText,
getSourceLink
} from "@/components/claude_code_plugins/helpers";
import { MarketplacePluginEntry } from "@/components/claude_code_plugins/types";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { ExternalLinkIcon } from "@heroicons/react/outline";
import { CopyOutlined } from "@ant-design/icons";
import { Badge, Button, Card, Text } from "@tremor/react";
import { Tooltip } from "antd";
import React from "react";
interface PluginCardProps {
plugin: MarketplacePluginEntry;

View file

@ -1,5 +1,5 @@
import { useState, useCallback, useRef, useEffect } from "react";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "../../molecules/notifications_manager";
import { CostEstimateRequest, CostEstimateResponse } from "../types";
import { PricingFormValues } from "./types";
@ -36,7 +36,7 @@ export function useCostEstimate(accessToken: string | null) {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),

View file

@ -1,5 +1,5 @@
import { useState, useCallback, useRef, useEffect } from "react";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import { CostEstimateRequest, CostEstimateResponse } from "../types";
import { ModelEntry, MultiModelResult } from "./types";
@ -59,7 +59,7 @@ export function useMultiCostEstimate(accessToken: string | null) {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),

View file

@ -1,5 +1,5 @@
import { useState, useCallback } from "react";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "../molecules/notifications_manager";
import { DiscountConfig } from "./types";
import { getProviderBackendValue } from "./provider_display_helpers";
@ -32,7 +32,7 @@ export function useDiscountConfig({ accessToken }: UseDiscountConfigProps): UseD
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
@ -59,7 +59,7 @@ export function useDiscountConfig({ accessToken }: UseDiscountConfigProps): UseD
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(config),

View file

@ -1,5 +1,5 @@
import { useState, useCallback } from "react";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "../molecules/notifications_manager";
import { MarginConfig } from "./types";
import { getProviderBackendValue } from "./provider_display_helpers";
@ -42,7 +42,7 @@ export function useMarginConfig({ accessToken }: UseMarginConfigProps): UseMargi
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
@ -69,7 +69,7 @@ export function useMarginConfig({ accessToken }: UseMarginConfigProps): UseMargi
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(config),

View file

@ -1,30 +1,29 @@
import React, { useState, useEffect } from "react";
import {
Card,
Title,
Text,
Button,
Badge,
Grid,
} from "@tremor/react";
import { Spin, Switch, Tooltip, Descriptions } from "antd";
import { ArrowLeftIcon, ExternalLinkIcon } from "@heroicons/react/outline";
import { CopyOutlined } from "@ant-design/icons";
import { ArrowLeftIcon, ExternalLinkIcon } from "@heroicons/react/outline";
import {
getClaudeCodePluginDetails,
enableClaudeCodePlugin,
disableClaudeCodePlugin,
} from "../networking";
Badge,
Button,
Card,
Grid,
Text,
Title,
} from "@tremor/react";
import { Spin, Switch, Tooltip } from "antd";
import React, { useEffect, useState } from "react";
import NotificationsManager from "../molecules/notifications_manager";
import { Plugin } from "./types";
import {
formatInstallCommand,
getSourceDisplayText,
getSourceLink,
getCategoryBadgeColor,
disableClaudeCodePlugin,
enableClaudeCodePlugin,
getClaudeCodePluginDetails,
} from "../networking";
import {
formatDateString,
formatKeywords,
formatInstallCommand,
getCategoryBadgeColor,
getSourceDisplayText,
getSourceLink
} from "./helpers";
import { Plugin } from "./types";
interface PluginInfoViewProps {
pluginId: string;
@ -169,7 +168,9 @@ const PluginInfoView: React.FC<PluginInfoViewProps> = ({
{/* Plugin Details */}
<Card>
<Title>Plugin Details</Title>
<Grid numColsSm={2} numColsLg={3} className="gap-6 mt-4">
<Grid
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-4"
>
{/* Plugin ID */}
<div>
<Text className="text-gray-600 text-xs">Plugin ID</Text>
@ -277,7 +278,7 @@ const PluginInfoView: React.FC<PluginInfoViewProps> = ({
{plugin.author && (
<Card>
<Title>Author Information</Title>
<Grid numColsSm={2} className="gap-4 mt-4">
<Grid className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
{plugin.author.name && (
<div>
<Text className="text-gray-600 text-xs">Name</Text>
@ -322,7 +323,7 @@ const PluginInfoView: React.FC<PluginInfoViewProps> = ({
{/* Timestamps */}
<Card>
<Title>Metadata</Title>
<Grid numColsSm={2} className="gap-4 mt-4">
<Grid className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
<div>
<Text className="text-gray-600 text-xs">Created At</Text>
<Text className="font-semibold mt-1">

View file

@ -1,32 +1,10 @@
import React, { useState } from "react";
import { CopyOutlined } from "@ant-design/icons";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Button,
Badge,
} from "@tremor/react";
import {
SwitchVerticalIcon,
ChevronUpIcon,
ChevronDownIcon,
ChevronUpIcon,
SwitchVerticalIcon,
TrashIcon,
} from "@heroicons/react/outline";
import { Tooltip, Switch } from "antd";
import { CopyOutlined } from "@ant-design/icons";
import { Plugin } from "./types";
import {
getCategoryBadgeColor,
formatDateString,
} from "./helpers";
import {
enableClaudeCodePlugin,
disableClaudeCodePlugin,
} from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
import {
ColumnDef,
flexRender,
@ -35,6 +13,27 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
import {
Badge,
Button,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
} from "@tremor/react";
import { Switch, Tooltip } from "antd";
import React, { useState } from "react";
import NotificationsManager from "../molecules/notifications_manager";
import {
disableClaudeCodePlugin,
enableClaudeCodePlugin,
} from "../networking";
import {
getCategoryBadgeColor
} from "./helpers";
import { Plugin } from "./types";
interface PluginTableProps {
pluginsList: Plugin[];
@ -209,33 +208,33 @@ const PluginTable: React.FC<PluginTableProps> = ({
},
...(isAdmin
? [
{
header: "Actions",
id: "actions",
enableSorting: false,
cell: ({ row }: any) => {
const plugin = row.original;
{
header: "Actions",
id: "actions",
enableSorting: false,
cell: ({ row }: any) => {
const plugin = row.original;
return (
<div className="flex items-center gap-1">
<Tooltip title="Delete plugin">
<Button
size="xs"
variant="light"
color="red"
onClick={(e) => {
e.stopPropagation();
onDeleteClick(plugin.name, plugin.name);
}}
icon={TrashIcon}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
/>
</Tooltip>
</div>
);
},
return (
<div className="flex items-center gap-1">
<Tooltip title="Delete plugin">
<Button
size="xs"
variant="light"
color="red"
onClick={(e) => {
e.stopPropagation();
onDeleteClick(plugin.name, plugin.name);
}}
icon={TrashIcon}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
/>
</Tooltip>
</div>
);
},
]
},
]
: []),
];
@ -261,11 +260,10 @@ const PluginTable: React.FC<PluginTableProps> = ({
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 ${
header.id === "actions"
className={`py-1 h-8 ${header.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
}`}
onClick={
header.column.getCanSort()
? header.column.getToggleSortingHandler()
@ -277,9 +275,9 @@ const PluginTable: React.FC<PluginTableProps> = ({
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
header.column.columnDef.header,
header.getContext()
)}
</div>
{header.column.getCanSort() && (
<div className="w-4">
@ -318,11 +316,10 @@ const PluginTable: React.FC<PluginTableProps> = ({
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === "actions"
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${cell.column.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
}`}
>
{flexRender(
cell.column.columnDef.cell,

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Text, Button, Callout, TextInput } from "@tremor/react";
import { Modal, Form, Spin, Select } from "antd";
import { getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "./molecules/notifications_manager";
interface CloudZeroExportModalProps {
@ -43,7 +44,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
const response = await fetch("/cloudzero/settings", {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
@ -88,7 +89,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
const response = await fetch(endpoint, {
method,
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
@ -128,7 +129,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
const response = await fetch("/cloudzero/export", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({

View file

@ -15,7 +15,7 @@ import {
Col,
Subtitle,
} from "@tremor/react";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "./molecules/notifications_manager";
interface CostTrackingSettingsProps {
@ -56,7 +56,7 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
@ -86,7 +86,7 @@ const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(discountConfig),

View file

@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { Form, Typography, Select, Input, Switch, Modal } from "antd";
import { Button, TextInput } from "@tremor/react";
import { guardrail_provider_map, guardrailLogoMap, getGuardrailProviders } from "./guardrail_info_helpers";
import { getGuardrailUISettings } from "../networking";
import { getGuardrailUISettings, getGlobalLitellmHeaderName } from "../networking";
import PiiConfiguration from "./pii_configuration";
import NotificationsManager from "../molecules/notifications_manager";
@ -183,7 +183,7 @@ const EditGuardrailForm: React.FC<EditGuardrailFormProps> = ({
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(guardrailData),

View file

@ -6256,7 +6256,7 @@ export const tagCreateCall = async (accessToken: string, formValues: TagNewReque
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify(formValues),
});
@ -6282,7 +6282,7 @@ export const tagUpdateCall = async (accessToken: string, formValues: TagUpdateRe
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify(formValues),
});
@ -6308,7 +6308,7 @@ export const tagInfoCall = async (accessToken: string, tagNames: string[]): Prom
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify({ names: tagNames }),
});
@ -6334,7 +6334,7 @@ export const tagListCall = async (accessToken: string): Promise<TagListResponse>
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
});
@ -6360,7 +6360,7 @@ export const tagDeleteCall = async (accessToken: string, tagName: string): Promi
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify({ name: tagName }),
});
@ -6452,7 +6452,7 @@ export const getTeamPermissionsCall = async (accessToken: string, teamId: string
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
});
@ -6480,7 +6480,7 @@ export const teamPermissionsUpdateCall = async (accessToken: string, teamId: str
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify({
team_id: teamId,
@ -6544,7 +6544,7 @@ export const vectorStoreCreateCall = async (accessToken: string, formValues: Rec
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify(formValues),
});
@ -6573,7 +6573,7 @@ export const vectorStoreListCall = async (
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
});
@ -6597,7 +6597,7 @@ export const vectorStoreDeleteCall = async (accessToken: string, vectorStoreId:
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify({ vector_store_id: vectorStoreId }),
});
@ -6622,7 +6622,7 @@ export const vectorStoreInfoCall = async (accessToken: string, vectorStoreId: st
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify({ vector_store_id: vectorStoreId }),
});
@ -6647,7 +6647,7 @@ export const vectorStoreUpdateCall = async (accessToken: string, formValues: Rec
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
},
body: JSON.stringify(formValues),
});
@ -7768,7 +7768,7 @@ export const vectorStoreSearchCall = async (
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
@ -7801,7 +7801,7 @@ export const searchToolQueryCall = async (
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({

View file

@ -19,7 +19,7 @@ vi.mock("../networking", () => ({
// Mock scrollIntoView which is not available in jsdom
beforeEach(() => {
Element.prototype.scrollIntoView = () => {};
Element.prototype.scrollIntoView = () => { };
});
describe("ChatUI", () => {
@ -270,4 +270,43 @@ describe("ChatUI", () => {
expect(mcpSelect).not.toHaveClass("ant-select-disabled");
});
});
it("should show Fill button and populate customProxyBaseUrl when proxySettings.LITELLM_UI_API_DOC_BASE_URL is provided", async () => {
const testProxyUrl = "http://localhost:5000";
render(
<ChatUI
accessToken="1234567890"
token="1234567890"
userRole="user"
userID="1234567890"
disabledPersonalKeyCreation={false}
proxySettings={{
LITELLM_UI_API_DOC_BASE_URL: testProxyUrl,
}}
/>,
);
await waitFor(() => {
expect(screen.getByText("Test Key")).toBeInTheDocument();
});
const fillButton = screen.getByText("Fill");
expect(fillButton).toBeInTheDocument();
act(() => {
fireEvent.click(fillButton);
});
await waitFor(() => {
expect(sessionStorage.getItem("customProxyBaseUrl")).toBe(testProxyUrl);
});
await waitFor(() => {
expect(screen.queryByText("Fill")).toBeNull();
});
const customProxyInput = screen.getByPlaceholderText("Optional: Enter custom proxy URL (e.g., http://localhost:5000)");
expect(customProxyInput).toHaveValue(testProxyUrl);
});
});

View file

@ -10,6 +10,7 @@ import {
FilePdfOutlined,
InfoCircleOutlined,
KeyOutlined,
LinkOutlined,
LoadingOutlined,
PictureOutlined,
RobotOutlined,
@ -1172,9 +1173,23 @@ const ChatUI: React.FC<ChatUIProps> = ({
<div>
<div className="flex items-center justify-between mb-2">
<Text className="font-medium text-gray-700 flex items-center">
<Text className="font-medium block text-gray-700 flex items-center">
<SettingOutlined className="mr-2" /> Custom Proxy Base URL
</Text>
{proxySettings?.LITELLM_UI_API_DOC_BASE_URL && !customProxyBaseUrl && (
<Button
type="link"
size="small"
icon={<LinkOutlined />}
onClick={() => {
setCustomProxyBaseUrl(proxySettings.LITELLM_UI_API_DOC_BASE_URL || "");
sessionStorage.setItem("customProxyBaseUrl", proxySettings.LITELLM_UI_API_DOC_BASE_URL || "");
}}
className="text-gray-500 hover:text-gray-700"
>
Fill
</Button>
)}
{customProxyBaseUrl && (
<Button
type="link"
@ -1219,7 +1234,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
try {
sessionStorage.removeItem("selectedModel");
sessionStorage.removeItem("selectedAgent");
} catch {}
} catch { }
}}
className="mb-4"
/>
@ -1575,7 +1590,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
enabled={codeInterpreter.enabled}
onEnabledChange={codeInterpreter.setEnabled}
selectedContainerId={null}
onContainerChange={() => {}}
onContainerChange={() => { }}
selectedModel={selectedModel || ""}
/>
</div>
@ -2071,11 +2086,10 @@ const ChatUI: React.FC<ChatUIProps> = ({
}
>
<button
className={`p-1.5 rounded-md transition-colors ${
codeInterpreter.enabled
? "bg-blue-100 text-blue-600"
: "text-gray-400 hover:text-gray-600 hover:bg-gray-100"
}`}
className={`p-1.5 rounded-md transition-colors ${codeInterpreter.enabled
? "bg-blue-100 text-blue-600"
: "text-gray-400 hover:text-gray-600 hover:bg-gray-100"
}`}
onClick={() => {
codeInterpreter.toggle();
if (!codeInterpreter.enabled) {
@ -2096,9 +2110,9 @@ const ChatUI: React.FC<ChatUIProps> = ({
onKeyDown={handleKeyDown}
placeholder={
endpointType === EndpointType.CHAT ||
endpointType === EndpointType.EMBEDDINGS ||
endpointType === EndpointType.RESPONSES ||
endpointType === EndpointType.ANTHROPIC_MESSAGES
endpointType === EndpointType.EMBEDDINGS ||
endpointType === EndpointType.RESPONSES ||
endpointType === EndpointType.ANTHROPIC_MESSAGES
? "Type your message... (Shift+Enter for new line)"
: endpointType === EndpointType.A2A_AGENTS
? "Send a message to the A2A agent..."

View file

@ -5,6 +5,7 @@ import CodeInterpreterOutput from "./CodeInterpreterOutput";
vi.mock("@/components/networking", () => ({
getProxyBaseUrl: vi.fn(() => "https://example.com"),
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
}));
global.fetch = vi.fn();

View file

@ -9,7 +9,7 @@ import {
} from "@ant-design/icons";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
interface ContainerFileCitation {
type: "container_file_citation";
@ -55,7 +55,7 @@ const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({
`${proxyBaseUrl}/v1/containers/${annotation.container_id}/files/${annotation.file_id}/content`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
},
}
);
@ -90,7 +90,7 @@ const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({
`${proxyBaseUrl}/v1/containers/${annotation.container_id}/files/${annotation.file_id}/content`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
},
}
);

View file

@ -2,7 +2,7 @@
// A2A Protocol (JSON-RPC 2.0) implementation for sending messages to agents
import { v4 as uuidv4 } from "uuid";
import { getProxyBaseUrl } from "../../networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "../../networking";
import { A2ATaskMetadata } from "../chat_ui/types";
interface A2AMessagePart {
@ -143,7 +143,7 @@ export const makeA2ASendMessageRequest = async (
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(jsonRpcRequest),
@ -276,7 +276,7 @@ export const makeA2AStreamMessageRequest = async (
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(jsonRpcRequest),

View file

@ -3,6 +3,7 @@ import { makeOpenAIEmbeddingsRequest } from "./embeddings_api";
vi.mock("@/components/networking", () => ({
getProxyBaseUrl: vi.fn(() => "https://example.com"),
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
}));
describe("embeddings_api", () => {

View file

@ -1,5 +1,5 @@
import NotificationManager from "@/components/molecules/notifications_manager";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
export async function makeOpenAIEmbeddingsRequest(
input: string,
@ -34,7 +34,7 @@ export async function makeOpenAIEmbeddingsRequest(
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
...headers,
},
body: JSON.stringify({

View file

@ -1,6 +1,6 @@
// fetch_agents.tsx
import { getProxyBaseUrl } from "../../networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "../../networking";
export interface Agent {
agent_id: string;
@ -27,7 +27,7 @@ export const fetchAvailableAgents = async (
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});

View file

@ -3,7 +3,7 @@ import NotificationsManager from "../../../molecules/notifications_manager";
import { TokenUsage } from "../../../playground/chat_ui/ResponseMetrics";
import { Message } from "./types";
import { convertToDotPrompt, extractVariables } from "../utils";
import { getProxyBaseUrl } from "../../../networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "../../../networking";
export const useConversation = (prompt: any, accessToken: string | null) => {
const [isLoading, setIsLoading] = useState(false);
@ -91,7 +91,7 @@ export const useConversation = (prompt: any, accessToken: string | null) => {
const response = await fetch(`${proxyBaseUrl}/prompts/test`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { Card, Title, Text, TextInput, Button } from "@tremor/react";
import { useTheme } from "@/contexts/ThemeContext";
import { getProxyBaseUrl } from "@/components/networking";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "./molecules/notifications_manager";
interface UIThemeSettingsProps {
@ -29,7 +29,7 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
@ -53,7 +53,7 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
@ -87,7 +87,7 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({