-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Description
Problem Statement
Currently, the call_type parameter's Literal type hint is duplicated across 21+ files (CustomLogger subclasses). This violates the Liskov Substitution Principle (LSP).
Current
Each CustomLogger subclass manually redefines the same Literal:
# litellm/integrations/custom_logger.py (base class)
async def async_pre_call_hook(
call_type: Literal[
"completion",
"text_completion",
"embeddings",
"image_generation",
"moderation",
"audio_transcription",
"pass_through_endpoint",
"rerank",
"mcp_call",
"anthropic_messages",
]
):
pass # litellm/proxy/guardrails/guardrail_hooks/aim/aim.py (subclass)
async def async_pre_call_hook(
call_type: Literal[
"completion", # ← DUPLICATED
"text_completion", # ← DUPLICATED
"embeddings", # ← DUPLICATED
# ... same list ...
]
):
pass
# ... repeated in 19+ more filesFiles (at least 21):
litellm/integrations/custom_logger.pylitellm/proxy/utils.pylitellm/proxy/guardrails/guardrail_hooks/aim/aim.pylitellm/proxy/guardrails/guardrail_hooks/aporia_ai/aporia_ai.pylitellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.pylitellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.pylitellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.pylitellm/proxy/guardrails/guardrail_hooks/noma/noma.pylitellm/proxy/guardrails/guardrail_hooks/javelin/javelin.pylitellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/ibm_detector.pylitellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.pylitellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.pylitellm/proxy/hooks/dynamic_rate_limiter.pylitellm/proxy/hooks/dynamic_rate_limiter_v3.pylitellm/proxy/hooks/responses_id_security.pylitellm/proxy/example_config_yaml/custom_guardrail.pylitellm/proxy/example_config_yaml/custom_callbacks1.py- ... and more
Issues Caused by This Design
1. DRY Violation
Adding a new call_type (e.g., "aembedding") requires editing 21+ files manually.
Recent example: PR #16328 needed to add "aembedding" to support async embeddings, but updating 21 files was too much work. We had to use # type: ignore as a workaround.
2. Liskov Substitution Principle Violation
Some subclasses have more restrictive Literal types than the base class:
# Base class accepts 3 values
class CustomLogger:
def method(call_type: Literal["a", "b", "c"]):
pass
```
```python
# Subclass only accepts 1 value ❌ LSP violation
class Subclass(CustomLogger):
def method(call_type: Literal["a"]): # More restrictive
passMyPy correctly flags this as an error because code expecting a CustomLogger might pass "b", which the subclass rejects.
3. Inconsistencies Between Systems
There are two naming systems in conflict:
CallTypes Enum (used internally, no "s"):
# litellm/types/utils.py
class CallTypes(str, Enum):
embedding = "embedding" # ← singular
aembedding = "aembedding"
completion = "completion"CustomLogger Literals (legacy, with "s"):
call_type: Literal["embeddings"] # ← plural (legacy naming)This caused bug #16240 where guardrails failed because they tried to validate call_type="embeddings" (with "s") against the CallTypes enum which only has "embedding" (no "s").
Background: Python Type Hints
To understand this issue, it's important to know:
Python is Dynamically Typed
def process(x: int) -> str:
return x * 2
# Python IGNORES type hints at runtime
result = process("hello") # ✅ Executes fine (returns "hellohello")Type Hints are HINTS (suggestions), not enforcement
Type hints in Python are optional annotations that help:
- ✅ IDEs (autocompletion, IntelliSense)
- ✅ MyPy/Pyright (static type checking in CI/CD)
- ✅ Documentation (what types are expected)
They do NOT:
- ❌ Enforce types at runtime
- ❌ Prevent execution of "incorrectly typed" code
- ❌ Validate like Pydantic does
Pydantic vs Type Hints
LiteLLM uses both:
| Feature | Type Hints | Pydantic |
|---|---|---|
| Runtime validation | ❌ No | ✅ Yes |
| IDE support | ✅ Yes | ✅ Yes |
| MyPy validation | ✅ Yes | ✅ Yes |
| Performance overhead | None (ignored) | Small (validation) |
| Used in | CustomLogger, utils | Request/response models |
Example:
# Type hints (no runtime validation)
class CustomLogger:
def method(x: int): # ← Python ignores this
pass
# Pydantic (runtime validation)
class RequestModel(BaseModel):
x: int # ← Pydantic validates this at runtimeProposed Solution
Create a reusable TypeAlias
# litellm/types/callbacks.py (new file)
from typing import Literal
# Centralized type definition
PreCallHookCallType = Literal[
"completion",
"text_completion",
"embeddings",
"aembedding", # ← Add new types here
"image_generation",
"moderation",
"audio_transcription",
"pass_through_endpoint",
"rerank",
"mcp_call",
"anthropic_messages",
]
ModerationHookCallType = Literal[
"completion",
"embeddings",
"aembedding", # ← Add here too
"image_generation",
"moderation",
"audio_transcription",
"responses",
"mcp_call",
"anthropic_messages",
]Update CustomLogger (base class)
# litellm/integrations/custom_logger.py
from litellm.types.callbacks import PreCallHookCallType, ModerationHookCallType
class CustomLogger:
async def async_pre_call_hook(
self,
user_api_key_dict: UserAPIKeyAuth,
cache: DualCache,
data: dict,
call_type: PreCallHookCallType, # ← Use TypeAlias
):
pass
async def async_moderation_hook(
self,
data: dict,
user_api_key_dict: UserAPIKeyAuth,
call_type: ModerationHookCallType, # ← Use TypeAlias
):
passUpdate All Subclasses (21+ files)
# litellm/proxy/guardrails/guardrail_hooks/aim/aim.py
from litellm.types.callbacks import PreCallHookCallType
class AimGuardrail(CustomLogger):
async def async_pre_call_hook(
self,
user_api_key_dict: UserAPIKeyAuth,
cache: DualCache,
data: dict,
call_type: PreCallHookCallType, # ← Use same TypeAlias
):
passBenefits
- ✅ Single source - change in 1 place affects all files
- ✅ Easier to maintain - adding new call types requires editing 1 line
- ✅ Consistent - all subclasses use the same type definition
- ✅ Fixes Liskov violations - all subclasses accept the same types as the base
- ✅ No more
# type: ignoreworkarounds
Implementation Checklist
- Create
litellm/types/callbacks.pywith TypeAliases - Update
CustomLoggerbase class - Update
ProxyLoggingclass - Update 21+ guardrail/hook subclasses:
- aim/aim.py
- aporia_ai/aporia_ai.py
- bedrock_guardrails.py
- lakera_ai_v2.py
- unified_guardrail/unified_guardrail.py
- noma/noma.py
- javelin/javelin.py
- ibm_guardrails/ibm_detector.py
- enkryptai/enkryptai.py
- dynamoai/dynamoai.py
- ... (11 more files)
- Remove
# type: ignoreworkarounds fromproxy/utils.py - Run MyPy to verify no type errors
- Update tests if needed
Related Issues
- [Bug]: Issue calling /embeddings in v1.79.0-stable #16240 - Bug where
call_type="embeddings"failed guardrail validation - fix: Use valid CallTypes enum value in embeddings endpoint #16328 - PR that added
"aembedding"but couldn't update all files due to duplication
Additional Context
This refactor would also be a good time to:
- Align naming between
CallTypesenum (singular:"embedding") andCustomLoggerLiterals (plural:"embeddings") - Consider whether async call types (
"aembedding","acompletion") should be normalized to their sync equivalents before
passing to middlewares (since middlewares typically don't care about async vs sync)