mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
Merge branch 'main' into litellm_claude_code_max
This commit is contained in:
commit
14593a5b3c
50 changed files with 626 additions and 316 deletions
66
.github/workflows/ghcr_deploy.yml
vendored
66
.github/workflows/ghcr_deploy.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
42
.github/workflows/ghcr_helm_deploy.yml
vendored
42
.github/workflows/ghcr_helm_deploy.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
BIN
litellm-proxy-extras/dist/litellm_proxy_extras-0.4.25-py3-none-any.whl
vendored
Normal file
BIN
litellm-proxy-extras/dist/litellm_proxy_extras-0.4.25-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
litellm-proxy-extras/dist/litellm_proxy_extras-0.4.25.tar.gz
vendored
Normal file
BIN
litellm-proxy-extras/dist/litellm_proxy_extras-0.4.25.tar.gz
vendored
Normal file
Binary file not shown.
|
|
@ -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==",
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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..."
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue