mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
Merge dcc05ef8d9 into 22a349ee70
This commit is contained in:
commit
06e1f7d398
6 changed files with 3661 additions and 0 deletions
103
litellm/proxy/guardrails/guardrail_hooks/aliyun/__init__.py
Normal file
103
litellm/proxy/guardrails/guardrail_hooks/aliyun/__init__.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""
|
||||
Aliyun AI Security Guardrail Integration for LiteLLM
|
||||
阿里云AI安全护栏集成
|
||||
This module provides integration with Aliyun's AI Security Guardrail service for:
|
||||
- ContentModeration 内容合规检测
|
||||
- PromptAttack 提示词攻击检测
|
||||
- SensitiveData 敏感内容检测
|
||||
- ModelHallucination 模型幻觉
|
||||
- MaliciousUrl 恶意URL检测
|
||||
...
|
||||
Documentation: https://help.aliyun.com/document_detail/2873209.html
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from litellm.types.guardrails import SupportedGuardrailIntegrations
|
||||
|
||||
from .aliyun_ai_guardrail import AliyunAIGuardrail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.guardrails import Guardrail, LitellmParams
|
||||
|
||||
|
||||
def _resolve_os_environ_reference(value: str | None) -> str | None:
|
||||
"""Resolve an ``os.environ/`` reference.
|
||||
|
||||
guardrail_registry.py only auto-resolves api_key/api_base, so the Aliyun
|
||||
credential fields have to be resolved here.
|
||||
"""
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
if isinstance(value, str) and value.startswith("os.environ/"):
|
||||
return get_secret_str(value)
|
||||
return value
|
||||
|
||||
|
||||
def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail") -> AliyunAIGuardrail:
|
||||
"""
|
||||
Initialize an Aliyun AI Guardrail instance.
|
||||
Credentials are configured in config.yaml (litellm_params) and support
|
||||
os.environ/ references:
|
||||
- access_key_id: Aliyun Access Key ID
|
||||
- access_key_secret: Aliyun Access Key Secret
|
||||
Args:
|
||||
litellm_params: The LiteLLM parameters for the guardrail
|
||||
guardrail: The guardrail configuration
|
||||
Returns:
|
||||
AliyunAIGuardrail instance
|
||||
"""
|
||||
import litellm
|
||||
|
||||
guardrail_name: Final = guardrail.get("guardrail_name")
|
||||
if not guardrail_name:
|
||||
raise ValueError("Aliyun AI Guardrail: guardrail_name is required")
|
||||
|
||||
level: Final = getattr(litellm_params, "level", None)
|
||||
max_text_length: Final = getattr(litellm_params, "max_text_length", None)
|
||||
stream_window_size: Final = getattr(litellm_params, "stream_window_size", None)
|
||||
stream_slide_step: Final = getattr(litellm_params, "stream_slide_step", None)
|
||||
stream_first_check_step: Final = getattr(litellm_params, "stream_first_check_step", None)
|
||||
region_id: Final = getattr(litellm_params, "region_id", None)
|
||||
service_input: Final = getattr(litellm_params, "service_input", None)
|
||||
service_output: Final = getattr(litellm_params, "service_output", None)
|
||||
service_mcp: Final = getattr(litellm_params, "service_mcp", None)
|
||||
|
||||
# These custom credential fields are not auto-resolved by guardrail_registry.py
|
||||
# (only api_key/api_base are), so os.environ/ references are resolved here.
|
||||
access_key_id: Final = _resolve_os_environ_reference(getattr(litellm_params, "access_key_id", None))
|
||||
access_key_secret: Final = _resolve_os_environ_reference(getattr(litellm_params, "access_key_secret", None))
|
||||
|
||||
aliyun_guardrail: Final = AliyunAIGuardrail(
|
||||
guardrail_name=guardrail_name,
|
||||
access_key_id=access_key_id,
|
||||
access_key_secret=access_key_secret,
|
||||
level=level,
|
||||
max_text_length=max_text_length,
|
||||
stream_window_size=stream_window_size,
|
||||
stream_slide_step=stream_slide_step,
|
||||
stream_first_check_step=stream_first_check_step,
|
||||
region_id=region_id,
|
||||
service_input=service_input,
|
||||
service_output=service_output,
|
||||
service_mcp=service_mcp,
|
||||
default_on=litellm_params.default_on,
|
||||
event_hook=litellm_params.mode,
|
||||
)
|
||||
|
||||
litellm.logging_callback_manager.add_litellm_callback(aliyun_guardrail)
|
||||
|
||||
return aliyun_guardrail
|
||||
|
||||
|
||||
# Registry for guardrail initializers.
|
||||
# Plain dicts: guardrail_registry.py gates discovery on `isinstance(registry, dict)`,
|
||||
# which a MappingProxyType would fail, silently skipping this guardrail's registration.
|
||||
guardrail_initializer_registry: Final = { # mutable-ok: loader requires a real dict
|
||||
SupportedGuardrailIntegrations.ALIYUN_AI_GUARDRAIL.value: initialize_guardrail,
|
||||
}
|
||||
|
||||
# Registry for guardrail classes
|
||||
guardrail_class_registry: Final = { # mutable-ok: loader requires a real dict
|
||||
SupportedGuardrailIntegrations.ALIYUN_AI_GUARDRAIL.value: AliyunAIGuardrail,
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
100
litellm/proxy/guardrails/guardrail_hooks/aliyun/base.py
Normal file
100
litellm/proxy/guardrails/guardrail_hooks/aliyun/base.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
Base class for Aliyun guardrails
|
||||
阿里云护栏基类
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, Sequence
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
|
||||
class AliyunGuardrailBase:
|
||||
"""
|
||||
Base class for Aliyun guardrails.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _iter_user_messages(messages: Sequence[AllMessageValues]) -> Iterator[AllMessageValues]:
|
||||
"""
|
||||
Yield every user message of the request, in order.
|
||||
Restricting this to the trailing user block would let a caller hide a
|
||||
prohibited turn behind an attacker-supplied assistant message.
|
||||
"""
|
||||
return (message for message in messages if message.get("role") == "user")
|
||||
|
||||
@staticmethod
|
||||
def _iter_audited_text_messages(messages: Sequence[AllMessageValues]) -> Iterator[AllMessageValues]:
|
||||
return (message for message in messages if message.get("role") in ("user", "tool"))
|
||||
|
||||
@staticmethod
|
||||
def _extract_image_url(part: object) -> str | None:
|
||||
"""
|
||||
Return the URL of an ``image_url`` content part.
|
||||
Args:
|
||||
part: A single content part of a message
|
||||
Returns:
|
||||
The URL string, or None when the part carries no image URL
|
||||
"""
|
||||
if not isinstance(part, dict) or part.get("type") not in ("image_url", "input_image"):
|
||||
return None
|
||||
image_url: Final = part.get("image_url")
|
||||
if isinstance(image_url, dict):
|
||||
url: Final = image_url.get("url")
|
||||
return url if isinstance(url, str) else None
|
||||
return image_url if isinstance(image_url, str) else None
|
||||
|
||||
def get_user_prompt(self, messages: Sequence[AllMessageValues]) -> str | None:
|
||||
"""
|
||||
Collect the text of every user message in the request.
|
||||
Scanning only the trailing user block would let a caller hide a
|
||||
prohibited prompt behind an attacker-supplied assistant message, so all
|
||||
user turns of the submitted request are audited.
|
||||
Example:
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello, how are you?"},
|
||||
{"role": "assistant", "content": "I'm good, thank you!"},
|
||||
{"role": "user", "content": "What is the weather in Tokyo?"},
|
||||
]
|
||||
get_user_prompt(messages) -> "Hello, how are you?\nWhat is the weather in Tokyo?"
|
||||
"""
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
convert_content_list_to_str,
|
||||
)
|
||||
|
||||
user_prompt: Final = "\n".join(
|
||||
convert_content_list_to_str(message) for message in self._iter_audited_text_messages(messages)
|
||||
).strip()
|
||||
return user_prompt or None
|
||||
|
||||
def _iter_public_image_urls(self, messages: Sequence[AllMessageValues]) -> Iterator[str]:
|
||||
"""Yield publicly reachable image URLs from user and tool messages."""
|
||||
for content in (message.get("content") for message in self._iter_audited_text_messages(messages)):
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
for url in (self._extract_image_url(part) for part in content):
|
||||
# Only public http(s) URLs are reachable by the Aliyun API, so
|
||||
# data: URIs and other inline payloads are skipped.
|
||||
if url is not None and url.startswith(("http://", "https://")):
|
||||
yield url
|
||||
|
||||
def get_image_urls(self, messages: Sequence[AllMessageValues]) -> tuple[str, ...]:
|
||||
"""
|
||||
Extract image URLs from every user and tool message in the request.
|
||||
Only publicly accessible http(s) URLs are collected (in order,
|
||||
de-duplicated). Uses the same message range as ``get_user_prompt``.
|
||||
Example:
|
||||
messages = [
|
||||
{"role": "user", "content": [
|
||||
{"type": "text", "text": "what is in this image?"},
|
||||
{"type": "image_url", "image_url": {"url": "https://a.com/x.png"}},
|
||||
]},
|
||||
]
|
||||
get_image_urls(messages) -> ("https://a.com/x.png",)
|
||||
"""
|
||||
# dict.fromkeys is the order-preserving dedup; it is transient and the
|
||||
# result is frozen into a tuple before it leaves this method.
|
||||
return tuple(dict.fromkeys(self._iter_public_image_urls(messages))) # mutable-ok: transient dedup, frozen here
|
||||
|
|
@ -134,6 +134,7 @@ class SupportedGuardrailIntegrations(Enum):
|
|||
HEADROOM = "headroom"
|
||||
COMPRESR = "compresr"
|
||||
STRAIKER = "straiker"
|
||||
ALIYUN_AI_GUARDRAIL = "aliyun_ai_guardrail"
|
||||
|
||||
|
||||
class Role(Enum):
|
||||
|
|
@ -667,6 +668,55 @@ class JavelinGuardrailConfigModel(BaseModel):
|
|||
config: dict | None = Field(default=None, description="Additional configuration for the guardrail")
|
||||
|
||||
|
||||
class AliyunAIGuardrailConfigModel(BaseModel):
|
||||
"""Configuration parameters for the Aliyun AI Security guardrail."""
|
||||
|
||||
level: str | None = Field(
|
||||
default=None,
|
||||
description="Protection level. 'low': block all risks (high protection), 'medium': block medium+high risks, 'high': block high only, 'max': observe mode. Default: medium",
|
||||
)
|
||||
max_text_length: int | None = Field(
|
||||
default=None,
|
||||
description="Maximum text length for a single API call. Text longer than this will be split.",
|
||||
)
|
||||
stream_window_size: int | None = Field(
|
||||
default=None,
|
||||
description="Sliding window size (in chars) for streaming output guardrail checks. Default: 500",
|
||||
)
|
||||
stream_slide_step: int | None = Field(
|
||||
default=None,
|
||||
description="Sliding step (in chars) for streaming output guardrail checks. Default: 300",
|
||||
)
|
||||
stream_first_check_step: int | None = Field(
|
||||
default=None,
|
||||
description="First check threshold (in chars) to reduce first-token latency. Default: 50",
|
||||
)
|
||||
region_id: str | None = Field(
|
||||
default=None,
|
||||
description="Aliyun region ID. Default: cn-shanghai",
|
||||
)
|
||||
service_input: str | None = Field(
|
||||
default=None,
|
||||
description="Service code for input (pre-call) detection. Default: query_security_check",
|
||||
)
|
||||
service_output: str | None = Field(
|
||||
default=None,
|
||||
description="Service code for output (post-call) detection. Default: response_security_check",
|
||||
)
|
||||
service_mcp: str | None = Field(
|
||||
default=None,
|
||||
description="Service code for MCP tool call detection (pre/post MCP call). Default: query_security_check",
|
||||
)
|
||||
access_key_id: str | None = Field(
|
||||
default=None,
|
||||
description="Aliyun Access Key ID for the guardrail. Configure in config.yaml, supports os.environ/ reference",
|
||||
)
|
||||
access_key_secret: str | None = Field(
|
||||
default=None,
|
||||
description="Aliyun Access Key Secret for the guardrail. Configure in config.yaml, supports os.environ/ reference",
|
||||
)
|
||||
|
||||
|
||||
class ContentFilterAction(str, Enum):
|
||||
"""Action to take when content filter detects a match"""
|
||||
|
||||
|
|
@ -1012,6 +1062,7 @@ class LitellmParams(
|
|||
QostodianNexusConfigModel,
|
||||
VigilGuardGuardrailConfigModel,
|
||||
SingulrGuardrailConfigModel,
|
||||
AliyunAIGuardrailConfigModel,
|
||||
):
|
||||
guardrail: str = Field(description="The type of guardrail integration to use")
|
||||
mode: str | list[str] | Mode = Field(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
"""
|
||||
Type definitions for Aliyun AI Security Guardrail
|
||||
阿里云AI安全护栏类型定义
|
||||
Aliyun AI Guardrail supports the following detection types:
|
||||
- contentModeration: Content safety moderation
|
||||
- sensitiveData: Sensitive data detection (PII, etc.)
|
||||
- promptAttack: Prompt injection attack detection
|
||||
- maliciousUrl: Malicious URL detection
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Literal, TypeAlias
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import NotRequired, ReadOnly, TypedDict
|
||||
|
||||
from ..base import GuardrailConfigModel
|
||||
|
||||
|
||||
# Response types
|
||||
class AliyunAIGuardrailResponseDetailResultExt(TypedDict, total=False):
|
||||
"""Extended information in result"""
|
||||
|
||||
Desensitization: ReadOnly[str | None] # Desensitized text when action is mask
|
||||
|
||||
|
||||
class AliyunAIGuardrailResponseDetailResult(TypedDict, total=False):
|
||||
"""Result item in detail"""
|
||||
|
||||
Confidence: ReadOnly[float | None]
|
||||
Label: ReadOnly[str | None]
|
||||
Ext: ReadOnly[AliyunAIGuardrailResponseDetailResultExt | None]
|
||||
# Per-result risk level. This is the shape documented for MultiModalGuard; the
|
||||
# ``_pro`` service codes report the severity on the parent Detail as ``Level``
|
||||
# instead, so both have to be honoured when deciding whether to block.
|
||||
RiskLevel: ReadOnly[str | None]
|
||||
|
||||
|
||||
class AliyunAIGuardrailResponseDetail(TypedDict):
|
||||
"""Detail item in response data"""
|
||||
|
||||
Type: ReadOnly[str] # contentModeration, sensitiveData, promptAttack, maliciousUrl
|
||||
Suggestion: ReadOnly[str] # pass, block, mask
|
||||
Result: ReadOnly[Sequence[AliyunAIGuardrailResponseDetailResult]]
|
||||
# Risk level as returned by the ``_pro`` service codes (none/low/medium/high, or
|
||||
# S0-S4 for sensitiveData). Absent in the documented response shape, which carries
|
||||
# the severity as Result[].RiskLevel.
|
||||
Level: ReadOnly[NotRequired[str]]
|
||||
|
||||
|
||||
class AliyunAIGuardrailResponseData(TypedDict, total=False):
|
||||
"""Response data from Aliyun AI Guardrail API"""
|
||||
|
||||
Suggestion: ReadOnly[str] # Overall suggestion: pass, block, mask
|
||||
Detail: ReadOnly[Sequence[AliyunAIGuardrailResponseDetail] | None]
|
||||
|
||||
|
||||
class AliyunAIGuardrailResponse(TypedDict):
|
||||
"""Response from Aliyun AI Guardrail API"""
|
||||
|
||||
RequestId: ReadOnly[str]
|
||||
Code: ReadOnly[int]
|
||||
Message: ReadOnly[str | None]
|
||||
Data: ReadOnly[AliyunAIGuardrailResponseData | None]
|
||||
|
||||
|
||||
# Suggestion type
|
||||
AliyunAIGuardrailSuggestion: TypeAlias = Literal["pass", "block", "watch"]
|
||||
|
||||
# Detection type
|
||||
AliyunAIGuardrailDetectionType: TypeAlias = Literal[
|
||||
"contentModeration", "sensitiveData", "promptAttack", "maliciousUrl"
|
||||
]
|
||||
|
||||
|
||||
class AliyunAIGuardrailRequestParams(TypedDict, total=False):
|
||||
"""Request parameters for Aliyun AI Guardrail API"""
|
||||
|
||||
Action: ReadOnly[str]
|
||||
Version: ReadOnly[str]
|
||||
AccessKeyId: ReadOnly[str]
|
||||
Timestamp: ReadOnly[str]
|
||||
SignatureMethod: ReadOnly[str]
|
||||
SignatureVersion: ReadOnly[str]
|
||||
SignatureNonce: ReadOnly[str]
|
||||
Format: ReadOnly[str]
|
||||
Service: ReadOnly[str]
|
||||
ServiceParameters: ReadOnly[str]
|
||||
Signature: ReadOnly[str]
|
||||
|
||||
|
||||
# Risk level literals
|
||||
AliyunRiskLevel: TypeAlias = Literal["none", "low", "medium", "high"]
|
||||
|
||||
# Protection level literals
|
||||
AliyunProtectionLevel: TypeAlias = Literal["low", "medium", "high", "max"]
|
||||
|
||||
|
||||
# Configuration models
|
||||
class AliyunAIGuardrailOptionalParams(BaseModel):
|
||||
"""
|
||||
Optional parameters for Aliyun AI Guardrail.
|
||||
Credentials (access_key_id / access_key_secret) are configured
|
||||
in config.yaml on the AliyunAIGuardrailConfigModel and support os.environ/ references.
|
||||
"""
|
||||
|
||||
level: AliyunProtectionLevel | None = Field(
|
||||
default="medium",
|
||||
description="Protection level for risk filtering. 'low': block all risks (high protection), 'medium': block medium and high risks, 'high': block only high risks (low protection), 'max': observation mode (no blocking). Default: medium",
|
||||
)
|
||||
max_text_length: int | None = Field(
|
||||
default=2000,
|
||||
description="Maximum text length for a single API call. Text longer than this will be split.",
|
||||
)
|
||||
stream_window_size: int | None = Field(
|
||||
default=500,
|
||||
description="Sliding window size (in chars) for streaming output guardrail checks. Each check sends the most recent N chars to the API.",
|
||||
)
|
||||
stream_slide_step: int | None = Field(
|
||||
default=300,
|
||||
description="Sliding step (in chars) for streaming output guardrail checks. A check is triggered every time N new chars accumulate since the last check.",
|
||||
)
|
||||
stream_first_check_step: int | None = Field(
|
||||
default=50,
|
||||
description="First check threshold (in chars) for streaming output. The first guardrail check triggers earlier (at N chars) to reduce first-token latency, subsequent checks use stream_slide_step.",
|
||||
)
|
||||
region_id: str | None = Field(
|
||||
default="cn-shanghai",
|
||||
description="Aliyun region ID. Default: cn-shanghai",
|
||||
)
|
||||
service_input: str | None = Field(
|
||||
default="query_security_check_pro",
|
||||
description="Service code for input (pre-call) detection. Default: query_security_check_pro",
|
||||
)
|
||||
service_output: str | None = Field(
|
||||
default="response_security_check_pro",
|
||||
description="Service code for output (post-call) detection. Default: response_security_check_pro",
|
||||
)
|
||||
service_mcp: str | None = Field(
|
||||
default="query_security_check_pro",
|
||||
description="Service code for MCP tool call detection (pre_mcp_call and post_mcp_call). Default: query_security_check_pro",
|
||||
)
|
||||
|
||||
|
||||
class AliyunAIGuardrailConfigModel(GuardrailConfigModel[AliyunAIGuardrailOptionalParams]):
|
||||
"""
|
||||
Configuration model for Aliyun AI Guardrail.
|
||||
Credentials are configured in config.yaml and support os.environ/ references:
|
||||
- access_key_id: Aliyun Access Key ID
|
||||
- access_key_secret: Aliyun Access Key Secret
|
||||
"""
|
||||
|
||||
access_key_id: str | None = Field(
|
||||
default=None,
|
||||
description="Aliyun Access Key ID. Configure in config.yaml, supports os.environ/ reference",
|
||||
)
|
||||
access_key_secret: str | None = Field(
|
||||
default=None,
|
||||
description="Aliyun Access Key Secret. Configure in config.yaml, supports os.environ/ reference",
|
||||
)
|
||||
optional_params: AliyunAIGuardrailOptionalParams | None = Field(
|
||||
default_factory=AliyunAIGuardrailOptionalParams,
|
||||
description="Optional parameters for the Aliyun AI Guardrail",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "Aliyun AI Security Guardrail"
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue