Skip to content

Refactor: duplicate call_type Literal definitions aross +21 CustomLogger subclasses #16362

@Chesars

Description

@Chesars

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 files

Files (at least 21):

  • litellm/integrations/custom_logger.py
  • litellm/proxy/utils.py
  • litellm/proxy/guardrails/guardrail_hooks/aim/aim.py
  • litellm/proxy/guardrails/guardrail_hooks/aporia_ai/aporia_ai.py
  • litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py
  • litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py
  • litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py
  • litellm/proxy/guardrails/guardrail_hooks/noma/noma.py
  • litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py
  • litellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/ibm_detector.py
  • litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py
  • litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py
  • litellm/proxy/hooks/dynamic_rate_limiter.py
  • litellm/proxy/hooks/dynamic_rate_limiter_v3.py
  • litellm/proxy/hooks/responses_id_security.py
  • litellm/proxy/example_config_yaml/custom_guardrail.py
  • litellm/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
        pass

MyPy 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 runtime

Proposed 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
    ):
        pass

Update 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
    ):
        pass

Benefits

  1. Single source - change in 1 place affects all files
  2. Easier to maintain - adding new call types requires editing 1 line
  3. Consistent - all subclasses use the same type definition
  4. Fixes Liskov violations - all subclasses accept the same types as the base
  5. No more # type: ignore workarounds

Implementation Checklist

  • Create litellm/types/callbacks.py with TypeAliases
  • Update CustomLogger base class
  • Update ProxyLogging class
  • 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: ignore workarounds from proxy/utils.py
  • Run MyPy to verify no type errors
  • Update tests if needed

Related Issues


Additional Context

This refactor would also be a good time to:

  1. Align naming between CallTypes enum (singular: "embedding") and CustomLogger Literals (plural: "embeddings")
  2. 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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions