from litellm.integrations.custom_logger import CustomLogger
LiteLLM ships a base class called CustomLogger. It's the "plug-in template" — LiteLLM already knows how to call methods on this class at specific points in a request's life (before the call, after success, after failure, etc.). You don't build that machinery yourself; you just inherit from it and override the one method you care about.
class SanitizeEmptyToolContent(CustomLogger):
This creates your own plug-in by inheriting from CustomLogger. The name is just a label — call it whatever you like. Because it inherits from CustomLogger, it automatically has all the hook methods (pre-call, post-call, logging, etc.) already defined as empty no-ops. You're about to override just one of them.
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
async def — this function runs asynchronously. LiteLLM's proxy server is built on asyncio, so all its hooks must be async functions (defined with awaitcompatible syntax) even if you don't personally use await inside. This lets LiteLLM juggle many simultaneous requests without one blocking another.async_pre_call_hook — the exact method name LiteLLM looks for. It calls this function itself, automatically, right before it sends anything to Sarvam. You never call this function yourself — LiteLLM does, behind the scenes.self — standard Python reference to the object itself.user_api_key_dict — info about which API key/user made the request (you don't use it here, but LiteLLM requires the parameter to exist).cache — a handle to LiteLLM's internal cache (also unused here, but required in the signature).data — the important one. This is the entire JSON request body opencode sent, as a Python dictionary: {"model": ..., "messages": [...]}.call_type — what kind of call this is ("completion", "embeddings", etc.), unused here.You must keep all five parameters in the signature even if you don't use most of them, because LiteLLM calls this method expecting that exact shape.
messages = data.get("messages", [])
data is a dictionary. .get("messages", []) means: "give me the value under the key "messages"; if that key doesn't exist, give me an empty list instead of crashing." So messages becomes the array of message objects — [{"role": "system", ...}, {"role": "user", ...}, {"role": "assistant", ...}, {"role": "tool", ...}] — that opencode sent.
for msg in messages:
This walks through the list one message at a time. On each pass, msg is one dictionary, e.g. {"role": "tool", "content": "", "tool_call_id": "call_abc123"}.