[BETA] 제네릭 가드레일 API - PR 없이 통합하기

[BETA] 제네릭 가드레일 API - PR 없이 통합하기

가드레일 프로바이더로서 LiteLLM에 통합하는 전통적인 방법은 다음을 요구했어요:

  • LiteLLM 저장소에 PR 제출
  • 리뷰와 병합 대기
  • LiteLLM 코드베이스에서 프로바이더별 코드 유지
  • API 변경에 맞춰 통합 업데이트

해결책

제네릭 가드레일 API(Generic Guardrail API)를 사용하면 간단한 API 엔드포인트를 구현해 PR 없이 즉시 LiteLLM에 통합할 수 있어요.

주요 이점

  1. PR 불필요 - 즉시 배포하고 통합하세요
  2. 범용 지원 - 모든 LiteLLM 엔드포인트에서 동작 (chat, embeddings, image generation 등)
  3. 간단한 계약 - 엔드포인트 하나, 응답 유형 세 가지
  4. 멀티모달 지원 - 요청/응답에서 텍스트와 이미지 모두 처리
  5. 커스텀 파라미터 - config를 통해 프로바이더별 params 전달
  6. 완전한 제어 - 가드레일 API를 직접 소유하고 유지

지원되는 엔드포인트

제네릭 가드레일 API는 다음 LiteLLM 엔드포인트와 동작해요:

  • /v1/chat/completions - OpenAI Chat Completions
  • /v1/completions - OpenAI Text Completions
  • /v1/responses - OpenAI Responses API
  • /v1/images/generations - OpenAI Image Generation
  • /v1/audio/transcriptions - OpenAI Audio Transcriptions
  • /v1/audio/speech - OpenAI Text-to-Speech
  • /v1/messages - Anthropic Messages
  • /v1/rerank - Cohere Rerank
  • 패스스루(pass-through) 엔드포인트

출처: 문서

본문

동작 방식

  1. LiteLLM이 모든 요청(챗 메시지, 임베딩, 이미지 프롬프트 등)에서 텍스트와 이미지를 추출합니다
  2. 추출된 콘텐츠 + 메타데이터를 내 API 엔드포인트로 보냅니다
  3. 내 API가 BLOCKED, NONE, GUARDRAIL_INTERVENED 중 하나로 응답합니다
  4. LiteLLM이 결정을 집행하고 수정 사항을 적용합니다

API 계약

엔드포인트

POST /beta/litellm_basic_guardrail_api를 구현하세요.

요청 형식

{
  "texts": ["extracted text from the request"],  // array of text strings
  "images": ["base64_encoded_image_data"],  // optional array of images
  "tools": [  // tool calls sent to the LLM (in the OpenAI Chat Completions spec)
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get the current weather",
        "parameters": {
          "type": "object",
          "properties": {
            "location": {"type": "string"}
          }
        }
      }
    }
  ],
  "tool_calls": [  // tool calls received from the LLM (in the OpenAI Chat Completions spec)
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"location\": \"San Francisco\"}"
      }
    }
  ],
  "structured_messages": [  // optional, full messages in OpenAI format (for chat endpoints)
    {"role": "system", "content": "You are a helpful assistant"},
    {"role": "user", "content": "Hello"}
  ],
  "request_data": {
    "user_api_key_hash": "hash of the litellm virtual key used",
    "user_api_key_alias": "alias of the litellm virtual key used",
    "user_api_key_user_id": "user id associated with the litellm virtual key used",
    "user_api_key_user_email": "user email associated with the litellm virtual key used",
    "user_api_key_team_id": "team id associated with the litellm virtual key used",
    "user_api_key_team_alias": "team alias associated with the litellm virtual key used",
    "user_api_key_end_user_id": "end user id associated with the litellm virtual key used",
    "user_api_key_org_id": "org id associated with the litellm virtual key used"
  },
  "request_headers": {  // optional: inbound request headers (allowlist). Allowed headers show their value; all others show "[present]" to indicate the header existed.
    "User-Agent": "OpenAI/Python 2.17.0",
    "Content-Type": "application/json",
    "X-Request-Id": "[present]"
  },
  "litellm_version": "1.x.y",  // optional: LiteLLM library version running this proxy
  "input_type": "request",  // "request" or "response"
  "litellm_call_id": "unique_call_id",  // the call id of the individual LLM call
  "litellm_trace_id": "trace_id",  // the trace id of the LLM call - useful if there are multiple LLM calls for the same conversation
  "additional_provider_specific_params": {
    // your custom params from config
  }
}

응답 형식

{
  "action": "BLOCKED" | "NONE" | "GUARDRAIL_INTERVENED",
  "blocked_reason": "why content was blocked",  // required if action=BLOCKED
  "texts": ["modified text"],  // optional array of modified text strings
  "images": ["modified_base64_image"],  // optional array of modified images
  "structured_messages": [{"role": "user", "content": "modified message"}]  // optional array of rewritten chat messages
}

액션:

  • BLOCKED - LiteLLM이 오류를 발생시키고 요청을 차단합니다
  • NONE - 요청이 변경 없이 진행됩니다
  • GUARDRAIL_INTERVENED - 수정된 texts/images로 요청이 진행됩니다 (texts 및/또는 images 필드 제공)

메시지별 재작성: texts는 LiteLLM이 보낸 texts 배열과 일대일로 맞아야 해요. 엔드포인트가 대신 채팅 메시지별로 요청을 재작성하면, 재작성된 행을 structured_messages로 반환하세요 (Returning rewritten messages 참고). /v1/responses에서 메시지당 하나를 세는 texts 배열은 LiteLLM이 추출한 것과 맞지 않으므로, 재작성되지 않은 채 전송되는 대신 가드레일을 명명한 500으로 요청이 거부됩니다.

파라미터

tools 파라미터

tools 파라미터는 요청에서 사용 가능한 함수/도구 정의에 대한 정보를 제공해요.

형식: OpenAI ChatCompletionToolParam 형식 (OpenAI API reference 참고)

type만 있고 function 블록이 없는 빌트인 도구(예: {"type": "code_interpreter"} 또는 {"type": "file_search", "vector_store_ids": [...]})도 허용되며, 원형 그대로(도구별 config 포함) 엔드포인트로 전달됩니다. 엔드포인트는 function을 선택 사항으로 취급하고 type으로 분기해야 해요.

예시:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather in a location",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "City and state, e.g. San Francisco, CA"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"]
        }
      },
      "required": ["location"]
    }
  }
}

가용성:

  • 입력 전용: toolsinput_type="request"(사전 호출 가드레일)에서만 전달됩니다. 출력/응답 가드레일은 현재 도구 정의를 받지 못해요.
  • 지원 엔드포인트: tools 파라미터는 /v1/chat/completions, /v1/responses, /v1/messages에서 지원됩니다. 다른 엔드포인트는 도구 지원이 없어요.

사용 사례:

  • 도구 권한 정책 강제 (예: 특정 사용자/팀만 특정 도구에 접근 허용)
  • LLM에 보내기 전 도구 스키마 검증
  • 감사 목적으로 도구 사용 기록
  • 사용자 컨텍스트 기반 민감 도구 차단

tool_calls 파라미터

tool_calls 파라미터는 요청 또는 응답에서 실제로 이뤄지는 함수/도구 호출을 담고 있어요.

형식: OpenAI ChatCompletionMessageToolCall 형식 (OpenAI API reference 참고)

예시:

{
  "id": "call_abc123",
  "type": "function",
  "function": {
    "name": "get_weather",
    "arguments": "{\"location\": \"San Francisco\", \"unit\": \"celsius\"}"
  }
}

tools와의 핵심 차이:

  • tools = 도구 정의/스키마 (어떤 도구가 사용 가능한지)
  • tool_calls = 도구 호출/실행 (어떤 인자로 어떤 도구가 호출되는지)

가용성:

  • 입력과 출력 모두: 도구 호출은 input_type="request"(도구 호출을 요청하는 assistant 메시지)와 input_type="response"(도구 호출을 가진 LLM 응답) 모두에 존재할 수 있어요.
  • 지원 엔드포인트: tool_calls 파라미터는 /v1/chat/completions, /v1/responses, /v1/messages에서 지원됩니다.

사용 사례:

  • 실행 전 도구 호출 인자 검증
  • 도구 호출 인자에서 민감 데이터(PII 등) 마스킹
  • 감사/디버깅을 위한 도구 호출 기록
  • 위험한 파라미터를 가진 도구 호출 차단
  • 도구 호출 인자 수정 (예: 제약 강제, 입력 정화)
  • 사용자/팀별 도구 사용 패턴 모니터링

structured_messages 파라미터

structured_messages 파라미터는 OpenAI 채팅 컴플리션 스펙 형식의 전체 입력을 제공하며, system과 user 메시지를 구분하는 데 유용해요.

형식: OpenAI 채팅 컴플리션 메시지 배열 (OpenAI API reference 참고)

예시:

[
  {"role": "system", "content": "You are a helpful assistant"},
  {"role": "user", "content": "Hello"}
]

가용성:

  • 지원 엔드포인트: /v1/chat/completions, /v1/messages, /v1/responses
  • 입력 전용: input_type="request"(사전 호출 가드레일)에서만 전달됩니다.

사용 사례:

  • system vs user 메시지에 다른 정책 적용
  • 역할 기반 콘텐츠 제한 강제
  • 구조화된 대화 컨텍스트 기록
재작성된 메시지 반환

요청을 메시지별로 재작성하려면, 받은 각 행에 대해 같은 순서와 역할·형태를 유지한 채 재작성하려는 콘텐츠만 바꿔 structured_messages를 응답에 반환하세요. LiteLLM은 지원되는 모든 엔드포인트에서 원래 요청에 행을 다시 써요. 여기에는 instructions나 tool 항목을 담은 /v1/responses 턴도 포함되며, 그 경우 메시지별 texts 배열을 넣을 수 없습니다. 받은 그대로 반환한 행은 변경된 것으로 간주되지 않으므로, 건드리지 않은 행을 그대로 에코할 수 있어요. 모든 행이 변경 없이 돌아오면 LiteLLM은 texts를 적용합니다. 받은 것과 길이가 다른 배열을 반환하면 대화 전체를 대체합니다.

예시:

{
  "action": "GUARDRAIL_INTERVENED",
  "structured_messages": [
    {"role": "system", "content": "You are a helpful assistant"},
    {"role": "user", "content": "My SSN is <US_SSN>"}
  ]
}

LiteLLM 설정

config.yaml에 추가하세요:

litellm_settings:
  guardrails:
    - guardrail_name: "my-guardrail"
      litellm_params:
        guardrail: generic_guardrail_api
        mode: pre_call  # or post_call, during_call
        api_base: https://your-guardrail-api.com
        api_key: os.environ/YOUR_GUARDRAIL_API_KEY  # optional
        unreachable_fallback: fail_closed  # default: fail_closed. Set to fail_open to proceed if the guardrail endpoint is unreachable (network errors, or HTTP 502/503/504 from an upstream proxy/LB).
        fail_on_error: true  # default: true (fail closed). Set to false to proceed on ANY guardrail error. See "Error handling" below before changing this.
        additional_provider_specific_params:
          # your custom parameters
          threshold: 0.8
          language: "en"

오류 처리: unreachable_fallback와 fail_on_error

가드레일 자체가 실패할 때(verdict를 반환하지 못할 때) LiteLLM이 무엇을 하는지 제어하는 두 가지 설정이 있어요. 엄격함에서 관대함으로 이어지는 스펙트럼 위에 있으며, 서로 결합됩니다:

  • unreachable_fallback (기본 fail_closed)는 가드레일 엔드포인트가 도달 불가능할 때만 반응합니다: 네트워크 오류, 타임아웃, 또는 업스트림 프록시/로드 밸런서의 HTTP 502/503/504. 그런 경우에만 요청을 진행하려면 fail_open으로 설정하세요.
  • fail_on_error (기본 true)는 더 넓은 제어예요. 도달 불가능뿐만 아니라 모든 가드레일 오류를 다룹니다.
fail_on_error 가드레일 오류 시 동작
true (기본) Fail closed. 모든 오류가 요청을 차단합니다: non-2xx 응답, 손상되었거나 파싱 불가능한 body, 네트워크 실패, 내부 직렬화/검증 오류. 기존 LiteLLM 동작을 유지합니다
false Fail open (전체). 모든 가드레일 오류가 critical 수준 로그 줄로 격하되고 요청은 가드레일이 설정되지 않은 것처럼 진행됩니다

유효한 가드레일 응답만이 행동할 수 있어요. fail_on_error: false에서 파싱된 BLOCKED 결정은 여전히 차단합니다. 유효한 응답이 아닌 모든 것(오류, 손상된 body, 도달 불가능한 엔드포인트)은 우회됩니다. 이는 요청 훅(pre_call)과 응답 훅(post_call) 모두에 적용되며, 응답 경로에서 fail-open은 이미 생성된 모델 출력을 반환하고, fail-closed는 성공적인 생성을 오류로 바꿉니다.

fail_on_error: false는 실패 시 완전한 우회예요. 가드레일이나 그 엔드포인트의 어떤 실패든, 어떤 이유든 해당 요청에 대해 가드레일을 차단 대신 건너뛰게 합니다. 그 트레이드오프를 이해하고 수락한 경우에만 활성화하세요: 보안 제약보다 가용성과 운영 제약이 더 강할 때 선택하세요. 가드레일이 하드 보안 경계라면 기본값 true(fail closed)로 두세요.

기본이 fail closed인 것은 가드레일이 보통 보안 제어이기 때문이에요. 매 fail-open 우회는 call id와 trace id와 함께 critical 수준으로 기록됩니다 (Generic Guardrail API error (fail-open) ...). 알림을 만들고 얼마나 자주 발생하는지 감사할 수 있어요.

정적 및 동적 헤더

가드레일 엔드포인트에 두 종류의 헤더를 보낼 수 있어요:

  • 정적 헤더 (headers): 가드레일에 매 요청과 함께 보내는 키/값 맵. 고정 값(예: API 키, X-Service-Name)에 사용하세요. litellm_params에서 구성:
litellm_params:
  guardrail: generic_guardrail_api
  api_base: https://your-guardrail-api.com
  headers:
    X-Service-Name: "my-app"
    X-API-Key: ***
  • 동적 헤더 (extra_headers): 클라이언트 요청에서 가드레일로 전달되는 헤더 이름 목록. 이 목록에 있는 헤더(작은 기본 allowlist인 x-litellm-* 등)만 값이 전송되고, 나머지는 [present]로 전송됩니다. 클라이언트 제공 헤더(예: x-request-id, x-correlation-id)를 패스스루하려면 사용하세요. litellm_params에서 구성:
litellm_params:
  guardrail: generic_guardrail_api
  api_base: https://your-guardrail-api.com
  extra_headers:
    - x-request-id
    - x-correlation-id
    - x-custom-auth

이는 MCP의 정적/추가 헤더 동작을 반영한 것입니다.

예시: Pillar Security

Pillar Security는 제네릭 가드레일 API를 사용해 AI 보안 스캐닝(프롬프트 인젝션 방지, PII/PCI 감지, 시크릿 감지, 콘텐츠 모더레이션)을 제공해요.

guardrails:
  - guardrail_name: "pillar-security"
    litellm_params:
      guardrail: generic_guardrail_api
      mode: [pre_call, post_call]
      api_base: https://api.pillar.security/api/v1/integrations/litellm
      api_key: os.environ/PILLAR_API_KEY
      default_on: true
      additional_provider_specific_params:
        plr_mask: true      # Enable automatic masking of sensitive data
        plr_evidence: true  # Include detection evidence in response
        plr_scanners: true  # Include scanner details in response

전체 구성 옵션은 Pillar Security 문서를 참고하세요.

사용법

사용자는 가드레일을 이름으로 적용해요:

response = client.chat.completions.create(
    model="gpt-5.6-terra",
    messages=[{"role": "user", "content": "hello"}],
    guardrails=["my-guardrail"]
)

또는 동적 파라미터로:

response = client.chat.completions.create(
    model="gpt-5.6-terra",
    messages=[{"role": "user", "content": "hello"}],
    guardrails=[{
        "my-guardrail": {
            "extra_body": {
                "custom_threshold": 0.9
            }
        }
    }]
)

구현 예시

완전한 참조 구현은 mock_bedrock_guardrail_server.py를 참고하세요.

최소 FastAPI 예시:

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional, Dict, Any

app = FastAPI()

class GuardrailRequest(BaseModel):
    texts: List[str]
    images: Optional[List[str]] = None
    tools: Optional[List[Dict[str, Any]]] = None  # OpenAI ChatCompletionToolParam format (tool definitions)
    tool_calls: Optional[List[Dict[str, Any]]] = None  # OpenAI ChatCompletionMessageToolCall format (tool invocations)
    structured_messages: Optional[List[Dict[str, Any]]] = None  # OpenAI messages format (for chat endpoints)
    request_data: Dict[str, Any]
    input_type: str  # "request" or "response"
    litellm_call_id: Optional[str] = None
    litellm_trace_id: Optional[str] = None
    additional_provider_specific_params: Dict[str, Any]

class GuardrailResponse(BaseModel):
    action: str  # BLOCKED, NONE, or GUARDRAIL_INTERVENED
    blocked_reason: Optional[str] = None
    texts: Optional[List[str]] = None
    images: Optional[List[str]] = None
    structured_messages: Optional[List[Dict[str, Any]]] = None  # rewritten OpenAI messages, one per row received

@app.post("/beta/litellm_basic_guardrail_api")
async def apply_guardrail(request: GuardrailRequest):
    # Your guardrail logic here
    
    # Example: Check text content
    for text in request.texts:
        if "badword" in text.lower():
            return GuardrailResponse(
                action="BLOCKED",
                blocked_reason="Content contains prohibited terms"
            )
    
    # Example: Check tool definitions (if present in request)
    if request.tools:
        for tool in request.tools:
            if tool.get("type") == "function":
                function_name = tool.get("function", {}).get("name", "")
                # Block sensitive tool definitions
                if function_name in ["delete_data", "access_admin_panel"]:
                    return GuardrailResponse(
                        action="BLOCKED",
                        blocked_reason=f"Tool '{function_name}' is not allowed"
                    )
    
    # Example: Check tool calls (if present in request or response)
    if request.tool_calls:
        for tool_call in request.tool_calls:
            if tool_call.get("type") == "function":
                function_name = tool_call.get("function", {}).get("name", "")
                arguments_str = tool_call.get("function", {}).get("arguments", "{}")
                
                # Parse arguments and validate
                import json
                try:
                    arguments = json.loads(arguments_str)
                    # Block dangerous arguments
                    if "file_path" in arguments and ".." in str(arguments["file_path"]):
                        return GuardrailResponse(
                            action="BLOCKED",
                            blocked_reason="Tool call contains path traversal attempt"
                        )
                except json.JSONDecodeError:
                    pass
    
    # Example: Check structured messages (if present in request)
    if request.structured_messages:
        for message in request.structured_messages:
            if message.get("role") == "system":
                # Apply stricter policies to system messages
                if "admin" in message.get("content", "").lower():
                    return GuardrailResponse(
                        action="BLOCKED",
                        blocked_reason="System message contains restricted terms"
                    )
    
    return GuardrailResponse(action="NONE")

언제 이것을 써야 하나

제네릭 가드레일 API를 사용하세요:

  • PR을 기다리지 않고 즉시 통합하고 싶을 때
  • 가드레일 서비스를 직접 유지할 때
  • 업데이트와 기능을 완전히 제어해야 할 때
  • 모든 LiteLLM 엔드포인트를 자동으로 지원하고 싶을 때

PR을 만들기:

  • LiteLLM 내부와 더 깊이 통합하고 싶을 때
  • 가드레일에 복잡한 LiteLLM 전용 로직이 필요할 때
  • 빌트인 프로바이더로 소개되고 싶을 때

질문?

이것은 베타 API예요. 피드백에 따라 적극적으로 개선 중입니다. 추가 기능이 필요하면 이슈나 PR을 열어 주세요.

더 알아보기 (Learn more)