mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Merge branch 'main' into litellm_json_log_issue_0001
This commit is contained in:
commit
db63ce4f07
70 changed files with 6733 additions and 680 deletions
|
|
@ -119,7 +119,7 @@ jobs:
|
|||
username: ${DOCKERHUB_USERNAME}
|
||||
password: ${DOCKERHUB_PASSWORD}
|
||||
working_directory: ~/project
|
||||
|
||||
parallelism: 4
|
||||
steps:
|
||||
- checkout
|
||||
- setup_google_dns
|
||||
|
|
@ -207,12 +207,22 @@ jobs:
|
|||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
pwd
|
||||
ls
|
||||
# Add --timeout to kill hanging tests after 300s (5 min)
|
||||
# Add -v to show test names as they run for debugging
|
||||
# Add --tb=short for shorter tracebacks
|
||||
python -m pytest -vv tests/local_testing --cov=litellm --cov-report=xml --junitxml=test-results/junit.xml --durations=20 -k "not test_python_38.py and not test_basic_python_version.py and not router and not assistants and not langfuse and not caching and not cache" -n 4 --timeout=300 --timeout_method=thread
|
||||
mkdir test-results
|
||||
# Discover test files
|
||||
TEST_FILES=$(circleci tests glob "tests/local_testing/**/test_*.py")
|
||||
echo "$TEST_FILES" | circleci tests run \
|
||||
--split-by=filesize \
|
||||
--verbose \
|
||||
--command="xargs python -m pytest \
|
||||
-vv \
|
||||
--cov=litellm \
|
||||
--cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=20 \
|
||||
-k \"not test_python_38.py and not test_basic_python_version.py and not router and not assistants and not langfuse and not caching and not cache\" \
|
||||
-n 4 \
|
||||
--timeout=300 \
|
||||
--timeout_method=thread"
|
||||
no_output_timeout: 120m
|
||||
- run:
|
||||
name: Rename the coverage files
|
||||
|
|
@ -499,7 +509,7 @@ jobs:
|
|||
username: ${DOCKERHUB_USERNAME}
|
||||
password: ${DOCKERHUB_PASSWORD}
|
||||
working_directory: ~/project
|
||||
|
||||
parallelism: 4
|
||||
steps:
|
||||
- checkout
|
||||
- setup_google_dns
|
||||
|
|
@ -513,6 +523,7 @@ jobs:
|
|||
pip install "pytest-cov==5.0.0"
|
||||
pip install "pytest-retry==1.6.3"
|
||||
pip install "pytest-asyncio==0.21.1"
|
||||
pip install "pytest-xdist==3.6.1"
|
||||
pip install semantic_router --no-deps
|
||||
pip install aurelio_sdk --no-deps
|
||||
# Run pytest and generate JUnit XML report
|
||||
|
|
@ -520,9 +531,21 @@ jobs:
|
|||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
pwd
|
||||
ls
|
||||
python -m pytest tests/local_testing --cov=litellm --cov-report=xml -vv -k "router" -v --junitxml=test-results/junit.xml --durations=5
|
||||
mkdir test-results
|
||||
# Find test files only in local_testing
|
||||
TEST_FILES=$(circleci tests glob "tests/local_testing/**/test_*.py")
|
||||
echo "$TEST_FILES" | circleci tests run \
|
||||
--split-by=filesize \
|
||||
--verbose \
|
||||
--command="xargs python -m pytest -o junit_family=legacy \
|
||||
-k 'router' \
|
||||
--cov=litellm \
|
||||
--cov-report=xml \
|
||||
-n 4 \
|
||||
--dist=loadscope \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-vv"
|
||||
no_output_timeout: 120m
|
||||
- run:
|
||||
name: Rename the coverage files
|
||||
|
|
@ -1743,13 +1766,14 @@ jobs:
|
|||
pip install "pytest-cov==5.0.0"
|
||||
pip install "pytest-asyncio==0.21.1"
|
||||
pip install "respx==0.22.0"
|
||||
pip install "pytest-xdist==3.6.1"
|
||||
# Run pytest and generate JUnit XML report
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
pwd
|
||||
ls
|
||||
python -m pytest -vv tests/image_gen_tests --cov=litellm --cov-report=xml -x -v --junitxml=test-results/junit.xml --durations=5
|
||||
python -m pytest -vv tests/image_gen_tests -n 4 --cov=litellm --cov-report=xml -x -v --junitxml=test-results/junit.xml --durations=5
|
||||
no_output_timeout: 120m
|
||||
- run:
|
||||
name: Rename the coverage files
|
||||
|
|
@ -2192,6 +2216,8 @@ jobs:
|
|||
pip install "asyncio==3.4.3"
|
||||
pip install "PyGithub==1.59.1"
|
||||
pip install "openai==1.100.1"
|
||||
pip install "litellm[proxy]"
|
||||
pip install "pytest-xdist==3.6.1"
|
||||
- run:
|
||||
name: Install dockerize
|
||||
command: |
|
||||
|
|
@ -2268,7 +2294,7 @@ jobs:
|
|||
command: |
|
||||
pwd
|
||||
ls
|
||||
python -m pytest -s -vv tests/*.py -x --junitxml=test-results/junit.xml --durations=5 --ignore=tests/otel_tests --ignore=tests/spend_tracking_tests --ignore=tests/pass_through_tests --ignore=tests/proxy_admin_ui_tests --ignore=tests/load_tests --ignore=tests/llm_translation --ignore=tests/llm_responses_api_testing --ignore=tests/mcp_tests --ignore=tests/guardrails_tests --ignore=tests/image_gen_tests --ignore=tests/pass_through_unit_tests
|
||||
python -m pytest -s -vv tests/*.py -x --junitxml=test-results/junit.xml -n 4 --durations=5 --ignore=tests/otel_tests --ignore=tests/spend_tracking_tests --ignore=tests/pass_through_tests --ignore=tests/proxy_admin_ui_tests --ignore=tests/load_tests --ignore=tests/llm_translation --ignore=tests/llm_responses_api_testing --ignore=tests/mcp_tests --ignore=tests/guardrails_tests --ignore=tests/image_gen_tests --ignore=tests/pass_through_unit_tests
|
||||
no_output_timeout: 120m
|
||||
|
||||
# Store test results
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
.python-version
|
||||
.venv
|
||||
.venv_policy_test
|
||||
.env
|
||||
.newenv
|
||||
newenv/*
|
||||
|
|
|
|||
|
|
@ -138,6 +138,21 @@ run_grype_scans() {
|
|||
"CVE-2026-22184" # zlib untgz buffer overflow - untgz unused + no fixed Wolfi build yet
|
||||
"GHSA-58pv-8j8x-9vj2" # jaraco.context path traversal - setuptools vendored only (v5.3.0), not used in application code (using v6.1.0+)
|
||||
"GHSA-r6q2-hw4h-h46w" # node-tar not used by application runtime, Linux-only container, not affect by macOS APFS-specific exploit
|
||||
"GHSA-8rrh-rw8j-w5fx" # wheel is from chainguard and will be handled by then TODO: Remove this after Chainguard updates the wheel
|
||||
"CVE-2025-59465" # We do not use Node in application runtime, only used for building Admin UI
|
||||
"CVE-2025-55131" # We do not use Node in application runtime, only used for building Admin UI
|
||||
"CVE-2025-59466" # We do not use Node in application runtime, only used for building Admin UI
|
||||
"CVE-2025-55130" # We do not use Node in application runtime, only used for building Admin UI
|
||||
"CVE-2025-59467" # We do not use Node in application runtime, only used for building Admin UI
|
||||
"CVE-2026-21637" # We do not use Node in application runtime, only used for building Admin UI
|
||||
"CVE-2025-15281" # No fix available yet
|
||||
"CVE-2026-0865" # No fix available yet
|
||||
"CVE-2025-15282" # No fix available yet
|
||||
"CVE-2026-0672" # No fix available yet
|
||||
"CVE-2025-15366" # No fix available yet
|
||||
"CVE-2025-15367" # No fix available yet
|
||||
"CVE-2025-12781" # No fix available yet
|
||||
"CVE-2025-11468" # No fix available yet
|
||||
)
|
||||
|
||||
# Build JSON array of allowlisted CVE IDs for jq
|
||||
|
|
|
|||
|
|
@ -611,6 +611,8 @@ router_settings:
|
|||
| GALILEO_USERNAME | Username for Galileo authentication
|
||||
| GOOGLE_SECRET_MANAGER_PROJECT_ID | Project ID for Google Secret Manager
|
||||
| GCS_BUCKET_NAME | Name of the Google Cloud Storage bucket
|
||||
| GCS_MOCK | Enable mock mode for GCS integration testing. When set to true, intercepts GCS API calls and returns mock responses without making actual network calls. Default is false
|
||||
| GCS_MOCK_LATENCY_MS | Mock latency in milliseconds for GCS API calls when mock mode is enabled. Simulates network round-trip time. Default is 150ms
|
||||
| GCS_PATH_SERVICE_ACCOUNT | Path to the Google Cloud service account JSON file
|
||||
| GCS_FLUSH_INTERVAL | Flush interval for GCS logging (in seconds). Specify how often you want a log to be sent to GCS. **Default is 20 seconds**
|
||||
| GCS_BATCH_SIZE | Batch size for GCS logging. Specify after how many logs you want to flush to GCS. If `BATCH_SIZE` is set to 10, logs are flushed every 10 logs. **Default is 2048**
|
||||
|
|
@ -697,6 +699,8 @@ router_settings:
|
|||
| LANGFUSE_FLUSH_INTERVAL | Interval for flushing Langfuse logs
|
||||
| LANGFUSE_TRACING_ENVIRONMENT | Environment for Langfuse tracing
|
||||
| LANGFUSE_HOST | Host URL for Langfuse service
|
||||
| LANGFUSE_MOCK | Enable mock mode for Langfuse integration testing. When set to true, intercepts Langfuse API calls and returns mock responses without making actual network calls. Default is false
|
||||
| LANGFUSE_MOCK_LATENCY_MS | Mock latency in milliseconds for Langfuse API calls when mock mode is enabled. Simulates network round-trip time. Default is 100ms
|
||||
| LANGFUSE_PUBLIC_KEY | Public key for Langfuse authentication
|
||||
| LANGFUSE_RELEASE | Release version of Langfuse integration
|
||||
| LANGFUSE_SECRET_KEY | Secret key for Langfuse authentication
|
||||
|
|
|
|||
46
docs/my-website/docs/troubleshoot/spend_queue_warnings.md
Normal file
46
docs/my-website/docs/troubleshoot/spend_queue_warnings.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Spend Update Queue Full Warnings
|
||||
|
||||
## Overview
|
||||
|
||||
The "Spend update queue is full" warning occurs in high-volume LiteLLM proxy deployments when the internal spend tracking queue reaches capacity. This is a protective mechanism to prevent memory issues during traffic spikes.
|
||||
|
||||
## Warning Message
|
||||
|
||||
```
|
||||
WARNING:litellm.proxy.db.db_transaction_queue.spend_update_queue:Spend update queue is full. Aggregating entries to prevent memory issues.
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
|
||||
The spend update queue has a default maximum size of 10,000 entries (`MAX_SIZE_IN_MEMORY_QUEUE=10000`). When this limit is reached:
|
||||
|
||||
1. New spend tracking entries are aggregated instead of queued individually
|
||||
2. This prevents memory exhaustion but may slightly delay spend updates
|
||||
3. The warning indicates your deployment is processing requests faster than the database can handle spend updates
|
||||
|
||||
## Solutions
|
||||
|
||||
### 1. Increase Queue Size
|
||||
|
||||
Set the `MAX_SIZE_IN_MEMORY_QUEUE` environment variable to a higher value:
|
||||
|
||||
```bash
|
||||
MAX_SIZE_IN_MEMORY_QUEUE=50000
|
||||
```
|
||||
|
||||
**Tradeoffs:**
|
||||
Higher queue sizes store more items in memory - provision at least 8GB RAM for large queues
|
||||
- Recommended for deployments with consistent high traffic
|
||||
|
||||
### 2. Horizontal Scaling
|
||||
|
||||
Deploy multiple proxy instances with load balancing. This distributes the spend tracking load across multiple queues, reducing the pressure on any single instance's spend update queue.
|
||||
|
||||
|
||||
|
||||
## Related Configuration
|
||||
|
||||
```yaml
|
||||
# Environment variables
|
||||
MAX_SIZE_IN_MEMORY_QUEUE: 10000 # Default queue size
|
||||
```
|
||||
|
|
@ -62,7 +62,7 @@ This means you can now use Claude Code's web search tool with any provider, not
|
|||
|
||||
Proxy Admins can configure web search interception in their LiteLLM proxy config to enable this capability for their teams using Claude Code with Bedrock, Azure, or any other supported provider.
|
||||
|
||||
[**Learn more →**](../../docs/tutorials/claude_code_websearch.md)
|
||||
[**Learn more →**](https://docs.litellm.ai/docs/tutorials/claude_code_websearch)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1017,6 +1017,7 @@ const sidebars = {
|
|||
items: [
|
||||
"troubleshoot/cpu_issues",
|
||||
"troubleshoot/memory_issues",
|
||||
"troubleshoot/spend_queue_warnings",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
BIN
litellm-proxy-extras/dist/litellm_proxy_extras-0.4.26-py3-none-any.whl
vendored
Normal file
BIN
litellm-proxy-extras/dist/litellm_proxy_extras-0.4.26-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
litellm-proxy-extras/dist/litellm_proxy_extras-0.4.26.tar.gz
vendored
Normal file
BIN
litellm-proxy-extras/dist/litellm_proxy_extras-0.4.26.tar.gz
vendored
Normal file
Binary file not shown.
|
|
@ -0,0 +1,51 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_DeletedTeamTable" ADD COLUMN "policies" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_DeletedVerificationToken" ADD COLUMN "policies" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_TeamTable" ADD COLUMN "policies" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_UserTable" ADD COLUMN "policies" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_VerificationToken" ADD COLUMN "policies" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LiteLLM_PolicyTable" (
|
||||
"policy_id" TEXT NOT NULL,
|
||||
"policy_name" TEXT NOT NULL,
|
||||
"inherit" TEXT,
|
||||
"description" TEXT,
|
||||
"guardrails_add" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"guardrails_remove" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"condition" JSONB DEFAULT '{}',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" TEXT,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_by" TEXT,
|
||||
|
||||
CONSTRAINT "LiteLLM_PolicyTable_pkey" PRIMARY KEY ("policy_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LiteLLM_PolicyAttachmentTable" (
|
||||
"attachment_id" TEXT NOT NULL,
|
||||
"policy_name" TEXT NOT NULL,
|
||||
"scope" TEXT,
|
||||
"teams" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"keys" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"models" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" TEXT,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_by" TEXT,
|
||||
|
||||
CONSTRAINT "LiteLLM_PolicyAttachmentTable_pkey" PRIMARY KEY ("attachment_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LiteLLM_PolicyTable_policy_name_key" ON "LiteLLM_PolicyTable"("policy_name");
|
||||
|
||||
|
|
@ -124,8 +124,9 @@ model LiteLLM_TeamTable {
|
|||
updated_at DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
team_member_permissions String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases
|
||||
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
||||
litellm_model_table LiteLLM_ModelTable? @relation(fields: [model_id], references: [id])
|
||||
|
|
@ -156,6 +157,7 @@ model LiteLLM_DeletedTeamTable {
|
|||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
team_member_permissions String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_id Int? // id for LiteLLM_ModelTable -> stores team-level model aliases
|
||||
|
||||
// Original timestamps from team creation/updates
|
||||
|
|
@ -197,6 +199,7 @@ model LiteLLM_UserTable {
|
|||
budget_duration String?
|
||||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
created_at DateTime? @default(now()) @map("created_at")
|
||||
|
|
@ -283,6 +286,7 @@ model LiteLLM_VerificationToken {
|
|||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
allowed_routes String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
budget_id String?
|
||||
|
|
@ -327,6 +331,7 @@ model LiteLLM_DeletedVerificationToken {
|
|||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
allowed_routes String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
|
|
@ -863,3 +868,32 @@ model LiteLLM_SkillsTable {
|
|||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
||||
// Policy table for storing guardrail policies
|
||||
model LiteLLM_PolicyTable {
|
||||
policy_id String @id @default(uuid())
|
||||
policy_name String @unique
|
||||
inherit String? // Name of parent policy to inherit from
|
||||
description String?
|
||||
guardrails_add String[] @default([])
|
||||
guardrails_remove String[] @default([])
|
||||
condition Json? @default("{}") // Policy conditions (e.g., model matching)
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
||||
// Policy attachment table for defining where policies apply
|
||||
model LiteLLM_PolicyAttachmentTable {
|
||||
attachment_id String @id @default(uuid())
|
||||
policy_name String // Name of the policy to attach
|
||||
scope String? // Use '*' for global scope
|
||||
teams String[] @default([]) // Team aliases or patterns
|
||||
keys String[] @default([]) // Key aliases or patterns
|
||||
models String[] @default([]) // Model names or patterns
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "litellm-proxy-extras"
|
||||
version = "0.4.25"
|
||||
version = "0.4.26"
|
||||
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.25"
|
||||
version = "0.4.26"
|
||||
version_files = [
|
||||
"pyproject.toml:version",
|
||||
"../requirements.txt:litellm-proxy-extras==",
|
||||
|
|
|
|||
|
|
@ -13,18 +13,20 @@ from litellm.types.utils import StandardLoggingPayload
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from opentelemetry.trace import Span
|
||||
from litellm.integrations._types.open_inference import (
|
||||
MessageAttributes,
|
||||
ImageAttributes,
|
||||
SpanAttributes,
|
||||
AudioAttributes,
|
||||
EmbeddingAttributes,
|
||||
OpenInferenceSpanKindValues
|
||||
)
|
||||
|
||||
|
||||
class ArizeOTELAttributes(BaseLLMObsOTELAttributes):
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def set_messages(span: "Span", kwargs: Dict[str, Any]):
|
||||
from litellm.integrations._types.open_inference import (
|
||||
MessageAttributes,
|
||||
SpanAttributes,
|
||||
)
|
||||
|
||||
messages = kwargs.get("messages")
|
||||
|
||||
# for /chat/completions
|
||||
|
|
@ -56,7 +58,6 @@ class ArizeOTELAttributes(BaseLLMObsOTELAttributes):
|
|||
def set_response_output_messages(span: "Span", response_obj):
|
||||
"""
|
||||
Sets output message attributes on the span from the LLM response.
|
||||
|
||||
Args:
|
||||
span: The OpenTelemetry span to set attributes on
|
||||
response_obj: The response object containing choices with messages
|
||||
|
|
@ -88,112 +89,243 @@ class ArizeOTELAttributes(BaseLLMObsOTELAttributes):
|
|||
)
|
||||
|
||||
|
||||
def _set_tool_attributes(span: "Span", optional_params: dict):
|
||||
"""Helper to set tool and function call attributes on span."""
|
||||
from litellm.integrations._types.open_inference import (
|
||||
MessageAttributes,
|
||||
SpanAttributes,
|
||||
ToolCallAttributes,
|
||||
)
|
||||
|
||||
tools = optional_params.get("tools")
|
||||
if tools:
|
||||
for idx, tool in enumerate(tools):
|
||||
function = tool.get("function")
|
||||
if not function:
|
||||
continue
|
||||
prefix = f"{SpanAttributes.LLM_TOOLS}.{idx}"
|
||||
safe_set_attribute(
|
||||
span, f"{prefix}.{SpanAttributes.TOOL_NAME}", function.get("name")
|
||||
)
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{prefix}.{SpanAttributes.TOOL_DESCRIPTION}",
|
||||
function.get("description"),
|
||||
)
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{prefix}.{SpanAttributes.TOOL_PARAMETERS}",
|
||||
json.dumps(function.get("parameters")),
|
||||
)
|
||||
|
||||
functions = optional_params.get("functions")
|
||||
if functions:
|
||||
for idx, function in enumerate(functions):
|
||||
prefix = f"{MessageAttributes.MESSAGE_TOOL_CALLS}.{idx}"
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{prefix}.{ToolCallAttributes.TOOL_CALL_FUNCTION_NAME}",
|
||||
function.get("name"),
|
||||
)
|
||||
|
||||
|
||||
def _set_response_attributes(span: "Span", response_obj):
|
||||
"""Helper to set response output and token usage attributes on span."""
|
||||
from litellm.integrations._types.open_inference import (
|
||||
MessageAttributes,
|
||||
SpanAttributes,
|
||||
)
|
||||
|
||||
if not hasattr(response_obj, "get"):
|
||||
return
|
||||
|
||||
_set_choice_outputs(span, response_obj, MessageAttributes, SpanAttributes)
|
||||
_set_image_outputs(span, response_obj, ImageAttributes, SpanAttributes)
|
||||
_set_audio_outputs(span, response_obj, AudioAttributes, SpanAttributes)
|
||||
_set_embedding_outputs(span, response_obj, EmbeddingAttributes, SpanAttributes)
|
||||
_set_structured_outputs(span, response_obj, MessageAttributes, SpanAttributes)
|
||||
_set_usage_outputs(span, response_obj, SpanAttributes)
|
||||
|
||||
|
||||
def _set_choice_outputs(span: "Span", response_obj, msg_attrs, span_attrs):
|
||||
for idx, choice in enumerate(response_obj.get("choices", [])):
|
||||
response_message = choice.get("message", {})
|
||||
safe_set_attribute(
|
||||
span,
|
||||
SpanAttributes.OUTPUT_VALUE,
|
||||
span_attrs.OUTPUT_VALUE,
|
||||
response_message.get("content", ""),
|
||||
)
|
||||
prefix = f"{SpanAttributes.LLM_OUTPUT_MESSAGES}.{idx}"
|
||||
prefix = f"{span_attrs.LLM_OUTPUT_MESSAGES}.{idx}"
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{prefix}.{MessageAttributes.MESSAGE_ROLE}",
|
||||
f"{prefix}.{msg_attrs.MESSAGE_ROLE}",
|
||||
response_message.get("role"),
|
||||
)
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{prefix}.{MessageAttributes.MESSAGE_CONTENT}",
|
||||
f"{prefix}.{msg_attrs.MESSAGE_CONTENT}",
|
||||
response_message.get("content", ""),
|
||||
)
|
||||
|
||||
output_items = response_obj.get("output", [])
|
||||
if output_items:
|
||||
for i, item in enumerate(output_items):
|
||||
prefix = f"{SpanAttributes.LLM_OUTPUT_MESSAGES}.{i}"
|
||||
if hasattr(item, "type"):
|
||||
item_type = item.type
|
||||
if item_type == "reasoning" and hasattr(item, "summary"):
|
||||
for summary in item.summary:
|
||||
if hasattr(summary, "text"):
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{prefix}.{MessageAttributes.MESSAGE_REASONING_SUMMARY}",
|
||||
summary.text,
|
||||
)
|
||||
elif item_type == "message" and hasattr(item, "content"):
|
||||
message_content = ""
|
||||
content_list = item.content
|
||||
if content_list and len(content_list) > 0:
|
||||
first_content = content_list[0]
|
||||
message_content = getattr(first_content, "text", "")
|
||||
message_role = getattr(item, "role", "assistant")
|
||||
safe_set_attribute(span, SpanAttributes.OUTPUT_VALUE, message_content)
|
||||
safe_set_attribute(span, f"{prefix}.{MessageAttributes.MESSAGE_CONTENT}", message_content)
|
||||
safe_set_attribute(span, f"{prefix}.{MessageAttributes.MESSAGE_ROLE}", message_role)
|
||||
|
||||
def _set_image_outputs(span: "Span", response_obj, image_attrs, span_attrs):
|
||||
images = response_obj.get("data", [])
|
||||
for i, image in enumerate(images):
|
||||
img_url = image.get("url")
|
||||
if img_url is None and image.get("b64_json"):
|
||||
img_url = f"data:image/png;base64,{image.get('b64_json')}"
|
||||
|
||||
if not img_url:
|
||||
continue
|
||||
|
||||
if i == 0:
|
||||
safe_set_attribute(span, span_attrs.OUTPUT_VALUE, img_url)
|
||||
|
||||
safe_set_attribute(span, f"{image_attrs.IMAGE_URL}.{i}", img_url)
|
||||
|
||||
|
||||
def _set_audio_outputs(span: "Span", response_obj, audio_attrs, span_attrs):
|
||||
audio = response_obj.get("audio", [])
|
||||
for i, audio_item in enumerate(audio):
|
||||
audio_url = audio_item.get("url")
|
||||
if audio_url is None and audio_item.get("b64_json"):
|
||||
audio_url = f"data:audio/wav;base64,{audio_item.get('b64_json')}"
|
||||
|
||||
if audio_url:
|
||||
if i == 0:
|
||||
safe_set_attribute(span, span_attrs.OUTPUT_VALUE, audio_url)
|
||||
safe_set_attribute(span, f"{audio_attrs.AUDIO_URL}.{i}", audio_url)
|
||||
|
||||
audio_mime = audio_item.get("mime_type")
|
||||
if audio_mime:
|
||||
safe_set_attribute(span, f"{audio_attrs.AUDIO_MIME_TYPE}.{i}", audio_mime)
|
||||
|
||||
audio_transcript = audio_item.get("transcript")
|
||||
if audio_transcript:
|
||||
safe_set_attribute(span, f"{audio_attrs.AUDIO_TRANSCRIPT}.{i}", audio_transcript)
|
||||
|
||||
|
||||
def _set_embedding_outputs(span: "Span", response_obj, embedding_attrs, span_attrs):
|
||||
embeddings = response_obj.get("data", [])
|
||||
for i, embedding_item in enumerate(embeddings):
|
||||
embedding_vector = embedding_item.get("embedding")
|
||||
if embedding_vector:
|
||||
if i == 0:
|
||||
safe_set_attribute(
|
||||
span,
|
||||
span_attrs.OUTPUT_VALUE,
|
||||
str(embedding_vector),
|
||||
)
|
||||
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{embedding_attrs.EMBEDDING_VECTOR}.{i}",
|
||||
str(embedding_vector),
|
||||
)
|
||||
|
||||
embedding_text = embedding_item.get("text")
|
||||
if embedding_text:
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{embedding_attrs.EMBEDDING_TEXT}.{i}",
|
||||
str(embedding_text),
|
||||
)
|
||||
|
||||
|
||||
def _set_structured_outputs(span: "Span", response_obj, msg_attrs, span_attrs):
|
||||
output_items = response_obj.get("output", [])
|
||||
for i, item in enumerate(output_items):
|
||||
prefix = f"{span_attrs.LLM_OUTPUT_MESSAGES}.{i}"
|
||||
if not hasattr(item, "type"):
|
||||
continue
|
||||
|
||||
item_type = item.type
|
||||
if item_type == "reasoning" and hasattr(item, "summary"):
|
||||
for summary in item.summary:
|
||||
if hasattr(summary, "text"):
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{prefix}.{msg_attrs.MESSAGE_REASONING_SUMMARY}",
|
||||
summary.text,
|
||||
)
|
||||
elif item_type == "message" and hasattr(item, "content"):
|
||||
message_content = ""
|
||||
content_list = item.content
|
||||
if content_list and len(content_list) > 0:
|
||||
first_content = content_list[0]
|
||||
message_content = getattr(first_content, "text", "")
|
||||
message_role = getattr(item, "role", "assistant")
|
||||
safe_set_attribute(span, span_attrs.OUTPUT_VALUE, message_content)
|
||||
safe_set_attribute(span, f"{prefix}.{msg_attrs.MESSAGE_CONTENT}", message_content)
|
||||
safe_set_attribute(span, f"{prefix}.{msg_attrs.MESSAGE_ROLE}", message_role)
|
||||
|
||||
|
||||
def _set_usage_outputs(span: "Span", response_obj, span_attrs):
|
||||
usage = response_obj and response_obj.get("usage")
|
||||
if usage:
|
||||
safe_set_attribute(span, SpanAttributes.LLM_TOKEN_COUNT_TOTAL, usage.get("total_tokens"))
|
||||
completion_tokens = usage.get("completion_tokens") or usage.get("output_tokens")
|
||||
if completion_tokens:
|
||||
safe_set_attribute(span, SpanAttributes.LLM_TOKEN_COUNT_COMPLETION, completion_tokens)
|
||||
prompt_tokens = usage.get("prompt_tokens") or usage.get("input_tokens")
|
||||
if prompt_tokens:
|
||||
safe_set_attribute(span, SpanAttributes.LLM_TOKEN_COUNT_PROMPT, prompt_tokens)
|
||||
reasoning_tokens = usage.get("output_tokens_details", {}).get("reasoning_tokens")
|
||||
if reasoning_tokens:
|
||||
safe_set_attribute(span, SpanAttributes.LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING, reasoning_tokens)
|
||||
if not usage:
|
||||
return
|
||||
|
||||
safe_set_attribute(span, span_attrs.LLM_TOKEN_COUNT_TOTAL, usage.get("total_tokens"))
|
||||
completion_tokens = usage.get("completion_tokens") or usage.get("output_tokens")
|
||||
if completion_tokens:
|
||||
safe_set_attribute(span, span_attrs.LLM_TOKEN_COUNT_COMPLETION, completion_tokens)
|
||||
prompt_tokens = usage.get("prompt_tokens") or usage.get("input_tokens")
|
||||
if prompt_tokens:
|
||||
safe_set_attribute(span, span_attrs.LLM_TOKEN_COUNT_PROMPT, prompt_tokens)
|
||||
reasoning_tokens = usage.get("output_tokens_details", {}).get("reasoning_tokens")
|
||||
if reasoning_tokens:
|
||||
safe_set_attribute(span, span_attrs.LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING, reasoning_tokens)
|
||||
|
||||
|
||||
def _infer_open_inference_span_kind(call_type: Optional[str]) -> str:
|
||||
"""
|
||||
Map LiteLLM call types to OpenInference span kinds.
|
||||
"""
|
||||
|
||||
if not call_type:
|
||||
return OpenInferenceSpanKindValues.UNKNOWN.value
|
||||
|
||||
lowered = str(call_type).lower()
|
||||
|
||||
if "embed" in lowered:
|
||||
return OpenInferenceSpanKindValues.EMBEDDING.value
|
||||
|
||||
if "rerank" in lowered:
|
||||
return OpenInferenceSpanKindValues.RERANKER.value
|
||||
|
||||
if "search" in lowered:
|
||||
return OpenInferenceSpanKindValues.RETRIEVER.value
|
||||
|
||||
if "moderation" in lowered or "guardrail" in lowered:
|
||||
return OpenInferenceSpanKindValues.GUARDRAIL.value
|
||||
|
||||
if lowered == "call_mcp_tool" or lowered == "mcp" or lowered.endswith("tool"):
|
||||
return OpenInferenceSpanKindValues.TOOL.value
|
||||
|
||||
if "asend_message" in lowered or "a2a" in lowered or "assistant" in lowered:
|
||||
return OpenInferenceSpanKindValues.AGENT.value
|
||||
|
||||
if any(
|
||||
keyword in lowered
|
||||
for keyword in (
|
||||
"completion",
|
||||
"chat",
|
||||
"image",
|
||||
"audio",
|
||||
"speech",
|
||||
"transcription",
|
||||
"generate_content",
|
||||
"response",
|
||||
"videos",
|
||||
"realtime",
|
||||
"pass_through",
|
||||
"anthropic_messages",
|
||||
"ocr",
|
||||
)
|
||||
):
|
||||
return OpenInferenceSpanKindValues.LLM.value
|
||||
|
||||
if any(keyword in lowered for keyword in ("file", "batch", "container", "fine_tuning_job")):
|
||||
return OpenInferenceSpanKindValues.CHAIN.value
|
||||
|
||||
return OpenInferenceSpanKindValues.UNKNOWN.value
|
||||
|
||||
def _set_tool_attributes(
|
||||
span: "Span", optional_tools: Optional[list], metadata_tools: Optional[list]
|
||||
):
|
||||
"""set tool attributes on span from optional_params or tool call metadata"""
|
||||
if optional_tools:
|
||||
for idx, tool in enumerate(optional_tools):
|
||||
if not isinstance(tool, dict):
|
||||
continue
|
||||
function = tool.get("function") if isinstance(tool.get("function"), dict) else None
|
||||
if not function:
|
||||
continue
|
||||
tool_name = function.get("name")
|
||||
if tool_name:
|
||||
safe_set_attribute(span, f"{SpanAttributes.LLM_TOOLS}.{idx}.name", tool_name)
|
||||
tool_description = function.get("description")
|
||||
if tool_description:
|
||||
safe_set_attribute(span, f"{SpanAttributes.LLM_TOOLS}.{idx}.description", tool_description)
|
||||
params = function.get("parameters")
|
||||
if params is not None:
|
||||
safe_set_attribute(span, f"{SpanAttributes.LLM_TOOLS}.{idx}.parameters", json.dumps(params))
|
||||
|
||||
if metadata_tools and isinstance(metadata_tools, list):
|
||||
for idx, tool in enumerate(metadata_tools):
|
||||
if not isinstance(tool, dict):
|
||||
continue
|
||||
tool_name = tool.get("name")
|
||||
if tool_name:
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{SpanAttributes.LLM_INVOCATION_PARAMETERS}.tools.{idx}.name",
|
||||
tool_name,
|
||||
)
|
||||
|
||||
tool_description = tool.get("description")
|
||||
if tool_description:
|
||||
safe_set_attribute(
|
||||
span,
|
||||
f"{SpanAttributes.LLM_INVOCATION_PARAMETERS}.tools.{idx}.description",
|
||||
tool_description,
|
||||
)
|
||||
|
||||
|
||||
def set_attributes(
|
||||
|
|
@ -202,70 +334,42 @@ def set_attributes(
|
|||
"""
|
||||
Populates span with OpenInference-compliant LLM attributes for Arize and Phoenix tracing.
|
||||
"""
|
||||
from litellm.integrations._types.open_inference import (
|
||||
OpenInferenceSpanKindValues,
|
||||
SpanAttributes,
|
||||
)
|
||||
|
||||
try:
|
||||
# Remove secret_fields to prevent leaking sensitive data (e.g., authorization headers)
|
||||
optional_params = kwargs.get("optional_params", {})
|
||||
if isinstance(optional_params, dict):
|
||||
optional_params.pop("secret_fields", None)
|
||||
litellm_params = kwargs.get("litellm_params", {})
|
||||
optional_params = _sanitize_optional_params(kwargs.get("optional_params"))
|
||||
litellm_params = kwargs.get("litellm_params", {}) or {}
|
||||
standard_logging_payload: Optional[StandardLoggingPayload] = kwargs.get(
|
||||
"standard_logging_object"
|
||||
)
|
||||
if standard_logging_payload is None:
|
||||
raise ValueError("standard_logging_object not found in kwargs")
|
||||
|
||||
metadata = (
|
||||
standard_logging_payload.get("metadata")
|
||||
if standard_logging_payload
|
||||
else None
|
||||
metadata = standard_logging_payload.get("metadata") if standard_logging_payload else None
|
||||
_set_metadata_attributes(span, metadata, SpanAttributes)
|
||||
|
||||
metadata_tools = _extract_metadata_tools(metadata)
|
||||
optional_tools = _extract_optional_tools(optional_params)
|
||||
|
||||
call_type = standard_logging_payload.get("call_type")
|
||||
_set_request_attributes(
|
||||
span=span,
|
||||
kwargs=kwargs,
|
||||
standard_logging_payload=standard_logging_payload,
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
response_obj=response_obj,
|
||||
span_attrs=SpanAttributes,
|
||||
)
|
||||
if metadata is not None:
|
||||
safe_set_attribute(span, SpanAttributes.METADATA, safe_dumps(metadata))
|
||||
|
||||
if kwargs.get("model"):
|
||||
safe_set_attribute(span, SpanAttributes.LLM_MODEL_NAME, kwargs.get("model"))
|
||||
span_kind = _infer_open_inference_span_kind(call_type=call_type)
|
||||
_set_tool_attributes(span, optional_tools, metadata_tools)
|
||||
if (optional_tools or metadata_tools) and span_kind != OpenInferenceSpanKindValues.TOOL.value:
|
||||
span_kind = OpenInferenceSpanKindValues.TOOL.value
|
||||
|
||||
safe_set_attribute(span, "llm.request.type", standard_logging_payload["call_type"])
|
||||
safe_set_attribute(span, SpanAttributes.LLM_PROVIDER, litellm_params.get("custom_llm_provider", "Unknown"))
|
||||
|
||||
if optional_params.get("max_tokens"):
|
||||
safe_set_attribute(span, "llm.request.max_tokens", optional_params.get("max_tokens"))
|
||||
if optional_params.get("temperature"):
|
||||
safe_set_attribute(span, "llm.request.temperature", optional_params.get("temperature"))
|
||||
if optional_params.get("top_p"):
|
||||
safe_set_attribute(span, "llm.request.top_p", optional_params.get("top_p"))
|
||||
|
||||
safe_set_attribute(span, "llm.is_streaming", str(optional_params.get("stream", False)))
|
||||
|
||||
if optional_params.get("user"):
|
||||
safe_set_attribute(span, "llm.user", optional_params.get("user"))
|
||||
|
||||
if response_obj and response_obj.get("id"):
|
||||
safe_set_attribute(span, "llm.response.id", response_obj.get("id"))
|
||||
if response_obj and response_obj.get("model"):
|
||||
safe_set_attribute(span, "llm.response.model", response_obj.get("model"))
|
||||
|
||||
safe_set_attribute(span, SpanAttributes.OPENINFERENCE_SPAN_KIND, OpenInferenceSpanKindValues.LLM.value)
|
||||
safe_set_attribute(span, SpanAttributes.OPENINFERENCE_SPAN_KIND, span_kind)
|
||||
attributes.set_messages(span, kwargs)
|
||||
|
||||
_set_tool_attributes(span=span, optional_params=optional_params)
|
||||
|
||||
model_params = (
|
||||
standard_logging_payload.get("model_parameters")
|
||||
if standard_logging_payload
|
||||
else None
|
||||
)
|
||||
if model_params:
|
||||
safe_set_attribute(span, SpanAttributes.LLM_INVOCATION_PARAMETERS, safe_dumps(model_params))
|
||||
if model_params.get("user"):
|
||||
user_id = model_params.get("user")
|
||||
if user_id is not None:
|
||||
safe_set_attribute(span, SpanAttributes.USER_ID, user_id)
|
||||
model_params = standard_logging_payload.get("model_parameters") if standard_logging_payload else None
|
||||
_set_model_params(span, model_params, SpanAttributes)
|
||||
|
||||
_set_response_attributes(span=span, response_obj=response_obj)
|
||||
|
||||
|
|
@ -275,3 +379,72 @@ def set_attributes(
|
|||
)
|
||||
if hasattr(span, "record_exception"):
|
||||
span.record_exception(e)
|
||||
|
||||
|
||||
def _sanitize_optional_params(optional_params: Optional[dict]) -> dict:
|
||||
if not isinstance(optional_params, dict):
|
||||
return {}
|
||||
optional_params.pop("secret_fields", None)
|
||||
return optional_params
|
||||
|
||||
|
||||
def _set_metadata_attributes(span: "Span", metadata: Optional[Any], span_attrs) -> None:
|
||||
if metadata is not None:
|
||||
safe_set_attribute(span, span_attrs.METADATA, safe_dumps(metadata))
|
||||
|
||||
|
||||
def _extract_metadata_tools(metadata: Optional[Any]) -> Optional[list]:
|
||||
if not isinstance(metadata, dict):
|
||||
return None
|
||||
llm_obj = metadata.get("llm")
|
||||
if isinstance(llm_obj, dict):
|
||||
return llm_obj.get("tools")
|
||||
return None
|
||||
|
||||
|
||||
def _extract_optional_tools(optional_params: dict) -> Optional[list]:
|
||||
return optional_params.get("tools") if isinstance(optional_params, dict) else None
|
||||
|
||||
|
||||
def _set_request_attributes(
|
||||
span: "Span",
|
||||
kwargs,
|
||||
standard_logging_payload: StandardLoggingPayload,
|
||||
optional_params: dict,
|
||||
litellm_params: dict,
|
||||
response_obj,
|
||||
span_attrs,
|
||||
):
|
||||
if kwargs.get("model"):
|
||||
safe_set_attribute(span, span_attrs.LLM_MODEL_NAME, kwargs.get("model"))
|
||||
|
||||
safe_set_attribute(span, "llm.request.type", standard_logging_payload.get("call_type"))
|
||||
safe_set_attribute(span, span_attrs.LLM_PROVIDER, litellm_params.get("custom_llm_provider", "Unknown"))
|
||||
|
||||
if optional_params.get("max_tokens"):
|
||||
safe_set_attribute(span, "llm.request.max_tokens", optional_params.get("max_tokens"))
|
||||
if optional_params.get("temperature"):
|
||||
safe_set_attribute(span, "llm.request.temperature", optional_params.get("temperature"))
|
||||
if optional_params.get("top_p"):
|
||||
safe_set_attribute(span, "llm.request.top_p", optional_params.get("top_p"))
|
||||
|
||||
safe_set_attribute(span, "llm.is_streaming", str(optional_params.get("stream", False)))
|
||||
|
||||
if optional_params.get("user"):
|
||||
safe_set_attribute(span, "llm.user", optional_params.get("user"))
|
||||
|
||||
if response_obj and response_obj.get("id"):
|
||||
safe_set_attribute(span, "llm.response.id", response_obj.get("id"))
|
||||
if response_obj and response_obj.get("model"):
|
||||
safe_set_attribute(span, "llm.response.model", response_obj.get("model"))
|
||||
|
||||
|
||||
def _set_model_params(span: "Span", model_params: Optional[dict], span_attrs) -> None:
|
||||
if not model_params:
|
||||
return
|
||||
|
||||
safe_set_attribute(span, span_attrs.LLM_INVOCATION_PARAMETERS, safe_dumps(model_params))
|
||||
if model_params.get("user"):
|
||||
user_id = model_params.get("user")
|
||||
if user_id is not None:
|
||||
safe_set_attribute(span, span_attrs.USER_ID, user_id)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@ import json
|
|||
import os
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from litellm.integrations.gcs_bucket.gcs_bucket_mock_client import (
|
||||
should_use_gcs_mock,
|
||||
create_mock_gcs_client,
|
||||
mock_vertex_auth_methods,
|
||||
)
|
||||
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.integrations.custom_batch_logger import CustomBatchLogger
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
|
|
@ -20,6 +27,12 @@ IAM_AUTH_KEY = "IAM_AUTH"
|
|||
|
||||
class GCSBucketBase(CustomBatchLogger):
|
||||
def __init__(self, bucket_name: Optional[str] = None, **kwargs) -> None:
|
||||
self.is_mock_mode = should_use_gcs_mock()
|
||||
|
||||
if self.is_mock_mode:
|
||||
mock_vertex_auth_methods()
|
||||
create_mock_gcs_client()
|
||||
|
||||
self.async_httpx_client = get_async_httpx_client(
|
||||
llm_provider=httpxSpecialProvider.LoggingCallback
|
||||
)
|
||||
|
|
|
|||
236
litellm/integrations/gcs_bucket/gcs_bucket_mock_client.py
Normal file
236
litellm/integrations/gcs_bucket/gcs_bucket_mock_client.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"""
|
||||
Mock client for GCS Bucket integration testing.
|
||||
|
||||
This module intercepts GCS API calls and Vertex AI auth calls, returning successful
|
||||
mock responses, allowing full code execution without making actual network calls.
|
||||
|
||||
Usage:
|
||||
Set GCS_MOCK=true in environment variables or config to enable mock mode.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Dict, Optional
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
|
||||
# Store original methods for restoration
|
||||
_original_async_handler_post = None
|
||||
_original_async_handler_get = None
|
||||
_original_async_handler_delete = None
|
||||
|
||||
# Track if mocks have been initialized to avoid duplicate initialization
|
||||
_mocks_initialized = False
|
||||
|
||||
# Default mock latency in seconds (simulates network round-trip)
|
||||
# Typical GCS API calls take 100-300ms for uploads, 50-150ms for GET/DELETE
|
||||
_MOCK_LATENCY_SECONDS = float(__import__("os").getenv("GCS_MOCK_LATENCY_MS", "150")) / 1000.0
|
||||
|
||||
|
||||
class MockGCSResponse:
|
||||
"""Mock httpx.Response that satisfies GCS API requirements."""
|
||||
|
||||
def __init__(self, status_code: int = 200, json_data: Optional[Dict] = None, url: Optional[str] = None, elapsed_seconds: float = 0.0):
|
||||
self.status_code = status_code
|
||||
self._json_data = json_data or {"kind": "storage#object", "name": "mock-object"}
|
||||
self.headers = httpx.Headers({})
|
||||
self.is_success = status_code < 400
|
||||
self.is_error = status_code >= 400
|
||||
self.is_redirect = 300 <= status_code < 400
|
||||
self.url = httpx.URL(url) if url else httpx.URL("")
|
||||
# Set realistic elapsed time based on mock latency
|
||||
elapsed_time = elapsed_seconds if elapsed_seconds > 0 else _MOCK_LATENCY_SECONDS
|
||||
self.elapsed = timedelta(seconds=elapsed_time)
|
||||
self._text = json.dumps(self._json_data)
|
||||
self._content = self._text.encode("utf-8")
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""Return response text."""
|
||||
return self._text
|
||||
|
||||
@property
|
||||
def content(self) -> bytes:
|
||||
"""Return response content."""
|
||||
return self._content
|
||||
|
||||
def json(self) -> Dict:
|
||||
"""Return JSON response data."""
|
||||
return self._json_data
|
||||
|
||||
def read(self) -> bytes:
|
||||
"""Read response content."""
|
||||
return self._content
|
||||
|
||||
def raise_for_status(self):
|
||||
"""Raise exception for error status codes."""
|
||||
if self.status_code >= 400:
|
||||
raise Exception(f"HTTP {self.status_code}")
|
||||
|
||||
|
||||
async def _mock_async_handler_post(self, url, data=None, json=None, params=None, headers=None, timeout=None, stream=False, logging_obj=None, files=None, content=None):
|
||||
"""Monkey-patched AsyncHTTPHandler.post that intercepts GCS calls."""
|
||||
# Only mock GCS API calls
|
||||
if isinstance(url, str) and "storage.googleapis.com" in url:
|
||||
verbose_logger.info(f"[GCS MOCK] POST to {url}")
|
||||
# Simulate network latency
|
||||
await asyncio.sleep(_MOCK_LATENCY_SECONDS)
|
||||
return MockGCSResponse(
|
||||
status_code=200,
|
||||
json_data={"kind": "storage#object", "name": "mock-object"},
|
||||
url=url,
|
||||
elapsed_seconds=_MOCK_LATENCY_SECONDS
|
||||
)
|
||||
# For non-GCS calls, use original method
|
||||
if _original_async_handler_post is not None:
|
||||
return await _original_async_handler_post(self, url=url, data=data, json=json, params=params, headers=headers, timeout=timeout, stream=stream, logging_obj=logging_obj, files=files, content=content)
|
||||
# Fallback: if original not set, raise error
|
||||
raise RuntimeError("Original AsyncHTTPHandler.post not available")
|
||||
|
||||
|
||||
async def _mock_async_handler_get(self, url, params=None, headers=None, follow_redirects=None):
|
||||
"""Monkey-patched AsyncHTTPHandler.get that intercepts GCS calls."""
|
||||
# Only mock GCS API calls
|
||||
if isinstance(url, str) and "storage.googleapis.com" in url:
|
||||
verbose_logger.info(f"[GCS MOCK] GET to {url}")
|
||||
# Simulate network latency
|
||||
await asyncio.sleep(_MOCK_LATENCY_SECONDS)
|
||||
return MockGCSResponse(
|
||||
status_code=200,
|
||||
json_data={"data": "mock-log-data"},
|
||||
url=url,
|
||||
elapsed_seconds=_MOCK_LATENCY_SECONDS
|
||||
)
|
||||
# For non-GCS calls, use original method
|
||||
if _original_async_handler_get is not None:
|
||||
return await _original_async_handler_get(self, url=url, params=params, headers=headers, follow_redirects=follow_redirects)
|
||||
# Fallback: if original not set, raise error
|
||||
raise RuntimeError("Original AsyncHTTPHandler.get not available")
|
||||
|
||||
|
||||
async def _mock_async_handler_delete(self, url, data=None, json=None, params=None, headers=None, timeout=None, stream=False, content=None):
|
||||
"""Monkey-patched AsyncHTTPHandler.delete that intercepts GCS calls."""
|
||||
# Only mock GCS API calls
|
||||
if isinstance(url, str) and "storage.googleapis.com" in url:
|
||||
verbose_logger.info(f"[GCS MOCK] DELETE to {url}")
|
||||
# Simulate network latency
|
||||
await asyncio.sleep(_MOCK_LATENCY_SECONDS)
|
||||
return MockGCSResponse(
|
||||
status_code=204,
|
||||
json_data={},
|
||||
url=url,
|
||||
elapsed_seconds=_MOCK_LATENCY_SECONDS
|
||||
)
|
||||
# For non-GCS calls, use original method
|
||||
if _original_async_handler_delete is not None:
|
||||
return await _original_async_handler_delete(self, url=url, data=data, json=json, params=params, headers=headers, timeout=timeout, stream=stream, content=content)
|
||||
# Fallback: if original not set, raise error
|
||||
raise RuntimeError("Original AsyncHTTPHandler.delete not available")
|
||||
|
||||
|
||||
def create_mock_gcs_client():
|
||||
"""
|
||||
Monkey-patch AsyncHTTPHandler methods to intercept GCS calls.
|
||||
|
||||
AsyncHTTPHandler is used by LiteLLM's get_async_httpx_client() which is what
|
||||
GCSBucketBase uses for making API calls.
|
||||
|
||||
This function is idempotent - it only initializes mocks once, even if called multiple times.
|
||||
"""
|
||||
global _original_async_handler_post, _original_async_handler_get, _original_async_handler_delete
|
||||
global _mocks_initialized
|
||||
|
||||
# If already initialized, skip
|
||||
if _mocks_initialized:
|
||||
return
|
||||
|
||||
verbose_logger.debug("[GCS MOCK] Initializing GCS mock client...")
|
||||
|
||||
# Patch AsyncHTTPHandler methods (used by LiteLLM's custom httpx handler)
|
||||
if _original_async_handler_post is None:
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
_original_async_handler_post = AsyncHTTPHandler.post
|
||||
AsyncHTTPHandler.post = _mock_async_handler_post # type: ignore
|
||||
verbose_logger.debug("[GCS MOCK] Patched AsyncHTTPHandler.post")
|
||||
|
||||
if _original_async_handler_get is None:
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
_original_async_handler_get = AsyncHTTPHandler.get
|
||||
AsyncHTTPHandler.get = _mock_async_handler_get # type: ignore
|
||||
verbose_logger.debug("[GCS MOCK] Patched AsyncHTTPHandler.get")
|
||||
|
||||
if _original_async_handler_delete is None:
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
_original_async_handler_delete = AsyncHTTPHandler.delete
|
||||
AsyncHTTPHandler.delete = _mock_async_handler_delete # type: ignore
|
||||
verbose_logger.debug("[GCS MOCK] Patched AsyncHTTPHandler.delete")
|
||||
|
||||
verbose_logger.debug(f"[GCS MOCK] Mock latency set to {_MOCK_LATENCY_SECONDS*1000:.0f}ms")
|
||||
verbose_logger.debug("[GCS MOCK] GCS mock client initialization complete")
|
||||
|
||||
_mocks_initialized = True
|
||||
|
||||
|
||||
def mock_vertex_auth_methods():
|
||||
"""
|
||||
Monkey-patch Vertex AI auth methods to return fake tokens.
|
||||
This prevents auth failures when GCS_MOCK is enabled.
|
||||
|
||||
This function is idempotent - it only patches once, even if called multiple times.
|
||||
"""
|
||||
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase
|
||||
|
||||
# Store original methods if not already stored
|
||||
if not hasattr(VertexBase, '_original_ensure_access_token_async'):
|
||||
setattr(VertexBase, '_original_ensure_access_token_async', VertexBase._ensure_access_token_async)
|
||||
setattr(VertexBase, '_original_ensure_access_token', VertexBase._ensure_access_token)
|
||||
setattr(VertexBase, '_original_get_token_and_url', VertexBase._get_token_and_url)
|
||||
|
||||
async def _mock_ensure_access_token_async(self, credentials, project_id, custom_llm_provider):
|
||||
"""Mock async auth method - returns fake token."""
|
||||
verbose_logger.debug("[GCS MOCK] Vertex AI auth: _ensure_access_token_async called")
|
||||
return ("mock-gcs-token", "mock-project-id")
|
||||
|
||||
def _mock_ensure_access_token(self, credentials, project_id, custom_llm_provider):
|
||||
"""Mock sync auth method - returns fake token."""
|
||||
verbose_logger.debug("[GCS MOCK] Vertex AI auth: _ensure_access_token called")
|
||||
return ("mock-gcs-token", "mock-project-id")
|
||||
|
||||
def _mock_get_token_and_url(self, model, auth_header, vertex_credentials, vertex_project,
|
||||
vertex_location, gemini_api_key, stream, custom_llm_provider, api_base):
|
||||
"""Mock get_token_and_url - returns fake token."""
|
||||
verbose_logger.debug("[GCS MOCK] Vertex AI auth: _get_token_and_url called")
|
||||
return ("mock-gcs-token", "https://storage.googleapis.com")
|
||||
|
||||
# Patch the methods
|
||||
VertexBase._ensure_access_token_async = _mock_ensure_access_token_async # type: ignore
|
||||
VertexBase._ensure_access_token = _mock_ensure_access_token # type: ignore
|
||||
VertexBase._get_token_and_url = _mock_get_token_and_url # type: ignore
|
||||
|
||||
verbose_logger.debug("[GCS MOCK] Patched Vertex AI auth methods")
|
||||
|
||||
|
||||
def should_use_gcs_mock() -> bool:
|
||||
"""
|
||||
Determine if GCS should run in mock mode.
|
||||
|
||||
Checks the GCS_MOCK environment variable.
|
||||
|
||||
Returns:
|
||||
bool: True if mock mode should be enabled
|
||||
"""
|
||||
import os
|
||||
from litellm.secret_managers.main import str_to_bool
|
||||
|
||||
mock_mode = os.getenv("GCS_MOCK", "false")
|
||||
result = str_to_bool(mock_mode)
|
||||
|
||||
# Ensure we return a bool, not None
|
||||
result = bool(result) if result is not None else False
|
||||
|
||||
if result:
|
||||
verbose_logger.info("GCS Mock Mode: ENABLED - API calls will be mocked")
|
||||
|
||||
return result
|
||||
|
|
@ -25,6 +25,10 @@ from litellm.litellm_core_utils.core_helpers import (
|
|||
reconstruct_model_name,
|
||||
)
|
||||
from litellm.litellm_core_utils.redact_messages import redact_user_api_key_info
|
||||
from litellm.integrations.langfuse.langfuse_mock_client import (
|
||||
create_mock_langfuse_client,
|
||||
should_use_langfuse_mock,
|
||||
)
|
||||
from litellm.llms.custom_httpx.http_handler import _get_httpx_client
|
||||
from litellm.secret_managers.main import str_to_bool
|
||||
from litellm.types.integrations.langfuse import *
|
||||
|
|
@ -119,8 +123,14 @@ class LangFuseLogger:
|
|||
self.langfuse_flush_interval = LangFuseLogger._get_langfuse_flush_interval(
|
||||
flush_interval
|
||||
)
|
||||
http_client = _get_httpx_client()
|
||||
self.langfuse_client = http_client.client
|
||||
|
||||
if should_use_langfuse_mock():
|
||||
self.langfuse_client = create_mock_langfuse_client()
|
||||
self.is_mock_mode = True
|
||||
else:
|
||||
http_client = _get_httpx_client()
|
||||
self.langfuse_client = http_client.client
|
||||
self.is_mock_mode = False
|
||||
|
||||
parameters = {
|
||||
"public_key": self.public_key,
|
||||
|
|
@ -139,11 +149,15 @@ class LangFuseLogger:
|
|||
|
||||
# set the current langfuse project id in the environ
|
||||
# this is used by Alerting to link to the correct project
|
||||
try:
|
||||
project_id = self.Langfuse.client.projects.get().data[0].id
|
||||
os.environ["LANGFUSE_PROJECT_ID"] = project_id
|
||||
except Exception:
|
||||
project_id = None
|
||||
if self.is_mock_mode:
|
||||
os.environ["LANGFUSE_PROJECT_ID"] = "mock-project-id"
|
||||
verbose_logger.debug("Langfuse Mock: Using mock project ID")
|
||||
else:
|
||||
try:
|
||||
project_id = self.Langfuse.client.projects.get().data[0].id
|
||||
os.environ["LANGFUSE_PROJECT_ID"] = project_id
|
||||
except Exception:
|
||||
project_id = None
|
||||
|
||||
if os.getenv("UPSTREAM_LANGFUSE_SECRET_KEY") is not None:
|
||||
upstream_langfuse_debug = (
|
||||
|
|
|
|||
121
litellm/integrations/langfuse/langfuse_mock_client.py
Normal file
121
litellm/integrations/langfuse/langfuse_mock_client.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""
|
||||
Mock httpx client for Langfuse integration testing.
|
||||
|
||||
This module intercepts Langfuse API calls and returns successful mock responses,
|
||||
allowing full code execution without making actual network calls.
|
||||
|
||||
Usage:
|
||||
Set LANGFUSE_MOCK=true in environment variables or config to enable mock mode.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from typing import Dict, Optional
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
|
||||
_original_httpx_post = None
|
||||
|
||||
# Default mock latency in seconds (simulates network round-trip)
|
||||
# Typical Langfuse API calls take 50-150ms
|
||||
_MOCK_LATENCY_SECONDS = float(__import__("os").getenv("LANGFUSE_MOCK_LATENCY_MS", "100")) / 1000.0
|
||||
|
||||
|
||||
class MockLangfuseResponse:
|
||||
"""Mock httpx.Response that satisfies Langfuse SDK requirements."""
|
||||
|
||||
def __init__(self, status_code: int = 200, json_data: Optional[Dict] = None, url: Optional[str] = None, elapsed_seconds: float = 0.0):
|
||||
self.status_code = status_code
|
||||
self._json_data = json_data or {"status": "success"}
|
||||
self.headers = httpx.Headers({})
|
||||
self.is_success = status_code < 400
|
||||
self.is_error = status_code >= 400
|
||||
self.is_redirect = 300 <= status_code < 400
|
||||
self.url = httpx.URL(url) if url else httpx.URL("")
|
||||
# Set realistic elapsed time based on mock latency
|
||||
elapsed_time = elapsed_seconds if elapsed_seconds > 0 else _MOCK_LATENCY_SECONDS
|
||||
self.elapsed = timedelta(seconds=elapsed_time)
|
||||
self._text = json.dumps(self._json_data)
|
||||
self._content = self._text.encode("utf-8")
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self._text
|
||||
|
||||
@property
|
||||
def content(self) -> bytes:
|
||||
return self._content
|
||||
|
||||
def json(self) -> Dict:
|
||||
return self._json_data
|
||||
|
||||
def read(self) -> bytes:
|
||||
return self._content
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
raise Exception(f"HTTP {self.status_code}")
|
||||
|
||||
|
||||
def _is_langfuse_url(url) -> bool:
|
||||
"""Check if URL is a Langfuse domain."""
|
||||
try:
|
||||
parsed_url = httpx.URL(url) if isinstance(url, str) else url
|
||||
hostname = parsed_url.host or ""
|
||||
|
||||
return (
|
||||
hostname.endswith(".langfuse.com") or
|
||||
hostname == "langfuse.com" or
|
||||
(hostname in ("localhost", "127.0.0.1") and "langfuse" in str(parsed_url).lower())
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _mock_httpx_post(self, url, **kwargs):
|
||||
"""Monkey-patched httpx.Client.post that intercepts Langfuse calls."""
|
||||
if _is_langfuse_url(url):
|
||||
verbose_logger.info(f"[LANGFUSE MOCK] POST to {url}")
|
||||
return MockLangfuseResponse(status_code=200, json_data={"status": "success"}, url=url, elapsed_seconds=_MOCK_LATENCY_SECONDS)
|
||||
|
||||
if _original_httpx_post is not None:
|
||||
return _original_httpx_post(self, url, **kwargs)
|
||||
|
||||
|
||||
def create_mock_langfuse_client():
|
||||
"""
|
||||
Monkey-patch httpx.Client.post to intercept Langfuse calls.
|
||||
|
||||
Returns a real httpx.Client instance - the monkey-patch intercepts all calls.
|
||||
"""
|
||||
global _original_httpx_post
|
||||
|
||||
if _original_httpx_post is None:
|
||||
_original_httpx_post = httpx.Client.post
|
||||
httpx.Client.post = _mock_httpx_post # type: ignore
|
||||
verbose_logger.debug("[LANGFUSE MOCK] Patched httpx.Client.post")
|
||||
|
||||
return httpx.Client()
|
||||
|
||||
|
||||
def should_use_langfuse_mock() -> bool:
|
||||
"""
|
||||
Determine if Langfuse should run in mock mode.
|
||||
|
||||
Checks the LANGFUSE_MOCK environment variable.
|
||||
|
||||
Returns:
|
||||
bool: True if mock mode should be enabled
|
||||
"""
|
||||
import os
|
||||
from litellm.secret_managers.main import str_to_bool
|
||||
|
||||
mock_mode = os.getenv("LANGFUSE_MOCK", "false")
|
||||
result = str_to_bool(mock_mode)
|
||||
result = bool(result) if result is not None else False
|
||||
|
||||
if result:
|
||||
verbose_logger.info("Langfuse Mock Mode: ENABLED - API calls will be mocked")
|
||||
|
||||
return result
|
||||
|
|
@ -17,6 +17,10 @@ from litellm.types.utils import (
|
|||
StandardCallbackDynamicParams,
|
||||
StandardLoggingPayload,
|
||||
)
|
||||
from litellm.integrations._types.open_inference import (
|
||||
OpenInferenceSpanKindValues,
|
||||
SpanAttributes,
|
||||
)
|
||||
|
||||
# OpenTelemetry imports moved to individual functions to avoid import errors when not installed
|
||||
|
||||
|
|
@ -660,6 +664,9 @@ class OpenTelemetry(CustomLogger):
|
|||
self._maybe_log_raw_request(
|
||||
kwargs, response_obj, start_time, end_time, span
|
||||
)
|
||||
# Ensure proxy-request parent span is annotated with the actual operation kind
|
||||
if parent_span is not None and parent_span.name == LITELLM_PROXY_REQUEST_SPAN_NAME:
|
||||
self.set_attributes(parent_span, kwargs, response_obj)
|
||||
else:
|
||||
# Do not create primary span (keep hierarchy shallow when parent exists)
|
||||
from opentelemetry.trace import Status, StatusCode
|
||||
|
|
@ -1106,6 +1113,12 @@ class OpenTelemetry(CustomLogger):
|
|||
context=context,
|
||||
)
|
||||
|
||||
self.safe_set_attribute(
|
||||
span=guardrail_span,
|
||||
key=SpanAttributes.OPENINFERENCE_SPAN_KIND,
|
||||
value=OpenInferenceSpanKindValues.GUARDRAIL.value,
|
||||
)
|
||||
|
||||
self.safe_set_attribute(
|
||||
span=guardrail_span,
|
||||
key="guardrail_name",
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ class SupportedDBObjectType(str, enum.Enum):
|
|||
MODELS = "models"
|
||||
MCP = "mcp"
|
||||
GUARDRAILS = "guardrails"
|
||||
POLICIES = "policies"
|
||||
VECTOR_STORES = "vector_stores"
|
||||
PASS_THROUGH_ENDPOINTS = "pass_through_endpoints"
|
||||
PROMPTS = "prompts"
|
||||
|
|
@ -844,6 +845,7 @@ class GenerateRequestBase(LiteLLMPydanticObjectBase):
|
|||
model_rpm_limit: Optional[dict] = None
|
||||
model_tpm_limit: Optional[dict] = None
|
||||
guardrails: Optional[List[str]] = None
|
||||
policies: Optional[List[str]] = None
|
||||
prompts: Optional[List[str]] = None
|
||||
blocked: Optional[bool] = None
|
||||
aliases: Optional[dict] = {}
|
||||
|
|
@ -1477,6 +1479,7 @@ class NewTeamRequest(TeamBase):
|
|||
model_aliases: Optional[dict] = None
|
||||
tags: Optional[list] = None
|
||||
guardrails: Optional[List[str]] = None
|
||||
policies: Optional[List[str]] = None
|
||||
prompts: Optional[List[str]] = None
|
||||
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
|
||||
allowed_passthrough_routes: Optional[list] = None
|
||||
|
|
@ -1526,6 +1529,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase):
|
|||
blocked: Optional[bool] = None
|
||||
budget_duration: Optional[str] = None
|
||||
guardrails: Optional[List[str]] = None
|
||||
policies: Optional[List[str]] = None
|
||||
"""
|
||||
|
||||
team_id: str # required
|
||||
|
|
@ -1541,6 +1545,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase):
|
|||
tags: Optional[list] = None
|
||||
model_aliases: Optional[dict] = None
|
||||
guardrails: Optional[List[str]] = None
|
||||
policies: Optional[List[str]] = None
|
||||
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
|
||||
team_member_budget: Optional[float] = None
|
||||
team_member_budget_duration: Optional[str] = None
|
||||
|
|
@ -3499,6 +3504,7 @@ LiteLLM_ManagementEndpoint_MetadataFields = [
|
|||
|
||||
LiteLLM_ManagementEndpoint_MetadataFields_Premium = [
|
||||
"guardrails",
|
||||
"policies",
|
||||
"tags",
|
||||
"team_member_key_duration",
|
||||
"prompts",
|
||||
|
|
|
|||
|
|
@ -1311,6 +1311,118 @@ def _add_guardrails_from_key_or_team_metadata(
|
|||
data[metadata_variable_name]["guardrails"] = list(combined_guardrails)
|
||||
|
||||
|
||||
def _add_guardrails_from_policies_in_metadata(
|
||||
key_metadata: Optional[dict],
|
||||
team_metadata: Optional[dict],
|
||||
data: dict,
|
||||
metadata_variable_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Helper to resolve guardrails from policies attached to key/team metadata.
|
||||
|
||||
This function:
|
||||
1. Gets policy names from key and team metadata
|
||||
2. Resolves guardrails from those policies (including inheritance)
|
||||
3. Adds resolved guardrails to request metadata
|
||||
|
||||
Args:
|
||||
key_metadata: The key metadata dictionary to check for policies
|
||||
team_metadata: The team metadata dictionary to check for policies
|
||||
data: The request data to update
|
||||
metadata_variable_name: The name of the metadata field in data
|
||||
"""
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||
from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
|
||||
from litellm.proxy.utils import _premium_user_check
|
||||
from litellm.types.proxy.policy_engine import PolicyMatchContext
|
||||
|
||||
# Collect policy names from key and team metadata
|
||||
policy_names: set = set()
|
||||
|
||||
# Add key-level policies first
|
||||
if key_metadata and "policies" in key_metadata:
|
||||
if (
|
||||
isinstance(key_metadata["policies"], list)
|
||||
and len(key_metadata["policies"]) > 0
|
||||
):
|
||||
_premium_user_check()
|
||||
policy_names.update(key_metadata["policies"])
|
||||
|
||||
# Add team-level policies
|
||||
if team_metadata and "policies" in team_metadata:
|
||||
if (
|
||||
isinstance(team_metadata["policies"], list)
|
||||
and len(team_metadata["policies"]) > 0
|
||||
):
|
||||
_premium_user_check()
|
||||
policy_names.update(team_metadata["policies"])
|
||||
|
||||
if not policy_names:
|
||||
return
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f"Policy engine: resolving guardrails from key/team policies: {policy_names}"
|
||||
)
|
||||
|
||||
# Check if policy registry is initialized
|
||||
registry = get_policy_registry()
|
||||
if not registry.is_initialized():
|
||||
verbose_proxy_logger.debug(
|
||||
"Policy engine not initialized, skipping policy resolution from metadata"
|
||||
)
|
||||
return
|
||||
|
||||
# Build context for policy resolution (model from request data)
|
||||
context = PolicyMatchContext(model=data.get("model"))
|
||||
|
||||
# Get all policies from registry
|
||||
all_policies = registry.get_all_policies()
|
||||
|
||||
# Resolve guardrails from the specified policies
|
||||
resolved_guardrails: set = set()
|
||||
for policy_name in policy_names:
|
||||
if registry.has_policy(policy_name):
|
||||
resolved_policy = PolicyResolver.resolve_policy_guardrails(
|
||||
policy_name=policy_name,
|
||||
policies=all_policies,
|
||||
context=context,
|
||||
)
|
||||
resolved_guardrails.update(resolved_policy.guardrails)
|
||||
verbose_proxy_logger.debug(
|
||||
f"Policy engine: resolved guardrails from policy '{policy_name}': {resolved_policy.guardrails}"
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Policy engine: policy '{policy_name}' not found in registry"
|
||||
)
|
||||
|
||||
if not resolved_guardrails:
|
||||
return
|
||||
|
||||
# Add resolved guardrails to request metadata
|
||||
if metadata_variable_name not in data:
|
||||
data[metadata_variable_name] = {}
|
||||
|
||||
existing_guardrails = data[metadata_variable_name].get("guardrails", [])
|
||||
if not isinstance(existing_guardrails, list):
|
||||
existing_guardrails = []
|
||||
|
||||
# Combine existing guardrails with policy-resolved guardrails (no duplicates)
|
||||
combined = set(existing_guardrails)
|
||||
combined.update(resolved_guardrails)
|
||||
data[metadata_variable_name]["guardrails"] = list(combined)
|
||||
|
||||
# Store applied policies in metadata for tracking
|
||||
if "applied_policies" not in data[metadata_variable_name]:
|
||||
data[metadata_variable_name]["applied_policies"] = []
|
||||
data[metadata_variable_name]["applied_policies"].extend(list(policy_names))
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f"Policy engine: added guardrails from key/team policies to request metadata: {list(resolved_guardrails)}"
|
||||
)
|
||||
|
||||
|
||||
def move_guardrails_to_metadata(
|
||||
data: dict,
|
||||
_metadata_variable_name: str,
|
||||
|
|
@ -1321,6 +1433,7 @@ def move_guardrails_to_metadata(
|
|||
|
||||
- If guardrails set on API Key metadata then sets guardrails on request metadata
|
||||
- If guardrails not set on API key, then checks request metadata
|
||||
- Adds guardrails from policies attached to key/team metadata
|
||||
- Adds guardrails from policy engine based on team/key/model context
|
||||
"""
|
||||
# Check key-level guardrails
|
||||
|
|
@ -1331,6 +1444,16 @@ def move_guardrails_to_metadata(
|
|||
metadata_variable_name=_metadata_variable_name,
|
||||
)
|
||||
|
||||
#########################################################################################
|
||||
# Add guardrails from policies attached to key/team metadata
|
||||
#########################################################################################
|
||||
_add_guardrails_from_policies_in_metadata(
|
||||
key_metadata=user_api_key_dict.metadata,
|
||||
team_metadata=user_api_key_dict.team_metadata,
|
||||
data=data,
|
||||
metadata_variable_name=_metadata_variable_name,
|
||||
)
|
||||
|
||||
#########################################################################################
|
||||
# Add guardrails from policy engine based on team/key/model context
|
||||
#########################################################################################
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ These are members of a Team on LiteLLM
|
|||
import asyncio
|
||||
import json
|
||||
import traceback
|
||||
from litellm._uuid import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
|
|
@ -23,6 +22,7 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
|||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm._uuid import uuid
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.hooks.user_management_event_hooks import UserManagementEventHooks
|
||||
|
|
@ -355,6 +355,7 @@ async def new_user(
|
|||
- allowed_cache_controls: Optional[list] - List of allowed cache control values. Example - ["no-cache", "no-store"]. See all values - https://docs.litellm.ai/docs/proxy/caching#turn-on--off-caching-per-request-
|
||||
- blocked: Optional[bool] - [Not Implemented Yet] Whether the user is blocked.
|
||||
- guardrails: Optional[List[str]] - [Not Implemented Yet] List of active guardrails for the user
|
||||
- policies: Optional[List[str]] - List of policy names to apply to the user. Policies define guardrails, conditions, and inheritance rules.
|
||||
- permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking.
|
||||
- metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
|
||||
- max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x.
|
||||
|
|
@ -1060,6 +1061,7 @@ async def user_update(
|
|||
- allowed_cache_controls: Optional[list] - List of allowed cache control values. Example - ["no-cache", "no-store"]. See all values - https://docs.litellm.ai/docs/proxy/caching#turn-on--off-caching-per-request-
|
||||
- blocked: Optional[bool] - [Not Implemented Yet] Whether the user is blocked.
|
||||
- guardrails: Optional[List[str]] - [Not Implemented Yet] List of active guardrails for the user
|
||||
- policies: Optional[List[str]] - List of policy names to apply to the user. Policies define guardrails, conditions, and inheritance rules.
|
||||
- permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking.
|
||||
- metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
|
||||
- max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x.
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import copy
|
|||
import json
|
||||
import secrets
|
||||
import traceback
|
||||
import yaml
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, cast
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
|
||||
import fastapi
|
||||
import yaml
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
|
||||
|
||||
import litellm
|
||||
|
|
@ -31,6 +31,7 @@ from litellm.constants import (
|
|||
UI_SESSION_TOKEN_TEAM_ID,
|
||||
)
|
||||
from litellm.litellm_core_utils.duration_parser import duration_in_seconds
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.proxy._experimental.mcp_server.db import (
|
||||
rotate_mcp_server_credentials_master_key,
|
||||
)
|
||||
|
|
@ -1010,6 +1011,7 @@ async def generate_key_fn(
|
|||
- max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x.
|
||||
- metadata: Optional[dict] - Metadata for key, store information for key. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
|
||||
- guardrails: Optional[List[str]] - List of active guardrails for the key
|
||||
- policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules.
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- permissions: Optional[dict] - key-specific permissions. Currently just used for turning off pii masking (if connected). Example - {"pii": false}
|
||||
- model_max_budget: Optional[Dict[str, BudgetConfig]] - Model-specific budgets {"gpt-4": {"budget_limit": 0.0005, "time_period": "30d"}}}. IF null or {} then no model specific budget.
|
||||
|
|
@ -1480,6 +1482,7 @@ async def update_key_fn(
|
|||
- permissions: Optional[dict] - Key-specific permissions
|
||||
- send_invite_email: Optional[bool] - Send invite email to user_id
|
||||
- guardrails: Optional[List[str]] - List of active guardrails for the key
|
||||
- policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules.
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- prompts: Optional[List[str]] - List of prompts that the key is allowed to use.
|
||||
- blocked: Optional[bool] - Whether the key is blocked
|
||||
|
|
@ -2077,6 +2080,7 @@ async def generate_key_helper_fn( # noqa: PLR0915
|
|||
model_rpm_limit: Optional[dict] = None,
|
||||
model_tpm_limit: Optional[dict] = None,
|
||||
guardrails: Optional[list] = None,
|
||||
policies: Optional[list] = None,
|
||||
prompts: Optional[list] = None,
|
||||
teams: Optional[list] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
|
|
@ -2139,6 +2143,9 @@ async def generate_key_helper_fn( # noqa: PLR0915
|
|||
if guardrails is not None:
|
||||
metadata = metadata or {}
|
||||
metadata["guardrails"] = guardrails
|
||||
if policies is not None:
|
||||
metadata = metadata or {}
|
||||
metadata["policies"] = policies
|
||||
if prompts is not None:
|
||||
metadata = metadata or {}
|
||||
metadata["prompts"] = prompts
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@ from pydantic import BaseModel
|
|||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm._uuid import uuid
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.proxy._types import (
|
||||
BlockTeamRequest,
|
||||
CommonProxyErrors,
|
||||
DeleteTeamRequest,
|
||||
LiteLLM_AuditLogs,
|
||||
LiteLLM_DeletedTeamTable,
|
||||
LiteLLM_ManagementEndpoint_MetadataFields,
|
||||
LiteLLM_ManagementEndpoint_MetadataFields_Premium,
|
||||
LiteLLM_ModelTable,
|
||||
|
|
@ -34,7 +36,6 @@ from litellm.proxy._types import (
|
|||
LiteLLM_OrganizationTableWithMembers,
|
||||
LiteLLM_TeamMembership,
|
||||
LiteLLM_TeamTable,
|
||||
LiteLLM_DeletedTeamTable,
|
||||
LiteLLM_TeamTableCachedObj,
|
||||
LiteLLM_UserTable,
|
||||
LiteLLM_VerificationToken,
|
||||
|
|
@ -102,7 +103,7 @@ from litellm.types.proxy.management_endpoints.team_endpoints import (
|
|||
TeamMemberAddResult,
|
||||
UpdateTeamMemberPermissionsRequest,
|
||||
)
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
|
|
@ -689,6 +690,7 @@ async def new_team( # noqa: PLR0915
|
|||
- organization_id: Optional[str] - The organization id of the team. Default is None. Create via `/organization/new`.
|
||||
- model_aliases: Optional[dict] - Model aliases for the team. [Docs](https://docs.litellm.ai/docs/proxy/team_based_routing#create-team-with-model-alias)
|
||||
- guardrails: Optional[List[str]] - Guardrails for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails)
|
||||
- policies: Optional[List[str]] - Policies for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies)
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - team-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"], "agents": ["agent_1", "agent_2"], "agent_access_groups": ["dev_group"]}. IF null or {} then no object permission.
|
||||
- team_member_budget: Optional[float] - The maximum budget allocated to an individual team member.
|
||||
|
|
@ -1228,6 +1230,7 @@ async def update_team( # noqa: PLR0915
|
|||
- organization_id: Optional[str] - The organization id of the team. Default is None. Create via `/organization/new`.
|
||||
- model_aliases: Optional[dict] - Model aliases for the team. [Docs](https://docs.litellm.ai/docs/proxy/team_based_routing#create-team-with-model-alias)
|
||||
- guardrails: Optional[List[str]] - Guardrails for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails)
|
||||
- policies: Optional[List[str]] - Policies for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies)
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - team-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"], "agents": ["agent_1", "agent_2"], "agent_access_groups": ["dev_group"]}. IF null or {} then no object permission.
|
||||
- team_member_budget: Optional[float] - The maximum budget allocated to an individual team member.
|
||||
|
|
|
|||
|
|
@ -16,23 +16,33 @@ sequenceDiagram
|
|||
|
||||
rect rgb(240, 240, 240)
|
||||
Note over Proxy: 1. URL Construction
|
||||
Note over Proxy: Build: https://us-central1-aiplatform.googleapis.com/...
|
||||
Note over Proxy: Build regional/provider-specific URL
|
||||
end
|
||||
|
||||
rect rgb(240, 240, 240)
|
||||
Note over Proxy: 2. Auth Header Replacement
|
||||
Note over Proxy: Replace litellm key → provider credentials
|
||||
Note over Proxy: LiteLLM key → provider credentials
|
||||
end
|
||||
|
||||
rect rgb(240, 240, 240)
|
||||
Note over Proxy: 3. Extra Operations
|
||||
Note over Proxy: • x-pass-* headers (strip prefix, forward)
|
||||
Note over Proxy: • x-litellm-tags → metadata
|
||||
Note over Proxy: • Guardrails (opt-in)
|
||||
Note over Proxy: • Multipart form reconstruction
|
||||
end
|
||||
|
||||
Proxy->>Provider: POST https://us-central1-aiplatform.googleapis.com/...
|
||||
Note over Proxy,Provider: Headers: Authorization: Bearer ya29.google-oauth...
|
||||
Note over Proxy,Provider: Body: { "contents": [...] } ← UNCHANGED
|
||||
|
||||
Provider-->>Proxy: Response
|
||||
|
||||
Provider-->>Proxy: Response (streaming or non-streaming)
|
||||
|
||||
rect rgb(240, 240, 240)
|
||||
Note over Proxy: 3. Logging (async, optional)
|
||||
Note over Proxy: Parse response → calculate cost → log
|
||||
Note over Proxy: 4. Response Handling (async)
|
||||
Note over Proxy: • Collect streaming chunks for logging
|
||||
Note over Proxy: • Cost injection (if enabled)
|
||||
Note over Proxy: • Parse response → calculate cost → log
|
||||
end
|
||||
|
||||
Proxy-->>Client: Response (unchanged)
|
||||
|
|
@ -42,7 +52,17 @@ sequenceDiagram
|
|||
|
||||
- **URL Construction** - Build correct provider URL (e.g., regional endpoints for Vertex AI, Bedrock)
|
||||
- **Auth Header Replacement** - Swap LiteLLM virtual key for actual provider credentials
|
||||
- **Logging** (optional) - Parse response to extract usage and calculate cost
|
||||
|
||||
## Extra Operations
|
||||
|
||||
| Operation | Description |
|
||||
|-----------|-------------|
|
||||
| `x-pass-*` headers | Strip prefix and forward (e.g., `x-pass-anthropic-beta` → `anthropic-beta`) |
|
||||
| `x-litellm-tags` header | Extract tags and add to request metadata for logging |
|
||||
| Streaming chunk collection | Collect chunks async for logging after stream completes |
|
||||
| Multipart form handling | Reconstruct multipart/form-data requests for file uploads |
|
||||
| Guardrails (opt-in) | Run content filtering when explicitly configured |
|
||||
| Cost injection | Inject cost into streaming chunks when `include_cost_in_streaming_usage` enabled |
|
||||
|
||||
## What Does NOT Change
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,20 @@ Attachments define WHERE policies apply, separate from the policy definitions.
|
|||
This allows the same policy to be attached to multiple scopes.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.types.proxy.policy_engine import (
|
||||
PolicyAttachment,
|
||||
PolicyAttachmentCreateRequest,
|
||||
PolicyAttachmentDBResponse,
|
||||
PolicyMatchContext,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
|
||||
|
||||
class AttachmentRegistry:
|
||||
"""
|
||||
|
|
@ -188,6 +194,238 @@ class AttachmentRegistry:
|
|||
)
|
||||
return removed_count
|
||||
|
||||
def remove_attachment_by_id(self, attachment_id: str) -> bool:
|
||||
"""
|
||||
Remove an attachment by its ID (for DB-synced attachments).
|
||||
|
||||
Args:
|
||||
attachment_id: The ID of the attachment to remove
|
||||
|
||||
Returns:
|
||||
True if removed, False if not found
|
||||
"""
|
||||
# Note: In-memory attachments don't have IDs, so this is primarily
|
||||
# for consistency after DB operations
|
||||
return False
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Database CRUD Methods
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def add_attachment_to_db(
|
||||
self,
|
||||
attachment_request: PolicyAttachmentCreateRequest,
|
||||
prisma_client: "PrismaClient",
|
||||
created_by: Optional[str] = None,
|
||||
) -> PolicyAttachmentDBResponse:
|
||||
"""
|
||||
Add a policy attachment to the database.
|
||||
|
||||
Args:
|
||||
attachment_request: The attachment creation request
|
||||
prisma_client: The Prisma client instance
|
||||
created_by: User who created the attachment
|
||||
|
||||
Returns:
|
||||
PolicyAttachmentDBResponse with the created attachment
|
||||
"""
|
||||
try:
|
||||
created_attachment = (
|
||||
await prisma_client.db.litellm_policyattachmenttable.create(
|
||||
data={
|
||||
"policy_name": attachment_request.policy_name,
|
||||
"scope": attachment_request.scope,
|
||||
"teams": attachment_request.teams or [],
|
||||
"keys": attachment_request.keys or [],
|
||||
"models": attachment_request.models or [],
|
||||
"created_at": datetime.now(timezone.utc),
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"created_by": created_by,
|
||||
"updated_by": created_by,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Also add to in-memory registry
|
||||
attachment = PolicyAttachment(
|
||||
policy=attachment_request.policy_name,
|
||||
scope=attachment_request.scope,
|
||||
teams=attachment_request.teams,
|
||||
keys=attachment_request.keys,
|
||||
models=attachment_request.models,
|
||||
)
|
||||
self.add_attachment(attachment)
|
||||
|
||||
return PolicyAttachmentDBResponse(
|
||||
attachment_id=created_attachment.attachment_id,
|
||||
policy_name=created_attachment.policy_name,
|
||||
scope=created_attachment.scope,
|
||||
teams=created_attachment.teams or [],
|
||||
keys=created_attachment.keys or [],
|
||||
models=created_attachment.models or [],
|
||||
created_at=created_attachment.created_at,
|
||||
updated_at=created_attachment.updated_at,
|
||||
created_by=created_attachment.created_by,
|
||||
updated_by=created_attachment.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error adding attachment to DB: {e}")
|
||||
raise Exception(f"Error adding attachment to DB: {str(e)}")
|
||||
|
||||
async def delete_attachment_from_db(
|
||||
self,
|
||||
attachment_id: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Delete a policy attachment from the database.
|
||||
|
||||
Args:
|
||||
attachment_id: The ID of the attachment to delete
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
Dict with success message
|
||||
"""
|
||||
try:
|
||||
# Get attachment before deleting
|
||||
attachment = (
|
||||
await prisma_client.db.litellm_policyattachmenttable.find_unique(
|
||||
where={"attachment_id": attachment_id}
|
||||
)
|
||||
)
|
||||
|
||||
if attachment is None:
|
||||
raise Exception(f"Attachment with ID {attachment_id} not found")
|
||||
|
||||
# Delete from DB
|
||||
await prisma_client.db.litellm_policyattachmenttable.delete(
|
||||
where={"attachment_id": attachment_id}
|
||||
)
|
||||
|
||||
# Note: In-memory attachments don't have IDs, so we need to sync from DB
|
||||
# to properly update in-memory state
|
||||
await self.sync_attachments_from_db(prisma_client)
|
||||
|
||||
return {"message": f"Attachment {attachment_id} deleted successfully"}
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting attachment from DB: {e}")
|
||||
raise Exception(f"Error deleting attachment from DB: {str(e)}")
|
||||
|
||||
async def get_attachment_by_id_from_db(
|
||||
self,
|
||||
attachment_id: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> Optional[PolicyAttachmentDBResponse]:
|
||||
"""
|
||||
Get a policy attachment by ID from the database.
|
||||
|
||||
Args:
|
||||
attachment_id: The ID of the attachment to retrieve
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
PolicyAttachmentDBResponse if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
attachment = (
|
||||
await prisma_client.db.litellm_policyattachmenttable.find_unique(
|
||||
where={"attachment_id": attachment_id}
|
||||
)
|
||||
)
|
||||
|
||||
if attachment is None:
|
||||
return None
|
||||
|
||||
return PolicyAttachmentDBResponse(
|
||||
attachment_id=attachment.attachment_id,
|
||||
policy_name=attachment.policy_name,
|
||||
scope=attachment.scope,
|
||||
teams=attachment.teams or [],
|
||||
keys=attachment.keys or [],
|
||||
models=attachment.models or [],
|
||||
created_at=attachment.created_at,
|
||||
updated_at=attachment.updated_at,
|
||||
created_by=attachment.created_by,
|
||||
updated_by=attachment.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting attachment from DB: {e}")
|
||||
raise Exception(f"Error getting attachment from DB: {str(e)}")
|
||||
|
||||
async def get_all_attachments_from_db(
|
||||
self,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> List[PolicyAttachmentDBResponse]:
|
||||
"""
|
||||
Get all policy attachments from the database.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
List of PolicyAttachmentDBResponse objects
|
||||
"""
|
||||
try:
|
||||
attachments = (
|
||||
await prisma_client.db.litellm_policyattachmenttable.find_many(
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
PolicyAttachmentDBResponse(
|
||||
attachment_id=a.attachment_id,
|
||||
policy_name=a.policy_name,
|
||||
scope=a.scope,
|
||||
teams=a.teams or [],
|
||||
keys=a.keys or [],
|
||||
models=a.models or [],
|
||||
created_at=a.created_at,
|
||||
updated_at=a.updated_at,
|
||||
created_by=a.created_by,
|
||||
updated_by=a.updated_by,
|
||||
)
|
||||
for a in attachments
|
||||
]
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting attachments from DB: {e}")
|
||||
raise Exception(f"Error getting attachments from DB: {str(e)}")
|
||||
|
||||
async def sync_attachments_from_db(
|
||||
self,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> None:
|
||||
"""
|
||||
Sync policy attachments from the database to in-memory registry.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
"""
|
||||
try:
|
||||
attachments = await self.get_all_attachments_from_db(prisma_client)
|
||||
|
||||
# Clear existing attachments and reload from DB
|
||||
self._attachments = []
|
||||
|
||||
for attachment_response in attachments:
|
||||
attachment = PolicyAttachment(
|
||||
policy=attachment_response.policy_name,
|
||||
scope=attachment_response.scope,
|
||||
teams=attachment_response.teams if attachment_response.teams else None,
|
||||
keys=attachment_response.keys if attachment_response.keys else None,
|
||||
models=attachment_response.models if attachment_response.models else None,
|
||||
)
|
||||
self._attachments.append(attachment)
|
||||
|
||||
self._initialized = True
|
||||
verbose_proxy_logger.info(
|
||||
f"Synced {len(attachments)} attachments from DB to in-memory registry"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error syncing attachments from DB: {e}")
|
||||
raise Exception(f"Error syncing attachments from DB: {str(e)}")
|
||||
|
||||
|
||||
# Global singleton instance
|
||||
_attachment_registry: Optional[AttachmentRegistry] = None
|
||||
|
|
|
|||
578
litellm/proxy/policy_engine/policy_endpoints.py
Normal file
578
litellm/proxy/policy_engine/policy_endpoints.py
Normal file
|
|
@ -0,0 +1,578 @@
|
|||
"""
|
||||
CRUD ENDPOINTS FOR POLICIES
|
||||
|
||||
Provides REST API endpoints for managing policies and policy attachments.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry
|
||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||
from litellm.types.proxy.policy_engine import (
|
||||
PolicyAttachmentCreateRequest,
|
||||
PolicyAttachmentDBResponse,
|
||||
PolicyAttachmentListResponse,
|
||||
PolicyCreateRequest,
|
||||
PolicyDBResponse,
|
||||
PolicyListDBResponse,
|
||||
PolicyUpdateRequest,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Get singleton instances
|
||||
POLICY_REGISTRY = get_policy_registry()
|
||||
ATTACHMENT_REGISTRY = get_attachment_registry()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Policy CRUD Endpoints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/list",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyListDBResponse,
|
||||
)
|
||||
async def list_policies():
|
||||
"""
|
||||
List all policies from the database.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/list" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"policies": [
|
||||
{
|
||||
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "global-baseline",
|
||||
"inherit": null,
|
||||
"description": "Base guardrails for all requests",
|
||||
"guardrails_add": ["pii_masking"],
|
||||
"guardrails_remove": [],
|
||||
"condition": null,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"total_count": 1
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
policies = await POLICY_REGISTRY.get_all_policies_from_db(prisma_client)
|
||||
return PolicyListDBResponse(policies=policies, total_count=len(policies))
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error listing policies: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/policies",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyDBResponse,
|
||||
)
|
||||
async def create_policy(
|
||||
request: PolicyCreateRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Create a new policy.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/policies" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"policy_name": "global-baseline",
|
||||
"description": "Base guardrails for all requests",
|
||||
"guardrails_add": ["pii_masking", "prompt_injection"],
|
||||
"guardrails_remove": []
|
||||
}'
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "global-baseline",
|
||||
"inherit": null,
|
||||
"description": "Base guardrails for all requests",
|
||||
"guardrails_add": ["pii_masking", "prompt_injection"],
|
||||
"guardrails_remove": [],
|
||||
"condition": null,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
created_by = user_api_key_dict.user_id
|
||||
result = await POLICY_REGISTRY.add_policy_to_db(
|
||||
policy_request=request,
|
||||
prisma_client=prisma_client,
|
||||
created_by=created_by,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error creating policy: {e}")
|
||||
if "unique constraint" in str(e).lower():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Policy with name '{request.policy_name}' already exists",
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/{policy_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyDBResponse,
|
||||
)
|
||||
async def get_policy(policy_id: str):
|
||||
"""
|
||||
Get a policy by ID.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
result = await POLICY_REGISTRY.get_policy_by_id_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if result is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Policy with ID {policy_id} not found"
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting policy: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put(
|
||||
"/policies/{policy_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyDBResponse,
|
||||
)
|
||||
async def update_policy(
|
||||
policy_id: str,
|
||||
request: PolicyUpdateRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Update an existing policy.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X PUT "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"description": "Updated description",
|
||||
"guardrails_add": ["pii_masking", "toxicity_filter"]
|
||||
}'
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Check if policy exists
|
||||
existing = await POLICY_REGISTRY.get_policy_by_id_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if existing is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Policy with ID {policy_id} not found"
|
||||
)
|
||||
|
||||
updated_by = user_api_key_dict.user_id
|
||||
result = await POLICY_REGISTRY.update_policy_in_db(
|
||||
policy_id=policy_id,
|
||||
policy_request=request,
|
||||
prisma_client=prisma_client,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error updating policy: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/policies/{policy_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def delete_policy(policy_id: str):
|
||||
"""
|
||||
Delete a policy.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"message": "Policy 123e4567-e89b-12d3-a456-426614174000 deleted successfully"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Check if policy exists
|
||||
existing = await POLICY_REGISTRY.get_policy_by_id_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if existing is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Policy with ID {policy_id} not found"
|
||||
)
|
||||
|
||||
result = await POLICY_REGISTRY.delete_policy_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting policy: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/{policy_id}/resolved-guardrails",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_resolved_guardrails(policy_id: str):
|
||||
"""
|
||||
Get the resolved guardrails for a policy (including inherited guardrails).
|
||||
|
||||
This endpoint resolves the full inheritance chain and returns the final
|
||||
set of guardrails that would be applied for this policy.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000/resolved-guardrails" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "healthcare-compliance",
|
||||
"resolved_guardrails": ["pii_masking", "prompt_injection", "toxicity_filter"]
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Get the policy
|
||||
policy = await POLICY_REGISTRY.get_policy_by_id_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if policy is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Policy with ID {policy_id} not found"
|
||||
)
|
||||
|
||||
# Resolve guardrails
|
||||
resolved = await POLICY_REGISTRY.resolve_guardrails_from_db(
|
||||
policy_name=policy.policy_name,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
|
||||
return {
|
||||
"policy_id": policy.policy_id,
|
||||
"policy_name": policy.policy_name,
|
||||
"resolved_guardrails": resolved,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error resolving guardrails: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Policy Attachment CRUD Endpoints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/attachments/list",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyAttachmentListResponse,
|
||||
)
|
||||
async def list_policy_attachments():
|
||||
"""
|
||||
List all policy attachments from the database.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/attachments/list" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"attachment_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "global-baseline",
|
||||
"scope": "*",
|
||||
"teams": [],
|
||||
"keys": [],
|
||||
"models": [],
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"total_count": 1
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
attachments = await ATTACHMENT_REGISTRY.get_all_attachments_from_db(
|
||||
prisma_client
|
||||
)
|
||||
return PolicyAttachmentListResponse(
|
||||
attachments=attachments, total_count=len(attachments)
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error listing policy attachments: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/policies/attachments",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyAttachmentDBResponse,
|
||||
)
|
||||
async def create_policy_attachment(
|
||||
request: PolicyAttachmentCreateRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Create a new policy attachment.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/policies/attachments" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"policy_name": "global-baseline",
|
||||
"scope": "*"
|
||||
}'
|
||||
```
|
||||
|
||||
Example with team-specific attachment:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/policies/attachments" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"policy_name": "healthcare-compliance",
|
||||
"teams": ["healthcare-team", "medical-research"]
|
||||
}'
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"attachment_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "global-baseline",
|
||||
"scope": "*",
|
||||
"teams": [],
|
||||
"keys": [],
|
||||
"models": [],
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Verify the policy exists
|
||||
policy = await POLICY_REGISTRY.get_all_policies_from_db(prisma_client)
|
||||
policy_names = [p.policy_name for p in policy]
|
||||
if request.policy_name not in policy_names:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Policy '{request.policy_name}' not found. Create the policy first.",
|
||||
)
|
||||
|
||||
created_by = user_api_key_dict.user_id
|
||||
result = await ATTACHMENT_REGISTRY.add_attachment_to_db(
|
||||
attachment_request=request,
|
||||
prisma_client=prisma_client,
|
||||
created_by=created_by,
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error creating policy attachment: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/attachments/{attachment_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyAttachmentDBResponse,
|
||||
)
|
||||
async def get_policy_attachment(attachment_id: str):
|
||||
"""
|
||||
Get a policy attachment by ID.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/attachments/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
result = await ATTACHMENT_REGISTRY.get_attachment_by_id_from_db(
|
||||
attachment_id=attachment_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if result is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Attachment with ID {attachment_id} not found",
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting policy attachment: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/policies/attachments/{attachment_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def delete_policy_attachment(attachment_id: str):
|
||||
"""
|
||||
Delete a policy attachment.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:4000/policies/attachments/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"message": "Attachment 123e4567-e89b-12d3-a456-426614174000 deleted successfully"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Check if attachment exists
|
||||
existing = await ATTACHMENT_REGISTRY.get_attachment_by_id_from_db(
|
||||
attachment_id=attachment_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if existing is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Attachment with ID {attachment_id} not found",
|
||||
)
|
||||
|
||||
result = await ATTACHMENT_REGISTRY.delete_attachment_from_db(
|
||||
attachment_id=attachment_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting policy attachment: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -7,15 +7,22 @@ Policies define WHAT guardrails to apply. WHERE they apply is defined
|
|||
by policy_attachments (see AttachmentRegistry).
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.types.proxy.policy_engine import (
|
||||
Policy,
|
||||
PolicyCondition,
|
||||
PolicyCreateRequest,
|
||||
PolicyDBResponse,
|
||||
PolicyGuardrails,
|
||||
PolicyUpdateRequest,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
|
||||
|
||||
class PolicyRegistry:
|
||||
"""
|
||||
|
|
@ -178,6 +185,364 @@ class PolicyRegistry:
|
|||
return True
|
||||
return False
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Database CRUD Methods
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def add_policy_to_db(
|
||||
self,
|
||||
policy_request: PolicyCreateRequest,
|
||||
prisma_client: "PrismaClient",
|
||||
created_by: Optional[str] = None,
|
||||
) -> PolicyDBResponse:
|
||||
"""
|
||||
Add a policy to the database.
|
||||
|
||||
Args:
|
||||
policy_request: The policy creation request
|
||||
prisma_client: The Prisma client instance
|
||||
created_by: User who created the policy
|
||||
|
||||
Returns:
|
||||
PolicyDBResponse with the created policy
|
||||
"""
|
||||
try:
|
||||
# Build data dict, only include condition if it's set
|
||||
data: Dict[str, Any] = {
|
||||
"policy_name": policy_request.policy_name,
|
||||
"guardrails_add": policy_request.guardrails_add or [],
|
||||
"guardrails_remove": policy_request.guardrails_remove or [],
|
||||
"created_at": datetime.now(timezone.utc),
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
}
|
||||
|
||||
# Only add optional fields if they have values
|
||||
if policy_request.inherit is not None:
|
||||
data["inherit"] = policy_request.inherit
|
||||
if policy_request.description is not None:
|
||||
data["description"] = policy_request.description
|
||||
if created_by is not None:
|
||||
data["created_by"] = created_by
|
||||
data["updated_by"] = created_by
|
||||
if policy_request.condition is not None:
|
||||
data["condition"] = policy_request.condition.model_dump()
|
||||
|
||||
created_policy = await prisma_client.db.litellm_policytable.create(
|
||||
data=data
|
||||
)
|
||||
|
||||
# Also add to in-memory registry
|
||||
policy = self._parse_policy(
|
||||
policy_request.policy_name,
|
||||
{
|
||||
"inherit": policy_request.inherit,
|
||||
"description": policy_request.description,
|
||||
"guardrails": {
|
||||
"add": policy_request.guardrails_add,
|
||||
"remove": policy_request.guardrails_remove,
|
||||
},
|
||||
"condition": policy_request.condition.model_dump()
|
||||
if policy_request.condition
|
||||
else None,
|
||||
},
|
||||
)
|
||||
self.add_policy(policy_request.policy_name, policy)
|
||||
|
||||
return PolicyDBResponse(
|
||||
policy_id=created_policy.policy_id,
|
||||
policy_name=created_policy.policy_name,
|
||||
inherit=created_policy.inherit,
|
||||
description=created_policy.description,
|
||||
guardrails_add=created_policy.guardrails_add or [],
|
||||
guardrails_remove=created_policy.guardrails_remove or [],
|
||||
condition=created_policy.condition,
|
||||
created_at=created_policy.created_at,
|
||||
updated_at=created_policy.updated_at,
|
||||
created_by=created_policy.created_by,
|
||||
updated_by=created_policy.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error adding policy to DB: {e}")
|
||||
raise Exception(f"Error adding policy to DB: {str(e)}")
|
||||
|
||||
async def update_policy_in_db(
|
||||
self,
|
||||
policy_id: str,
|
||||
policy_request: PolicyUpdateRequest,
|
||||
prisma_client: "PrismaClient",
|
||||
updated_by: Optional[str] = None,
|
||||
) -> PolicyDBResponse:
|
||||
"""
|
||||
Update a policy in the database.
|
||||
|
||||
Args:
|
||||
policy_id: The ID of the policy to update
|
||||
policy_request: The policy update request
|
||||
prisma_client: The Prisma client instance
|
||||
updated_by: User who updated the policy
|
||||
|
||||
Returns:
|
||||
PolicyDBResponse with the updated policy
|
||||
"""
|
||||
try:
|
||||
# Build update data - only include fields that are set
|
||||
update_data: Dict[str, Any] = {
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"updated_by": updated_by,
|
||||
}
|
||||
|
||||
if policy_request.policy_name is not None:
|
||||
update_data["policy_name"] = policy_request.policy_name
|
||||
if policy_request.inherit is not None:
|
||||
update_data["inherit"] = policy_request.inherit
|
||||
if policy_request.description is not None:
|
||||
update_data["description"] = policy_request.description
|
||||
if policy_request.guardrails_add is not None:
|
||||
update_data["guardrails_add"] = policy_request.guardrails_add
|
||||
if policy_request.guardrails_remove is not None:
|
||||
update_data["guardrails_remove"] = policy_request.guardrails_remove
|
||||
if policy_request.condition is not None:
|
||||
update_data["condition"] = policy_request.condition.model_dump()
|
||||
|
||||
updated_policy = await prisma_client.db.litellm_policytable.update(
|
||||
where={"policy_id": policy_id},
|
||||
data=update_data,
|
||||
)
|
||||
|
||||
# Update in-memory registry
|
||||
policy = self._parse_policy(
|
||||
updated_policy.policy_name,
|
||||
{
|
||||
"inherit": updated_policy.inherit,
|
||||
"description": updated_policy.description,
|
||||
"guardrails": {
|
||||
"add": updated_policy.guardrails_add,
|
||||
"remove": updated_policy.guardrails_remove,
|
||||
},
|
||||
"condition": updated_policy.condition,
|
||||
},
|
||||
)
|
||||
self.add_policy(updated_policy.policy_name, policy)
|
||||
|
||||
return PolicyDBResponse(
|
||||
policy_id=updated_policy.policy_id,
|
||||
policy_name=updated_policy.policy_name,
|
||||
inherit=updated_policy.inherit,
|
||||
description=updated_policy.description,
|
||||
guardrails_add=updated_policy.guardrails_add or [],
|
||||
guardrails_remove=updated_policy.guardrails_remove or [],
|
||||
condition=updated_policy.condition,
|
||||
created_at=updated_policy.created_at,
|
||||
updated_at=updated_policy.updated_at,
|
||||
created_by=updated_policy.created_by,
|
||||
updated_by=updated_policy.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error updating policy in DB: {e}")
|
||||
raise Exception(f"Error updating policy in DB: {str(e)}")
|
||||
|
||||
async def delete_policy_from_db(
|
||||
self,
|
||||
policy_id: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Delete a policy from the database.
|
||||
|
||||
Args:
|
||||
policy_id: The ID of the policy to delete
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
Dict with success message
|
||||
"""
|
||||
try:
|
||||
# Get policy name before deleting
|
||||
policy = await prisma_client.db.litellm_policytable.find_unique(
|
||||
where={"policy_id": policy_id}
|
||||
)
|
||||
|
||||
if policy is None:
|
||||
raise Exception(f"Policy with ID {policy_id} not found")
|
||||
|
||||
# Delete from DB
|
||||
await prisma_client.db.litellm_policytable.delete(
|
||||
where={"policy_id": policy_id}
|
||||
)
|
||||
|
||||
# Remove from in-memory registry
|
||||
self.remove_policy(policy.policy_name)
|
||||
|
||||
return {"message": f"Policy {policy_id} deleted successfully"}
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting policy from DB: {e}")
|
||||
raise Exception(f"Error deleting policy from DB: {str(e)}")
|
||||
|
||||
async def get_policy_by_id_from_db(
|
||||
self,
|
||||
policy_id: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> Optional[PolicyDBResponse]:
|
||||
"""
|
||||
Get a policy by ID from the database.
|
||||
|
||||
Args:
|
||||
policy_id: The ID of the policy to retrieve
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
PolicyDBResponse if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
policy = await prisma_client.db.litellm_policytable.find_unique(
|
||||
where={"policy_id": policy_id}
|
||||
)
|
||||
|
||||
if policy is None:
|
||||
return None
|
||||
|
||||
return PolicyDBResponse(
|
||||
policy_id=policy.policy_id,
|
||||
policy_name=policy.policy_name,
|
||||
inherit=policy.inherit,
|
||||
description=policy.description,
|
||||
guardrails_add=policy.guardrails_add or [],
|
||||
guardrails_remove=policy.guardrails_remove or [],
|
||||
condition=policy.condition,
|
||||
created_at=policy.created_at,
|
||||
updated_at=policy.updated_at,
|
||||
created_by=policy.created_by,
|
||||
updated_by=policy.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting policy from DB: {e}")
|
||||
raise Exception(f"Error getting policy from DB: {str(e)}")
|
||||
|
||||
async def get_all_policies_from_db(
|
||||
self,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> List[PolicyDBResponse]:
|
||||
"""
|
||||
Get all policies from the database.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
List of PolicyDBResponse objects
|
||||
"""
|
||||
try:
|
||||
policies = await prisma_client.db.litellm_policytable.find_many(
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
|
||||
return [
|
||||
PolicyDBResponse(
|
||||
policy_id=p.policy_id,
|
||||
policy_name=p.policy_name,
|
||||
inherit=p.inherit,
|
||||
description=p.description,
|
||||
guardrails_add=p.guardrails_add or [],
|
||||
guardrails_remove=p.guardrails_remove or [],
|
||||
condition=p.condition,
|
||||
created_at=p.created_at,
|
||||
updated_at=p.updated_at,
|
||||
created_by=p.created_by,
|
||||
updated_by=p.updated_by,
|
||||
)
|
||||
for p in policies
|
||||
]
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting policies from DB: {e}")
|
||||
raise Exception(f"Error getting policies from DB: {str(e)}")
|
||||
|
||||
async def sync_policies_from_db(
|
||||
self,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> None:
|
||||
"""
|
||||
Sync policies from the database to in-memory registry.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
"""
|
||||
try:
|
||||
policies = await self.get_all_policies_from_db(prisma_client)
|
||||
|
||||
for policy_response in policies:
|
||||
policy = self._parse_policy(
|
||||
policy_response.policy_name,
|
||||
{
|
||||
"inherit": policy_response.inherit,
|
||||
"description": policy_response.description,
|
||||
"guardrails": {
|
||||
"add": policy_response.guardrails_add,
|
||||
"remove": policy_response.guardrails_remove,
|
||||
},
|
||||
"condition": policy_response.condition,
|
||||
},
|
||||
)
|
||||
self.add_policy(policy_response.policy_name, policy)
|
||||
|
||||
verbose_proxy_logger.info(
|
||||
f"Synced {len(policies)} policies from DB to in-memory registry"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error syncing policies from DB: {e}")
|
||||
raise Exception(f"Error syncing policies from DB: {str(e)}")
|
||||
|
||||
async def resolve_guardrails_from_db(
|
||||
self,
|
||||
policy_name: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> List[str]:
|
||||
"""
|
||||
Resolve all guardrails for a policy from the database.
|
||||
|
||||
Uses the existing PolicyResolver to handle inheritance chain resolution.
|
||||
|
||||
Args:
|
||||
policy_name: Name of the policy to resolve
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
List of resolved guardrail names
|
||||
"""
|
||||
from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
|
||||
|
||||
try:
|
||||
# Load all policies from DB to ensure we have the full inheritance chain
|
||||
policies = await self.get_all_policies_from_db(prisma_client)
|
||||
|
||||
# Build a temporary in-memory map for resolution
|
||||
temp_policies = {}
|
||||
for policy_response in policies:
|
||||
policy = self._parse_policy(
|
||||
policy_response.policy_name,
|
||||
{
|
||||
"inherit": policy_response.inherit,
|
||||
"description": policy_response.description,
|
||||
"guardrails": {
|
||||
"add": policy_response.guardrails_add,
|
||||
"remove": policy_response.guardrails_remove,
|
||||
},
|
||||
"condition": policy_response.condition,
|
||||
},
|
||||
)
|
||||
temp_policies[policy_response.policy_name] = policy
|
||||
|
||||
# Use the existing PolicyResolver to resolve guardrails
|
||||
resolved_policy = PolicyResolver.resolve_policy_guardrails(
|
||||
policy_name=policy_name,
|
||||
policies=temp_policies,
|
||||
context=None, # No context needed for simple resolution
|
||||
)
|
||||
|
||||
return sorted(resolved_policy.guardrails)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error resolving guardrails from DB: {e}")
|
||||
raise Exception(f"Error resolving guardrails from DB: {str(e)}")
|
||||
|
||||
|
||||
# Global singleton instance
|
||||
_policy_registry: Optional[PolicyRegistry] = None
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
|||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
router as pass_through_router,
|
||||
)
|
||||
from litellm.proxy.policy_engine.policy_endpoints import router as policy_crud_router
|
||||
from litellm.proxy.prompts.prompt_endpoints import router as prompts_router
|
||||
from litellm.proxy.public_endpoints import router as public_endpoints_router
|
||||
from litellm.proxy.rag_endpoints.endpoints import router as rag_router
|
||||
|
|
@ -3801,6 +3802,9 @@ class ProxyConfig:
|
|||
if self._should_load_db_object(object_type="guardrails"):
|
||||
await self._init_guardrails_in_db(prisma_client=prisma_client)
|
||||
|
||||
if self._should_load_db_object(object_type="policies"):
|
||||
await self._init_policies_in_db(prisma_client=prisma_client)
|
||||
|
||||
if self._should_load_db_object(object_type="vector_stores"):
|
||||
await self._init_vector_stores_in_db(prisma_client=prisma_client)
|
||||
|
||||
|
|
@ -4024,6 +4028,36 @@ class ProxyConfig:
|
|||
)
|
||||
)
|
||||
|
||||
async def _init_policies_in_db(self, prisma_client: PrismaClient):
|
||||
"""
|
||||
Initialize policies and policy attachments from database into the in-memory registries.
|
||||
"""
|
||||
from litellm.proxy.policy_engine.attachment_registry import (
|
||||
get_attachment_registry,
|
||||
)
|
||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||
|
||||
try:
|
||||
# Get the global singleton instances
|
||||
policy_registry = get_policy_registry()
|
||||
attachment_registry = get_attachment_registry()
|
||||
|
||||
# Sync policies from DB to in-memory registry
|
||||
await policy_registry.sync_policies_from_db(prisma_client=prisma_client)
|
||||
|
||||
# Sync attachments from DB to in-memory registry
|
||||
await attachment_registry.sync_attachments_from_db(prisma_client=prisma_client)
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
"Successfully synced policies and attachments from DB"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(
|
||||
"litellm.proxy.proxy_server.py::ProxyConfig:_init_policies_in_db - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
|
||||
async def _init_vector_stores_in_db(self, prisma_client: PrismaClient):
|
||||
from litellm.vector_stores.vector_store_registry import VectorStoreRegistry
|
||||
|
||||
|
|
@ -10702,6 +10736,7 @@ app.include_router(caching_router)
|
|||
app.include_router(analytics_router)
|
||||
app.include_router(guardrails_router)
|
||||
app.include_router(policy_router)
|
||||
app.include_router(policy_crud_router)
|
||||
app.include_router(search_tool_management_router)
|
||||
app.include_router(prompts_router)
|
||||
app.include_router(callback_management_endpoints_router)
|
||||
|
|
|
|||
|
|
@ -124,8 +124,9 @@ model LiteLLM_TeamTable {
|
|||
updated_at DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
team_member_permissions String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases
|
||||
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
||||
litellm_model_table LiteLLM_ModelTable? @relation(fields: [model_id], references: [id])
|
||||
|
|
@ -156,6 +157,7 @@ model LiteLLM_DeletedTeamTable {
|
|||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
team_member_permissions String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_id Int? // id for LiteLLM_ModelTable -> stores team-level model aliases
|
||||
|
||||
// Original timestamps from team creation/updates
|
||||
|
|
@ -197,6 +199,7 @@ model LiteLLM_UserTable {
|
|||
budget_duration String?
|
||||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
created_at DateTime? @default(now()) @map("created_at")
|
||||
|
|
@ -283,6 +286,7 @@ model LiteLLM_VerificationToken {
|
|||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
allowed_routes String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
budget_id String?
|
||||
|
|
@ -327,6 +331,7 @@ model LiteLLM_DeletedVerificationToken {
|
|||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
allowed_routes String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
|
|
@ -864,19 +869,31 @@ model LiteLLM_SkillsTable {
|
|||
updated_by String?
|
||||
}
|
||||
|
||||
// Claude Code Marketplace - stores plugins for Claude Code integration
|
||||
model LiteLLM_ClaudeCodePluginTable {
|
||||
id String @id @default(uuid())
|
||||
name String @unique // Plugin name (kebab-case)
|
||||
version String? // Semantic version
|
||||
description String? // Plugin description
|
||||
manifest_json String // Full plugin.json as JSON string
|
||||
files_json String // All files as JSON: {"path": "content"}
|
||||
enabled Boolean @default(true)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
created_by String?
|
||||
|
||||
@@index([name])
|
||||
@@map("litellm_claudecodeplugin")
|
||||
// Policy table for storing guardrail policies
|
||||
model LiteLLM_PolicyTable {
|
||||
policy_id String @id @default(uuid())
|
||||
policy_name String @unique
|
||||
inherit String? // Name of parent policy to inherit from
|
||||
description String?
|
||||
guardrails_add String[] @default([])
|
||||
guardrails_remove String[] @default([])
|
||||
condition Json? @default("{}") // Policy conditions (e.g., model matching)
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
||||
// Policy attachment table for defining where policies apply
|
||||
model LiteLLM_PolicyAttachmentTable {
|
||||
attachment_id String @id @default(uuid())
|
||||
policy_name String // Name of the policy to attach
|
||||
scope String? // Use '*' for global scope
|
||||
teams String[] @default([]) // Team aliases or patterns
|
||||
keys String[] @default([]) // Key aliases or patterns
|
||||
models String[] @default([]) // Model names or patterns
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -563,7 +563,7 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
|
||||
return create_response
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
except httpx.TimeoutException:
|
||||
verbose_logger.exception("Timeout error occurred during secret rotation")
|
||||
return {"status": "error", "message": "Timeout error occurred"}
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -19,13 +19,21 @@ from litellm.types.proxy.policy_engine.policy_types import (
|
|||
PolicyScope,
|
||||
)
|
||||
from litellm.types.proxy.policy_engine.resolver_types import (
|
||||
PolicyAttachmentCreateRequest,
|
||||
PolicyAttachmentDBResponse,
|
||||
PolicyAttachmentListResponse,
|
||||
PolicyConditionRequest,
|
||||
PolicyCreateRequest,
|
||||
PolicyDBResponse,
|
||||
PolicyGuardrailsResponse,
|
||||
PolicyInfoResponse,
|
||||
PolicyListDBResponse,
|
||||
PolicyListResponse,
|
||||
PolicyMatchContext,
|
||||
PolicyScopeResponse,
|
||||
PolicySummaryItem,
|
||||
PolicyTestResponse,
|
||||
PolicyUpdateRequest,
|
||||
ResolvedPolicy,
|
||||
)
|
||||
from litellm.types.proxy.policy_engine.validation_types import (
|
||||
|
|
@ -58,4 +66,13 @@ __all__ = [
|
|||
"PolicyScopeResponse",
|
||||
"PolicySummaryItem",
|
||||
"PolicyTestResponse",
|
||||
# CRUD Request/Response types
|
||||
"PolicyConditionRequest",
|
||||
"PolicyCreateRequest",
|
||||
"PolicyUpdateRequest",
|
||||
"PolicyDBResponse",
|
||||
"PolicyListDBResponse",
|
||||
"PolicyAttachmentCreateRequest",
|
||||
"PolicyAttachmentDBResponse",
|
||||
"PolicyAttachmentListResponse",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ These types are used for matching requests to policies and resolving
|
|||
the final guardrails list.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
|
@ -108,3 +109,168 @@ class PolicyTestResponse(BaseModel):
|
|||
matching_policies: List[str]
|
||||
resolved_guardrails: List[str]
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CRUD Request/Response Types for Policy Endpoints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class PolicyConditionRequest(BaseModel):
|
||||
"""Condition for when a policy applies."""
|
||||
|
||||
model: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Model name pattern (exact match or regex) for when policy applies.",
|
||||
)
|
||||
|
||||
|
||||
class PolicyCreateRequest(BaseModel):
|
||||
"""Request body for creating a new policy."""
|
||||
|
||||
policy_name: str = Field(description="Unique name for the policy.")
|
||||
inherit: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Name of parent policy to inherit from.",
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Human-readable description of the policy.",
|
||||
)
|
||||
guardrails_add: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of guardrail names to add.",
|
||||
)
|
||||
guardrails_remove: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of guardrail names to remove (from inherited).",
|
||||
)
|
||||
condition: Optional[PolicyConditionRequest] = Field(
|
||||
default=None,
|
||||
description="Condition for when this policy applies.",
|
||||
)
|
||||
|
||||
|
||||
class PolicyUpdateRequest(BaseModel):
|
||||
"""Request body for updating a policy."""
|
||||
|
||||
policy_name: Optional[str] = Field(
|
||||
default=None,
|
||||
description="New name for the policy.",
|
||||
)
|
||||
inherit: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Name of parent policy to inherit from.",
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Human-readable description of the policy.",
|
||||
)
|
||||
guardrails_add: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of guardrail names to add.",
|
||||
)
|
||||
guardrails_remove: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of guardrail names to remove (from inherited).",
|
||||
)
|
||||
condition: Optional[PolicyConditionRequest] = Field(
|
||||
default=None,
|
||||
description="Condition for when this policy applies.",
|
||||
)
|
||||
|
||||
|
||||
class PolicyDBResponse(BaseModel):
|
||||
"""Response for a policy from the database."""
|
||||
|
||||
policy_id: str = Field(description="Unique ID of the policy.")
|
||||
policy_name: str = Field(description="Name of the policy.")
|
||||
inherit: Optional[str] = Field(default=None, description="Parent policy name.")
|
||||
description: Optional[str] = Field(default=None, description="Policy description.")
|
||||
guardrails_add: List[str] = Field(
|
||||
default_factory=list, description="Guardrails to add."
|
||||
)
|
||||
guardrails_remove: List[str] = Field(
|
||||
default_factory=list, description="Guardrails to remove."
|
||||
)
|
||||
condition: Optional[Dict[str, Any]] = Field(
|
||||
default=None, description="Policy condition."
|
||||
)
|
||||
created_at: Optional[datetime] = Field(
|
||||
default=None, description="When the policy was created."
|
||||
)
|
||||
updated_at: Optional[datetime] = Field(
|
||||
default=None, description="When the policy was last updated."
|
||||
)
|
||||
created_by: Optional[str] = Field(default=None, description="Who created the policy.")
|
||||
updated_by: Optional[str] = Field(
|
||||
default=None, description="Who last updated the policy."
|
||||
)
|
||||
|
||||
|
||||
class PolicyListDBResponse(BaseModel):
|
||||
"""Response for listing policies from the database."""
|
||||
|
||||
policies: List[PolicyDBResponse] = Field(
|
||||
default_factory=list, description="List of policies."
|
||||
)
|
||||
total_count: int = Field(default=0, description="Total number of policies.")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Policy Attachment CRUD Types
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class PolicyAttachmentCreateRequest(BaseModel):
|
||||
"""Request body for creating a policy attachment."""
|
||||
|
||||
policy_name: str = Field(description="Name of the policy to attach.")
|
||||
scope: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Use '*' for global scope (applies to all requests).",
|
||||
)
|
||||
teams: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="Team aliases or patterns this attachment applies to.",
|
||||
)
|
||||
keys: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="Key aliases or patterns this attachment applies to.",
|
||||
)
|
||||
models: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="Model names or patterns this attachment applies to.",
|
||||
)
|
||||
|
||||
|
||||
class PolicyAttachmentDBResponse(BaseModel):
|
||||
"""Response for a policy attachment from the database."""
|
||||
|
||||
attachment_id: str = Field(description="Unique ID of the attachment.")
|
||||
policy_name: str = Field(description="Name of the attached policy.")
|
||||
scope: Optional[str] = Field(default=None, description="Scope of the attachment.")
|
||||
teams: List[str] = Field(default_factory=list, description="Team patterns.")
|
||||
keys: List[str] = Field(default_factory=list, description="Key patterns.")
|
||||
models: List[str] = Field(default_factory=list, description="Model patterns.")
|
||||
created_at: Optional[datetime] = Field(
|
||||
default=None, description="When the attachment was created."
|
||||
)
|
||||
updated_at: Optional[datetime] = Field(
|
||||
default=None, description="When the attachment was last updated."
|
||||
)
|
||||
created_by: Optional[str] = Field(
|
||||
default=None, description="Who created the attachment."
|
||||
)
|
||||
updated_by: Optional[str] = Field(
|
||||
default=None, description="Who last updated the attachment."
|
||||
)
|
||||
|
||||
|
||||
class PolicyAttachmentListResponse(BaseModel):
|
||||
"""Response for listing policy attachments."""
|
||||
|
||||
attachments: List[PolicyAttachmentDBResponse] = Field(
|
||||
default_factory=list, description="List of policy attachments."
|
||||
)
|
||||
total_count: int = Field(default=0, description="Total number of attachments.")
|
||||
|
|
|
|||
599
poetry.lock
generated
599
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "a2a-sdk"
|
||||
|
|
@ -275,7 +275,7 @@ files = [
|
|||
{file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"},
|
||||
{file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"},
|
||||
]
|
||||
markers = {main = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\""}
|
||||
markers = {main = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\""}
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
|
|
@ -343,7 +343,7 @@ zookeeper = ["kazoo"]
|
|||
name = "async-timeout"
|
||||
version = "5.0.1"
|
||||
description = "Timeout context manager for asyncio programs"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_full_version < \"3.11.3\" and (extra == \"extra-proxy\" or extra == \"proxy\" or python_version < \"3.11\")"
|
||||
|
|
@ -557,48 +557,48 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.36.0"
|
||||
version = "1.40.76"
|
||||
description = "The AWS SDK for Python"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"proxy\""
|
||||
files = [
|
||||
{file = "boto3-1.36.0-py3-none-any.whl", hash = "sha256:d0ca7a58ce25701a52232cc8df9d87854824f1f2964b929305722ebc7959d5a9"},
|
||||
{file = "boto3-1.36.0.tar.gz", hash = "sha256:159898f51c2997a12541c0e02d6e5a8fe2993ddb307b9478fd9a339f98b57e00"},
|
||||
{file = "boto3-1.40.76-py3-none-any.whl", hash = "sha256:8df6df755727be40ad9e309cfda07f9a12c147e17b639430c55d4e4feee8a167"},
|
||||
{file = "boto3-1.40.76.tar.gz", hash = "sha256:16f4cf97f8dd8e0aae015f4dc66219bd7716a91a40d1e2daa0dafa241a4761c5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.36.0,<1.37.0"
|
||||
botocore = ">=1.40.76,<1.41.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.11.0,<0.12.0"
|
||||
s3transfer = ">=0.14.0,<0.15.0"
|
||||
|
||||
[package.extras]
|
||||
crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.36.26"
|
||||
version = "1.40.76"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"proxy\""
|
||||
files = [
|
||||
{file = "botocore-1.36.26-py3-none-any.whl", hash = "sha256:4e3f19913887a58502e71ef8d696fe7eaa54de7813ff73390cd5883f837dfa6e"},
|
||||
{file = "botocore-1.36.26.tar.gz", hash = "sha256:4a63bcef7ecf6146fd3a61dc4f9b33b7473b49bdaf1770e9aaca6eee0c9eab62"},
|
||||
{file = "botocore-1.40.76-py3-none-any.whl", hash = "sha256:fe425d386e48ac64c81cbb4a7181688d813df2e2b4c78b95ebe833c9e868c6f4"},
|
||||
{file = "botocore-1.40.76.tar.gz", hash = "sha256:2b16024d68b29b973005adfb5039adfe9099ebe772d40a90ca89f2e165c495dc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
python-dateutil = ">=2.1,<3.0.0"
|
||||
urllib3 = [
|
||||
{version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""},
|
||||
{version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""},
|
||||
{version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
crt = ["awscrt (==0.23.8)"]
|
||||
crt = ["awscrt (==0.28.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
|
|
@ -607,7 +607,7 @@ description = "Extensible memoizing collections and decorators"
|
|||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\""
|
||||
markers = "(extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") and python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\""
|
||||
files = [
|
||||
{file = "cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace"},
|
||||
{file = "cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6"},
|
||||
|
|
@ -1393,6 +1393,24 @@ docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"]
|
|||
ssh = ["paramiko (>=2.4.3)"]
|
||||
websockets = ["websocket-client (>=1.3.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.17.0"
|
||||
description = "Parse Python docstrings in reST, Google and Numpydoc format"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\""
|
||||
files = [
|
||||
{file = "docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708"},
|
||||
{file = "docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit (>=2.16.0) ; python_version >= \"3.9\"", "pydoctor (>=25.4.0)", "pytest"]
|
||||
docs = ["pydoctor (>=25.4.0)"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.21.2"
|
||||
|
|
@ -1453,7 +1471,7 @@ files = [
|
|||
{file = "fastapi-0.121.3-py3-none-any.whl", hash = "sha256:0c78fc87587fcd910ca1bbf5bc8ba37b80e119b388a7206b39f0ecc95ebf53e9"},
|
||||
{file = "fastapi-0.121.3.tar.gz", hash = "sha256:0055bc24fe53e56a40e9e0ad1ae2baa81622c406e548e501e717634e2dfbc40b"},
|
||||
]
|
||||
markers = {main = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\""}
|
||||
markers = {main = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\""}
|
||||
|
||||
[package.dependencies]
|
||||
annotated-doc = ">=0.0.2"
|
||||
|
|
@ -1982,7 +2000,7 @@ description = "Google API client core library"
|
|||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.14\" and extra == \"extra-proxy\""
|
||||
markers = "python_version >= \"3.14\" and (extra == \"extra-proxy\" or extra == \"google\")"
|
||||
files = [
|
||||
{file = "google_api_core-2.25.2-py3-none-any.whl", hash = "sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7"},
|
||||
{file = "google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300"},
|
||||
|
|
@ -2010,7 +2028,7 @@ description = "Google API client core library"
|
|||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"extra-proxy\" and python_version < \"3.14\""
|
||||
markers = "python_version == \"3.9\" and (extra == \"google\" or extra == \"extra-proxy\") or python_version < \"3.14\" and (extra == \"extra-proxy\" or extra == \"google\")"
|
||||
files = [
|
||||
{file = "google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c"},
|
||||
{file = "google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8"},
|
||||
|
|
@ -2020,12 +2038,12 @@ files = [
|
|||
google-auth = ">=2.14.1,<3.0.0"
|
||||
googleapis-common-protos = ">=1.56.2,<2.0.0"
|
||||
grpcio = [
|
||||
{version = ">=1.33.2,<2.0.0", optional = true, markers = "extra == \"grpc\""},
|
||||
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
{version = ">=1.33.2,<2.0.0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""},
|
||||
]
|
||||
grpcio-status = [
|
||||
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
{version = ">=1.33.2,<2.0.0", optional = true, markers = "extra == \"grpc\""},
|
||||
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
]
|
||||
proto-plus = [
|
||||
{version = ">=1.22.3,<2.0.0"},
|
||||
|
|
@ -2047,7 +2065,7 @@ description = "Google Authentication Library"
|
|||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\""
|
||||
markers = "(extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") and python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\""
|
||||
files = [
|
||||
{file = "google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16"},
|
||||
{file = "google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483"},
|
||||
|
|
@ -2056,6 +2074,7 @@ files = [
|
|||
[package.dependencies]
|
||||
cachetools = ">=2.0.0,<7.0"
|
||||
pyasn1-modules = ">=0.2.1"
|
||||
requests = {version = ">=2.20.0,<3.0.0", optional = true, markers = "extra == \"requests\""}
|
||||
rsa = ">=3.1.4,<5"
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -2068,6 +2087,120 @@ requests = ["requests (>=2.20.0,<3.0.0)"]
|
|||
testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"]
|
||||
urllib3 = ["packaging", "urllib3"]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-aiplatform"
|
||||
version = "1.130.0"
|
||||
description = "Vertex AI API client library"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\""
|
||||
files = [
|
||||
{file = "google_cloud_aiplatform-1.130.0-py2.py3-none-any.whl", hash = "sha256:f578ccee55655dd9e2300cfcafb178e47c3dfdcf746ad465234b875d3e955929"},
|
||||
{file = "google_cloud_aiplatform-1.130.0.tar.gz", hash = "sha256:f66aeb23f0a6848fc2d5bbdf1b5777c3cf8e06056f73ef815317abf89d5a0262"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
docstring_parser = "<1"
|
||||
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.8.dev0,<3.0.0", extras = ["grpc"]}
|
||||
google-auth = ">=2.14.1,<3.0.0"
|
||||
google-cloud-bigquery = ">=1.15.0,<3.20.0 || >3.20.0,<4.0.0"
|
||||
google-cloud-resource-manager = ">=1.3.3,<3.0.0"
|
||||
google-cloud-storage = [
|
||||
{version = ">=1.32.0,<4.0.0", markers = "python_version < \"3.13\""},
|
||||
{version = ">=2.10.0,<4.0.0", markers = "python_version >= \"3.13\""},
|
||||
]
|
||||
google-genai = ">=1.37.0,<2.0.0"
|
||||
packaging = ">=14.3"
|
||||
proto-plus = ">=1.22.3,<2.0.0"
|
||||
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
pydantic = "<3"
|
||||
shapely = "<3.0.0"
|
||||
typing_extensions = "*"
|
||||
|
||||
[package.extras]
|
||||
adk = ["google-adk (>=1.0.0,<2.0.0)", "opentelemetry-instrumentation-google-genai (>=0.3b0,<1.0.0)"]
|
||||
ag2 = ["ag2[gemini]", "openinference-instrumentation-autogen (>=0.1.6,<0.2)"]
|
||||
ag2-testing = ["absl-py", "ag2[gemini]", "cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "openinference-instrumentation-autogen (>=0.1.6,<0.2)", "opentelemetry-exporter-gcp-logging (>=1.11.0a0,<2.0.0)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-exporter-otlp-proto-http (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.11.1,<3)", "pytest-xdist", "typing_extensions"]
|
||||
agent-engines = ["cloudpickle (>=3.0,<4.0)", "google-cloud-logging (<4)", "google-cloud-trace (<2)", "opentelemetry-exporter-gcp-logging (>=1.11.0a0,<2.0.0)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-exporter-otlp-proto-http (<2)", "opentelemetry-sdk (<2)", "packaging (>=24.0)", "pydantic (>=2.11.1,<3)", "typing_extensions"]
|
||||
autologging = ["mlflow (>=1.27.0) ; python_version >= \"3.13\"", "mlflow (>=1.27.0,<=2.16.0) ; python_version < \"3.13\""]
|
||||
cloud-profiler = ["tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "werkzeug (>=2.0.0,<4.0.0)"]
|
||||
datasets = ["pyarrow (>=10.0.1) ; python_version == \"3.11\"", "pyarrow (>=14.0.0) ; python_version >= \"3.12\"", "pyarrow (>=3.0.0,<8.0.0) ; python_version < \"3.11\""]
|
||||
endpoint = ["requests (>=2.28.1)", "requests-toolbelt (<=1.0.0)"]
|
||||
evaluation = ["jsonschema", "litellm (>=1.72.4,!=1.77.2,!=1.77.3,!=1.77.4)", "pandas (>=1.0.0)", "pyyaml", "ruamel.yaml", "scikit-learn (<1.6.0) ; python_version <= \"3.10\"", "scikit-learn ; python_version > \"3.10\"", "tqdm (>=4.23.0)"]
|
||||
full = ["docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0) ; python_version < \"3.13\"", "fastapi (>=0.71.0,<=0.114.0)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-vizier (>=0.1.6)", "httpx (>=0.23.0,<=0.28.1)", "immutabledict", "jsonschema", "lit-nlp (==0.4.0) ; python_version < \"3.14\"", "litellm (>=1.72.4,!=1.77.2,!=1.77.3,!=1.77.4)", "mlflow (>=1.27.0) ; python_version >= \"3.13\"", "mlflow (>=1.27.0,<=2.16.0) ; python_version < \"3.13\"", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pyarrow (>=10.0.1) ; python_version == \"3.11\"", "pyarrow (>=14.0.0) ; python_version >= \"3.12\"", "pyarrow (>=3.0.0,<8.0.0) ; python_version < \"3.11\"", "pyarrow (>=6.0.1)", "pyyaml", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || ==2.33.* || >=2.42.dev0,<=2.42.0) ; python_version < \"3.11\"", "ray[default] (>=2.5,<=2.47.1) ; python_version == \"3.11\"", "requests (>=2.28.1)", "requests-toolbelt (<=1.0.0)", "ruamel.yaml", "scikit-learn (<1.6.0) ; python_version <= \"3.10\"", "scikit-learn ; python_version > \"3.10\"", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (>=2.3.0,<3.0.0) ; python_version < \"3.13\"", "tensorflow (>=2.3.0,<3.0.0) ; python_version < \"3.13\"", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<4.0.0)"]
|
||||
langchain = ["langchain (>=0.3,<0.4)", "langchain-core (>=0.3,<0.4)", "langchain-google-vertexai (>=2.0.22,<3)", "langgraph (>=0.2.45,<0.4)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)"]
|
||||
langchain-testing = ["absl-py", "cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "langchain (>=0.3,<0.4)", "langchain-core (>=0.3,<0.4)", "langchain-google-vertexai (>=2.0.22,<3)", "langgraph (>=0.2.45,<0.4)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)", "opentelemetry-exporter-gcp-logging (>=1.11.0a0,<2.0.0)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-exporter-otlp-proto-http (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.11.1,<3)", "pytest-xdist", "typing_extensions"]
|
||||
lit = ["explainable-ai-sdk (>=1.0.0) ; python_version < \"3.13\"", "lit-nlp (==0.4.0) ; python_version < \"3.14\"", "pandas (>=1.0.0)", "tensorflow (>=2.3.0,<3.0.0) ; python_version < \"3.13\""]
|
||||
llama-index = ["llama-index", "llama-index-llms-google-genai", "openinference-instrumentation-llama-index (>=3.0,<4.0)"]
|
||||
llama-index-testing = ["absl-py", "cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "llama-index", "llama-index-llms-google-genai", "openinference-instrumentation-llama-index (>=3.0,<4.0)", "opentelemetry-exporter-gcp-logging (>=1.11.0a0,<2.0.0)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-exporter-otlp-proto-http (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.11.1,<3)", "pytest-xdist", "typing_extensions"]
|
||||
metadata = ["numpy (>=1.15.0)", "pandas (>=1.0.0)"]
|
||||
pipelines = ["pyyaml (>=5.3.1,<7)"]
|
||||
prediction = ["docker (>=5.0.3)", "fastapi (>=0.71.0,<=0.114.0)", "httpx (>=0.23.0,<=0.28.1)", "starlette (>=0.17.1)", "uvicorn[standard] (>=0.16.0)"]
|
||||
private-endpoints = ["requests (>=2.28.1)", "urllib3 (>=1.21.1,<1.27)"]
|
||||
ray = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0)", "pyarrow (>=6.0.1)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || ==2.33.* || >=2.42.dev0,<=2.42.0) ; python_version < \"3.11\"", "ray[default] (>=2.5,<=2.47.1) ; python_version == \"3.11\""]
|
||||
ray-testing = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0)", "pyarrow (>=6.0.1)", "pytest-xdist", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || ==2.33.* || >=2.42.dev0,<=2.42.0) ; python_version < \"3.11\"", "ray[default] (>=2.5,<=2.47.1) ; python_version == \"3.11\"", "ray[train]", "scikit-learn (<1.6.0)", "tensorflow ; python_version < \"3.13\"", "torch (>=2.0.0,<2.1.0)", "xgboost", "xgboost_ray"]
|
||||
reasoningengine = ["cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "opentelemetry-exporter-gcp-logging (>=1.11.0a0,<2.0.0)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-exporter-otlp-proto-http (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.11.1,<3)", "typing_extensions"]
|
||||
tensorboard = ["tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "werkzeug (>=2.0.0,<4.0.0)"]
|
||||
testing = ["Pillow", "aiohttp", "bigframes ; python_version >= \"3.10\" and python_version < \"3.14\"", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0) ; python_version < \"3.13\"", "fastapi (>=0.71.0,<=0.114.0)", "google-api-core (>=2.11,<3.0.0)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-vizier (>=0.1.6)", "google-vizier (>=0.1.6)", "grpcio-testing", "grpcio-tools (>=1.63.0) ; python_version >= \"3.13\"", "httpx (>=0.23.0,<=0.28.1)", "immutabledict", "immutabledict", "ipython", "jsonschema", "kfp (>=2.6.0,<3.0.0) ; python_version < \"3.13\"", "lit-nlp (==0.4.0) ; python_version < \"3.14\"", "litellm (>=1.72.4,!=1.77.2,!=1.77.3,!=1.77.4)", "mlflow (>=1.27.0) ; python_version >= \"3.13\"", "mlflow (>=1.27.0,<=2.16.0) ; python_version < \"3.13\"", "mock", "nltk", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "protobuf (<=5.29.4)", "pyarrow (>=10.0.1) ; python_version == \"3.11\"", "pyarrow (>=14.0.0) ; python_version >= \"3.12\"", "pyarrow (>=3.0.0,<8.0.0) ; python_version < \"3.11\"", "pyarrow (>=6.0.1)", "pytest-asyncio", "pytest-cov", "pytest-xdist", "pyyaml", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<2.10.dev0 || ==2.33.* || >=2.42.dev0,<=2.42.0) ; python_version < \"3.11\"", "ray[default] (>=2.5,<=2.47.1) ; python_version == \"3.11\"", "requests (>=2.28.1)", "requests-toolbelt (<=1.0.0)", "requests-toolbelt (<=1.0.0)", "ruamel.yaml", "scikit-learn (<1.6.0) ; python_version <= \"3.10\"", "scikit-learn (<1.6.0) ; python_version <= \"3.10\"", "scikit-learn ; python_version > \"3.10\"", "scikit-learn ; python_version > \"3.10\"", "sentencepiece (>=0.2.0)", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorboard-plugin-profile (>=2.4.0,<2.18.0)", "tensorflow (==2.14.1) ; python_version <= \"3.11\"", "tensorflow (==2.19.0) ; python_version > \"3.11\" and python_version < \"3.13\"", "tensorflow (>=2.3.0,<3.0.0) ; python_version < \"3.13\"", "tensorflow (>=2.3.0,<3.0.0) ; python_version < \"3.13\"", "torch (>=2.0.0,<2.1.0) ; python_version <= \"3.11\"", "torch (>=2.2.0) ; python_version > \"3.11\" and python_version < \"3.13\"", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<4.0.0)", "werkzeug (>=2.0.0,<4.0.0)", "xgboost"]
|
||||
tokenization = ["sentencepiece (>=0.2.0)"]
|
||||
vizier = ["google-vizier (>=0.1.6)"]
|
||||
xai = ["tensorflow (>=2.3.0,<3.0.0) ; python_version < \"3.13\""]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-bigquery"
|
||||
version = "3.40.0"
|
||||
description = "Google BigQuery API client library"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\""
|
||||
files = [
|
||||
{file = "google_cloud_bigquery-3.40.0-py3-none-any.whl", hash = "sha256:0469bcf9e3dad3cab65b67cce98180c8c0aacf3253d47f0f8e976f299b49b5ab"},
|
||||
{file = "google_cloud_bigquery-3.40.0.tar.gz", hash = "sha256:b3ccb11caf0029f15b29569518f667553fe08f6f1459b959020c83fbbd8f2e68"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-api-core = {version = ">=2.11.1,<3.0.0", extras = ["grpc"]}
|
||||
google-auth = ">=2.14.1,<3.0.0"
|
||||
google-cloud-core = ">=2.4.1,<3.0.0"
|
||||
google-resumable-media = ">=2.0.0,<3.0.0"
|
||||
packaging = ">=24.2.0"
|
||||
python-dateutil = ">=2.8.2,<3.0.0"
|
||||
requests = ">=2.21.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["google-cloud-bigquery[bigquery-v2,bqstorage,geopandas,ipython,ipywidgets,matplotlib,opentelemetry,pandas,tqdm]"]
|
||||
bigquery-v2 = ["proto-plus (>=1.22.3,<2.0.0)", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0)"]
|
||||
bqstorage = ["google-cloud-bigquery-storage (>=2.18.0,<3.0.0)", "grpcio (>=1.47.0,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "pyarrow (>=4.0.0)"]
|
||||
geopandas = ["Shapely (>=1.8.4,<3.0.0)", "geopandas (>=0.9.0,<2.0.0)"]
|
||||
ipython = ["bigquery-magics (>=0.6.0)", "ipython (>=7.23.1)"]
|
||||
ipywidgets = ["ipykernel (>=6.2.0)", "ipywidgets (>=7.7.1)"]
|
||||
matplotlib = ["matplotlib (>=3.10.3) ; python_version >= \"3.10\"", "matplotlib (>=3.7.1,<=3.9.2) ; python_version == \"3.9\""]
|
||||
opentelemetry = ["opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)"]
|
||||
pandas = ["db-dtypes (>=1.0.4,<2.0.0)", "grpcio (>=1.47.0,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "pandas (>=1.3.0)", "pandas-gbq (>=0.26.1)", "pyarrow (>=3.0.0)"]
|
||||
tqdm = ["tqdm (>=4.23.4,<5.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-core"
|
||||
version = "2.5.0"
|
||||
description = "Google Cloud API client core library"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\""
|
||||
files = [
|
||||
{file = "google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc"},
|
||||
{file = "google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0"
|
||||
google-auth = ">=1.25.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
grpc = ["grpcio (>=1.38.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.38.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-iam"
|
||||
version = "2.20.0"
|
||||
|
|
@ -2115,6 +2248,204 @@ grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev"
|
|||
proto-plus = ">=1.22.3,<2.0.0dev"
|
||||
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-resource-manager"
|
||||
version = "1.16.0"
|
||||
description = "Google Cloud Resource Manager API client library"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\""
|
||||
files = [
|
||||
{file = "google_cloud_resource_manager-1.16.0-py3-none-any.whl", hash = "sha256:fb9a2ad2b5053c508e1c407ac31abfd1a22e91c32876c1892830724195819a28"},
|
||||
{file = "google_cloud_resource_manager-1.16.0.tar.gz", hash = "sha256:cc938f87cc36c2672f062b1e541650629e0d954c405a4dac35ceedee70c267c3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]}
|
||||
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
|
||||
grpc-google-iam-v1 = ">=0.14.0,<1.0.0"
|
||||
grpcio = [
|
||||
{version = ">=1.33.2,<2.0.0"},
|
||||
{version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""},
|
||||
]
|
||||
proto-plus = [
|
||||
{version = ">=1.22.3,<2.0.0"},
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
]
|
||||
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-storage"
|
||||
version = "3.4.1"
|
||||
description = "Google Cloud Storage API client library"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\" and python_version >= \"3.14\""
|
||||
files = [
|
||||
{file = "google_cloud_storage-3.4.1-py3-none-any.whl", hash = "sha256:972764cc0392aa097be8f49a5354e22eb47c3f62370067fb1571ffff4a1c1189"},
|
||||
{file = "google_cloud_storage-3.4.1.tar.gz", hash = "sha256:6f041a297e23a4b485fad8c305a7a6e6831855c208bcbe74d00332a909f82268"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-api-core = ">=2.15.0,<3.0.0"
|
||||
google-auth = ">=2.26.1,<3.0.0"
|
||||
google-cloud-core = ">=2.4.2,<3.0.0"
|
||||
google-crc32c = ">=1.1.3,<2.0.0"
|
||||
google-resumable-media = ">=2.7.2,<3.0.0"
|
||||
requests = ">=2.22.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
protobuf = ["protobuf (>=3.20.2,<7.0.0)"]
|
||||
tracing = ["opentelemetry-api (>=1.1.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-storage"
|
||||
version = "3.8.0"
|
||||
description = "Google Cloud Storage API client library"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\" and python_version < \"3.14\""
|
||||
files = [
|
||||
{file = "google_cloud_storage-3.8.0-py3-none-any.whl", hash = "sha256:78cfeae7cac2ca9441d0d0271c2eb4ebfa21aa4c6944dd0ccac0389e81d955a7"},
|
||||
{file = "google_cloud_storage-3.8.0.tar.gz", hash = "sha256:cc67952dce84ebc9d44970e24647a58260630b7b64d72360cedaf422d6727f28"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-api-core = ">=2.27.0,<3.0.0"
|
||||
google-auth = ">=2.26.1,<3.0.0"
|
||||
google-cloud-core = ">=2.4.2,<3.0.0"
|
||||
google-crc32c = ">=1.1.3,<2.0.0"
|
||||
google-resumable-media = ">=2.7.2,<3.0.0"
|
||||
requests = ">=2.22.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
grpc = ["google-api-core[grpc] (>=2.27.0,<3.0.0)", "grpc-google-iam-v1 (>=0.14.0,<1.0.0)", "grpcio (>=1.33.2,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.76.0,<2.0.0)", "proto-plus (>=1.22.3,<2.0.0) ; python_version < \"3.13\"", "proto-plus (>=1.25.0,<2.0.0) ; python_version >= \"3.13\"", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0)"]
|
||||
protobuf = ["protobuf (>=3.20.2,<7.0.0)"]
|
||||
tracing = ["opentelemetry-api (>=1.1.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-crc32c"
|
||||
version = "1.8.0"
|
||||
description = "A python wrapper of the C library 'Google CRC32C'"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\""
|
||||
files = [
|
||||
{file = "google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff"},
|
||||
{file = "google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288"},
|
||||
{file = "google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d"},
|
||||
{file = "google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092"},
|
||||
{file = "google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733"},
|
||||
{file = "google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8"},
|
||||
{file = "google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7"},
|
||||
{file = "google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15"},
|
||||
{file = "google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a"},
|
||||
{file = "google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2"},
|
||||
{file = "google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113"},
|
||||
{file = "google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb"},
|
||||
{file = "google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411"},
|
||||
{file = "google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454"},
|
||||
{file = "google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962"},
|
||||
{file = "google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b"},
|
||||
{file = "google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27"},
|
||||
{file = "google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa"},
|
||||
{file = "google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8"},
|
||||
{file = "google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f"},
|
||||
{file = "google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697"},
|
||||
{file = "google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651"},
|
||||
{file = "google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2"},
|
||||
{file = "google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21"},
|
||||
{file = "google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2"},
|
||||
{file = "google_crc32c-1.8.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc"},
|
||||
{file = "google_crc32c-1.8.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2"},
|
||||
{file = "google_crc32c-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215"},
|
||||
{file = "google_crc32c-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a"},
|
||||
{file = "google_crc32c-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f"},
|
||||
{file = "google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93"},
|
||||
{file = "google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c"},
|
||||
{file = "google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-genai"
|
||||
version = "1.47.0"
|
||||
description = "GenAI Python SDK"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.9\" and extra == \"google\""
|
||||
files = [
|
||||
{file = "google_genai-1.47.0-py3-none-any.whl", hash = "sha256:e3851237556cbdec96007d8028b4b1f2425cdc5c099a8dc36b72a57e42821b60"},
|
||||
{file = "google_genai-1.47.0.tar.gz", hash = "sha256:ecece00d0a04e6739ea76cc8dad82ec9593d9380aaabef078990e60574e5bf59"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=4.8.0,<5.0.0"
|
||||
google-auth = ">=2.14.1,<3.0.0"
|
||||
httpx = ">=0.28.1,<1.0.0"
|
||||
pydantic = ">=2.9.0,<3.0.0"
|
||||
requests = ">=2.28.1,<3.0.0"
|
||||
tenacity = ">=8.2.3,<9.2.0"
|
||||
typing-extensions = ">=4.11.0,<5.0.0"
|
||||
websockets = ">=13.0.0,<15.1.0"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (<4.0.0)"]
|
||||
local-tokenizer = ["protobuf", "sentencepiece (>=0.2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-genai"
|
||||
version = "1.55.0"
|
||||
description = "GenAI Python SDK"
|
||||
optional = true
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and extra == \"google\""
|
||||
files = [
|
||||
{file = "google_genai-1.55.0-py3-none-any.whl", hash = "sha256:98c422762b5ff6e16b8d9a1e4938e8e0ad910392a5422e47f5301498d7f373a1"},
|
||||
{file = "google_genai-1.55.0.tar.gz", hash = "sha256:ae9f1318fedb05c7c1b671a4148724751201e8908a87568364a309804064d986"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=4.8.0,<5.0.0"
|
||||
distro = ">=1.7.0,<2"
|
||||
google-auth = {version = ">=2.14.1,<3.0.0", extras = ["requests"]}
|
||||
httpx = ">=0.28.1,<1.0.0"
|
||||
pydantic = ">=2.9.0,<3.0.0"
|
||||
requests = ">=2.28.1,<3.0.0"
|
||||
sniffio = "*"
|
||||
tenacity = ">=8.2.3,<9.2.0"
|
||||
typing-extensions = ">=4.11.0,<5.0.0"
|
||||
websockets = ">=13.0.0,<15.1.0"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (<3.13.3)"]
|
||||
local-tokenizer = ["protobuf", "sentencepiece (>=0.2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-resumable-media"
|
||||
version = "2.8.0"
|
||||
description = "Utilities for Google Media Downloads and Resumable Uploads"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"google\""
|
||||
files = [
|
||||
{file = "google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582"},
|
||||
{file = "google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-crc32c = ">=1.0.0,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "google-auth (>=1.22.0,<2.0.0)"]
|
||||
requests = ["requests (>=2.18.0,<3.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "googleapis-common-protos"
|
||||
version = "1.72.0"
|
||||
|
|
@ -2126,7 +2457,7 @@ files = [
|
|||
{file = "googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038"},
|
||||
{file = "googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5"},
|
||||
]
|
||||
markers = {main = "extra == \"extra-proxy\""}
|
||||
markers = {main = "extra == \"extra-proxy\" or extra == \"google\""}
|
||||
|
||||
[package.dependencies]
|
||||
grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""}
|
||||
|
|
@ -2194,7 +2525,7 @@ description = "Lightweight in-process concurrent programming"
|
|||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and extra == \"mlflow\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"
|
||||
markers = "python_version >= \"3.10\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and extra == \"mlflow\""
|
||||
files = [
|
||||
{file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"},
|
||||
|
|
@ -2275,7 +2606,7 @@ description = "IAM API client library"
|
|||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"extra-proxy\""
|
||||
markers = "extra == \"extra-proxy\" or extra == \"google\""
|
||||
files = [
|
||||
{file = "grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6"},
|
||||
{file = "grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389"},
|
||||
|
|
@ -2356,7 +2687,7 @@ files = [
|
|||
{file = "grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a"},
|
||||
{file = "grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73"},
|
||||
]
|
||||
markers = {main = "extra == \"extra-proxy\" or extra == \"grpc\""}
|
||||
markers = {main = "extra == \"extra-proxy\" or extra == \"google\" or extra == \"grpc\""}
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.12,<5.0"
|
||||
|
|
@ -2371,7 +2702,7 @@ description = "Status proto mapping for gRPC"
|
|||
optional = true
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"extra-proxy\""
|
||||
markers = "extra == \"extra-proxy\" or extra == \"google\""
|
||||
files = [
|
||||
{file = "grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485"},
|
||||
{file = "grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8"},
|
||||
|
|
@ -2389,7 +2720,7 @@ description = "WSGI HTTP Server for UNIX"
|
|||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"proxy\" or (extra == \"proxy\" or extra == \"mlflow\") and platform_system != \"Windows\" and python_version >= \"3.10\""
|
||||
markers = "(python_version < \"3.14\" or extra == \"mlflow\" or extra == \"proxy\") and (platform_system != \"Windows\" or extra == \"proxy\") and (python_version >= \"3.10\" or extra == \"proxy\") and (extra == \"proxy\" or extra == \"mlflow\")"
|
||||
files = [
|
||||
{file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
|
||||
{file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
|
||||
|
|
@ -3095,15 +3426,15 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "litellm-proxy-extras"
|
||||
version = "0.4.23"
|
||||
version = "0.4.26"
|
||||
description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package."
|
||||
optional = true
|
||||
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"proxy\""
|
||||
files = [
|
||||
{file = "litellm_proxy_extras-0.4.23-py3-none-any.whl", hash = "sha256:dfda21203dde9fd97cf364396a9b5be0cfdf00fa9846439ee33ce11b7a52f9ce"},
|
||||
{file = "litellm_proxy_extras-0.4.23.tar.gz", hash = "sha256:8e3f95576dc2a296e7f73d8c87e73628bd899b4644c45863960fe3c3762d8f64"},
|
||||
{file = "litellm_proxy_extras-0.4.26-py3-none-any.whl", hash = "sha256:6b8720c7ae0b51637a3155b6cb02783c316443fe002641061ed9162d2667aee4"},
|
||||
{file = "litellm_proxy_extras-0.4.26.tar.gz", hash = "sha256:cb9544703580172fc6436670e1a693a46ccb34a9057548c94f43e97b7b032754"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3446,8 +3777,8 @@ files = [
|
|||
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.23.3", markers = "python_version >= \"3.11\""},
|
||||
{version = ">1.20"},
|
||||
{version = ">=1.23.3", markers = "python_version >= \"3.11\""},
|
||||
{version = ">=1.21.2", markers = "python_version >= \"3.10\""},
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
]
|
||||
|
|
@ -3860,7 +4191,7 @@ description = "Fundamental package for array computing in Python"
|
|||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "(python_version >= \"3.10\" or extra == \"extra-proxy\" or extra == \"semantic-router\") and python_version < \"3.12\" and (extra == \"extra-proxy\" or extra == \"semantic-router\" or extra == \"mlflow\")"
|
||||
markers = "python_version < \"3.12\" and (python_version >= \"3.10\" or extra == \"extra-proxy\" or extra == \"semantic-router\" or extra == \"google\") and (extra == \"extra-proxy\" or extra == \"semantic-router\" or extra == \"google\" or extra == \"mlflow\")"
|
||||
files = [
|
||||
{file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
|
||||
{file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
|
||||
|
|
@ -3907,7 +4238,7 @@ description = "Fundamental package for array computing in Python"
|
|||
optional = true
|
||||
python-versions = ">=3.11"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.12\" and (extra == \"extra-proxy\" or extra == \"semantic-router\" or extra == \"mlflow\") and (python_version < \"3.14\" or extra == \"mlflow\")"
|
||||
markers = "python_version >= \"3.12\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"semantic-router\" or extra == \"mlflow\") and (python_version < \"3.14\" or extra == \"mlflow\" or extra == \"google\")"
|
||||
files = [
|
||||
{file = "numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10"},
|
||||
{file = "numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218"},
|
||||
|
|
@ -4838,7 +5169,7 @@ description = "Beautiful, Pythonic protocol buffers"
|
|||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"extra-proxy\""
|
||||
markers = "extra == \"google\" or extra == \"extra-proxy\""
|
||||
files = [
|
||||
{file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"},
|
||||
{file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"},
|
||||
|
|
@ -4870,7 +5201,7 @@ files = [
|
|||
{file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"},
|
||||
{file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"},
|
||||
]
|
||||
markers = {main = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\""}
|
||||
markers = {main = "(extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") and python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\""}
|
||||
|
||||
[[package]]
|
||||
name = "pyarrow"
|
||||
|
|
@ -4940,7 +5271,7 @@ description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs
|
|||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\""
|
||||
markers = "(extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") and python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\""
|
||||
files = [
|
||||
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
|
||||
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
|
||||
|
|
@ -4953,7 +5284,7 @@ description = "A collection of ASN.1-based protocols modules"
|
|||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\""
|
||||
markers = "(extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") and python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\""
|
||||
files = [
|
||||
{file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"},
|
||||
{file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"},
|
||||
|
|
@ -4985,7 +5316,7 @@ files = [
|
|||
{file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"},
|
||||
{file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"},
|
||||
]
|
||||
markers = {main = "(platform_python_implementation != \"PyPy\" or extra == \"proxy\") and implementation_name != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""}
|
||||
markers = {main = "implementation_name != \"PyPy\" and (platform_python_implementation != \"PyPy\" or extra == \"proxy\")", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", proxy-dev = "implementation_name != \"PyPy\" and platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
|
|
@ -5362,7 +5693,7 @@ description = "Extensions to the standard Python datetime module"
|
|||
optional = true
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\""
|
||||
markers = "(extra == \"mlflow\" or extra == \"proxy\" or extra == \"google\") and python_version >= \"3.10\" or extra == \"proxy\" or extra == \"google\""
|
||||
files = [
|
||||
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||
|
|
@ -5422,7 +5753,7 @@ description = "World timezone definitions, modern and historical"
|
|||
optional = true
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\""
|
||||
markers = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\""
|
||||
files = [
|
||||
{file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"},
|
||||
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
|
||||
|
|
@ -5435,7 +5766,7 @@ description = "Python for Window Extensions"
|
|||
optional = true
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"proxy\" or extra == \"mlflow\") and sys_platform == \"win32\""
|
||||
markers = "python_version >= \"3.10\" and sys_platform == \"win32\" and (extra == \"proxy\" or extra == \"mlflow\")"
|
||||
files = [
|
||||
{file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"},
|
||||
{file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"},
|
||||
|
|
@ -6241,7 +6572,7 @@ description = "Pure-Python RSA implementation"
|
|||
optional = true
|
||||
python-versions = "<4,>=3.6"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\""
|
||||
markers = "(extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") and python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\""
|
||||
files = [
|
||||
{file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"},
|
||||
{file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"},
|
||||
|
|
@ -6279,22 +6610,22 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.11.3"
|
||||
version = "0.14.0"
|
||||
description = "An Amazon S3 Transfer Manager"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"proxy\""
|
||||
files = [
|
||||
{file = "s3transfer-0.11.3-py3-none-any.whl", hash = "sha256:ca855bdeb885174b5ffa95b9913622459d4ad8e331fc98eb01e6d5eb6a30655d"},
|
||||
{file = "s3transfer-0.11.3.tar.gz", hash = "sha256:edae4977e3a122445660c7c114bba949f9d191bae3b34a096f18a1c8c354527a"},
|
||||
{file = "s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456"},
|
||||
{file = "s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.36.0,<2.0a.0"
|
||||
botocore = ">=1.37.4,<2.0a.0"
|
||||
|
||||
[package.extras]
|
||||
crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"]
|
||||
crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "scikit-learn"
|
||||
|
|
@ -6542,6 +6873,141 @@ postgres = ["psycopg[binary] (>=3.1.0,<4)"]
|
|||
qdrant = ["qdrant-client (>=1.11.1,<2)"]
|
||||
vision = ["pillow (>=10.2.0,<11.0.0) ; python_version < \"3.13\"", "torch (>=2.6.0) ; python_version < \"3.13\"", "torchvision (>=0.17.0) ; python_version < \"3.13\"", "transformers (>=4.36.2) ; python_version < \"3.13\""]
|
||||
|
||||
[[package]]
|
||||
name = "shapely"
|
||||
version = "2.0.7"
|
||||
description = "Manipulation and analysis of geometric objects"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.9\" and extra == \"google\""
|
||||
files = [
|
||||
{file = "shapely-2.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:33fb10e50b16113714ae40adccf7670379e9ccf5b7a41d0002046ba2b8f0f691"},
|
||||
{file = "shapely-2.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f44eda8bd7a4bccb0f281264b34bf3518d8c4c9a8ffe69a1a05dabf6e8461147"},
|
||||
{file = "shapely-2.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6c50cd879831955ac47af9c907ce0310245f9d162e298703f82e1785e38c98"},
|
||||
{file = "shapely-2.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04a65d882456e13c8b417562c36324c0cd1e5915f3c18ad516bb32ee3f5fc895"},
|
||||
{file = "shapely-2.0.7-cp310-cp310-win32.whl", hash = "sha256:7e97104d28e60b69f9b6a957c4d3a2a893b27525bc1fc96b47b3ccef46726bf2"},
|
||||
{file = "shapely-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:35524cc8d40ee4752520819f9894b9f28ba339a42d4922e92c99b148bed3be39"},
|
||||
{file = "shapely-2.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5cf23400cb25deccf48c56a7cdda8197ae66c0e9097fcdd122ac2007e320bc34"},
|
||||
{file = "shapely-2.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f1da01c04527f7da59ee3755d8ee112cd8967c15fab9e43bba936b81e2a013"},
|
||||
{file = "shapely-2.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f623b64bb219d62014781120f47499a7adc30cf7787e24b659e56651ceebcb0"},
|
||||
{file = "shapely-2.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6d95703efaa64aaabf278ced641b888fc23d9c6dd71f8215091afd8a26a66e3"},
|
||||
{file = "shapely-2.0.7-cp311-cp311-win32.whl", hash = "sha256:2f6e4759cf680a0f00a54234902415f2fa5fe02f6b05546c662654001f0793a2"},
|
||||
{file = "shapely-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:b52f3ab845d32dfd20afba86675c91919a622f4627182daec64974db9b0b4608"},
|
||||
{file = "shapely-2.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4c2b9859424facbafa54f4a19b625a752ff958ab49e01bc695f254f7db1835fa"},
|
||||
{file = "shapely-2.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5aed1c6764f51011d69a679fdf6b57e691371ae49ebe28c3edb5486537ffbd51"},
|
||||
{file = "shapely-2.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c9ae8cf443187d784d57202199bf9fd2d4bb7d5521fe8926ba40db1bc33e8e"},
|
||||
{file = "shapely-2.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9469f49ff873ef566864cb3516091881f217b5d231c8164f7883990eec88b73"},
|
||||
{file = "shapely-2.0.7-cp312-cp312-win32.whl", hash = "sha256:6bca5095e86be9d4ef3cb52d56bdd66df63ff111d580855cb8546f06c3c907cd"},
|
||||
{file = "shapely-2.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:f86e2c0259fe598c4532acfcf638c1f520fa77c1275912bbc958faecbf00b108"},
|
||||
{file = "shapely-2.0.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a0c09e3e02f948631c7763b4fd3dd175bc45303a0ae04b000856dedebefe13cb"},
|
||||
{file = "shapely-2.0.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06ff6020949b44baa8fc2e5e57e0f3d09486cd5c33b47d669f847c54136e7027"},
|
||||
{file = "shapely-2.0.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6dbf096f961ca6bec5640e22e65ccdec11e676344e8157fe7d636e7904fd36"},
|
||||
{file = "shapely-2.0.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adeddfb1e22c20548e840403e5e0b3d9dc3daf66f05fa59f1fcf5b5f664f0e98"},
|
||||
{file = "shapely-2.0.7-cp313-cp313-win32.whl", hash = "sha256:a7f04691ce1c7ed974c2f8b34a1fe4c3c5dfe33128eae886aa32d730f1ec1913"},
|
||||
{file = "shapely-2.0.7-cp313-cp313-win_amd64.whl", hash = "sha256:aaaf5f7e6cc234c1793f2a2760da464b604584fb58c6b6d7d94144fd2692d67e"},
|
||||
{file = "shapely-2.0.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19cbc8808efe87a71150e785b71d8a0e614751464e21fb679d97e274eca7bd43"},
|
||||
{file = "shapely-2.0.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc19b78cc966db195024d8011649b4e22812f805dd49264323980715ab80accc"},
|
||||
{file = "shapely-2.0.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd37d65519b3f8ed8976fa4302a2827cbb96e0a461a2e504db583b08a22f0b98"},
|
||||
{file = "shapely-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:25085a30a2462cee4e850a6e3fb37431cbbe4ad51cbcc163af0cea1eaa9eb96d"},
|
||||
{file = "shapely-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:1a2e03277128e62f9a49a58eb7eb813fa9b343925fca5e7d631d50f4c0e8e0b8"},
|
||||
{file = "shapely-2.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e1c4f1071fe9c09af077a69b6c75f17feb473caeea0c3579b3e94834efcbdc36"},
|
||||
{file = "shapely-2.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3697bd078b4459f5a1781015854ef5ea5d824dbf95282d0b60bfad6ff83ec8dc"},
|
||||
{file = "shapely-2.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e9fed9a7d6451979d914cb6ebbb218b4b4e77c0d50da23e23d8327948662611"},
|
||||
{file = "shapely-2.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2934834c7f417aeb7cba3b0d9b4441a76ebcecf9ea6e80b455c33c7c62d96a24"},
|
||||
{file = "shapely-2.0.7-cp38-cp38-win32.whl", hash = "sha256:2e4a1749ad64bc6e7668c8f2f9479029f079991f4ae3cb9e6b25440e35a4b532"},
|
||||
{file = "shapely-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8ae5cb6b645ac3fba34ad84b32fbdccb2ab321facb461954925bde807a0d3b74"},
|
||||
{file = "shapely-2.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4abeb44b3b946236e4e1a1b3d2a0987fb4d8a63bfb3fdefb8a19d142b72001e5"},
|
||||
{file = "shapely-2.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd0e75d9124b73e06a42bf1615ad3d7d805f66871aa94538c3a9b7871d620013"},
|
||||
{file = "shapely-2.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7977d8a39c4cf0e06247cd2dca695ad4e020b81981d4c82152c996346cf1094b"},
|
||||
{file = "shapely-2.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0145387565fcf8f7c028b073c802956431308da933ef41d08b1693de49990d27"},
|
||||
{file = "shapely-2.0.7-cp39-cp39-win32.whl", hash = "sha256:98697c842d5c221408ba8aa573d4f49caef4831e9bc6b6e785ce38aca42d1999"},
|
||||
{file = "shapely-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:a3fb7fbae257e1b042f440289ee7235d03f433ea880e73e687f108d044b24db5"},
|
||||
{file = "shapely-2.0.7.tar.gz", hash = "sha256:28fe2997aab9a9dc026dc6a355d04e85841546b2a5d232ed953e3321ab958ee5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
numpy = ">=1.14,<3"
|
||||
|
||||
[package.extras]
|
||||
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "shapely"
|
||||
version = "2.1.2"
|
||||
description = "Manipulation and analysis of geometric objects"
|
||||
optional = true
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and extra == \"google\""
|
||||
files = [
|
||||
{file = "shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f"},
|
||||
{file = "shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea"},
|
||||
{file = "shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f"},
|
||||
{file = "shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142"},
|
||||
{file = "shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4"},
|
||||
{file = "shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0"},
|
||||
{file = "shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e"},
|
||||
{file = "shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f"},
|
||||
{file = "shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618"},
|
||||
{file = "shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d"},
|
||||
{file = "shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09"},
|
||||
{file = "shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26"},
|
||||
{file = "shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7"},
|
||||
{file = "shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2"},
|
||||
{file = "shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6"},
|
||||
{file = "shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc"},
|
||||
{file = "shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94"},
|
||||
{file = "shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359"},
|
||||
{file = "shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3"},
|
||||
{file = "shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b"},
|
||||
{file = "shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc"},
|
||||
{file = "shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d"},
|
||||
{file = "shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454"},
|
||||
{file = "shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179"},
|
||||
{file = "shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8"},
|
||||
{file = "shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a"},
|
||||
{file = "shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e"},
|
||||
{file = "shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6"},
|
||||
{file = "shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af"},
|
||||
{file = "shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd"},
|
||||
{file = "shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350"},
|
||||
{file = "shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715"},
|
||||
{file = "shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40"},
|
||||
{file = "shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b"},
|
||||
{file = "shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801"},
|
||||
{file = "shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0"},
|
||||
{file = "shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c"},
|
||||
{file = "shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99"},
|
||||
{file = "shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf"},
|
||||
{file = "shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c"},
|
||||
{file = "shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223"},
|
||||
{file = "shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c"},
|
||||
{file = "shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df"},
|
||||
{file = "shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf"},
|
||||
{file = "shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4"},
|
||||
{file = "shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc"},
|
||||
{file = "shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566"},
|
||||
{file = "shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c"},
|
||||
{file = "shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a"},
|
||||
{file = "shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076"},
|
||||
{file = "shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1"},
|
||||
{file = "shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0"},
|
||||
{file = "shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26"},
|
||||
{file = "shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0"},
|
||||
{file = "shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735"},
|
||||
{file = "shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9"},
|
||||
{file = "shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
numpy = ">=1.21"
|
||||
|
||||
[package.extras]
|
||||
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
|
||||
test = ["pytest", "pytest-cov", "scipy-doctest"]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
|
|
@ -6561,7 +7027,7 @@ description = "Python 2 and 3 compatibility utilities"
|
|||
optional = true
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\""
|
||||
markers = "(extra == \"mlflow\" or extra == \"proxy\" or extra == \"google\") and python_version >= \"3.10\" or extra == \"proxy\" or extra == \"google\""
|
||||
files = [
|
||||
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||
|
|
@ -6984,6 +7450,26 @@ examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio]
|
|||
granian = ["granian (>=2.3.1)"]
|
||||
uvicorn = ["uvicorn (>=0.34.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.49.3"
|
||||
description = "The little ASGI library that shines."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f"},
|
||||
{file = "starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284"},
|
||||
]
|
||||
markers = {main = "python_version == \"3.9\" and extra == \"proxy\"", dev = "python_version == \"3.9\""}
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.6.2,<5"
|
||||
typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.50.0"
|
||||
|
|
@ -7044,7 +7530,7 @@ description = "Retry code until it succeeds"
|
|||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"extra-proxy\" and python_version < \"3.14\""
|
||||
markers = "(extra == \"extra-proxy\" or extra == \"google\") and (python_version < \"3.14\" or extra == \"google\")"
|
||||
files = [
|
||||
{file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"},
|
||||
{file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"},
|
||||
|
|
@ -7522,7 +8008,7 @@ description = "The lightning-fast ASGI server."
|
|||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\""
|
||||
markers = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\""
|
||||
files = [
|
||||
{file = "uvicorn-0.31.1-py3-none-any.whl", hash = "sha256:adc42d9cac80cf3e51af97c1851648066841e7cfb6993a4ca8de29ac1548ed41"},
|
||||
{file = "uvicorn-0.31.1.tar.gz", hash = "sha256:f5167919867b161b7bcaf32646c6a94cdbd4c3aa2eb5c17d36bb9aa5cfd8c493"},
|
||||
|
|
@ -7543,7 +8029,7 @@ description = "Fast implementation of asyncio event loop on top of libuv"
|
|||
optional = true
|
||||
python-versions = ">=3.8.0"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform != \"win32\" and extra == \"proxy\""
|
||||
markers = "extra == \"proxy\" and sys_platform != \"win32\""
|
||||
files = [
|
||||
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"},
|
||||
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"},
|
||||
|
|
@ -7596,7 +8082,7 @@ description = "Waitress WSGI server"
|
|||
optional = true
|
||||
python-versions = ">=3.9.0"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and extra == \"mlflow\" and platform_system == \"Windows\""
|
||||
markers = "python_version >= \"3.10\" and platform_system == \"Windows\" and extra == \"mlflow\""
|
||||
files = [
|
||||
{file = "waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e"},
|
||||
{file = "waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f"},
|
||||
|
|
@ -7613,7 +8099,7 @@ description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
|||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"proxy\""
|
||||
markers = "extra == \"google\" or extra == \"proxy\""
|
||||
files = [
|
||||
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"},
|
||||
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"},
|
||||
|
|
@ -7996,6 +8482,7 @@ type = ["pytest-mypy"]
|
|||
[extras]
|
||||
caching = ["diskcache"]
|
||||
extra-proxy = ["a2a-sdk", "azure-identity", "azure-keyvault-secrets", "google-cloud-iam", "google-cloud-kms", "prisma", "redisvl", "resend"]
|
||||
google = ["google-cloud-aiplatform"]
|
||||
grpc = ["grpcio", "grpcio"]
|
||||
mlflow = ["mlflow"]
|
||||
proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "soundfile", "uvicorn", "uvloop", "websockets"]
|
||||
|
|
@ -8005,4 +8492,4 @@ utils = ["numpydoc"]
|
|||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9,<4.0"
|
||||
content-hash = "f6a98e687d478db6e30274a4cf70391960775cbf648da0783558444da3a662ea"
|
||||
content-hash = "ee8411854731cb6cc038a6135d7589a81789a269e718a226101953ba24fb2033"
|
||||
|
|
|
|||
|
|
@ -61,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.25", optional = true}
|
||||
litellm-proxy-extras = {version = "0.4.26", optional = true}
|
||||
rich = {version = "13.7.1", optional = true}
|
||||
litellm-enterprise = {version = "0.1.27", optional = true}
|
||||
diskcache = {version = "^5.6.1", optional = true}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,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.25 # for proxy extras - e.g. prisma migrations
|
||||
litellm-proxy-extras==0.4.26 # 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
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ model LiteLLM_TeamTable {
|
|||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
team_member_permissions String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases
|
||||
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
||||
litellm_model_table LiteLLM_ModelTable? @relation(fields: [model_id], references: [id])
|
||||
|
|
@ -156,6 +157,7 @@ model LiteLLM_DeletedTeamTable {
|
|||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
team_member_permissions String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_id Int? // id for LiteLLM_ModelTable -> stores team-level model aliases
|
||||
|
||||
// Original timestamps from team creation/updates
|
||||
|
|
@ -197,6 +199,7 @@ model LiteLLM_UserTable {
|
|||
budget_duration String?
|
||||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
created_at DateTime? @default(now()) @map("created_at")
|
||||
|
|
@ -283,6 +286,7 @@ model LiteLLM_VerificationToken {
|
|||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
allowed_routes String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
budget_id String?
|
||||
|
|
@ -327,6 +331,7 @@ model LiteLLM_DeletedVerificationToken {
|
|||
budget_reset_at DateTime?
|
||||
allowed_cache_controls String[] @default([])
|
||||
allowed_routes String[] @default([])
|
||||
policies String[] @default([])
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
|
|
@ -863,3 +868,32 @@ model LiteLLM_SkillsTable {
|
|||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
||||
// Policy table for storing guardrail policies
|
||||
model LiteLLM_PolicyTable {
|
||||
policy_id String @id @default(uuid())
|
||||
policy_name String @unique
|
||||
inherit String? // Name of parent policy to inherit from
|
||||
description String?
|
||||
guardrails_add String[] @default([])
|
||||
guardrails_remove String[] @default([])
|
||||
condition Json? @default("{}") // Policy conditions (e.g., model matching)
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
||||
// Policy attachment table for defining where policies apply
|
||||
model LiteLLM_PolicyAttachmentTable {
|
||||
attachment_id String @id @default(uuid())
|
||||
policy_name String // Name of the policy to attach
|
||||
scope String? // Use '*' for global scope
|
||||
teams String[] @default([]) // Team aliases or patterns
|
||||
keys String[] @default([]) // Key aliases or patterns
|
||||
models String[] @default([]) // Model names or patterns
|
||||
created_at DateTime @default(now())
|
||||
created_by String?
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ async def test_call_router_callbacks_on_success():
|
|||
)
|
||||
assert increment["increment_value"] == 1
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
@pytest.mark.asyncio
|
||||
async def test_call_router_callbacks_on_failure():
|
||||
router = Router(
|
||||
|
|
@ -288,7 +288,7 @@ async def test_call_router_callbacks_on_failure():
|
|||
mock_response="litellm.RateLimitError",
|
||||
num_retries=0,
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(3)
|
||||
print(mock_callback.call_args_list)
|
||||
assert mock_callback.call_count == 1
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,25 @@ sys.path.insert(
|
|||
|
||||
import pytest
|
||||
import litellm
|
||||
from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig, Span
|
||||
import asyncio
|
||||
import logging
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.integrations.arize.arize_phoenix import ArizePhoenixLogger
|
||||
from litellm.integrations._types.open_inference import (
|
||||
OpenInferenceSpanKindValues,
|
||||
SpanAttributes as OISpanAttributes,
|
||||
)
|
||||
from litellm.integrations.opentelemetry import (
|
||||
LITELLM_PROXY_REQUEST_SPAN_NAME,
|
||||
LITELLM_TRACER_NAME,
|
||||
LITELLM_REQUEST_SPAN_NAME,
|
||||
OpenTelemetry,
|
||||
OpenTelemetryConfig,
|
||||
RAW_REQUEST_SPAN_NAME,
|
||||
Span,
|
||||
)
|
||||
from litellm.proxy._types import SpanAttributes
|
||||
|
||||
verbose_logger.setLevel(logging.DEBUG)
|
||||
|
|
@ -242,3 +256,51 @@ def validate_redacted_message_span_attributes(span):
|
|||
), f"Non-metadata attribute found: {attr}"
|
||||
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_arize_phoenix_adds_openinference_kind_and_avoids_duplicate_litellm_spans():
|
||||
"""
|
||||
Ensure Arize Phoenix spans include OpenInference span kind and do not create
|
||||
a duplicate litellm_request span when a proxy parent span is already active.
|
||||
"""
|
||||
|
||||
exporter.clear()
|
||||
litellm.logging_callback_manager._reset_all_callbacks()
|
||||
|
||||
otel_logger = ArizePhoenixLogger(config=OpenTelemetryConfig(exporter=exporter))
|
||||
litellm.callbacks = [otel_logger]
|
||||
litellm.success_callback = []
|
||||
litellm.failure_callback = []
|
||||
|
||||
tracer = trace.get_tracer(LITELLM_TRACER_NAME)
|
||||
parent_span = tracer.start_span(LITELLM_PROXY_REQUEST_SPAN_NAME)
|
||||
|
||||
# Keep parent span active; OpenTelemetry logger will attach attributes and end it.
|
||||
with trace.use_span(parent_span, end_on_exit=False):
|
||||
await litellm.acompletion(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "ping"}],
|
||||
mock_response="pong",
|
||||
)
|
||||
|
||||
# Flush span processing
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if parent_span.is_recording():
|
||||
parent_span.end()
|
||||
|
||||
spans = exporter.get_finished_spans()
|
||||
|
||||
span_names = [span.name for span in spans]
|
||||
assert LITELLM_REQUEST_SPAN_NAME not in span_names
|
||||
assert span_names.count(LITELLM_PROXY_REQUEST_SPAN_NAME) == 1
|
||||
assert span_names.count(RAW_REQUEST_SPAN_NAME) == 1
|
||||
|
||||
# All spans should belong to the same trace (parent + raw child)
|
||||
assert len({span.context.trace_id for span in spans}) == 1
|
||||
assert len(spans) == 2
|
||||
|
||||
proxy_span = next(span for span in spans if span.name == LITELLM_PROXY_REQUEST_SPAN_NAME)
|
||||
assert proxy_span.attributes.get(OISpanAttributes.OPENINFERENCE_SPAN_KIND) == OpenInferenceSpanKindValues.LLM.value
|
||||
|
||||
exporter.clear()
|
||||
|
|
|
|||
|
|
@ -215,28 +215,21 @@ class TestProxyInitializationHelpers:
|
|||
assert "pool_timeout=60" in modified_url
|
||||
|
||||
@patch("uvicorn.run")
|
||||
@patch("builtins.print")
|
||||
def test_skip_server_startup(self, mock_print, mock_uvicorn_run):
|
||||
"""Test that the skip_server_startup flag prevents server startup when True"""
|
||||
@patch("atexit.register") # 🔥 critical
|
||||
def test_skip_server_startup(self, mock_atexit_register, mock_uvicorn_run):
|
||||
from click.testing import CliRunner
|
||||
|
||||
from litellm.proxy.proxy_cli import run_server
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
mock_app = MagicMock()
|
||||
mock_proxy_config = MagicMock()
|
||||
mock_key_mgmt = MagicMock()
|
||||
mock_save_worker_config = MagicMock()
|
||||
|
||||
with patch.dict(
|
||||
"sys.modules",
|
||||
{
|
||||
"proxy_server": MagicMock(
|
||||
app=mock_app,
|
||||
ProxyConfig=mock_proxy_config,
|
||||
KeyManagementSettings=mock_key_mgmt,
|
||||
save_worker_config=mock_save_worker_config,
|
||||
app=MagicMock(),
|
||||
ProxyConfig=MagicMock(),
|
||||
KeyManagementSettings=MagicMock(),
|
||||
save_worker_config=MagicMock(),
|
||||
)
|
||||
},
|
||||
), patch(
|
||||
|
|
@ -248,16 +241,15 @@ class TestProxyInitializationHelpers:
|
|||
"port": 8000,
|
||||
}
|
||||
|
||||
# --- skip startup ---
|
||||
result = runner.invoke(run_server, ["--local", "--skip_server_startup"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Skipping server startup" in result.output
|
||||
mock_uvicorn_run.assert_not_called()
|
||||
mock_print.assert_any_call(
|
||||
"LiteLLM: Setup complete. Skipping server startup as requested."
|
||||
)
|
||||
|
||||
# --- normal startup ---
|
||||
mock_uvicorn_run.reset_mock()
|
||||
mock_print.reset_mock()
|
||||
|
||||
result = runner.invoke(run_server, ["--local"])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
|
||||
import pytest
|
||||
@pytest.mark.skip(reason="Very Flaky in CI, will debug later")
|
||||
def test_restructure_ui_html_files_skipped_in_non_root(monkeypatch):
|
||||
"""
|
||||
Test that _restructure_ui_html_files is SKIPPED when:
|
||||
|
|
@ -36,7 +36,7 @@ def test_restructure_ui_html_files_skipped_in_non_root(monkeypatch):
|
|||
# Verify it was NOT called
|
||||
mock_restructure.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Very Flaky in CI, will debug later")
|
||||
def test_restructure_ui_html_files_NOT_skipped_locally(monkeypatch):
|
||||
"""
|
||||
Test that _restructure_ui_html_files is NOT skipped for local development
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
ExperimentOutlined,
|
||||
ToolOutlined,
|
||||
TagsOutlined,
|
||||
AuditOutlined,
|
||||
} from "@ant-design/icons";
|
||||
// import {
|
||||
// all_admin_roles,
|
||||
|
|
@ -102,6 +103,8 @@ const routeFor = (slug: string): string => {
|
|||
return "logs";
|
||||
case "guardrails":
|
||||
return "guardrails";
|
||||
case "policies":
|
||||
return "policies";
|
||||
|
||||
// tools
|
||||
case "mcp-servers":
|
||||
|
|
@ -202,6 +205,13 @@ const menuItems: MenuItemCfg[] = [
|
|||
icon: <SafetyOutlined style={{ fontSize: 18 }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "28",
|
||||
page: "policies",
|
||||
label: "Policies",
|
||||
icon: <AuditOutlined style={{ fontSize: 18 }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "26",
|
||||
page: "tools",
|
||||
|
|
|
|||
37
ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx
Normal file
37
ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"use client";
|
||||
|
||||
import PoliciesPanel from "@/components/policies";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import {
|
||||
getPoliciesList,
|
||||
createPolicyCall,
|
||||
updatePolicyCall,
|
||||
deletePolicyCall,
|
||||
getPolicyInfo,
|
||||
getPolicyAttachmentsList,
|
||||
createPolicyAttachmentCall,
|
||||
deletePolicyAttachmentCall,
|
||||
getGuardrailsList,
|
||||
} from "@/components/networking";
|
||||
|
||||
const PoliciesPage = () => {
|
||||
const { accessToken, userRole } = useAuthorized();
|
||||
|
||||
return (
|
||||
<PoliciesPanel
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
getPoliciesList={getPoliciesList}
|
||||
createPolicy={createPolicyCall}
|
||||
updatePolicy={updatePolicyCall}
|
||||
deletePolicy={deletePolicyCall}
|
||||
getPolicy={getPolicyInfo}
|
||||
getAttachmentsList={getPolicyAttachmentsList}
|
||||
createAttachment={createPolicyAttachmentCall}
|
||||
deleteAttachment={deletePolicyAttachmentCall}
|
||||
getGuardrailsList={getGuardrailsList}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoliciesPage;
|
||||
|
|
@ -14,6 +14,7 @@ import LoadingScreen from "@/components/common_components/LoadingScreen";
|
|||
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
|
||||
import GeneralSettings from "@/components/general_settings";
|
||||
import GuardrailsPanel from "@/components/guardrails";
|
||||
import PoliciesPanel from "@/components/policies";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { MCPServers } from "@/components/mcp_tools";
|
||||
import ModelHubTable from "@/components/AIHub/ModelHubTable";
|
||||
|
|
@ -472,6 +473,8 @@ export default function CreateKeyPage() {
|
|||
<BudgetPanel accessToken={accessToken} />
|
||||
) : page == "guardrails" ? (
|
||||
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "policies" ? (
|
||||
<PoliciesPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "agents" ? (
|
||||
<AgentsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "prompts" ? (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,309 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import AddFallbacks, { Fallbacks } from "./AddFallbacks";
|
||||
import * as fetchModelsModule from "../../../playground/llm_calls/fetch_models";
|
||||
|
||||
vi.mock("../../../playground/llm_calls/fetch_models", () => ({
|
||||
fetchAvailableModels: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("antd", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("antd")>();
|
||||
return {
|
||||
...actual,
|
||||
message: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./FallbackSelectionForm", () => ({
|
||||
FallbackSelectionForm: ({ groups, onGroupsChange }: any) => {
|
||||
const handleUpdateGroup = () => {
|
||||
if (groups.length > 0) {
|
||||
const updatedGroups = groups.map((group: any, index: number) => {
|
||||
if (index === 0 && !group.primaryModel) {
|
||||
return {
|
||||
...group,
|
||||
primaryModel: "gpt-4",
|
||||
fallbackModels: ["gpt-3.5-turbo"],
|
||||
};
|
||||
}
|
||||
return group;
|
||||
});
|
||||
onGroupsChange(updatedGroups);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="fallback-selection-form">
|
||||
<button onClick={handleUpdateGroup} data-testid="update-group-button">
|
||||
Update Group
|
||||
</button>
|
||||
<div data-testid="groups-count">{groups.length}</div>
|
||||
{groups.map((group: any) => (
|
||||
<div key={group.id} data-testid={`group-${group.id}`}>
|
||||
Primary: {group.primaryModel || "None"}, Fallbacks: {group.fallbackModels.length}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("AddFallbacks", () => {
|
||||
const mockOnChange = vi.fn();
|
||||
const mockAccessToken = "test-token";
|
||||
const mockModelGroups = [
|
||||
{ model_group: "gpt-4", mode: "chat" },
|
||||
{ model_group: "gpt-3.5-turbo", mode: "chat" },
|
||||
{ model_group: "claude-3-opus", mode: "chat" },
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
accessToken: mockAccessToken,
|
||||
value: [] as Fallbacks,
|
||||
onChange: mockOnChange,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fetchModelsModule.fetchAvailableModels).mockResolvedValue(mockModelGroups);
|
||||
});
|
||||
|
||||
it("should render the component", () => {
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
expect(screen.getByRole("button", { name: /add fallbacks/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open modal when Add Fallbacks button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch available models when modal opens", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchModelsModule.fetchAvailableModels).toHaveBeenCalledWith(mockAccessToken);
|
||||
});
|
||||
});
|
||||
|
||||
it("should close modal when Cancel button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error when saving incomplete groups", async () => {
|
||||
const user = userEvent.setup();
|
||||
const antd = await import("antd");
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const saveButton = screen.getByRole("button", { name: /save all configurations/i });
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save all configurations/i });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(antd.message.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error message when saving incomplete groups", async () => {
|
||||
const user = userEvent.setup();
|
||||
const antd = await import("antd");
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save all configurations/i });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(antd.message.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call onChange with new fallbacks when Save is clicked with valid configuration", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockOnChange.mockResolvedValue(undefined);
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("fallback-selection-form")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const updateGroupButton = screen.getByTestId("update-group-button");
|
||||
await user.click(updateGroupButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Primary: gpt-4/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save all configurations/i });
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
const callArgs = mockOnChange.mock.calls[0][0];
|
||||
expect(callArgs).toHaveLength(1);
|
||||
expect(callArgs[0]).toHaveProperty("gpt-4");
|
||||
expect(callArgs[0]["gpt-4"]).toContain("gpt-3.5-turbo");
|
||||
});
|
||||
});
|
||||
|
||||
it("should append new fallbacks to existing value", async () => {
|
||||
const user = userEvent.setup();
|
||||
const existingFallbacks: Fallbacks = [{ "existing-model": ["fallback-1"] }];
|
||||
mockOnChange.mockResolvedValue(undefined);
|
||||
render(<AddFallbacks {...defaultProps} value={existingFallbacks} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("fallback-selection-form")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const updateGroupButton = screen.getByTestId("update-group-button");
|
||||
await user.click(updateGroupButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Primary: gpt-4/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save all configurations/i });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
const callArgs = mockOnChange.mock.calls[0][0];
|
||||
expect(callArgs).toHaveLength(2);
|
||||
expect(callArgs[0]).toEqual({ "existing-model": ["fallback-1"] });
|
||||
expect(callArgs[1]).toHaveProperty("gpt-4");
|
||||
});
|
||||
});
|
||||
|
||||
it("should reset form state when modal is closed", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("fallback-selection-form")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle onChange error gracefully", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Save failed");
|
||||
mockOnChange.mockRejectedValue(error);
|
||||
render(<AddFallbacks {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("fallback-selection-form")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const updateGroupButton = screen.getByTestId("update-group-button");
|
||||
await user.click(updateGroupButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Primary: gpt-4/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save all configurations/i });
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call onChange when onChange prop is not provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AddFallbacks accessToken={mockAccessToken} value={[]} />);
|
||||
|
||||
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* Parent component for adding fallbacks to the proxy router config
|
||||
* Handles value/onChange logic and form submission
|
||||
* Works with forms - reads from and writes to router_settings.fallbacks
|
||||
*/
|
||||
|
||||
import { Button as TremorButton } from "@tremor/react";
|
||||
import { Button, message } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import NotificationManager from "../../../molecules/notifications_manager";
|
||||
import { fetchAvailableModels, ModelGroup } from "../../../playground/llm_calls/fetch_models";
|
||||
import { AddFallbacksModal } from "./AddFallbacksModal";
|
||||
import { FallbackGroup } from "./FallbackGroupConfig";
|
||||
import { FallbackSelectionForm } from "./FallbackSelectionForm";
|
||||
|
||||
export type FallbackEntry = { [modelName: string]: string[] };
|
||||
export type Fallbacks = FallbackEntry[];
|
||||
|
||||
interface AddFallbacksProps {
|
||||
models?: string[];
|
||||
accessToken: string;
|
||||
value?: Fallbacks; // Current fallbacks value from form
|
||||
onChange?: (fallbacks: Fallbacks) => Promise<void>; // Callback to update form value
|
||||
}
|
||||
|
||||
export default function AddFallbacks({
|
||||
models,
|
||||
accessToken,
|
||||
value = [],
|
||||
onChange,
|
||||
}: AddFallbacksProps) {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
|
||||
const [modalKey, setModalKey] = useState(0); // Key to force remount of form when modal opens
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [groups, setGroups] = useState<FallbackGroup[]>([
|
||||
{
|
||||
id: "1",
|
||||
primaryModel: null,
|
||||
fallbackModels: [],
|
||||
},
|
||||
]);
|
||||
|
||||
// Reset groups state and increment modal key when modal opens
|
||||
useEffect(() => {
|
||||
if (isModalVisible) {
|
||||
setGroups([
|
||||
{
|
||||
id: "1",
|
||||
primaryModel: null,
|
||||
fallbackModels: [],
|
||||
},
|
||||
]);
|
||||
setModalKey((prev) => prev + 1); // Force remount of form
|
||||
}
|
||||
}, [isModalVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const uniqueModels = await fetchAvailableModels(accessToken);
|
||||
console.log("Fetched models for fallbacks:", uniqueModels);
|
||||
setModelInfo(uniqueModels);
|
||||
} catch (error) {
|
||||
console.error("Error fetching model info for fallbacks:", error);
|
||||
}
|
||||
};
|
||||
if (isModalVisible) {
|
||||
loadModels();
|
||||
}
|
||||
}, [accessToken, isModalVisible]);
|
||||
|
||||
const availableModels = Array.from(new Set(modelInfo.map((option) => option.model_group))).sort();
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
// Reset to initial state
|
||||
setGroups([
|
||||
{
|
||||
id: "1",
|
||||
primaryModel: null,
|
||||
fallbackModels: [],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleSaveAll = async () => {
|
||||
// Validation
|
||||
const invalidGroups = groups.filter(
|
||||
(g) => !g.primaryModel || g.fallbackModels.length === 0,
|
||||
);
|
||||
if (invalidGroups.length > 0) {
|
||||
message.error(
|
||||
`Please complete configuration for all groups. ${invalidGroups.length} group(s) incomplete.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create fallback objects in the format expected by the API
|
||||
const newFallbacks = groups.map((g) => ({
|
||||
[g.primaryModel!]: g.fallbackModels,
|
||||
}));
|
||||
|
||||
// Get current fallbacks from form value, or an empty array if it's null/undefined
|
||||
const currentFallbacks = value || [];
|
||||
|
||||
// Add new fallbacks to the current fallbacks
|
||||
const updatedFallbacks = [...currentFallbacks, ...newFallbacks];
|
||||
|
||||
// Call onChange to update the form value and wait for it to complete
|
||||
if (onChange) {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onChange(updatedFallbacks);
|
||||
NotificationManager.success(`${groups.length} fallback configuration(s) added successfully!`);
|
||||
handleCancel();
|
||||
} catch (error) {
|
||||
// Error handling is done in handleFallbacksChange, so we don't need to show another notification here
|
||||
console.error("Error saving fallbacks:", error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
} else {
|
||||
NotificationManager.fromBackend("onChange callback not provided");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TremorButton
|
||||
className="mx-auto"
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
icon={() => <span className="mr-1">+</span>}
|
||||
>
|
||||
Add Fallbacks
|
||||
</TremorButton>
|
||||
<AddFallbacksModal open={isModalVisible} onCancel={handleCancel}>
|
||||
<FallbackSelectionForm
|
||||
key={modalKey}
|
||||
groups={groups}
|
||||
onGroupsChange={setGroups}
|
||||
availableModels={availableModels}
|
||||
maxFallbacks={5}
|
||||
maxGroups={5}
|
||||
/>
|
||||
{/* Footer with Cancel and Save buttons */}
|
||||
{groups.length > 0 && (
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 mt-6 border-t border-gray-100">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleSaveAll}
|
||||
disabled={groups.length === 0 || isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
{isSaving ? "Saving Configuration..." : "Save All Configurations"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</AddFallbacksModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AddFallbacksModal } from "./AddFallbacksModal";
|
||||
|
||||
describe("AddFallbacksModal", () => {
|
||||
const mockOnCancel = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the modal when open is true", () => {
|
||||
render(
|
||||
<AddFallbacksModal open={true} onCancel={mockOnCancel}>
|
||||
<div>Test Content</div>
|
||||
</AddFallbacksModal>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
expect(screen.getByText("Configure Model Fallbacks")).toBeInTheDocument();
|
||||
expect(screen.getByText("Manage multiple fallback chains for different models (up to 5 groups at a time)")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render the modal when open is false", () => {
|
||||
render(
|
||||
<AddFallbacksModal open={false} onCancel={mockOnCancel}>
|
||||
<div>Test Content</div>
|
||||
</AddFallbacksModal>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render children content when modal is open", () => {
|
||||
render(
|
||||
<AddFallbacksModal open={true} onCancel={mockOnCancel}>
|
||||
<div data-testid="child-content">Child Component</div>
|
||||
</AddFallbacksModal>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("child-content")).toBeInTheDocument();
|
||||
expect(screen.getByText("Child Component")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the correct title and description", () => {
|
||||
render(
|
||||
<AddFallbacksModal open={true} onCancel={mockOnCancel}>
|
||||
<div>Content</div>
|
||||
</AddFallbacksModal>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Configure Model Fallbacks")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Manage multiple fallback chains/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Modal wrapper for the fallback selection form
|
||||
* Handles modal visibility and layout, but delegates content to children
|
||||
*/
|
||||
|
||||
import { Modal } from "antd";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
interface AddFallbacksModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function AddFallbacksModal({
|
||||
open,
|
||||
onCancel,
|
||||
children,
|
||||
}: AddFallbacksModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="pb-4 border-b border-gray-100">
|
||||
<div className="flex items-center gap-2 text-gray-800">
|
||||
<div className="p-2 bg-indigo-50 rounded-lg">
|
||||
<ArrowRight className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold m-0">Configure Model Fallbacks</h2>
|
||||
<p className="text-sm text-gray-500 font-normal m-0">
|
||||
Manage multiple fallback chains for different models (up to 5 groups at a time)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
open={open}
|
||||
width={900}
|
||||
footer={null}
|
||||
onCancel={onCancel}
|
||||
maskClosable={false}
|
||||
className="top-8"
|
||||
styles={{
|
||||
body: { padding: "24px" },
|
||||
header: { padding: "24px 24px 0 24px", border: "none" },
|
||||
}}
|
||||
>
|
||||
<div className="mt-6">{children}</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
/**
|
||||
* Component for configuring a single fallback group
|
||||
* Handles primary model selection and fallback chain configuration
|
||||
*/
|
||||
|
||||
import { Select, Tooltip } from "antd";
|
||||
import { AlertCircle, ArrowDown, X } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
export interface FallbackGroup {
|
||||
id: string;
|
||||
primaryModel: string | null;
|
||||
fallbackModels: string[];
|
||||
}
|
||||
|
||||
interface FallbackGroupConfigProps {
|
||||
group: FallbackGroup;
|
||||
onChange: (updatedGroup: FallbackGroup) => void;
|
||||
availableModels: string[];
|
||||
maxFallbacks: number;
|
||||
}
|
||||
|
||||
export function FallbackGroupConfig({
|
||||
group,
|
||||
onChange,
|
||||
availableModels,
|
||||
maxFallbacks,
|
||||
}: FallbackGroupConfigProps) {
|
||||
// Filter available options for fallbacks (exclude primary only, allow already selected to be shown for deselection)
|
||||
const availableFallbackOptions = availableModels.filter(
|
||||
(m) => m !== group.primaryModel,
|
||||
);
|
||||
|
||||
const handlePrimaryChange = (value: string) => {
|
||||
let newFallbacks = [...group.fallbackModels];
|
||||
// Remove from fallbacks if it was there
|
||||
if (newFallbacks.includes(value)) {
|
||||
newFallbacks = newFallbacks.filter((m) => m !== value);
|
||||
}
|
||||
onChange({
|
||||
...group,
|
||||
primaryModel: value,
|
||||
fallbackModels: newFallbacks,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFallbackSelect = (values: string[]) => {
|
||||
// Limit to maxFallbacks
|
||||
const limitedValues = values.slice(0, maxFallbacks);
|
||||
|
||||
onChange({
|
||||
...group,
|
||||
fallbackModels: limitedValues,
|
||||
});
|
||||
};
|
||||
|
||||
const removeFallback = (indexToRemove: number) => {
|
||||
const newFallbacks = group.fallbackModels.filter((_, index) => index !== indexToRemove);
|
||||
onChange({
|
||||
...group,
|
||||
fallbackModels: newFallbacks,
|
||||
});
|
||||
};
|
||||
|
||||
const canAddMoreFallbacks = group.fallbackModels.length < maxFallbacks;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 py-4">
|
||||
{/* Primary Model Section */}
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Primary Model <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
className="w-full h-12"
|
||||
size="large"
|
||||
placeholder="Select primary model"
|
||||
value={group.primaryModel}
|
||||
onChange={handlePrimaryChange}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={availableModels.map((m) => ({ label: m, value: m }))}
|
||||
/>
|
||||
{!group.primaryModel && (
|
||||
<div className="mt-2 flex items-center gap-2 text-amber-600 text-xs bg-amber-50 p-2 rounded">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span>Select a model to begin configuring fallbacks</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Visual Connection */}
|
||||
<div className="flex items-center justify-center -my-4 z-10">
|
||||
<div className="bg-indigo-50 text-indigo-500 px-4 py-1 rounded-full text-xs font-bold border border-indigo-100 flex items-center gap-2 shadow-sm">
|
||||
<ArrowDown className="w-4 h-4" />
|
||||
IF FAILS, TRY...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fallback Models Section */}
|
||||
<div
|
||||
className={`transition-opacity duration-300 ${!group.primaryModel ? "opacity-50 pointer-events-none" : "opacity-100"}`}
|
||||
>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Fallback Chain <span className="text-red-500">*</span>
|
||||
<span className="text-xs text-gray-500 font-normal ml-2">
|
||||
(Max {maxFallbacks} fallbacks at a time)
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="bg-gray-50 rounded-xl p-4 border border-gray-200">
|
||||
{/* Add Fallback Input */}
|
||||
<div className="mb-4">
|
||||
<Select
|
||||
mode="multiple"
|
||||
className="w-full"
|
||||
size="large"
|
||||
placeholder={
|
||||
canAddMoreFallbacks
|
||||
? "Select fallback models to add..."
|
||||
: `Maximum ${maxFallbacks} fallbacks reached`
|
||||
}
|
||||
value={group.fallbackModels}
|
||||
onChange={handleFallbackSelect}
|
||||
disabled={!group.primaryModel}
|
||||
options={availableFallbackOptions.map((m) => ({
|
||||
label: m,
|
||||
value: m,
|
||||
}))}
|
||||
optionRender={(option, info) => {
|
||||
const isSelected = group.fallbackModels.includes(option.value as string);
|
||||
const orderIndex = isSelected
|
||||
? group.fallbackModels.indexOf(option.value as string) + 1
|
||||
: null;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{isSelected && orderIndex !== null && (
|
||||
<span className="flex items-center justify-center w-5 h-5 rounded bg-indigo-100 text-indigo-600 text-xs font-bold">
|
||||
{orderIndex}
|
||||
</span>
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
maxTagCount="responsive"
|
||||
maxTagPlaceholder={(omittedValues) => (
|
||||
<Tooltip
|
||||
styles={{ root: { pointerEvents: "none" } }}
|
||||
title={omittedValues.map(({ value }) => value).join(", ")}
|
||||
>
|
||||
<span>+{omittedValues.length} more</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1 ml-1">
|
||||
{canAddMoreFallbacks
|
||||
? `Search and select multiple models. Selected models will appear below in order. (${group.fallbackModels.length}/${maxFallbacks} used)`
|
||||
: `Maximum ${maxFallbacks} fallbacks reached. Remove some to add more.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Fallback List */}
|
||||
<div className="space-y-2 min-h-[100px]">
|
||||
{group.fallbackModels.length === 0 ? (
|
||||
<div className="h-32 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center text-gray-400">
|
||||
<span className="text-sm">No fallback models selected</span>
|
||||
<span className="text-xs mt-1">Add models from the dropdown above</span>
|
||||
</div>
|
||||
) : (
|
||||
group.fallbackModels.map((modelValue, index) => {
|
||||
return (
|
||||
<div
|
||||
key={`${modelValue}-${index}`}
|
||||
className="group flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200 hover:border-indigo-300 hover:shadow-sm transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded bg-gray-100 text-gray-400 group-hover:text-indigo-500 group-hover:bg-indigo-50">
|
||||
<span className="text-xs font-bold">{index + 1}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-800">{modelValue}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFallback(index)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-red-500 p-1"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* Form component for selecting and configuring fallback groups
|
||||
* Manages groups state internally, but does not handle submission
|
||||
* Decoupled from form submission logic
|
||||
*/
|
||||
|
||||
import { Button } from "@tremor/react";
|
||||
import { message, Tabs } from "antd";
|
||||
import { Plus } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FallbackGroup, FallbackGroupConfig } from "./FallbackGroupConfig";
|
||||
|
||||
interface FallbackSelectionFormProps {
|
||||
groups: FallbackGroup[];
|
||||
onGroupsChange: (groups: FallbackGroup[]) => void;
|
||||
availableModels: string[];
|
||||
maxFallbacks?: number;
|
||||
maxGroups?: number;
|
||||
}
|
||||
|
||||
export function FallbackSelectionForm({
|
||||
groups,
|
||||
onGroupsChange,
|
||||
availableModels,
|
||||
maxFallbacks = 5,
|
||||
maxGroups = 5,
|
||||
}: FallbackSelectionFormProps) {
|
||||
const [activeKey, setActiveKey] = useState(groups.length > 0 ? groups[0].id : "1");
|
||||
|
||||
// Reset activeKey when groups change (e.g., when modal reopens)
|
||||
useEffect(() => {
|
||||
if (groups.length > 0) {
|
||||
// If current activeKey doesn't exist in groups, reset to first group
|
||||
const activeKeyExists = groups.some((g) => g.id === activeKey);
|
||||
if (!activeKeyExists) {
|
||||
setActiveKey(groups[0].id);
|
||||
}
|
||||
} else {
|
||||
// If groups is empty, reset activeKey
|
||||
setActiveKey("1");
|
||||
}
|
||||
}, [groups]);
|
||||
|
||||
const handleAddGroup = () => {
|
||||
if (groups.length >= maxGroups) {
|
||||
return;
|
||||
}
|
||||
const newId = Date.now().toString();
|
||||
const newGroups = [
|
||||
...groups,
|
||||
{
|
||||
id: newId,
|
||||
primaryModel: null,
|
||||
fallbackModels: [],
|
||||
},
|
||||
];
|
||||
onGroupsChange(newGroups);
|
||||
setActiveKey(newId);
|
||||
};
|
||||
|
||||
const handleRemoveGroup = (targetId: string) => {
|
||||
if (groups.length === 1) {
|
||||
message.warning("At least one group is required");
|
||||
return;
|
||||
}
|
||||
const newGroups = groups.filter((g) => g.id !== targetId);
|
||||
onGroupsChange(newGroups);
|
||||
if (activeKey === targetId && newGroups.length > 0) {
|
||||
setActiveKey(newGroups[newGroups.length - 1].id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGroupUpdate = (updatedGroup: FallbackGroup) => {
|
||||
const newGroups = groups.map((g) => (g.id === updatedGroup.id ? updatedGroup : g));
|
||||
onGroupsChange(newGroups);
|
||||
};
|
||||
|
||||
// Generate tab items
|
||||
const items = groups.map((group, index) => {
|
||||
const label = group.primaryModel
|
||||
? group.primaryModel
|
||||
: `Group ${index + 1}`;
|
||||
return {
|
||||
key: group.id,
|
||||
label: label,
|
||||
closable: groups.length > 1, // Only allow closing if there's more than 1 group
|
||||
children: (
|
||||
<FallbackGroupConfig
|
||||
group={group}
|
||||
onChange={handleGroupUpdate}
|
||||
availableModels={availableModels}
|
||||
maxFallbacks={maxFallbacks}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
if (groups.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 bg-gray-50 rounded-lg border border-dashed border-gray-300">
|
||||
<p className="text-gray-500 mb-4">No fallback groups configured</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleAddGroup}
|
||||
icon={() => <Plus className="w-4 h-4" />}
|
||||
>
|
||||
Create First Group
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
type="editable-card"
|
||||
activeKey={activeKey}
|
||||
onChange={setActiveKey}
|
||||
onEdit={(targetKey, action) => {
|
||||
if (action === "add") handleAddGroup();
|
||||
else if (action === "remove" && groups.length > 1) {
|
||||
handleRemoveGroup(targetKey as string);
|
||||
}
|
||||
}}
|
||||
items={items}
|
||||
className="fallback-tabs"
|
||||
tabBarStyle={{
|
||||
marginBottom: 0,
|
||||
}}
|
||||
hideAdd={groups.length >= maxGroups}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Fallbacks from "./fallbacks";
|
||||
import * as networkingModule from "../../../networking";
|
||||
import * as fetchModelsModule from "../../../playground/llm_calls/fetch_models";
|
||||
|
||||
vi.mock("../../../networking", () => ({
|
||||
getCallbacksCall: vi.fn(),
|
||||
setCallbacksCall: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../playground/llm_calls/fetch_models", () => ({
|
||||
fetchAvailableModels: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openai", () => ({
|
||||
default: {
|
||||
OpenAI: vi.fn().mockImplementation(() => ({
|
||||
chat: {
|
||||
completions: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../../common_components/DeleteResourceModal", () => ({
|
||||
__esModule: true,
|
||||
default: ({ isOpen, onOk, onCancel, title, message, resourceInformation, confirmLoading }: any) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div data-testid="delete-modal">
|
||||
<div>{title}</div>
|
||||
<div>{message}</div>
|
||||
{resourceInformation?.map((info: any, idx: number) => (
|
||||
<div key={idx}>
|
||||
{info.label}: {info.value}
|
||||
</div>
|
||||
))}
|
||||
<button onClick={onCancel} disabled={confirmLoading}>
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={onOk} disabled={confirmLoading}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./AddFallbacks", () => ({
|
||||
__esModule: true,
|
||||
default: ({ value, onChange }: any) => {
|
||||
const handleClick = async () => {
|
||||
if (onChange) {
|
||||
try {
|
||||
const newFallbacks = [...(value || []), { "test-model": ["test-fallback"] }];
|
||||
await onChange(newFallbacks);
|
||||
} catch (error) {
|
||||
// Error is handled by the component
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<button onClick={handleClick} data-testid="add-fallbacks-button">
|
||||
Add Fallbacks
|
||||
</button>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Fallbacks", () => {
|
||||
const mockAccessToken = "test-token";
|
||||
const mockUserRole = "Admin";
|
||||
const mockUserID = "user-123";
|
||||
const mockModelData = {
|
||||
data: [
|
||||
{ model_name: "gpt-4" },
|
||||
{ model_name: "gpt-3.5-turbo" },
|
||||
{ model_name: "claude-3-opus" },
|
||||
],
|
||||
};
|
||||
|
||||
const mockRouterSettings = {
|
||||
fallbacks: [
|
||||
{ "gpt-4": ["gpt-3.5-turbo", "claude-3-opus"] },
|
||||
{ "claude-3-opus": ["gpt-4"] },
|
||||
],
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
accessToken: mockAccessToken,
|
||||
userRole: mockUserRole,
|
||||
userID: mockUserID,
|
||||
modelData: mockModelData,
|
||||
};
|
||||
|
||||
const findDeleteButton = (container: HTMLElement) => {
|
||||
const tableRows = container.querySelectorAll("tbody tr");
|
||||
if (tableRows.length === 0) return null;
|
||||
const firstRow = tableRows[0];
|
||||
const actionCells = firstRow.querySelectorAll("td");
|
||||
const lastCell = actionCells[actionCells.length - 1];
|
||||
const buttons = lastCell.querySelectorAll("button");
|
||||
if (buttons.length >= 2) {
|
||||
return buttons[buttons.length - 1];
|
||||
}
|
||||
const clickableElements = lastCell.querySelectorAll("[class*='cursor-pointer'], button");
|
||||
return Array.from(clickableElements).find((el) =>
|
||||
el.className.includes("red") || el.className.includes("hover:text-red")
|
||||
) || clickableElements[clickableElements.length - 1];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(networkingModule.getCallbacksCall).mockResolvedValue({
|
||||
router_settings: mockRouterSettings,
|
||||
});
|
||||
vi.mocked(networkingModule.setCallbacksCall).mockResolvedValue(undefined);
|
||||
vi.mocked(fetchModelsModule.fetchAvailableModels).mockResolvedValue([
|
||||
{ model_group: "gpt-4", mode: "chat" },
|
||||
{ model_group: "gpt-3.5-turbo", mode: "chat" },
|
||||
{ model_group: "claude-3-opus", mode: "chat" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should render the component", async () => {
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-fallbacks-button")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render when accessToken is null", () => {
|
||||
const { container } = render(<Fallbacks {...defaultProps} accessToken={null} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should fetch router settings on mount", async () => {
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networkingModule.getCallbacksCall).toHaveBeenCalledWith(
|
||||
mockAccessToken,
|
||||
mockUserID,
|
||||
mockUserRole,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should display fallback entries in table", async () => {
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("gpt-3.5-turbo, claude-3-opus")).toBeInTheDocument();
|
||||
expect(screen.getByText("claude-3-opus")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should open delete modal when delete icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButton = findDeleteButton(container);
|
||||
expect(deleteButton).not.toBeNull();
|
||||
|
||||
await user.click(deleteButton as HTMLElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("delete-modal")).toBeInTheDocument();
|
||||
expect(screen.getByText("Delete Fallback?")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete fallback when confirmed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButton = findDeleteButton(container);
|
||||
expect(deleteButton).not.toBeNull();
|
||||
|
||||
await user.click(deleteButton as HTMLElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("delete-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: /delete/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networkingModule.setCallbacksCall).toHaveBeenCalled();
|
||||
const callArgs = networkingModule.setCallbacksCall.mock.calls[0];
|
||||
expect(callArgs[0]).toBe(mockAccessToken);
|
||||
expect(callArgs[1].router_settings.fallbacks).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should close delete modal when cancel is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButton = findDeleteButton(container);
|
||||
expect(deleteButton).not.toBeNull();
|
||||
|
||||
await user.click(deleteButton as HTMLElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("delete-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("delete-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification on delete failure", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Delete failed");
|
||||
vi.mocked(networkingModule.setCallbacksCall).mockRejectedValueOnce(error);
|
||||
const { container } = render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButton = findDeleteButton(container);
|
||||
expect(deleteButton).not.toBeNull();
|
||||
|
||||
await user.click(deleteButton as HTMLElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("delete-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: /delete/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networkingModule.setCallbacksCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle delete error gracefully", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Delete failed");
|
||||
vi.mocked(networkingModule.setCallbacksCall).mockRejectedValueOnce(error);
|
||||
const { container } = render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButton = findDeleteButton(container);
|
||||
expect(deleteButton).not.toBeNull();
|
||||
|
||||
await user.click(deleteButton as HTMLElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("delete-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const confirmButton = screen.getByRole("button", { name: /delete/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networkingModule.setCallbacksCall).toHaveBeenCalled();
|
||||
expect(screen.queryByTestId("delete-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle empty fallbacks array", async () => {
|
||||
vi.mocked(networkingModule.getCallbacksCall).mockResolvedValueOnce({
|
||||
router_settings: { fallbacks: [] },
|
||||
});
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-fallbacks-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByText("gpt-4")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle router settings without fallbacks property", async () => {
|
||||
vi.mocked(networkingModule.getCallbacksCall).mockResolvedValueOnce({
|
||||
router_settings: {},
|
||||
});
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-fallbacks-button")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove model_group_retry_policy from router settings", async () => {
|
||||
vi.mocked(networkingModule.getCallbacksCall).mockResolvedValueOnce({
|
||||
router_settings: {
|
||||
...mockRouterSettings,
|
||||
model_group_retry_policy: { some: "policy" },
|
||||
},
|
||||
});
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networkingModule.getCallbacksCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should update fallbacks when AddFallbacks onChange is called", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-fallbacks-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const addButton = screen.getByTestId("add-fallbacks-button");
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networkingModule.setCallbacksCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle fallbacks change error and refetch", async () => {
|
||||
const user = userEvent.setup();
|
||||
const error = new Error("Update failed");
|
||||
vi.mocked(networkingModule.setCallbacksCall).mockRejectedValueOnce(error);
|
||||
vi.mocked(networkingModule.getCallbacksCall).mockResolvedValue({
|
||||
router_settings: mockRouterSettings,
|
||||
});
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("add-fallbacks-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const addButton = screen.getByTestId("add-fallbacks-button");
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networkingModule.setCallbacksCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(networkingModule.getCallbacksCall).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -3,10 +3,10 @@ import { Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow
|
|||
import { Tooltip } from "antd";
|
||||
import openai from "openai";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AddFallbacks from "./add_fallbacks";
|
||||
import DeleteResourceModal from "./common_components/DeleteResourceModal";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { getCallbacksCall, setCallbacksCall } from "./networking";
|
||||
import DeleteResourceModal from "../../../common_components/DeleteResourceModal";
|
||||
import NotificationsManager from "../../../molecules/notifications_manager";
|
||||
import { getCallbacksCall, setCallbacksCall } from "../../../networking";
|
||||
import AddFallbacks from "./AddFallbacks";
|
||||
|
||||
type FallbackEntry = { [modelName: string]: string[] };
|
||||
type Fallbacks = FallbackEntry[];
|
||||
|
|
@ -21,7 +21,7 @@ interface FallbacksProps {
|
|||
async function testFallbackModelResponse(selectedModel: string, accessToken: string) {
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal != true) {
|
||||
console.log = function () {};
|
||||
console.log = function () { };
|
||||
}
|
||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : window.location.origin;
|
||||
const client = new openai.OpenAI({
|
||||
|
|
@ -142,13 +142,48 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, mo
|
|||
return null;
|
||||
}
|
||||
|
||||
const handleFallbacksChange = async (fallbacks: Fallbacks): Promise<void> => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSettings = {
|
||||
...routerSettings,
|
||||
fallbacks: fallbacks,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
router_settings: updatedSettings,
|
||||
};
|
||||
|
||||
try {
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
// Update UI only after successful API call
|
||||
setRouterSettings(updatedSettings);
|
||||
} catch (error) {
|
||||
// Revert on error by refetching from server
|
||||
NotificationsManager.fromBackend("Failed to update router settings: " + error);
|
||||
if (accessToken && userRole && userID) {
|
||||
getCallbacksCall(accessToken, userID, userRole).then((data) => {
|
||||
let router_settings = data.router_settings;
|
||||
if ("model_group_retry_policy" in router_settings) {
|
||||
delete router_settings["model_group_retry_policy"];
|
||||
}
|
||||
setRouterSettings(router_settings);
|
||||
});
|
||||
}
|
||||
// Re-throw error so caller can handle it
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddFallbacks
|
||||
models={modelData?.data ? modelData.data.map((data: any) => data.model_name) : []}
|
||||
accessToken={accessToken}
|
||||
routerSettings={routerSettings}
|
||||
setRouterSettings={setRouterSettings}
|
||||
accessToken={accessToken || ""}
|
||||
value={routerSettings.fallbacks || []}
|
||||
onChange={handleFallbacksChange}
|
||||
/>
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import AddFallbacks from "./add_fallbacks";
|
||||
|
||||
vi.mock("./networking", () => ({
|
||||
setCallbacksCall: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./playground/llm_calls/fetch_models", () => ({
|
||||
fetchAvailableModels: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{ model_group: "gpt-4" },
|
||||
{ model_group: "gpt-3.5-turbo" },
|
||||
{ model_group: "claude-3-opus" },
|
||||
{ model_group: "claude-3-sonnet" },
|
||||
]),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./molecules/notifications_manager", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
fromBackend: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("AddFallbacks", () => {
|
||||
const mockAccessToken = "test-token";
|
||||
const mockRouterSettings = { fallbacks: [] };
|
||||
const mockSetRouterSettings = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the component", () => {
|
||||
render(
|
||||
<AddFallbacks
|
||||
accessToken={mockAccessToken}
|
||||
routerSettings={mockRouterSettings}
|
||||
setRouterSettings={mockSetRouterSettings}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: /Add Fallbacks/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
/**
|
||||
* Modal to add fallbacks to the proxy router config
|
||||
*/
|
||||
|
||||
import { Button } from "@tremor/react";
|
||||
import { Form, Modal, Select } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import NotificationManager from "./molecules/notifications_manager";
|
||||
import { setCallbacksCall } from "./networking";
|
||||
import { fetchAvailableModels, ModelGroup } from "./playground/llm_calls/fetch_models";
|
||||
|
||||
interface AddFallbacksProps {
|
||||
models?: string[];
|
||||
accessToken: string;
|
||||
routerSettings: { [key: string]: any };
|
||||
setRouterSettings: React.Dispatch<React.SetStateAction<{ [key: string]: any }>>;
|
||||
}
|
||||
|
||||
const AddFallbacks: React.FC<AddFallbacksProps> = ({ models, accessToken, routerSettings, setRouterSettings }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState("");
|
||||
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
|
||||
const [selectedFallbacks, setSelectedFallbacks] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const uniqueModels = await fetchAvailableModels(accessToken);
|
||||
console.log("Fetched models for fallbacks:", uniqueModels);
|
||||
setModelInfo(uniqueModels);
|
||||
} catch (error) {
|
||||
console.error("Error fetching model info for fallbacks:", error);
|
||||
}
|
||||
};
|
||||
loadModels();
|
||||
}, [accessToken]);
|
||||
const handleOk = () => {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedFallbacks([]);
|
||||
setSelectedModel("");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedFallbacks([]);
|
||||
setSelectedModel("");
|
||||
};
|
||||
|
||||
const updateFallbacks = (formValues: Record<string, any>) => {
|
||||
// Print the received value
|
||||
console.log(formValues);
|
||||
|
||||
// Extract model_name and models from formValues
|
||||
const { model_name, models } = formValues;
|
||||
|
||||
// Create new fallback
|
||||
const newFallback = { [model_name]: models };
|
||||
|
||||
// Get current fallbacks, or an empty array if it's null
|
||||
const currentFallbacks = routerSettings.fallbacks || [];
|
||||
|
||||
// Add new fallback to the current fallbacks
|
||||
const updatedFallbacks = [...currentFallbacks, newFallback];
|
||||
|
||||
// Create a new routerSettings object with updated fallbacks
|
||||
const updatedRouterSettings = { ...routerSettings, fallbacks: updatedFallbacks };
|
||||
|
||||
// Print updated routerSettings
|
||||
console.log(updatedRouterSettings);
|
||||
|
||||
const payload = {
|
||||
router_settings: updatedRouterSettings,
|
||||
};
|
||||
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
// Update routerSettings state
|
||||
setRouterSettings(updatedRouterSettings);
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to update router settings: " + error);
|
||||
}
|
||||
|
||||
NotificationManager.success("router settings updated successfully");
|
||||
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedFallbacks([]);
|
||||
setSelectedModel("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button className="mx-auto" onClick={() => setIsModalVisible(true)} icon={() => <span className="mr-1">+</span>}>
|
||||
Add Fallbacks
|
||||
</Button>
|
||||
<Modal
|
||||
title={
|
||||
<div className="pb-4 border-b border-gray-100">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Add Fallbacks</h2>
|
||||
</div>
|
||||
}
|
||||
open={isModalVisible}
|
||||
width={900}
|
||||
footer={null}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
className="top-8"
|
||||
styles={{
|
||||
body: { padding: "24px" },
|
||||
header: { padding: "24px 24px 0 24px", border: "none" },
|
||||
}}
|
||||
>
|
||||
<div className="mt-6">
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-600">
|
||||
Configure fallback models to improve reliability. When the primary model fails or is unavailable, requests
|
||||
will automatically route to the specified fallback models in order.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form form={form} onFinish={updateFallbacks} layout="vertical" className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Primary Model <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="model_name"
|
||||
rules={[{ required: true, message: "Please select the primary model that needs fallbacks" }]}
|
||||
className="!mb-0"
|
||||
>
|
||||
<Select
|
||||
placeholder="Select the model that needs fallback protection"
|
||||
value={selectedModel || undefined}
|
||||
onChange={(value: string) => {
|
||||
setSelectedModel(value);
|
||||
// Remove the selected model from fallbacks if it was selected
|
||||
const updatedFallbacks = selectedFallbacks.filter((model) => model !== value);
|
||||
setSelectedFallbacks(updatedFallbacks);
|
||||
form.setFieldValue("models", updatedFallbacks);
|
||||
form.setFieldValue("model_name", value);
|
||||
}}
|
||||
showSearch
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{Array.from(new Set(modelInfo.map((option) => option.model_group))).map(
|
||||
(model: string, index: number) => (
|
||||
<Select.Option key={index} value={model}>
|
||||
{model}
|
||||
</Select.Option>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
<p className="text-sm text-gray-500 mt-1">This is the primary model that users will request</p>
|
||||
</Form.Item>
|
||||
|
||||
<div className="border-t border-gray-200 my-6"></div>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Fallback Models (select multiple) <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="models"
|
||||
rules={[{ required: true, message: "Please select at least one fallback model" }]}
|
||||
className="!mb-0"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{/* Show selected models in order */}
|
||||
{selectedFallbacks.length > 0 && (
|
||||
<div className="border border-gray-200 rounded-lg p-3 bg-gray-50">
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">Fallback Order:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedFallbacks.map((model, index) => (
|
||||
<div
|
||||
key={model}
|
||||
className="flex items-center bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"
|
||||
>
|
||||
<span className="font-medium mr-2">{index + 1}.</span>
|
||||
<span>{model}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newFallbacks = selectedFallbacks.filter((m) => m !== model);
|
||||
setSelectedFallbacks(newFallbacks);
|
||||
form.setFieldValue("models", newFallbacks);
|
||||
}}
|
||||
className="ml-2 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model selector */}
|
||||
<Select
|
||||
placeholder="Add a fallback model"
|
||||
value={undefined}
|
||||
onChange={(value: string) => {
|
||||
if (value && !selectedFallbacks.includes(value)) {
|
||||
const newFallbacks = [...selectedFallbacks, value];
|
||||
setSelectedFallbacks(newFallbacks);
|
||||
form.setFieldValue("models", newFallbacks);
|
||||
}
|
||||
}}
|
||||
showSearch
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{Array.from(new Set(modelInfo.map((option) => option.model_group)))
|
||||
.filter((data: string) => data !== selectedModel && !selectedFallbacks.includes(data))
|
||||
.sort()
|
||||
.map((model: string) => (
|
||||
<Select.Option key={model} value={model}>
|
||||
{model}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
<strong>Order matters:</strong> Models will be tried in the order shown above (1st, 2nd, 3rd, etc.)
|
||||
</p>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
|
||||
<Button variant="secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Add Fallbacks
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddFallbacks;
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Fallbacks from "./fallbacks";
|
||||
import { getCallbacksCall, setCallbacksCall } from "./networking";
|
||||
|
||||
vi.mock("./networking", () => ({
|
||||
getCallbacksCall: vi.fn(),
|
||||
setCallbacksCall: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./add_fallbacks", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>Mock Add Fallbacks</div>,
|
||||
}));
|
||||
|
||||
vi.mock("openai", () => ({
|
||||
default: {
|
||||
OpenAI: vi.fn().mockImplementation(() => ({
|
||||
chat: {
|
||||
completions: {
|
||||
create: vi.fn().mockResolvedValue({
|
||||
model: "test-model",
|
||||
}),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Fallbacks", () => {
|
||||
const defaultProps = {
|
||||
accessToken: "token",
|
||||
userRole: "admin",
|
||||
userID: "user-123",
|
||||
modelData: { data: [] },
|
||||
};
|
||||
const mockGetCallbacksCall = vi.mocked(getCallbacksCall);
|
||||
const mockSetCallbacksCall = vi.mocked(setCallbacksCall);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetCallbacksCall.mockResolvedValue({
|
||||
router_settings: {
|
||||
fallbacks: [],
|
||||
},
|
||||
});
|
||||
mockSetCallbacksCall.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("columnheader", { name: "Model Name" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render fallback data when callback data is returned from network call", async () => {
|
||||
const mockFallbackData = {
|
||||
router_settings: {
|
||||
fallbacks: [{ "xai/grok-2": ["xai/grok-4", "gpt-4"] }, { "gpt-3.5-turbo": ["gpt-4"] }],
|
||||
},
|
||||
};
|
||||
|
||||
mockGetCallbacksCall.mockResolvedValue(mockFallbackData);
|
||||
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("xai/grok-2")).toBeInTheDocument();
|
||||
expect(screen.getByText("xai/grok-4, gpt-4")).toBeInTheDocument();
|
||||
expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument();
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(mockGetCallbacksCall).toHaveBeenCalledWith(
|
||||
defaultProps.accessToken,
|
||||
defaultProps.userID,
|
||||
defaultProps.userRole,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render AddFallbacks component", async () => {
|
||||
render(<Fallbacks {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Mock Add Fallbacks")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render when access token is not provided", () => {
|
||||
const { container } = render(<Fallbacks {...defaultProps} accessToken={null} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -22,7 +22,7 @@ import { InputNumber } from "antd";
|
|||
import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline";
|
||||
|
||||
import RouterSettings from "./router_settings";
|
||||
import Fallbacks from "./fallbacks";
|
||||
import Fallbacks from "./Settings/RouterSettings/Fallbacks/Fallbacks";
|
||||
interface GeneralSettingsPageProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
|||
import {
|
||||
ApiOutlined,
|
||||
AppstoreOutlined,
|
||||
AuditOutlined,
|
||||
BankOutlined,
|
||||
BarChartOutlined,
|
||||
BgColorsOutlined,
|
||||
|
|
@ -123,6 +124,13 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
icon: <SafetyOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "policies",
|
||||
page: "policies",
|
||||
label: "Policies",
|
||||
icon: <AuditOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "tools",
|
||||
page: "tools",
|
||||
|
|
|
|||
|
|
@ -5347,6 +5347,249 @@ export const getGuardrailsList = async (accessToken: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Policy CRUD API Calls
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const getPoliciesList = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/list` : `/policies/list`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get policies list:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createPolicyCall = async (accessToken: string, policyData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies` : `/policies`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(policyData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create policy:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePolicyCall = async (accessToken: string, policyId: string, policyData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(policyData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to update policy:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePolicyCall = async (accessToken: string, policyId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete policy:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPolicyInfo = async (accessToken: string, policyId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get policy info:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Policy Attachments API Calls
|
||||
|
||||
export const getPolicyAttachmentsList = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/list` : `/policies/attachments/list`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get policy attachments list:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createPolicyAttachmentCall = async (accessToken: string, attachmentData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments` : `/policies/attachments`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(attachmentData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create policy attachment:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePolicyAttachmentCall = async (accessToken: string, attachmentId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/${attachmentId}` : `/policies/attachments/${attachmentId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete policy attachment:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getResolvedGuardrails = async (accessToken: string, policyId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}/resolved-guardrails` : `/policies/${policyId}/resolved-guardrails`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get resolved guardrails:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPromptsList = async (accessToken: string): Promise<ListPromptsResponse> => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/list` : `/prompts/list`;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { truncateString } from "../../../utils/textUtils";
|
||||
import GuardrailSelector from "../../guardrails/GuardrailSelector";
|
||||
import PolicySelector from "../../policies/PolicySelector";
|
||||
import { MCPServer } from "../../mcp_tools/types";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
import { fetchMCPServers, listMCPTools } from "../../networking";
|
||||
|
|
@ -188,6 +189,15 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
return [];
|
||||
}
|
||||
});
|
||||
const [selectedPolicies, setSelectedPolicies] = useState<string[]>(() => {
|
||||
const saved = sessionStorage.getItem("selectedPolicies");
|
||||
try {
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch (error) {
|
||||
console.error("Error parsing selectedPolicies from sessionStorage", error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const [messageTraceId, setMessageTraceId] = useState<string | null>(
|
||||
() => sessionStorage.getItem("messageTraceId") || null,
|
||||
);
|
||||
|
|
@ -261,6 +271,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedPolicies,
|
||||
selectedMCPServers,
|
||||
mcpServers,
|
||||
mcpServerToolRestrictions,
|
||||
|
|
@ -283,6 +294,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedPolicies,
|
||||
selectedMCPServers,
|
||||
mcpServers,
|
||||
mcpServerToolRestrictions,
|
||||
|
|
@ -308,6 +320,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
sessionStorage.setItem("selectedTags", JSON.stringify(selectedTags));
|
||||
sessionStorage.setItem("selectedVectorStores", JSON.stringify(selectedVectorStores));
|
||||
sessionStorage.setItem("selectedGuardrails", JSON.stringify(selectedGuardrails));
|
||||
sessionStorage.setItem("selectedPolicies", JSON.stringify(selectedPolicies));
|
||||
sessionStorage.setItem("selectedMCPServers", JSON.stringify(selectedMCPServers));
|
||||
sessionStorage.setItem("mcpServerToolRestrictions", JSON.stringify(mcpServerToolRestrictions));
|
||||
sessionStorage.setItem("selectedVoice", selectedVoice);
|
||||
|
|
@ -338,6 +351,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedPolicies,
|
||||
messageTraceId,
|
||||
responsesSessionId,
|
||||
useApiSessionManagement,
|
||||
|
|
@ -897,6 +911,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
traceId,
|
||||
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
selectedMCPServers,
|
||||
updateChatImageUI,
|
||||
updateSearchResults,
|
||||
|
|
@ -977,6 +992,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
traceId,
|
||||
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
selectedMCPServers, // Pass the selected servers array
|
||||
useApiSessionManagement ? responsesSessionId : null, // Only pass session ID if API mode is enabled
|
||||
handleResponseId, // Pass callback to capture new response ID
|
||||
|
|
@ -1008,6 +1024,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
traceId,
|
||||
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
selectedMCPServers, // Pass the selected tools array
|
||||
customProxyBaseUrl || undefined,
|
||||
);
|
||||
|
|
@ -1587,6 +1604,32 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<SafetyOutlined className="mr-2" /> Policies
|
||||
<Tooltip
|
||||
className="ml-1"
|
||||
title={
|
||||
<span>
|
||||
Select policy/policies to apply to this LLM API call. Policies define which guardrails are applied based on conditions. You can set up your policies{" "}
|
||||
<a href="?page=policies" style={{ color: "#1890ff" }}>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Text>
|
||||
<PolicySelector
|
||||
value={selectedPolicies}
|
||||
onChange={setSelectedPolicies}
|
||||
className="mb-4"
|
||||
accessToken={accessToken || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Code Interpreter Toggle - Only for Responses endpoint */}
|
||||
{endpointType === EndpointType.RESPONSES && (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ interface CodeGenMetadata {
|
|||
tags?: string[];
|
||||
vector_stores?: string[];
|
||||
guardrails?: string[];
|
||||
policies?: string[];
|
||||
}
|
||||
|
||||
interface GenerateCodeParams {
|
||||
|
|
@ -17,6 +18,7 @@ interface GenerateCodeParams {
|
|||
selectedTags: string[];
|
||||
selectedVectorStores: string[];
|
||||
selectedGuardrails: string[];
|
||||
selectedPolicies: string[];
|
||||
selectedMCPServers: string[];
|
||||
mcpServers?: MCPServer[];
|
||||
mcpServerToolRestrictions?: Record<string, string[]>;
|
||||
|
|
@ -40,6 +42,7 @@ export const generateCodeSnippet = (params: GenerateCodeParams): string => {
|
|||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedPolicies,
|
||||
selectedMCPServers,
|
||||
mcpServers,
|
||||
mcpServerToolRestrictions,
|
||||
|
|
@ -72,6 +75,7 @@ export const generateCodeSnippet = (params: GenerateCodeParams): string => {
|
|||
if (selectedTags.length > 0) metadata.tags = selectedTags;
|
||||
if (selectedVectorStores.length > 0) metadata.vector_stores = selectedVectorStores;
|
||||
if (selectedGuardrails.length > 0) metadata.guardrails = selectedGuardrails;
|
||||
if (selectedPolicies.length > 0) metadata.policies = selectedPolicies;
|
||||
|
||||
const modelNameForCode = selectedModel || "your-model-name";
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export async function makeAnthropicMessagesRequest(
|
|||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
policies?: string[],
|
||||
selectedMCPTools?: string[],
|
||||
customBaseUrl?: string,
|
||||
) {
|
||||
|
|
@ -59,6 +60,7 @@ export async function makeAnthropicMessagesRequest(
|
|||
|
||||
if (vector_store_ids) requestBody.vector_store_ids = vector_store_ids;
|
||||
if (guardrails) requestBody.guardrails = guardrails;
|
||||
if (policies) requestBody.policies = policies;
|
||||
// Use the streaming helper method for cleaner async iteration
|
||||
// @ts-ignore - The SDK types might not include all litellm-specific parameters
|
||||
const stream = client.messages.stream(requestBody, { signal });
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export async function makeOpenAIChatCompletionRequest(
|
|||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
policies?: string[],
|
||||
selectedMCPServers?: string[],
|
||||
onImageGenerated?: (imageUrl: string, model?: string) => void,
|
||||
onSearchResults?: (searchResults: VectorStoreSearchResponse[]) => void,
|
||||
|
|
@ -110,6 +111,7 @@ export async function makeOpenAIChatCompletionRequest(
|
|||
messages: chatHistory as ChatCompletionMessageParam[],
|
||||
...(vector_store_ids ? { vector_store_ids } : {}),
|
||||
...(guardrails ? { guardrails } : {}),
|
||||
...(policies ? { policies } : {}),
|
||||
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
...(max_tokens !== undefined ? { max_tokens } : {}),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export async function makeOpenAIResponsesRequest(
|
|||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
policies?: string[],
|
||||
selectedMCPServers?: string[],
|
||||
previousResponseId?: string | null,
|
||||
onResponseId?: (responseId: string) => void,
|
||||
|
|
@ -137,6 +138,7 @@ export async function makeOpenAIResponsesRequest(
|
|||
...(previousResponseId ? { previous_response_id: previousResponseId } : {}),
|
||||
...(vector_store_ids ? { vector_store_ids } : {}),
|
||||
...(guardrails ? { guardrails } : {}),
|
||||
...(policies ? { policies } : {}),
|
||||
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
|
||||
},
|
||||
{ signal },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import { Policy } from "./types";
|
||||
import { getPoliciesList } from "../networking";
|
||||
|
||||
interface PolicySelectorProps {
|
||||
onChange: (selectedPolicies: string[]) => void;
|
||||
value?: string[];
|
||||
className?: string;
|
||||
accessToken: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const PolicySelector: React.FC<PolicySelectorProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
className,
|
||||
accessToken,
|
||||
disabled
|
||||
}) => {
|
||||
const [policies, setPolicies] = useState<Policy[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPolicies = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getPoliciesList(accessToken);
|
||||
console.log("Policies response:", response);
|
||||
if (response.policies) {
|
||||
console.log("Policies data:", response.policies);
|
||||
setPolicies(response.policies);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching policies:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPolicies();
|
||||
}, [accessToken]);
|
||||
|
||||
const handlePolicyChange = (selectedValues: string[]) => {
|
||||
console.log("Selected policies:", selectedValues);
|
||||
onChange(selectedValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
mode="multiple"
|
||||
disabled={disabled}
|
||||
placeholder={disabled ? "Setting policies is a premium feature." : "Select policies"}
|
||||
onChange={handlePolicyChange}
|
||||
value={value}
|
||||
loading={loading}
|
||||
className={className}
|
||||
allowClear
|
||||
options={policies.map((policy) => {
|
||||
console.log("Mapping policy:", policy);
|
||||
return {
|
||||
label: `${policy.policy_name}${policy.description ? ` - ${policy.description}` : ""}`,
|
||||
value: policy.policy_name,
|
||||
};
|
||||
})}
|
||||
optionFilterProp="label"
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicySelector;
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
|
||||
import { Button } from "@tremor/react";
|
||||
import { Policy, PolicyAttachmentCreateRequest } from "./types";
|
||||
import { createPolicyAttachmentCall, teamListCall, keyInfoCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface AddAttachmentFormProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
policies: Policy[];
|
||||
}
|
||||
|
||||
const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSuccess,
|
||||
accessToken,
|
||||
policies,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [scopeType, setScopeType] = useState<"global" | "specific">("global");
|
||||
const [availableTeams, setAvailableTeams] = useState<string[]>([]);
|
||||
const [availableKeys, setAvailableKeys] = useState<string[]>([]);
|
||||
const [isLoadingTeams, setIsLoadingTeams] = useState(false);
|
||||
const [isLoadingKeys, setIsLoadingKeys] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && accessToken) {
|
||||
loadTeamsAndKeys();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, accessToken]);
|
||||
|
||||
const loadTeamsAndKeys = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
// Load teams
|
||||
setIsLoadingTeams(true);
|
||||
try {
|
||||
const teamsResponse = await teamListCall(accessToken);
|
||||
if (teamsResponse?.data) {
|
||||
const teamAliases = teamsResponse.data
|
||||
.map((t: any) => t.team_alias)
|
||||
.filter(Boolean);
|
||||
setAvailableTeams(teamAliases);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load teams:", error);
|
||||
} finally {
|
||||
setIsLoadingTeams(false);
|
||||
}
|
||||
|
||||
// Load keys
|
||||
setIsLoadingKeys(true);
|
||||
try {
|
||||
const keysResponse = await keyInfoCall(accessToken, null, null);
|
||||
if (keysResponse?.data) {
|
||||
const keyAliases = keysResponse.data
|
||||
.map((k: any) => k.key_alias)
|
||||
.filter(Boolean);
|
||||
setAvailableKeys(keyAliases);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load keys:", error);
|
||||
} finally {
|
||||
setIsLoadingKeys(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.resetFields();
|
||||
setScopeType("global");
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error("No access token available");
|
||||
}
|
||||
|
||||
const data: PolicyAttachmentCreateRequest = {
|
||||
policy_name: values.policy_name,
|
||||
};
|
||||
|
||||
if (scopeType === "global") {
|
||||
data.scope = "*";
|
||||
} else {
|
||||
if (values.teams && values.teams.length > 0) {
|
||||
data.teams = values.teams;
|
||||
}
|
||||
if (values.keys && values.keys.length > 0) {
|
||||
data.keys = values.keys;
|
||||
}
|
||||
if (values.models && values.models.length > 0) {
|
||||
data.models = values.models;
|
||||
}
|
||||
}
|
||||
|
||||
await createPolicyAttachmentCall(accessToken, data);
|
||||
NotificationsManager.success("Attachment created successfully");
|
||||
|
||||
resetForm();
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to create attachment:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to create attachment: " + (error instanceof Error ? error.message : String(error))
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const policyOptions = policies.map((p) => ({
|
||||
label: p.policy_name,
|
||||
value: p.policy_name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Create Policy Attachment"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
scope_type: "global",
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="policy_name"
|
||||
label="Policy"
|
||||
rules={[{ required: true, message: "Please select a policy" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Select a policy to attach"
|
||||
options={policyOptions}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Scope</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item label="Scope Type">
|
||||
<Radio.Group
|
||||
value={scopeType}
|
||||
onChange={(e) => setScopeType(e.target.value)}
|
||||
>
|
||||
<Radio value="global">Global (applies to all requests)</Radio>
|
||||
<Radio value="specific">Specific (teams, keys, or models)</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{scopeType === "specific" && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="teams"
|
||||
label="Teams"
|
||||
tooltip="Select team aliases or enter custom patterns. Supports wildcards (e.g., healthcare-*)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={isLoadingTeams ? "Loading teams..." : "Select or enter team aliases"}
|
||||
loading={isLoadingTeams}
|
||||
options={availableTeams.map((team) => ({
|
||||
label: team,
|
||||
value: team,
|
||||
}))}
|
||||
tokenSeparators={[","]}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="keys"
|
||||
label="Keys"
|
||||
tooltip="Select key aliases or enter custom patterns. Supports wildcards (e.g., dev-*)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={isLoadingKeys ? "Loading keys..." : "Select or enter key aliases"}
|
||||
loading={isLoadingKeys}
|
||||
options={availableKeys.map((key) => ({
|
||||
label: key,
|
||||
value: key,
|
||||
}))}
|
||||
tokenSeparators={[","]}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="models"
|
||||
label="Models"
|
||||
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Enter model names (e.g., gpt-4, bedrock/*)"
|
||||
tokenSeparators={[","]}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={isSubmitting}>
|
||||
Create Attachment
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAttachmentForm;
|
||||
395
ui/litellm-dashboard/src/components/policies/add_policy_form.tsx
Normal file
395
ui/litellm-dashboard/src/components/policies/add_policy_form.tsx
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Select, Modal, Divider, Typography, Tag, Alert, Radio } from "antd";
|
||||
import { Button, TextInput, Textarea } from "@tremor/react";
|
||||
import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types";
|
||||
import { Guardrail } from "../guardrails/types";
|
||||
import { createPolicyCall, updatePolicyCall, getResolvedGuardrails, modelAvailableCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
interface AddPolicyFormProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
editingPolicy?: Policy | null;
|
||||
existingPolicies: Policy[];
|
||||
availableGuardrails: Guardrail[];
|
||||
}
|
||||
|
||||
const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSuccess,
|
||||
accessToken,
|
||||
editingPolicy,
|
||||
existingPolicies,
|
||||
availableGuardrails,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
|
||||
const [isLoadingResolved, setIsLoadingResolved] = useState(false);
|
||||
const [modelConditionType, setModelConditionType] = useState<"model" | "regex">("model");
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
|
||||
const isEditing = !!editingPolicy;
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && editingPolicy) {
|
||||
const modelCondition = editingPolicy.condition?.model;
|
||||
// Detect if it's a regex pattern (contains *, ., [, ], etc.)
|
||||
const isRegex = modelCondition && /[.*+?^${}()|[\]\\]/.test(modelCondition);
|
||||
setModelConditionType(isRegex ? "regex" : "model");
|
||||
|
||||
form.setFieldsValue({
|
||||
policy_name: editingPolicy.policy_name,
|
||||
description: editingPolicy.description,
|
||||
inherit: editingPolicy.inherit,
|
||||
guardrails_add: editingPolicy.guardrails_add || [],
|
||||
guardrails_remove: editingPolicy.guardrails_remove || [],
|
||||
model_condition: modelCondition,
|
||||
});
|
||||
// Load resolved guardrails for editing
|
||||
if (editingPolicy.policy_id && accessToken) {
|
||||
loadResolvedGuardrails(editingPolicy.policy_id);
|
||||
}
|
||||
} else if (visible) {
|
||||
form.resetFields();
|
||||
setResolvedGuardrails([]);
|
||||
setModelConditionType("model");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, editingPolicy, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && accessToken) {
|
||||
loadAvailableModels();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, accessToken]);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
const response = await modelAvailableCall(accessToken, null, null, null);
|
||||
if (response?.data) {
|
||||
const models = response.data.map((m: any) => m.id || m.model_name).filter(Boolean);
|
||||
setAvailableModels(models);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load available models:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadResolvedGuardrails = async (policyId: string) => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setIsLoadingResolved(true);
|
||||
try {
|
||||
const data = await getResolvedGuardrails(accessToken, policyId);
|
||||
setResolvedGuardrails(data.resolved_guardrails || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to load resolved guardrails:", error);
|
||||
} finally {
|
||||
setIsLoadingResolved(false);
|
||||
}
|
||||
};
|
||||
|
||||
const computeResolvedGuardrails = (): string[] => {
|
||||
const values = form.getFieldsValue(true);
|
||||
const inheritFrom = values.inherit;
|
||||
const guardrailsAdd = values.guardrails_add || [];
|
||||
const guardrailsRemove = values.guardrails_remove || [];
|
||||
|
||||
let resolved = new Set<string>();
|
||||
|
||||
// If inheriting, find parent policy and get its guardrails
|
||||
if (inheritFrom) {
|
||||
const parentPolicy = existingPolicies.find(p => p.policy_name === inheritFrom);
|
||||
if (parentPolicy) {
|
||||
// Recursively resolve parent's guardrails
|
||||
const parentResolved = resolveParentGuardrails(parentPolicy);
|
||||
parentResolved.forEach(g => resolved.add(g));
|
||||
}
|
||||
}
|
||||
|
||||
// Add guardrails
|
||||
guardrailsAdd.forEach((g: string) => resolved.add(g));
|
||||
|
||||
// Remove guardrails
|
||||
guardrailsRemove.forEach((g: string) => resolved.delete(g));
|
||||
|
||||
return Array.from(resolved).sort();
|
||||
};
|
||||
|
||||
const resolveParentGuardrails = (policy: Policy): string[] => {
|
||||
let resolved = new Set<string>();
|
||||
|
||||
// If parent inherits, resolve recursively
|
||||
if (policy.inherit) {
|
||||
const grandparent = existingPolicies.find(p => p.policy_name === policy.inherit);
|
||||
if (grandparent) {
|
||||
const grandparentResolved = resolveParentGuardrails(grandparent);
|
||||
grandparentResolved.forEach(g => resolved.add(g));
|
||||
}
|
||||
}
|
||||
|
||||
// Add parent's guardrails
|
||||
if (policy.guardrails_add) {
|
||||
policy.guardrails_add.forEach(g => resolved.add(g));
|
||||
}
|
||||
|
||||
// Remove parent's removed guardrails
|
||||
if (policy.guardrails_remove) {
|
||||
policy.guardrails_remove.forEach(g => resolved.delete(g));
|
||||
}
|
||||
|
||||
return Array.from(resolved);
|
||||
};
|
||||
|
||||
// Recompute resolved guardrails when form values change
|
||||
const handleFormChange = () => {
|
||||
const resolved = computeResolvedGuardrails();
|
||||
setResolvedGuardrails(resolved);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error("No access token available");
|
||||
}
|
||||
|
||||
const data: PolicyCreateRequest | PolicyUpdateRequest = {
|
||||
policy_name: values.policy_name,
|
||||
description: values.description || undefined,
|
||||
inherit: values.inherit || undefined,
|
||||
guardrails_add: values.guardrails_add || [],
|
||||
guardrails_remove: values.guardrails_remove || [],
|
||||
condition: values.model_condition
|
||||
? { model: values.model_condition }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (isEditing && editingPolicy) {
|
||||
await updatePolicyCall(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest);
|
||||
NotificationsManager.success("Policy updated successfully");
|
||||
} else {
|
||||
await createPolicyCall(accessToken, data as PolicyCreateRequest);
|
||||
NotificationsManager.success("Policy created successfully");
|
||||
}
|
||||
|
||||
resetForm();
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to save policy:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to save policy: " + (error instanceof Error ? error.message : String(error))
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const guardrailOptions = availableGuardrails.map((g) => ({
|
||||
label: g.guardrail_name || g.guardrail_id,
|
||||
value: g.guardrail_name || g.guardrail_id,
|
||||
}));
|
||||
|
||||
const policyOptions = existingPolicies
|
||||
.filter((p) => !editingPolicy || p.policy_id !== editingPolicy.policy_id)
|
||||
.map((p) => ({
|
||||
label: p.policy_name,
|
||||
value: p.policy_name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isEditing ? "Edit Policy" : "Create New Policy"}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
guardrails_add: [],
|
||||
guardrails_remove: [],
|
||||
}}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="policy_name"
|
||||
label="Policy Name"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter a policy name" },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_-]+$/,
|
||||
message:
|
||||
"Policy name can only contain letters, numbers, hyphens, and underscores",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="e.g., global-baseline, healthcare-compliance"
|
||||
disabled={isEditing}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Description">
|
||||
<Textarea
|
||||
rows={2}
|
||||
placeholder="Describe what this policy does..."
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Inheritance</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="inherit"
|
||||
label="Inherit From"
|
||||
tooltip="Inherit guardrails from another policy. The child policy will include all guardrails from the parent."
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="Select a parent policy (optional)"
|
||||
options={policyOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Guardrails</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="guardrails_add"
|
||||
label="Guardrails to Add"
|
||||
tooltip="These guardrails will be added to requests matching this policy"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder="Select guardrails to add"
|
||||
options={guardrailOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="guardrails_remove"
|
||||
label="Guardrails to Remove"
|
||||
tooltip="These guardrails will be removed from inherited guardrails"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder="Select guardrails to remove (from inherited)"
|
||||
options={guardrailOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{resolvedGuardrails.length > 0 && (
|
||||
<Alert
|
||||
message="Resolved Guardrails"
|
||||
description={
|
||||
<div>
|
||||
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
|
||||
These are the final guardrails that will be applied (including inheritance):
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{resolvedGuardrails.map((g) => (
|
||||
<Tag key={g} color="blue">
|
||||
{g}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Conditions (Optional)</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item label="Model Condition Type">
|
||||
<Radio.Group
|
||||
value={modelConditionType}
|
||||
onChange={(e) => {
|
||||
setModelConditionType(e.target.value);
|
||||
form.setFieldValue("model_condition", undefined);
|
||||
}}
|
||||
>
|
||||
<Radio value="model">Select Model</Radio>
|
||||
<Radio value="regex">Custom Regex Pattern</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="model_condition"
|
||||
label={modelConditionType === "model" ? "Model" : "Regex Pattern"}
|
||||
tooltip={
|
||||
modelConditionType === "model"
|
||||
? "Select a specific model to apply this policy to"
|
||||
: "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*)"
|
||||
}
|
||||
>
|
||||
{modelConditionType === "model" ? (
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="Select a model"
|
||||
options={availableModels.map((model) => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}))}
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<TextInput placeholder="e.g., gpt-4.* or bedrock/claude-.*" />
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={isSubmitting}>
|
||||
{isEditing ? "Update Policy" : "Create Policy"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPolicyForm;
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
import React, { useState } from "react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Badge } from "@tremor/react";
|
||||
import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
|
||||
import { Tooltip, Tag } from "antd";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { PolicyAttachment } from "./types";
|
||||
|
||||
interface AttachmentTableProps {
|
||||
attachments: PolicyAttachment[];
|
||||
isLoading: boolean;
|
||||
onDeleteClick: (attachmentId: string) => void;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const AttachmentTable: React.FC<AttachmentTableProps> = ({
|
||||
attachments,
|
||||
isLoading,
|
||||
onDeleteClick,
|
||||
isAdmin,
|
||||
}) => {
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
|
||||
|
||||
// Format date helper function
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const columns: ColumnDef<PolicyAttachment>[] = [
|
||||
{
|
||||
header: "Attachment ID",
|
||||
accessorKey: "attachment_id",
|
||||
cell: (info: any) => (
|
||||
<Tooltip title={String(info.getValue() || "")}>
|
||||
<span className="font-mono text-xs text-gray-600">
|
||||
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Policy",
|
||||
accessorKey: "policy_name",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
return (
|
||||
<Badge color="blue" size="xs">
|
||||
{attachment.policy_name}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Scope",
|
||||
accessorKey: "scope",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
if (attachment.scope === "*") {
|
||||
return (
|
||||
<Badge color="amber" size="xs">
|
||||
Global (*)
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return attachment.scope ? (
|
||||
<span className="text-xs">{attachment.scope}</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Teams",
|
||||
accessorKey: "teams",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
const teams = attachment.teams || [];
|
||||
if (teams.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{teams.slice(0, 2).map((t, i) => (
|
||||
<Tag key={i} color="cyan" className="text-xs">
|
||||
{t}
|
||||
</Tag>
|
||||
))}
|
||||
{teams.length > 2 && (
|
||||
<Tooltip title={teams.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{teams.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Keys",
|
||||
accessorKey: "keys",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
const keys = attachment.keys || [];
|
||||
if (keys.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{keys.slice(0, 2).map((k, i) => (
|
||||
<Tag key={i} color="purple" className="text-xs">
|
||||
{k}
|
||||
</Tag>
|
||||
))}
|
||||
{keys.length > 2 && (
|
||||
<Tooltip title={keys.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{keys.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Models",
|
||||
accessorKey: "models",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
const models = attachment.models || [];
|
||||
if (models.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models.slice(0, 2).map((m, i) => (
|
||||
<Tag key={i} color="green" className="text-xs">
|
||||
{m}
|
||||
</Tag>
|
||||
))}
|
||||
{models.length > 2 && (
|
||||
<Tooltip title={models.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{models.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
return (
|
||||
<Tooltip title={attachment.created_at}>
|
||||
<span className="text-xs">{formatDate(attachment.created_at)}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{isAdmin && (
|
||||
<Tooltip title="Delete attachment">
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => onDeleteClick(attachment.attachment_id)}
|
||||
className="cursor-pointer hover:text-red-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: attachments,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableSorting: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
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.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
{header.id !== "actions" && (
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : attachments.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="h-8">
|
||||
{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"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No attachments found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentTable;
|
||||
270
ui/litellm-dashboard/src/components/policies/index.tsx
Normal file
270
ui/litellm-dashboard/src/components/policies/index.tsx
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
|
||||
import { Modal, message } from "antd";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import PolicyTable from "./policy_table";
|
||||
import PolicyInfoView from "./policy_info";
|
||||
import AddPolicyForm from "./add_policy_form";
|
||||
import AttachmentTable from "./attachment_table";
|
||||
import AddAttachmentForm from "./add_attachment_form";
|
||||
import {
|
||||
getPoliciesList,
|
||||
deletePolicyCall,
|
||||
getPolicyAttachmentsList,
|
||||
deletePolicyAttachmentCall,
|
||||
getGuardrailsList,
|
||||
} from "../networking";
|
||||
import {
|
||||
Policy,
|
||||
PolicyAttachment,
|
||||
} from "./types";
|
||||
import { Guardrail } from "../guardrails/types";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
|
||||
interface PoliciesPanelProps {
|
||||
accessToken: string | null;
|
||||
userRole?: string;
|
||||
}
|
||||
|
||||
const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
|
||||
accessToken,
|
||||
userRole,
|
||||
}) => {
|
||||
const [policiesList, setPoliciesList] = useState<Policy[]>([]);
|
||||
const [attachmentsList, setAttachmentsList] = useState<PolicyAttachment[]>([]);
|
||||
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAttachmentsLoading, setIsAttachmentsLoading] = useState(false);
|
||||
const [isAddPolicyModalVisible, setIsAddPolicyModalVisible] = useState(false);
|
||||
const [isAddAttachmentModalVisible, setIsAddAttachmentModalVisible] = useState(false);
|
||||
const [editingPolicy, setEditingPolicy] = useState<Policy | null>(null);
|
||||
const [selectedPolicyId, setSelectedPolicyId] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [policyToDelete, setPolicyToDelete] = useState<Policy | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const isAdmin = userRole ? isAdminRole(userRole) : false;
|
||||
|
||||
const fetchPolicies = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await getPoliciesList(accessToken);
|
||||
setPoliciesList(response.policies || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching policies:", error);
|
||||
message.error("Failed to fetch policies");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
const fetchAttachments = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setIsAttachmentsLoading(true);
|
||||
try {
|
||||
const response = await getPolicyAttachmentsList(accessToken);
|
||||
setAttachmentsList(response.attachments || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching attachments:", error);
|
||||
message.error("Failed to fetch attachments");
|
||||
} finally {
|
||||
setIsAttachmentsLoading(false);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
const fetchGuardrails = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
const response = await getGuardrailsList(accessToken);
|
||||
setGuardrailsList(response.guardrails || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching guardrails:", error);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicies();
|
||||
fetchAttachments();
|
||||
fetchGuardrails();
|
||||
}, [fetchPolicies, fetchAttachments, fetchGuardrails]);
|
||||
|
||||
const handleAddPolicy = () => {
|
||||
if (selectedPolicyId) {
|
||||
setSelectedPolicyId(null);
|
||||
}
|
||||
setEditingPolicy(null);
|
||||
setIsAddPolicyModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsAddPolicyModalVisible(false);
|
||||
setEditingPolicy(null);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
fetchPolicies();
|
||||
setEditingPolicy(null);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (policyId: string, policyName: string) => {
|
||||
const policy = policiesList.find((p) => p.policy_id === policyId) || null;
|
||||
setPolicyToDelete(policy);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!policyToDelete || !accessToken) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deletePolicyCall(accessToken, policyToDelete.policy_id);
|
||||
message.success(`Policy "${policyToDelete.policy_name}" deleted successfully`);
|
||||
await fetchPolicies();
|
||||
} catch (error) {
|
||||
console.error("Error deleting policy:", error);
|
||||
message.error("Failed to delete policy");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setIsDeleteModalOpen(false);
|
||||
setPolicyToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setPolicyToDelete(null);
|
||||
};
|
||||
|
||||
const handleDeleteAttachment = (attachmentId: string) => {
|
||||
Modal.confirm({
|
||||
title: "Delete Attachment",
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: "Are you sure you want to delete this attachment? This action cannot be undone.",
|
||||
okText: "Delete",
|
||||
okType: "danger",
|
||||
cancelText: "Cancel",
|
||||
onOk: async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
await deletePolicyAttachmentCall(accessToken, attachmentId);
|
||||
message.success("Attachment deleted successfully");
|
||||
fetchAttachments();
|
||||
} catch (error) {
|
||||
console.error("Error deleting attachment:", error);
|
||||
message.error("Failed to delete attachment");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleAttachmentSuccess = () => {
|
||||
fetchAttachments();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<TabGroup index={activeTab} onIndexChange={setActiveTab}>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Policies</Tab>
|
||||
<Tab>Attachments</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Button onClick={handleAddPolicy} disabled={!accessToken}>
|
||||
+ Add New Policy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedPolicyId ? (
|
||||
<PolicyInfoView
|
||||
policyId={selectedPolicyId}
|
||||
onClose={() => setSelectedPolicyId(null)}
|
||||
onEdit={(policy) => {
|
||||
setEditingPolicy(policy);
|
||||
setIsAddPolicyModalVisible(true);
|
||||
setSelectedPolicyId(null);
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
) : (
|
||||
<PolicyTable
|
||||
policies={policiesList}
|
||||
isLoading={isLoading}
|
||||
onDeleteClick={handleDeleteClick}
|
||||
onEditClick={(policy) => {
|
||||
setEditingPolicy(policy);
|
||||
setIsAddPolicyModalVisible(true);
|
||||
}}
|
||||
onViewClick={(policyId) => setSelectedPolicyId(policyId)}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AddPolicyForm
|
||||
visible={isAddPolicyModalVisible}
|
||||
onClose={handleCloseModal}
|
||||
onSuccess={handleSuccess}
|
||||
accessToken={accessToken}
|
||||
editingPolicy={editingPolicy}
|
||||
existingPolicies={policiesList}
|
||||
availableGuardrails={guardrailsList}
|
||||
/>
|
||||
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Policy"
|
||||
message={`Are you sure you want to delete policy: ${policyToDelete?.policy_name}? This action cannot be undone.`}
|
||||
resourceInformationTitle="Policy Information"
|
||||
resourceInformation={[
|
||||
{ label: "Name", value: policyToDelete?.policy_name },
|
||||
{ label: "ID", value: policyToDelete?.policy_id, code: true },
|
||||
{ label: "Description", value: policyToDelete?.description || "-" },
|
||||
{ label: "Inherits From", value: policyToDelete?.inherit || "-" },
|
||||
]}
|
||||
onCancel={handleDeleteCancel}
|
||||
onOk={handleDeleteConfirm}
|
||||
confirmLoading={isDeleting}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Button
|
||||
onClick={() => setIsAddAttachmentModalVisible(true)}
|
||||
disabled={!accessToken || policiesList.length === 0}
|
||||
>
|
||||
+ Add New Attachment
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AttachmentTable
|
||||
attachments={attachmentsList}
|
||||
isLoading={isAttachmentsLoading}
|
||||
onDeleteClick={handleDeleteAttachment}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
|
||||
<AddAttachmentForm
|
||||
visible={isAddAttachmentModalVisible}
|
||||
onClose={() => setIsAddAttachmentModalVisible(false)}
|
||||
onSuccess={handleAttachmentSuccess}
|
||||
accessToken={accessToken}
|
||||
policies={policiesList}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoliciesPanel;
|
||||
206
ui/litellm-dashboard/src/components/policies/policy_info.tsx
Normal file
206
ui/litellm-dashboard/src/components/policies/policy_info.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Card, Badge, Button } from "@tremor/react";
|
||||
import { ArrowLeftIcon, PencilIcon } from "@heroicons/react/outline";
|
||||
import { Descriptions, Tag, Spin, Divider, Typography, Alert } from "antd";
|
||||
import { Policy } from "./types";
|
||||
import { getPolicyInfo, getResolvedGuardrails } from "../networking";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface PolicyInfoViewProps {
|
||||
policyId: string;
|
||||
onClose: () => void;
|
||||
onEdit: (policy: Policy) => void;
|
||||
accessToken: string | null;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
|
||||
policyId,
|
||||
onClose,
|
||||
onEdit,
|
||||
accessToken,
|
||||
isAdmin,
|
||||
}) => {
|
||||
const [policy, setPolicy] = useState<Policy | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
|
||||
const [isLoadingResolved, setIsLoadingResolved] = useState(false);
|
||||
|
||||
const fetchPolicy = useCallback(async () => {
|
||||
if (!accessToken || !policyId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await getPolicyInfo(accessToken, policyId);
|
||||
setPolicy(data);
|
||||
|
||||
// Also fetch resolved guardrails
|
||||
setIsLoadingResolved(true);
|
||||
try {
|
||||
const resolvedData = await getResolvedGuardrails(accessToken, policyId);
|
||||
setResolvedGuardrails(resolvedData.resolved_guardrails || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching resolved guardrails:", error);
|
||||
} finally {
|
||||
setIsLoadingResolved(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching policy:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [policyId, accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicy();
|
||||
}, [fetchPolicy]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-12">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!policy) {
|
||||
return (
|
||||
<Card>
|
||||
<Text type="danger">Policy not found</Text>
|
||||
<br />
|
||||
<Button onClick={onClose} className="mt-4">
|
||||
Go Back
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={ArrowLeftIcon}
|
||||
onClick={onClose}
|
||||
>
|
||||
Back to Policies
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
icon={PencilIcon}
|
||||
onClick={() => onEdit(policy)}
|
||||
>
|
||||
Edit Policy
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Title level={4}>{policy.policy_name}</Title>
|
||||
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Policy ID">
|
||||
<code className="text-xs bg-gray-100 px-2 py-1 rounded">{policy.policy_id}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Description">
|
||||
{policy.description || <Text type="secondary">No description</Text>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Inherits From">
|
||||
{policy.inherit ? (
|
||||
<Badge color="blue" size="sm">{policy.inherit}</Badge>
|
||||
) : (
|
||||
<Text type="secondary">None</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Created At">
|
||||
{policy.created_at
|
||||
? new Date(policy.created_at).toLocaleString()
|
||||
: "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Updated At">
|
||||
{policy.updated_at
|
||||
? new Date(policy.updated_at).toLocaleString()
|
||||
: "-"}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Guardrails Configuration</Text>
|
||||
</Divider>
|
||||
|
||||
{resolvedGuardrails.length > 0 && (
|
||||
<Alert
|
||||
message="Resolved Guardrails"
|
||||
description={
|
||||
<div>
|
||||
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
|
||||
Final guardrails that will be applied (including inheritance):
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{resolvedGuardrails.map((g) => (
|
||||
<Tag key={g} color="blue">
|
||||
{g}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Guardrails to Add">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policy.guardrails_add && policy.guardrails_add.length > 0 ? (
|
||||
policy.guardrails_add.map((g) => (
|
||||
<Tag key={g} color="green">
|
||||
{g}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Text type="secondary">None</Text>
|
||||
)}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Guardrails to Remove">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policy.guardrails_remove && policy.guardrails_remove.length > 0 ? (
|
||||
policy.guardrails_remove.map((g) => (
|
||||
<Tag key={g} color="red">
|
||||
{g}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Text type="secondary">None</Text>
|
||||
)}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Conditions</Text>
|
||||
</Divider>
|
||||
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Model Condition">
|
||||
{policy.condition?.model ? (
|
||||
<Tag color="purple">
|
||||
{typeof policy.condition.model === "string"
|
||||
? policy.condition.model
|
||||
: JSON.stringify(policy.condition.model)}
|
||||
</Tag>
|
||||
) : (
|
||||
<Text type="secondary">No model condition (applies to all models)</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicyInfoView;
|
||||
309
ui/litellm-dashboard/src/components/policies/policy_table.tsx
Normal file
309
ui/litellm-dashboard/src/components/policies/policy_table.tsx
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
import React, { useState } from "react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button, Badge } from "@tremor/react";
|
||||
import { TrashIcon, PencilIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
|
||||
import { Tooltip, Tag } from "antd";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Policy } from "./types";
|
||||
|
||||
interface PolicyTableProps {
|
||||
policies: Policy[];
|
||||
isLoading: boolean;
|
||||
onDeleteClick: (policyId: string, policyName: string) => void;
|
||||
onEditClick: (policy: Policy) => void;
|
||||
onViewClick: (policyId: string) => void;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
const PolicyTable: React.FC<PolicyTableProps> = ({
|
||||
policies,
|
||||
isLoading,
|
||||
onDeleteClick,
|
||||
onEditClick,
|
||||
onViewClick,
|
||||
isAdmin = false,
|
||||
}) => {
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
|
||||
|
||||
// Format date helper function
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const columns: ColumnDef<Policy>[] = [
|
||||
{
|
||||
header: "Policy ID",
|
||||
accessorKey: "policy_id",
|
||||
cell: (info: any) => (
|
||||
<Tooltip title={String(info.getValue() || "")}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
onClick={() => info.getValue() && onViewClick(info.getValue())}
|
||||
>
|
||||
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "policy_name",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<Tooltip title={policy.policy_name}>
|
||||
<span className="text-xs font-medium">{policy.policy_name || "-"}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessorKey: "description",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<Tooltip title={policy.description}>
|
||||
<span className="text-xs truncate max-w-[200px] block">
|
||||
{policy.description || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Inherits From",
|
||||
accessorKey: "inherit",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return policy.inherit ? (
|
||||
<Badge color="blue" size="xs">
|
||||
{policy.inherit}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Guardrails (Add)",
|
||||
accessorKey: "guardrails_add",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
const guardrails = policy.guardrails_add || [];
|
||||
if (guardrails.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{guardrails.slice(0, 2).map((g, i) => (
|
||||
<Tag key={i} color="green" className="text-xs">
|
||||
{g}
|
||||
</Tag>
|
||||
))}
|
||||
{guardrails.length > 2 && (
|
||||
<Tooltip title={guardrails.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{guardrails.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Guardrails (Remove)",
|
||||
accessorKey: "guardrails_remove",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
const guardrails = policy.guardrails_remove || [];
|
||||
if (guardrails.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{guardrails.slice(0, 2).map((g, i) => (
|
||||
<Tag key={i} color="red" className="text-xs">
|
||||
{g}
|
||||
</Tag>
|
||||
))}
|
||||
{guardrails.length > 2 && (
|
||||
<Tooltip title={guardrails.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{guardrails.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Model Condition",
|
||||
accessorKey: "condition",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
const modelCondition = policy.condition?.model;
|
||||
if (!modelCondition) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={typeof modelCondition === "string" ? modelCondition : JSON.stringify(modelCondition)}>
|
||||
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
|
||||
{typeof modelCondition === "string"
|
||||
? modelCondition.length > 20
|
||||
? modelCondition.slice(0, 20) + "..."
|
||||
: modelCondition
|
||||
: "Multiple"}
|
||||
</code>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<Tooltip title={policy.created_at}>
|
||||
<span className="text-xs">{formatDate(policy.created_at)}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Tooltip title="Edit policy">
|
||||
<Icon
|
||||
icon={PencilIcon}
|
||||
size="sm"
|
||||
onClick={() => onEditClick(policy)}
|
||||
className="cursor-pointer hover:text-blue-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete policy">
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
policy.policy_id &&
|
||||
onDeleteClick(policy.policy_id, policy.policy_name || "Unnamed Policy")
|
||||
}
|
||||
className="cursor-pointer hover:text-red-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: policies,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableSorting: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
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.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
{header.id !== "actions" && (
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : policies.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="h-8">
|
||||
{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"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No policies found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicyTable;
|
||||
66
ui/litellm-dashboard/src/components/policies/types.ts
Normal file
66
ui/litellm-dashboard/src/components/policies/types.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
export interface Policy {
|
||||
policy_id: string;
|
||||
policy_name: string;
|
||||
inherit: string | null;
|
||||
description: string | null;
|
||||
guardrails_add: string[];
|
||||
guardrails_remove: string[];
|
||||
condition: PolicyCondition | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface PolicyCondition {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface PolicyAttachment {
|
||||
attachment_id: string;
|
||||
policy_name: string;
|
||||
scope: string | null;
|
||||
teams: string[];
|
||||
keys: string[];
|
||||
models: string[];
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface PolicyCreateRequest {
|
||||
policy_name: string;
|
||||
inherit?: string;
|
||||
description?: string;
|
||||
guardrails_add?: string[];
|
||||
guardrails_remove?: string[];
|
||||
condition?: PolicyCondition;
|
||||
}
|
||||
|
||||
export interface PolicyUpdateRequest {
|
||||
policy_name?: string;
|
||||
inherit?: string;
|
||||
description?: string;
|
||||
guardrails_add?: string[];
|
||||
guardrails_remove?: string[];
|
||||
condition?: PolicyCondition;
|
||||
}
|
||||
|
||||
export interface PolicyAttachmentCreateRequest {
|
||||
policy_name: string;
|
||||
scope?: string;
|
||||
teams?: string[];
|
||||
keys?: string[];
|
||||
models?: string[];
|
||||
}
|
||||
|
||||
export interface PolicyListResponse {
|
||||
policies: Policy[];
|
||||
total_count: number;
|
||||
}
|
||||
|
||||
export interface PolicyAttachmentListResponse {
|
||||
attachments: PolicyAttachment[];
|
||||
total_count: number;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue