mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
Merge main into eu-ai-act-article5-policy-template
Resolved conflicts: - patterns.json: added allow_word_numbers field to eu_vat and eu_passport_generic - test_eu_patterns.py: added test_pattern_requires_keyword_context test - test_gdpr_policy_e2e.py: updated VAT test comment, added two new contextual guard tests, removed unused HTTPException import
This commit is contained in:
commit
790f87b7a3
54 changed files with 1038 additions and 178 deletions
|
|
@ -26,6 +26,7 @@ from litellm.types.utils import (
|
|||
CallTypes,
|
||||
GenericGuardrailAPIInputs,
|
||||
GuardrailStatus,
|
||||
GuardrailTracingDetail,
|
||||
LLMResponseTypes,
|
||||
StandardLoggingGuardrailInformation,
|
||||
)
|
||||
|
|
@ -520,9 +521,15 @@ class CustomGuardrail(CustomLogger):
|
|||
masked_entity_count: Optional[Dict[str, int]] = None,
|
||||
guardrail_provider: Optional[str] = None,
|
||||
event_type: Optional[GuardrailEventHooks] = None,
|
||||
tracing_detail: Optional[GuardrailTracingDetail] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Builds `StandardLoggingGuardrailInformation` and adds it to the request metadata so it can be used for logging to DataDog, Langfuse, etc.
|
||||
|
||||
Args:
|
||||
tracing_detail: Optional typed dict with provider-specific tracing fields
|
||||
(guardrail_id, policy_template, detection_method, confidence_score,
|
||||
classification, match_details, patterns_checked, alert_recipients).
|
||||
"""
|
||||
if isinstance(guardrail_json_response, Exception):
|
||||
guardrail_json_response = str(guardrail_json_response)
|
||||
|
|
@ -559,6 +566,7 @@ class CustomGuardrail(CustomLogger):
|
|||
end_time=end_time,
|
||||
duration=duration,
|
||||
masked_entity_count=masked_entity_count,
|
||||
**(tracing_detail or {}),
|
||||
)
|
||||
|
||||
def _append_guardrail_info(container: dict) -> None:
|
||||
|
|
|
|||
|
|
@ -1083,7 +1083,7 @@
|
|||
"supports_vision": true,
|
||||
"tool_use_system_prompt_tokens": 346
|
||||
},
|
||||
"apac.anthropic.claude-opus-4-6-v1": {
|
||||
"au.anthropic.claude-opus-4-6-v1": {
|
||||
"cache_creation_input_token_cost": 6.875e-06,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 1.375e-05,
|
||||
"cache_read_input_token_cost": 5.5e-07,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ def initialize_guardrail(
|
|||
|
||||
content_filter_guardrail = ContentFilterGuardrail(
|
||||
guardrail_name=guardrail_name,
|
||||
guardrail_id=guardrail.get("guardrail_id"),
|
||||
policy_template=guardrail.get("policy_template"),
|
||||
patterns=litellm_params.patterns,
|
||||
blocked_words=litellm_params.blocked_words,
|
||||
blocked_words_file=litellm_params.blocked_words_file,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from litellm import Router
|
|||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.types.utils import ModelResponseStream
|
||||
from litellm.types.utils import GuardrailTracingDetail, ModelResponseStream
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
|
@ -133,6 +133,8 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
def __init__(
|
||||
self,
|
||||
guardrail_name: Optional[str] = None,
|
||||
guardrail_id: Optional[str] = None,
|
||||
policy_template: Optional[str] = None,
|
||||
patterns: Optional[List[ContentFilterPattern]] = None,
|
||||
blocked_words: Optional[List[BlockedWord]] = None,
|
||||
blocked_words_file: Optional[str] = None,
|
||||
|
|
@ -177,6 +179,8 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
)
|
||||
|
||||
self.guardrail_provider = "litellm_content_filter"
|
||||
self.config_guardrail_id = guardrail_id
|
||||
self.config_policy_template = policy_template
|
||||
self.pattern_redaction_format = (
|
||||
pattern_redaction_format or self.PATTERN_REDACTION_FORMAT
|
||||
)
|
||||
|
|
@ -1352,6 +1356,48 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
masked_entity_count.get(category, 0) + 1
|
||||
)
|
||||
|
||||
def _build_match_details(
|
||||
self, detections: List[ContentFilterDetection]
|
||||
) -> List[dict]:
|
||||
"""Build match_details list from content filter detections."""
|
||||
match_details: List[dict] = []
|
||||
for detection in detections:
|
||||
detail: dict = {"type": detection["type"], "action_taken": detection["action"]}
|
||||
if detection["type"] == "pattern":
|
||||
detail["detection_method"] = "regex"
|
||||
detail["snippet"] = cast(PatternDetection, detection).get("pattern_name", "")
|
||||
elif detection["type"] == "blocked_word":
|
||||
detail["detection_method"] = "keyword"
|
||||
detail["snippet"] = cast(BlockedWordDetection, detection).get("keyword", "")
|
||||
elif detection["type"] == "category_keyword":
|
||||
detail["detection_method"] = "keyword"
|
||||
cat_det = cast(CategoryKeywordDetection, detection)
|
||||
detail["snippet"] = cat_det.get("keyword", "")
|
||||
detail["category"] = cat_det.get("category", "")
|
||||
match_details.append(detail)
|
||||
return match_details
|
||||
|
||||
def _get_detection_methods(self, detections: List[ContentFilterDetection]) -> str:
|
||||
"""Get comma-separated detection methods used."""
|
||||
methods: set = set()
|
||||
for detection in detections:
|
||||
if detection["type"] == "pattern":
|
||||
methods.add("regex")
|
||||
else:
|
||||
methods.add("keyword")
|
||||
return ",".join(sorted(methods)) if methods else ""
|
||||
|
||||
def _get_patterns_checked_count(self) -> int:
|
||||
"""Get total number of patterns and keywords that were evaluated."""
|
||||
return len(self.compiled_patterns) + len(self.blocked_words) + len(self.category_keywords)
|
||||
|
||||
def _get_policy_templates(self) -> Optional[str]:
|
||||
"""Get comma-separated policy template names from loaded categories."""
|
||||
if not self.loaded_categories:
|
||||
return None
|
||||
names = [cat.description or cat.category_name for cat in self.loaded_categories.values()]
|
||||
return ", ".join(names) if names else None
|
||||
|
||||
def _log_guardrail_information(
|
||||
self,
|
||||
request_data: dict,
|
||||
|
|
@ -1392,6 +1438,13 @@ class ContentFilterGuardrail(CustomGuardrail):
|
|||
end_time=datetime.now().timestamp(),
|
||||
duration=(datetime.now() - start_time).total_seconds(),
|
||||
masked_entity_count=masked_entity_count,
|
||||
tracing_detail=GuardrailTracingDetail(
|
||||
guardrail_id=self.config_guardrail_id or self.guardrail_name,
|
||||
policy_template=self.config_policy_template or self._get_policy_templates(),
|
||||
detection_method=self._get_detection_methods(detections) if detections else None,
|
||||
match_details=self._build_match_details(detections) if detections else None,
|
||||
patterns_checked=self._get_patterns_checked_count(),
|
||||
),
|
||||
)
|
||||
|
||||
async def apply_guardrail(
|
||||
|
|
|
|||
|
|
@ -425,7 +425,8 @@
|
|||
"pattern": "\\b(AT|BE|BG|CY|CZ|DE|DK|EE|EL|ES|FI|FR|HR|HU|IE|IT|LT|LU|LV|MT|NL|PL|PT|RO|SE|SI|SK)[0-9A-Z]{8,12}\\b",
|
||||
"category": "EU PII Patterns",
|
||||
"description": "Detects EU VAT identification numbers (2-letter country code + 8-12 alphanumeric characters covering all EU member states)",
|
||||
"keyword_pattern": "\\b(?:VAT|V\\.A\\.T\\.|TVA|IVA|BTW|MWST|value\\s*added\\s*tax|tax\\s*number|tax\\s*id|fiscal\\s*number|fiscal\\s*code)\\b"
|
||||
"keyword_pattern": "\\b(?:VAT|V\\.A\\.T\\.|TVA|IVA|BTW|MWST|value\\s*added\\s*tax|tax\\s*number|tax\\s*id|fiscal\\s*number|fiscal\\s*code)\\b",
|
||||
"allow_word_numbers": false
|
||||
},
|
||||
{
|
||||
"name": "eu_passport_generic",
|
||||
|
|
@ -433,7 +434,8 @@
|
|||
"pattern": "\\b[0-9]{2}[A-Z]{2}[0-9]{5}\\b",
|
||||
"category": "EU PII Patterns",
|
||||
"description": "Detects generic EU passport format (2 digits + 2 letters + 5 digits) - covers France and similar EU formats",
|
||||
"keyword_pattern": "\\b(?:passport|passeport|travel\\s*document|document\\s*number|reisepass|paspoort|paszport)\\b"
|
||||
"keyword_pattern": "\\b(?:passport|passeport|travel\\s*document|document\\s*number|reisepass|paspoort|paszport)\\b",
|
||||
"allow_word_numbers": false
|
||||
},
|
||||
{
|
||||
"name": "fr_postal_code",
|
||||
|
|
|
|||
|
|
@ -731,6 +731,7 @@ class Guardrail(TypedDict, total=False):
|
|||
guardrail_name: Required[str]
|
||||
litellm_params: Required[LitellmParams]
|
||||
guardrail_info: Optional[Dict]
|
||||
policy_template: Optional[str]
|
||||
created_at: Optional[datetime]
|
||||
updated_at: Optional[datetime]
|
||||
|
||||
|
|
|
|||
|
|
@ -2620,6 +2620,48 @@ class StandardLoggingGuardrailInformation(TypedDict, total=False):
|
|||
}
|
||||
"""
|
||||
|
||||
guardrail_id: Optional[str]
|
||||
"""Unique identifier for the guardrail configuration, e.g. 'gd-eu-pii-001'"""
|
||||
|
||||
policy_template: Optional[str]
|
||||
"""Name of the policy template this guardrail belongs to, e.g. 'EU AI Act Article 5'"""
|
||||
|
||||
detection_method: Optional[str]
|
||||
"""How detection was performed: 'regex', 'keyword', 'llm-judge', 'presidio', etc."""
|
||||
|
||||
confidence_score: Optional[float]
|
||||
"""For LLM-judge guardrails: confidence score 0.0-1.0"""
|
||||
|
||||
classification: Optional[dict]
|
||||
"""For LLM-judge guardrails: structured classification output"""
|
||||
|
||||
match_details: Optional[List[dict]]
|
||||
"""Detailed match information for each detected pattern"""
|
||||
|
||||
patterns_checked: Optional[int]
|
||||
"""Total number of patterns evaluated by this guardrail"""
|
||||
|
||||
alert_recipients: Optional[List[str]]
|
||||
"""Email addresses that were notified"""
|
||||
|
||||
|
||||
class GuardrailTracingDetail(TypedDict, total=False):
|
||||
"""
|
||||
Typed fields for guardrail tracing metadata.
|
||||
|
||||
Passed to add_standard_logging_guardrail_information_to_request_data()
|
||||
to enrich the StandardLoggingGuardrailInformation with provider-specific details.
|
||||
"""
|
||||
|
||||
guardrail_id: Optional[str]
|
||||
policy_template: Optional[str]
|
||||
detection_method: Optional[str]
|
||||
confidence_score: Optional[float]
|
||||
classification: Optional[dict]
|
||||
match_details: Optional[List[dict]]
|
||||
patterns_checked: Optional[int]
|
||||
alert_recipients: Optional[List[str]]
|
||||
|
||||
|
||||
StandardLoggingPayloadStatus = Literal["success", "failure"]
|
||||
|
||||
|
|
|
|||
|
|
@ -1083,7 +1083,7 @@
|
|||
"supports_vision": true,
|
||||
"tool_use_system_prompt_tokens": 346
|
||||
},
|
||||
"apac.anthropic.claude-opus-4-6-v1": {
|
||||
"au.anthropic.claude-opus-4-6-v1": {
|
||||
"cache_creation_input_token_cost": 6.875e-06,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 1.375e-05,
|
||||
"cache_read_input_token_cost": 5.5e-07,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import pytest
|
|||
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.proxy._types import CallTypes, UserAPIKeyAuth
|
||||
from litellm.types.utils import GuardrailTracingDetail
|
||||
|
||||
|
||||
class TestCustomGuardrailDeploymentHook:
|
||||
|
|
@ -723,3 +724,99 @@ class TestEventTypeLogging:
|
|||
logged_info = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(logged_info) == 1
|
||||
assert logged_info[0]["guardrail_mode"] == GuardrailEventHooks.pre_call
|
||||
|
||||
|
||||
class TestTracingFieldsPopulation:
|
||||
"""Verify add_standard_logging_guardrail_information_to_request_data passes tracing_detail fields."""
|
||||
|
||||
def test_new_fields_set_on_slg(self):
|
||||
cg = CustomGuardrail(guardrail_name="test-rail")
|
||||
request_data = {"metadata": {}}
|
||||
cg.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response={"result": "ok"},
|
||||
request_data=request_data,
|
||||
guardrail_status="success",
|
||||
tracing_detail=GuardrailTracingDetail(
|
||||
guardrail_id="rail-123",
|
||||
policy_template="EU AI Act Article 5",
|
||||
detection_method="regex",
|
||||
confidence_score=0.95,
|
||||
match_details=[{"type": "pattern", "action_taken": "BLOCK"}],
|
||||
patterns_checked=12,
|
||||
alert_recipients=["admin@example.com"],
|
||||
),
|
||||
)
|
||||
slg_list = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(slg_list) == 1
|
||||
slg = slg_list[0]
|
||||
assert slg["guardrail_id"] == "rail-123"
|
||||
assert slg["policy_template"] == "EU AI Act Article 5"
|
||||
assert slg["detection_method"] == "regex"
|
||||
assert slg["confidence_score"] == 0.95
|
||||
assert slg["patterns_checked"] == 12
|
||||
assert slg["alert_recipients"] == ["admin@example.com"]
|
||||
assert len(slg["match_details"]) == 1
|
||||
|
||||
def test_new_fields_default_to_absent(self):
|
||||
"""When tracing_detail is not passed, new fields are absent from the SLG dict."""
|
||||
cg = CustomGuardrail(guardrail_name="test-rail")
|
||||
request_data = {"metadata": {}}
|
||||
cg.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response="ok",
|
||||
request_data=request_data,
|
||||
guardrail_status="success",
|
||||
)
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg.get("guardrail_id") is None
|
||||
assert slg.get("policy_template") is None
|
||||
assert slg.get("confidence_score") is None
|
||||
|
||||
def test_multiple_guardrails_with_different_policies(self):
|
||||
"""One request, multiple guardrails each with own policy_template."""
|
||||
cg1 = CustomGuardrail(guardrail_name="rail-1")
|
||||
cg2 = CustomGuardrail(guardrail_name="rail-2")
|
||||
request_data = {"metadata": {}}
|
||||
|
||||
cg1.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response="ok",
|
||||
request_data=request_data,
|
||||
guardrail_status="success",
|
||||
tracing_detail=GuardrailTracingDetail(policy_template="GDPR"),
|
||||
)
|
||||
cg2.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response="blocked",
|
||||
request_data=request_data,
|
||||
guardrail_status="guardrail_intervened",
|
||||
tracing_detail=GuardrailTracingDetail(policy_template="EU AI Act Article 5"),
|
||||
)
|
||||
|
||||
slg_list = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(slg_list) == 2
|
||||
assert slg_list[0]["policy_template"] == "GDPR"
|
||||
assert slg_list[1]["policy_template"] == "EU AI Act Article 5"
|
||||
|
||||
def test_classification_field_passed_through(self):
|
||||
"""Classification dict for LLM-judge guardrails is passed through."""
|
||||
cg = CustomGuardrail(guardrail_name="judge-rail")
|
||||
request_data = {"metadata": {}}
|
||||
classification = {
|
||||
"flagged": True,
|
||||
"category": "workplace_emotion_recognition",
|
||||
"article_reference": "Article 5(1)(f)",
|
||||
"confidence": 0.94,
|
||||
"reason": "Request asks to analyze employee sentiment",
|
||||
}
|
||||
cg.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response="blocked",
|
||||
request_data=request_data,
|
||||
guardrail_status="guardrail_intervened",
|
||||
tracing_detail=GuardrailTracingDetail(
|
||||
classification=classification,
|
||||
detection_method="llm-judge",
|
||||
confidence_score=0.94,
|
||||
),
|
||||
)
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["classification"] == classification
|
||||
assert slg["detection_method"] == "llm-judge"
|
||||
assert slg["confidence_score"] == 0.94
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ from litellm.types.guardrails import (
|
|||
ContentFilterPattern,
|
||||
GuardrailEventHooks,
|
||||
)
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
|
||||
ContentFilterCategoryConfig,
|
||||
)
|
||||
|
||||
|
||||
class TestContentFilterGuardrail:
|
||||
|
|
@ -1842,3 +1845,212 @@ class TestContentFilterGuardrail:
|
|||
)
|
||||
# Should pass - 'Indian' in sentence 1, 'lazy' in sentence 2
|
||||
assert len(result["texts"]) == 1
|
||||
|
||||
|
||||
class TestTracingFieldsE2E:
|
||||
"""E2E tests for new tracing fields (guardrail_id, policy_template, detection_method, match_details, patterns_checked)."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_populated_on_mask_detection(self):
|
||||
"""New tracing fields are populated in SpendLog metadata when content is masked."""
|
||||
patterns = [
|
||||
ContentFilterPattern(
|
||||
pattern_type="prebuilt",
|
||||
pattern_name="email",
|
||||
action=ContentFilterAction.MASK,
|
||||
),
|
||||
]
|
||||
blocked_words = [
|
||||
BlockedWord(
|
||||
keyword="secret",
|
||||
action=ContentFilterAction.MASK,
|
||||
description="Secret keyword",
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="tracing-test",
|
||||
guardrail_id="gd-tracing-001",
|
||||
policy_template="Test Policy Template",
|
||||
patterns=patterns,
|
||||
blocked_words=blocked_words,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["Email me at user@test.com, it's a secret"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg_list = request_data["metadata"]["standard_logging_guardrail_information"]
|
||||
assert len(slg_list) == 1
|
||||
slg = slg_list[0]
|
||||
|
||||
# New tracing fields
|
||||
assert slg["guardrail_id"] == "gd-tracing-001"
|
||||
assert slg["policy_template"] == "Test Policy Template"
|
||||
assert slg["detection_method"] == "keyword,regex"
|
||||
assert slg["patterns_checked"] >= 2 # at least 1 pattern + 1 keyword
|
||||
|
||||
# match_details
|
||||
assert isinstance(slg["match_details"], list)
|
||||
assert len(slg["match_details"]) >= 2
|
||||
methods = {d["detection_method"] for d in slg["match_details"]}
|
||||
assert "regex" in methods
|
||||
assert "keyword" in methods
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_fallback_when_no_config_id(self):
|
||||
"""guardrail_id falls back to guardrail_name when config id not provided."""
|
||||
patterns = [
|
||||
ContentFilterPattern(
|
||||
pattern_type="prebuilt",
|
||||
pattern_name="us_ssn",
|
||||
action=ContentFilterAction.MASK,
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="fallback-test",
|
||||
patterns=patterns,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["SSN: 123-45-6789"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["guardrail_id"] == "fallback-test"
|
||||
assert slg.get("policy_template") is None # no categories loaded
|
||||
assert slg["detection_method"] == "regex"
|
||||
assert slg["patterns_checked"] >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_with_category_keywords(self):
|
||||
"""Tracing fields populated correctly when category keywords trigger detections."""
|
||||
categories = [
|
||||
ContentFilterCategoryConfig(
|
||||
category="harm_toxic_abuse",
|
||||
enabled=True,
|
||||
action=ContentFilterAction.MASK,
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="category-tracing",
|
||||
guardrail_id="gd-cat-001",
|
||||
categories=categories,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
# Use a word from the harm_toxic_abuse category
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["You are an idiot and stupid"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["guardrail_id"] == "gd-cat-001"
|
||||
assert slg["patterns_checked"] >= 1 # category keywords counted
|
||||
|
||||
if slg.get("match_details"):
|
||||
# If detections happened, verify category info
|
||||
cat_matches = [d for d in slg["match_details"] if d.get("category")]
|
||||
for m in cat_matches:
|
||||
assert m["detection_method"] == "keyword"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_on_blocked_request(self):
|
||||
"""Tracing fields populated even when request is blocked."""
|
||||
patterns = [
|
||||
ContentFilterPattern(
|
||||
pattern_type="prebuilt",
|
||||
pattern_name="us_ssn",
|
||||
action=ContentFilterAction.BLOCK,
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="block-tracing",
|
||||
guardrail_id="gd-block-001",
|
||||
policy_template="SSN Protection",
|
||||
patterns=patterns,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
with pytest.raises(HTTPException):
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["SSN: 123-45-6789"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["guardrail_id"] == "gd-block-001"
|
||||
assert slg["policy_template"] == "SSN Protection"
|
||||
assert slg["guardrail_status"] == "guardrail_intervened"
|
||||
assert slg["patterns_checked"] >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracing_fields_no_detections(self):
|
||||
"""When no detections occur, tracing fields still populated with metadata."""
|
||||
patterns = [
|
||||
ContentFilterPattern(
|
||||
pattern_type="prebuilt",
|
||||
pattern_name="email",
|
||||
action=ContentFilterAction.MASK,
|
||||
),
|
||||
]
|
||||
|
||||
guardrail = ContentFilterGuardrail(
|
||||
guardrail_name="clean-tracing",
|
||||
guardrail_id="gd-clean-001",
|
||||
policy_template="Email Protection",
|
||||
patterns=patterns,
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"messages": [{"role": "user", "content": "Test"}],
|
||||
"model": "gpt-4o",
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["Hello world, no sensitive content here"]},
|
||||
request_data=request_data,
|
||||
input_type="request",
|
||||
)
|
||||
|
||||
slg = request_data["metadata"]["standard_logging_guardrail_information"][0]
|
||||
assert slg["guardrail_id"] == "gd-clean-001"
|
||||
assert slg["policy_template"] == "Email Protection"
|
||||
assert slg["guardrail_status"] == "success"
|
||||
assert slg["patterns_checked"] >= 1
|
||||
# No detections, so these should be None
|
||||
assert slg.get("detection_method") is None
|
||||
assert slg.get("match_details") is None
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@ class TestEUVAT:
|
|||
assert pattern.search("DE123456789") is not None
|
||||
assert pattern.search("IT12345678901") is not None
|
||||
|
||||
def test_pattern_requires_keyword_context(self):
|
||||
"""
|
||||
NOTE: The eu_vat raw pattern CAN match common words like DEPARTMENT (DE+PARTMENT).
|
||||
This is why the pattern REQUIRES keyword_pattern in production use.
|
||||
The ContentFilterGuardrail enforces keyword context, preventing false positives.
|
||||
This test documents the raw pattern's broad matching behavior.
|
||||
"""
|
||||
pattern = get_compiled_pattern("eu_vat")
|
||||
# These WILL match the raw pattern (by design - pattern is broad)
|
||||
assert pattern.search("DEPARTMENT") is not None # DE + PARTMENT
|
||||
assert pattern.search("ITALY12345678") is not None # IT + digits
|
||||
|
||||
# But in production, keyword_pattern guard prevents these false positives
|
||||
|
||||
|
||||
class TestEUPassportGeneric:
|
||||
"""Test generic EU passport detection"""
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import pytest
|
|||
|
||||
sys.path.insert(0, os.path.abspath("../../"))
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.content_filter import (
|
||||
ContentFilterGuardrail,
|
||||
)
|
||||
|
|
@ -138,11 +136,12 @@ class TestGDPRPolicyE2E:
|
|||
@pytest.mark.asyncio
|
||||
async def test_eu_vat_masked(self):
|
||||
"""
|
||||
Test 4 - SHOULD MASK: EU VAT number is detected and masked
|
||||
Test 4 - SHOULD MASK: EU VAT number with keyword context is detected and masked
|
||||
"""
|
||||
guardrail = self.setup_gdpr_guardrail()
|
||||
|
||||
text = "Company VAT number is FR12345678901"
|
||||
# Include VAT keyword for contextual matching (max 1 word gap)
|
||||
text = "Company VAT number: FR12345678901"
|
||||
guardrailed_inputs = await guardrail.apply_guardrail(
|
||||
inputs={"texts": [text]},
|
||||
request_data={},
|
||||
|
|
@ -250,3 +249,45 @@ class TestGDPRPolicyE2E:
|
|||
assert "jean@example.com" not in result
|
||||
assert "+33612345678" not in result
|
||||
assert "192057512345678" not in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vat_number_without_keyword_context_passes(self):
|
||||
"""
|
||||
Test 10 - SHOULD NOT MASK: VAT-like pattern without keyword context
|
||||
Contextual keyword guard prevents false positives
|
||||
"""
|
||||
guardrail = self.setup_gdpr_guardrail()
|
||||
|
||||
# Text with VAT-like format but no VAT keyword context
|
||||
text = "Product code FR12345678 for the shipment"
|
||||
guardrailed_inputs = await guardrail.apply_guardrail(
|
||||
inputs={"texts": [text]},
|
||||
request_data={},
|
||||
input_type="request",
|
||||
)
|
||||
result = guardrailed_inputs.get("texts", [])[0]
|
||||
|
||||
# Should not mask without VAT keyword context
|
||||
assert "FR12345678" in result
|
||||
assert "REDACTED" not in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passport_number_without_keyword_context_passes(self):
|
||||
"""
|
||||
Test 11 - SHOULD NOT MASK: Passport-like pattern without keyword context
|
||||
Contextual keyword guard prevents false positives
|
||||
"""
|
||||
guardrail = self.setup_gdpr_guardrail()
|
||||
|
||||
# Text with passport-like format but no passport keyword context
|
||||
text = "Reference number 12AB34567 for your order"
|
||||
guardrailed_inputs = await guardrail.apply_guardrail(
|
||||
inputs={"texts": [text]},
|
||||
request_data={},
|
||||
input_type="request",
|
||||
)
|
||||
result = guardrailed_inputs.get("texts", [])[0]
|
||||
|
||||
# Should not mask without passport keyword context
|
||||
assert "12AB34567" in result
|
||||
assert "REDACTED" not in result
|
||||
|
|
|
|||
|
|
@ -8,6 +8,43 @@ import os
|
|||
import litellm
|
||||
|
||||
|
||||
def test_opus_4_6_australia_region_uses_au_prefix_not_apac():
|
||||
"""
|
||||
Test that Australia region uses 'au.' prefix instead of incorrect 'apac.' prefix.
|
||||
|
||||
AWS Bedrock cross-region inference uses specific regional prefixes:
|
||||
- 'us.' for United States
|
||||
- 'eu.' for Europe
|
||||
- 'au.' for Australia (ap-southeast-2)
|
||||
- 'apac.' for Asia-Pacific (Singapore, ap-southeast-1)
|
||||
|
||||
This test ensures the Claude Opus 4.6 model correctly uses 'au.' for Australia,
|
||||
and that 'apac.' is NOT incorrectly used for Australia region.
|
||||
|
||||
Related: The 'apac.' prefix is valid for Asia-Pacific (Singapore) region models,
|
||||
but should not be used for Australia which has its own 'au.' prefix.
|
||||
"""
|
||||
json_path = os.path.join(os.path.dirname(__file__), "../../model_prices_and_context_window.json")
|
||||
with open(json_path) as f:
|
||||
model_data = json.load(f)
|
||||
|
||||
# Verify au.anthropic.claude-opus-4-6-v1 exists (correct)
|
||||
assert "au.anthropic.claude-opus-4-6-v1" in model_data, \
|
||||
"Missing Australia region model: au.anthropic.claude-opus-4-6-v1"
|
||||
|
||||
# Verify apac.anthropic.claude-opus-4-6-v1 does NOT exist (incorrect)
|
||||
assert "apac.anthropic.claude-opus-4-6-v1" not in model_data, \
|
||||
"Incorrect model entry exists: apac.anthropic.claude-opus-4-6-v1 should be au.anthropic.claude-opus-4-6-v1"
|
||||
|
||||
# Verify the au. model is registered in bedrock_converse_models
|
||||
assert "au.anthropic.claude-opus-4-6-v1" in litellm.bedrock_converse_models, \
|
||||
"au.anthropic.claude-opus-4-6-v1 not registered in bedrock_converse_models"
|
||||
|
||||
# Verify apac. is NOT registered for this model
|
||||
assert "apac.anthropic.claude-opus-4-6-v1" not in litellm.bedrock_converse_models, \
|
||||
"apac.anthropic.claude-opus-4-6-v1 should not be in bedrock_converse_models"
|
||||
|
||||
|
||||
def test_opus_4_6_model_pricing_and_capabilities():
|
||||
json_path = os.path.join(os.path.dirname(__file__), "../../model_prices_and_context_window.json")
|
||||
with open(json_path) as f:
|
||||
|
|
@ -112,17 +149,7 @@ def test_opus_4_6_bedrock_regional_model_pricing():
|
|||
"cache_creation_input_token_cost_above_200k_tokens": 1.375e-05,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 1.1e-06,
|
||||
},
|
||||
"apac.anthropic.claude-opus-4-6-v1": {
|
||||
"input_cost_per_token": 5.5e-06,
|
||||
"output_cost_per_token": 2.75e-05,
|
||||
"cache_creation_input_token_cost": 6.875e-06,
|
||||
"cache_read_input_token_cost": 5.5e-07,
|
||||
"input_cost_per_token_above_200k_tokens": 1.1e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 4.125e-05,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 1.375e-05,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 1.1e-06,
|
||||
},
|
||||
"apac.anthropic.claude-opus-4-6-v1": {
|
||||
"au.anthropic.claude-opus-4-6-v1": {
|
||||
"input_cost_per_token": 5.5e-06,
|
||||
"output_cost_per_token": 2.75e-05,
|
||||
"cache_creation_input_token_cost": 6.875e-06,
|
||||
|
|
@ -180,4 +207,4 @@ def test_opus_4_6_bedrock_converse_registration():
|
|||
assert "global.anthropic.claude-opus-4-6-v1" in litellm.bedrock_converse_models
|
||||
assert "us.anthropic.claude-opus-4-6-v1" in litellm.bedrock_converse_models
|
||||
assert "eu.anthropic.claude-opus-4-6-v1" in litellm.bedrock_converse_models
|
||||
assert "apac.anthropic.claude-opus-4-6-v1" in litellm.bedrock_converse_models
|
||||
assert "au.anthropic.claude-opus-4-6-v1" in litellm.bedrock_converse_models
|
||||
|
|
|
|||
215
ui/litellm-dashboard/package-lock.json
generated
215
ui/litellm-dashboard/package-lock.json
generated
|
|
@ -90,7 +90,6 @@
|
|||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
|
@ -1769,9 +1768,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
||||
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1795,7 +1794,6 @@
|
|||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
|
|
@ -1806,7 +1804,6 @@
|
|||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
|
@ -1816,14 +1813,12 @@
|
|||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
|
|
@ -2001,7 +1996,6 @@
|
|||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
|
|
@ -2015,7 +2009,6 @@
|
|||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
|
|
@ -2025,7 +2018,6 @@
|
|||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
|
|
@ -2349,7 +2341,7 @@
|
|||
"version": "1.58.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.1.tgz",
|
||||
"integrity": "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.1"
|
||||
|
|
@ -3454,14 +3446,12 @@
|
|||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.48",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
|
||||
"integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
|
|
@ -3503,7 +3493,6 @@
|
|||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
|
|
@ -4390,14 +4379,12 @@
|
|||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
|
|
@ -4407,11 +4394,22 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
|
|
@ -4780,7 +4778,6 @@
|
|||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
|
|
@ -4804,7 +4801,6 @@
|
|||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
|
|
@ -4920,7 +4916,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
|
|
@ -5044,7 +5039,6 @@
|
|||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
|
|
@ -5069,7 +5063,6 @@
|
|||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
|
|
@ -5145,7 +5138,6 @@
|
|||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
|
|
@ -5213,7 +5205,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
|
|
@ -5627,14 +5618,12 @@
|
|||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dlv": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
|
|
@ -6548,7 +6537,6 @@
|
|||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
|
|
@ -6581,7 +6569,6 @@
|
|||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
|
|
@ -6619,7 +6606,6 @@
|
|||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
|
|
@ -6780,7 +6766,6 @@
|
|||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
|
@ -6931,7 +6916,6 @@
|
|||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
|
|
@ -7445,7 +7429,6 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
|
|
@ -7498,7 +7481,6 @@
|
|||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
|
|
@ -7559,7 +7541,6 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -7605,7 +7586,6 @@
|
|||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
|
|
@ -7654,7 +7634,6 @@
|
|||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
|
|
@ -7931,7 +7910,6 @@
|
|||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
|
|
@ -8156,19 +8134,6 @@
|
|||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/strip-json-comments": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
||||
|
|
@ -8182,6 +8147,16 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/language-subtag-registry": {
|
||||
"version": "0.3.23",
|
||||
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
|
||||
|
|
@ -8220,7 +8195,6 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
|
@ -8233,7 +8207,6 @@
|
|||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
|
|
@ -8548,7 +8521,6 @@
|
|||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
|
|
@ -9000,7 +8972,6 @@
|
|||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
|
|
@ -9010,6 +8981,18 @@
|
|||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
|
|
@ -9113,7 +9096,6 @@
|
|||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
|
|
@ -9325,7 +9307,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -9344,7 +9325,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
|
|
@ -9689,7 +9669,6 @@
|
|||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
|
|
@ -9733,13 +9712,12 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
|
|
@ -9749,7 +9727,6 @@
|
|||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -9759,7 +9736,6 @@
|
|||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
|
|
@ -9769,7 +9745,7 @@
|
|||
"version": "1.58.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz",
|
||||
"integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.1"
|
||||
|
|
@ -9788,7 +9764,7 @@
|
|||
"version": "1.58.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz",
|
||||
"integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
|
@ -9811,7 +9787,6 @@
|
|||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -9840,7 +9815,6 @@
|
|||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.0.0",
|
||||
|
|
@ -9858,7 +9832,6 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
||||
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -9884,7 +9857,6 @@
|
|||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -9927,7 +9899,6 @@
|
|||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -9953,7 +9924,6 @@
|
|||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
|
|
@ -9967,7 +9937,6 @@
|
|||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
|
|
@ -10081,7 +10050,6 @@
|
|||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -10870,7 +10838,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pify": "^2.3.0"
|
||||
|
|
@ -10880,7 +10847,6 @@
|
|||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
|
|
@ -10889,6 +10855,18 @@
|
|||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
|
||||
|
|
@ -11145,7 +11123,6 @@
|
|||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.16.1",
|
||||
|
|
@ -11186,7 +11163,6 @@
|
|||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
|
|
@ -11242,7 +11218,6 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -11883,7 +11858,6 @@
|
|||
"version": "3.35.1",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
|
|
@ -11919,7 +11893,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
|
@ -11955,7 +11928,6 @@
|
|||
"version": "3.4.19",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
|
|
@ -11993,7 +11965,6 @@
|
|||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
|
|
@ -12010,7 +11981,6 @@
|
|||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
|
|
@ -12064,7 +12034,6 @@
|
|||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
|
|
@ -12074,7 +12043,6 @@
|
|||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
|
|
@ -12116,7 +12084,6 @@
|
|||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -12129,19 +12096,6 @@
|
|||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
||||
|
|
@ -12196,7 +12150,6 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
|
|
@ -12284,7 +12237,6 @@
|
|||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
|
|
@ -12401,7 +12353,7 @@
|
|||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
@ -12603,7 +12555,6 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
|
|
@ -12782,19 +12733,6 @@
|
|||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
|
|
@ -12868,19 +12806,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
|
@ -13083,7 +13008,7 @@
|
|||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
|
@ -13141,11 +13066,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
@ -13159,21 +13085,6 @@
|
|||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
|
||||
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import AdditionalModelSettings from "./AdditionalModelSettings";
|
||||
|
|
@ -47,4 +47,55 @@ describe("AdditionalModelSettings", () => {
|
|||
expect(temperatureSlider).not.toBeDisabled();
|
||||
expect(maxTokensSlider).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("should not show Simulate failure to test fallbacks when onMockTestFallbacksChange is not provided", () => {
|
||||
render(<AdditionalModelSettings />);
|
||||
expect(screen.queryByText(/Simulate failure to test fallbacks/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show and toggle Simulate failure to test fallbacks when callback is provided", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onMockTestFallbacksChange = vi.fn();
|
||||
let currentValue = false;
|
||||
const handleChange = (value: boolean) => {
|
||||
currentValue = value;
|
||||
onMockTestFallbacksChange(value);
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<AdditionalModelSettings
|
||||
mockTestFallbacks={currentValue}
|
||||
onMockTestFallbacksChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const fallbacksCheckbox = screen.getByRole("checkbox", {
|
||||
name: /Simulate failure to test fallbacks/i,
|
||||
});
|
||||
expect(fallbacksCheckbox).toBeInTheDocument();
|
||||
expect(fallbacksCheckbox).not.toBeChecked();
|
||||
|
||||
await act(async () => {
|
||||
await user.click(fallbacksCheckbox);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onMockTestFallbacksChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
rerender(
|
||||
<AdditionalModelSettings
|
||||
mockTestFallbacks={currentValue}
|
||||
onMockTestFallbacksChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await user.click(screen.getByRole("checkbox", { name: /Simulate failure to test fallbacks/i }));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onMockTestFallbacksChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Text } from "@tremor/react";
|
||||
import { Checkbox, InputNumber, Slider, Tooltip } from "antd";
|
||||
import { Checkbox, InputNumber, Popover, Slider, Tooltip, Typography } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface AdditionalModelSettingsProps {
|
||||
|
|
@ -10,6 +10,8 @@ interface AdditionalModelSettingsProps {
|
|||
onTemperatureChange?: (value: number) => void;
|
||||
onMaxTokensChange?: (value: number) => void;
|
||||
onUseAdvancedParamsChange?: (value: boolean) => void;
|
||||
mockTestFallbacks?: boolean;
|
||||
onMockTestFallbacksChange?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
|
||||
|
|
@ -19,6 +21,8 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
|
|||
onTemperatureChange,
|
||||
onMaxTokensChange,
|
||||
onUseAdvancedParamsChange,
|
||||
mockTestFallbacks,
|
||||
onMockTestFallbacksChange,
|
||||
}) => {
|
||||
const [internalUseAdvancedParams, setInternalUseAdvancedParams] = useState(false);
|
||||
const useAdvancedParams =
|
||||
|
|
@ -64,6 +68,45 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
|
|||
<span className="font-medium">Use Advanced Parameters</span>
|
||||
</Checkbox>
|
||||
|
||||
{onMockTestFallbacksChange && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Checkbox
|
||||
checked={mockTestFallbacks ?? false}
|
||||
onChange={(e) => onMockTestFallbacksChange(e.target.checked)}
|
||||
>
|
||||
<span className="font-medium">Simulate failure to test fallbacks</span>
|
||||
</Checkbox>
|
||||
<Popover
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
content={
|
||||
<div style={{ maxWidth: 340 }}>
|
||||
<Typography.Paragraph className="text-sm" style={{ marginBottom: 8 }}>
|
||||
Causes the first request to fail so the router tries fallbacks (if configured). Use
|
||||
this to verify your fallback setup.
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="text-sm" style={{ marginBottom: 0 }}>
|
||||
Behavior can differ when keys, teams, or router settings are configured.{" "}
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/keys_teams_router_settings"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined
|
||||
className="text-xs text-gray-400 cursor-pointer shrink-0 hover:text-gray-600"
|
||||
aria-label="Help: Simulate failure to test fallbacks"
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4 transition-opacity duration-200" style={{ opacity: disabledOpacity }}>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
|
|
|
|||
|
|
@ -271,6 +271,70 @@ describe("ChatUI", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should show Simulate failure to test fallbacks in Model Settings when chat endpoint is selected", async () => {
|
||||
render(
|
||||
<ChatUI
|
||||
accessToken="1234567890"
|
||||
token="1234567890"
|
||||
userRole="user"
|
||||
userID="1234567890"
|
||||
disabledPersonalKeyCreation={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Key")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Model Settings button only appears when a chat model is selected; select "Model 1" first
|
||||
const selectModelLabel = screen.getByText("Select Model");
|
||||
const modelSelectContainer = selectModelLabel.closest("div");
|
||||
const modelSelect = modelSelectContainer?.querySelector(".ant-select-selector");
|
||||
expect(modelSelect).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.mouseDown(modelSelect!);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("Model 1").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Ant Design Select options may not have role="option"; click the dropdown option by text
|
||||
const model1Options = screen.getAllByText("Model 1");
|
||||
await act(async () => {
|
||||
fireEvent.click(model1Options[model1Options.length - 1]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const modelSettingsButton = screen.getByTestId("model-settings-button");
|
||||
expect(modelSettingsButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const modelSettingsButton = screen.getByTestId("model-settings-button");
|
||||
await act(async () => {
|
||||
fireEvent.click(modelSettingsButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Model Settings")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Simulate failure to test fallbacks/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const fallbacksCheckbox = screen.getByRole("checkbox", {
|
||||
name: /Simulate failure to test fallbacks/i,
|
||||
});
|
||||
expect(fallbacksCheckbox).not.toBeChecked();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(fallbacksCheckbox);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("checkbox", { name: /Simulate failure to test fallbacks/i })).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show Fill button and populate customProxyBaseUrl when proxySettings.LITELLM_UI_API_DOC_BASE_URL is provided", async () => {
|
||||
const testProxyUrl = "http://localhost:5000";
|
||||
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
const [temperature, setTemperature] = useState<number>(1.0);
|
||||
const [maxTokens, setMaxTokens] = useState<number>(2048);
|
||||
const [useAdvancedParams, setUseAdvancedParams] = useState<boolean>(false);
|
||||
const [mockTestFallbacks, setMockTestFallbacks] = useState<boolean>(false);
|
||||
|
||||
// Code Interpreter state (using custom hook)
|
||||
const codeInterpreter = useCodeInterpreter();
|
||||
|
|
@ -982,6 +983,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
mcpServers,
|
||||
mcpServerToolRestrictions,
|
||||
handleMCPEvent,
|
||||
mockTestFallbacks,
|
||||
);
|
||||
} else if (endpointType === EndpointType.IMAGE) {
|
||||
// For image generation
|
||||
|
|
@ -1401,6 +1403,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
onTemperatureChange={setTemperature}
|
||||
onMaxTokensChange={setMaxTokens}
|
||||
onUseAdvancedParamsChange={setUseAdvancedParams}
|
||||
mockTestFallbacks={mockTestFallbacks}
|
||||
onMockTestFallbacksChange={setMockTestFallbacks}
|
||||
/>
|
||||
}
|
||||
title="Model Settings"
|
||||
|
|
@ -1412,6 +1416,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
size="small"
|
||||
icon={<SettingOutlined />}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
aria-label="Model Settings"
|
||||
data-testid="model-settings-button"
|
||||
/>
|
||||
</Popover>
|
||||
) : (
|
||||
|
|
@ -2390,7 +2396,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
</Card>
|
||||
<Modal
|
||||
title="Generated Code"
|
||||
visible={isGetCodeModalVisible}
|
||||
open={isGetCodeModalVisible}
|
||||
onCancel={() => setIsGetCodeModalVisible(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
|
|
|
|||
|
|
@ -190,4 +190,70 @@ describe("chat_completion", () => {
|
|||
expect(secondTool.require_approval).toBe("never");
|
||||
expect(secondTool.allowed_tools).toEqual(["toolC"]);
|
||||
});
|
||||
|
||||
it("should include mock_testing_fallbacks in request body when mockTestFallbacks is true", async () => {
|
||||
await makeOpenAIChatCompletionRequest(
|
||||
mockChatHistory,
|
||||
mockUpdateUI,
|
||||
"gpt-4",
|
||||
"test-token",
|
||||
undefined, // tags
|
||||
undefined, // signal
|
||||
undefined, // onReasoningContent
|
||||
undefined, // onTimingData
|
||||
undefined, // onUsageData
|
||||
undefined, // traceId
|
||||
undefined, // vector_store_ids
|
||||
undefined, // guardrails
|
||||
undefined, // policies
|
||||
undefined, // selectedMCPServers
|
||||
undefined, // onImageGenerated
|
||||
undefined, // onSearchResults
|
||||
undefined, // temperature
|
||||
undefined, // max_tokens
|
||||
undefined, // onTotalLatency
|
||||
undefined, // customBaseUrl
|
||||
undefined, // mcpServers
|
||||
undefined, // mcpServerToolRestrictions
|
||||
undefined, // onMCPEvent
|
||||
true, // mockTestFallbacks
|
||||
);
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockCreate.mock.calls[0][0];
|
||||
expect(callArgs.mock_testing_fallbacks).toBe(true);
|
||||
});
|
||||
|
||||
it("should not include mock_testing_fallbacks in request body when mockTestFallbacks is false or undefined", async () => {
|
||||
await makeOpenAIChatCompletionRequest(
|
||||
mockChatHistory,
|
||||
mockUpdateUI,
|
||||
"gpt-4",
|
||||
"test-token",
|
||||
undefined, // tags
|
||||
undefined, // signal
|
||||
undefined, // onReasoningContent
|
||||
undefined, // onTimingData
|
||||
undefined, // onUsageData
|
||||
undefined, // traceId
|
||||
undefined, // vector_store_ids
|
||||
undefined, // guardrails
|
||||
undefined, // policies
|
||||
undefined, // selectedMCPServers
|
||||
undefined, // onImageGenerated
|
||||
undefined, // onSearchResults
|
||||
undefined, // temperature
|
||||
undefined, // max_tokens
|
||||
undefined, // onTotalLatency
|
||||
undefined, // customBaseUrl
|
||||
undefined, // mcpServers
|
||||
undefined, // mcpServerToolRestrictions
|
||||
undefined, // onMCPEvent
|
||||
false, // mockTestFallbacks
|
||||
);
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockCreate.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty("mock_testing_fallbacks");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export async function makeOpenAIChatCompletionRequest(
|
|||
mcpServers?: MCPServer[],
|
||||
mcpServerToolRestrictions?: Record<string, string[]>,
|
||||
onMCPEvent?: (event: MCPEvent) => void,
|
||||
mockTestFallbacks?: boolean,
|
||||
) {
|
||||
// base url should be the current base_url
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
|
|
@ -115,6 +116,7 @@ export async function makeOpenAIChatCompletionRequest(
|
|||
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
...(max_tokens !== undefined ? { max_tokens } : {}),
|
||||
...(mockTestFallbacks ? { mock_testing_fallbacks: true } : {}),
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,15 @@ interface MaskedEntityCount {
|
|||
[key: string]: number;
|
||||
}
|
||||
|
||||
interface MatchDetail {
|
||||
type: string;
|
||||
detection_method?: string;
|
||||
action_taken?: string;
|
||||
snippet?: string;
|
||||
category?: string;
|
||||
position?: number;
|
||||
}
|
||||
|
||||
interface GuardrailInformation {
|
||||
duration: number;
|
||||
end_time: number;
|
||||
|
|
@ -33,7 +42,15 @@ interface GuardrailInformation {
|
|||
guardrail_status: string;
|
||||
guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse | any;
|
||||
masked_entity_count: MaskedEntityCount;
|
||||
guardrail_provider?: string; // "presidio" | "bedrock" | "litellm_content_filter" | other providers
|
||||
guardrail_provider?: string;
|
||||
guardrail_id?: string;
|
||||
policy_template?: string;
|
||||
detection_method?: string;
|
||||
confidence_score?: number;
|
||||
classification?: Record<string, any>;
|
||||
match_details?: MatchDetail[];
|
||||
patterns_checked?: number;
|
||||
alert_recipients?: string[];
|
||||
}
|
||||
|
||||
interface GuardrailViewerProps {
|
||||
|
|
@ -87,6 +104,179 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const PolicyDetectionRow = ({ entry }: { entry: GuardrailInformation }) => {
|
||||
const hasData = entry.policy_template || entry.detection_method || entry.confidence_score != null || entry.patterns_checked != null;
|
||||
if (!hasData) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{entry.policy_template && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">Policy:</span>
|
||||
<span className="px-2 py-1 bg-purple-50 text-purple-700 rounded-md text-xs font-medium">
|
||||
{entry.policy_template}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.detection_method && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">Detection:</span>
|
||||
{entry.detection_method.split(",").map((method) => (
|
||||
<span key={method} className="px-2 py-1 bg-slate-100 text-slate-700 rounded-md text-xs font-medium">
|
||||
{method.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{entry.confidence_score != null && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">Confidence:</span>
|
||||
<span className={`px-2 py-1 rounded-md text-xs font-medium ${
|
||||
entry.confidence_score >= 0.8 ? "bg-red-100 text-red-800" :
|
||||
entry.confidence_score >= 0.5 ? "bg-amber-100 text-amber-800" :
|
||||
"bg-green-100 text-green-800"
|
||||
}`}>
|
||||
{(entry.confidence_score * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.patterns_checked != null && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">Patterns checked:</span>
|
||||
<span className="text-sm text-gray-600">{entry.patterns_checked}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) => {
|
||||
if (!matchDetails || matchDetails.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<h5 className="font-medium mb-2">Match Details ({matchDetails.length})</h5>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-gray-500">
|
||||
<th className="pb-2 pr-4">Type</th>
|
||||
<th className="pb-2 pr-4">Method</th>
|
||||
<th className="pb-2 pr-4">Action</th>
|
||||
<th className="pb-2">Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{matchDetails.map((match, idx) => (
|
||||
<tr key={idx} className="border-b border-gray-100">
|
||||
<td className="py-2 pr-4">{match.type}</td>
|
||||
<td className="py-2 pr-4">
|
||||
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-xs">
|
||||
{match.detection_method ?? "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
match.action_taken === "BLOCK" ? "bg-red-100 text-red-800" : "bg-blue-50 text-blue-700"
|
||||
}`}>
|
||||
{match.action_taken ?? "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 font-mono text-xs text-gray-600 break-all">
|
||||
{match.category ? `[${match.category}] ` : ""}{match.snippet ?? "-"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ClassificationDetails = ({ classification }: { classification: Record<string, any> }) => {
|
||||
if (!classification) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<h5 className="font-medium mb-2">Classification</h5>
|
||||
<div className="space-y-2 bg-gray-50 rounded-lg p-3">
|
||||
{classification.category && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3 text-sm">Category:</span>
|
||||
<span className="text-sm">{classification.category}</span>
|
||||
</div>
|
||||
)}
|
||||
{classification.article_reference && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3 text-sm">Reference:</span>
|
||||
<span className="text-sm font-mono">{classification.article_reference}</span>
|
||||
</div>
|
||||
)}
|
||||
{classification.confidence != null && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3 text-sm">Confidence:</span>
|
||||
<span className="text-sm">{(classification.confidence * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{classification.reason && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3 text-sm">Reason:</span>
|
||||
<span className="text-sm">{classification.reason}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExecutionTimeline = ({ entries }: { entries: GuardrailInformation[] }) => {
|
||||
if (entries.length <= 1) return null;
|
||||
|
||||
const sorted = [...entries].sort((a, b) => (a.start_time ?? 0) - (b.start_time ?? 0));
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h5 className="font-medium mb-3">Execution Timeline</h5>
|
||||
<div className="relative pl-4 border-l-2 border-gray-200 space-y-3">
|
||||
{sorted.map((e, idx) => {
|
||||
const isSuccess = (e.guardrail_status ?? "").toLowerCase() === "success";
|
||||
return (
|
||||
<div key={idx} className="relative">
|
||||
<div
|
||||
className={`absolute -left-[calc(1rem+5px)] w-2.5 h-2.5 rounded-full mt-1.5 ${
|
||||
isSuccess ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-mono text-xs text-gray-500">
|
||||
{e.duration?.toFixed(3)}s
|
||||
</span>
|
||||
<span className="text-sm font-medium">{e.guardrail_name}</span>
|
||||
<span className="px-1.5 py-0.5 bg-gray-100 text-gray-600 rounded text-xs">
|
||||
{e.guardrail_mode}
|
||||
</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||
isSuccess ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
|
||||
}`}>
|
||||
{e.guardrail_status}
|
||||
</span>
|
||||
{e.policy_template && (
|
||||
<span className="px-1.5 py-0.5 bg-purple-50 text-purple-700 rounded text-xs">
|
||||
{e.policy_template}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
|
||||
const guardrailProvider = entry.guardrail_provider ?? "presidio";
|
||||
const statusLabel = entry.guardrail_status ?? "unknown";
|
||||
|
|
@ -127,6 +317,12 @@ const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
|
|||
<span className="font-medium w-1/3">Guardrail Name:</span>
|
||||
<span className="font-mono break-words">{entry.guardrail_name}</span>
|
||||
</div>
|
||||
{entry.guardrail_id && entry.guardrail_id !== entry.guardrail_name && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Guardrail ID:</span>
|
||||
<span className="font-mono break-words text-gray-600">{entry.guardrail_id}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Mode:</span>
|
||||
<span className="font-mono break-words">{entry.guardrail_mode}</span>
|
||||
|
|
@ -161,6 +357,17 @@ const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Policy, detection method, confidence, patterns checked */}
|
||||
<PolicyDetectionRow entry={entry} />
|
||||
|
||||
{/* Classification details (LLM-judge) */}
|
||||
{entry.classification && <ClassificationDetails classification={entry.classification} />}
|
||||
|
||||
{/* Match details table */}
|
||||
{entry.match_details && entry.match_details.length > 0 && (
|
||||
<MatchDetailsTable matchDetails={entry.match_details} />
|
||||
)}
|
||||
|
||||
{totalMaskedEntities > 0 && (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<h5 className="font-medium mb-2">Masked Entity Summary</h5>
|
||||
|
|
@ -222,6 +429,10 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
);
|
||||
}, 0);
|
||||
|
||||
const policyTemplates = Array.from(
|
||||
new Set(guardrailEntries.map((e) => e.policy_template).filter(Boolean))
|
||||
);
|
||||
|
||||
const tooltipTitle = allSucceeded ? null : "Guardrail failed to run.";
|
||||
|
||||
if (guardrailEntries.length === 0) {
|
||||
|
|
@ -237,7 +448,7 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
{
|
||||
key: "1",
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-lg font-medium text-gray-900">Guardrail Information</h3>
|
||||
|
||||
<Tooltip title={tooltipTitle} placement="top" arrow destroyTooltipOnHide>
|
||||
|
|
@ -257,10 +468,17 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
|
|||
{totalMaskedEntities} masked {totalMaskedEntities === 1 ? "entity" : "entities"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{policyTemplates.map((pt) => (
|
||||
<span key={pt} className="px-2 py-1 bg-purple-50 text-purple-700 rounded-md text-xs font-medium">
|
||||
{pt}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<div className="p-4 space-y-6">
|
||||
<ExecutionTimeline entries={guardrailEntries} />
|
||||
{guardrailEntries.map((entry, index) => (
|
||||
<GuardrailDetails
|
||||
key={`${entry.guardrail_name ?? "guardrail"}-${index}`}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue