Add persisted LLM audit logging

This commit is contained in:
2026-03-23 11:41:43 +08:00
parent 5e85129869
commit bad1e16ab4
4 changed files with 282 additions and 2 deletions

View File

@ -3,11 +3,23 @@
import json
import json_repair
import os
import traceback
import uuid
from typing import Any
import litellm
from litellm import acompletion
from loguru import logger
from nanobot.llm_audit import (
redact_mapping,
summarize_exception,
summarize_messages,
summarize_tool_calls,
summarize_tools,
truncate_traceback,
write_llm_audit_event,
)
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
from nanobot.providers.registry import find_by_model, find_gateway
@ -186,9 +198,12 @@ class LiteLLMProvider(LLMProvider):
"""
original_model = model or self.default_model
model = self._resolve_model(original_model)
request_id = str(uuid.uuid4())
sanitized_messages = self._sanitize_messages(self._sanitize_empty_content(messages))
if self._supports_cache_control(original_model):
messages, tools = self._apply_cache_control(messages, tools)
sanitized_messages = self._sanitize_messages(self._sanitize_empty_content(messages))
# Clamp max_tokens to at least 1 — negative or zero values cause
# LiteLLM to reject the request with "max_tokens must be at least 1".
@ -196,7 +211,7 @@ class LiteLLMProvider(LLMProvider):
kwargs: dict[str, Any] = {
"model": model,
"messages": self._sanitize_messages(self._sanitize_empty_content(messages)),
"messages": sanitized_messages,
"max_tokens": max_tokens,
"temperature": temperature,
}
@ -219,11 +234,83 @@ class LiteLLMProvider(LLMProvider):
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = "auto"
request_event = {
"event": "llm_request",
"request_id": request_id,
"provider_impl": type(self).__name__,
"gateway": self._gateway.name if self._gateway else None,
"original_model": original_model,
"resolved_model": model,
"api_base": self.api_base,
"has_api_key": bool(self.api_key),
"temperature": kwargs.get("temperature"),
"max_tokens": kwargs.get("max_tokens"),
"tool_choice": kwargs.get("tool_choice"),
"message_count": len(sanitized_messages),
"messages": summarize_messages(sanitized_messages),
"tools": summarize_tools(tools),
"extra_headers": redact_mapping(self.extra_headers),
}
write_llm_audit_event(request_event)
logger.info(
"LLM request [{}]: model={} messages={} tools={}",
request_id,
model,
len(sanitized_messages),
len(tools or []),
)
try:
response = await acompletion(**kwargs)
return self._parse_response(response)
parsed = self._parse_response(response)
write_llm_audit_event({
"event": "llm_response",
"request_id": request_id,
"provider_impl": type(self).__name__,
"original_model": original_model,
"resolved_model": model,
"finish_reason": parsed.finish_reason,
"usage": parsed.usage,
"content_preview": parsed.content[:1000] if parsed.content else None,
"reasoning_preview": parsed.reasoning_content[:1000] if parsed.reasoning_content else None,
"tool_calls": [
{
"id": tc.id,
"name": tc.name,
"arguments_preview": str(tc.arguments)[:1000],
}
for tc in parsed.tool_calls
],
"raw_tool_calls": summarize_tool_calls(
getattr(response.choices[0].message, "tool_calls", None) or []
),
})
logger.info(
"LLM response [{}]: model={} finish_reason={} tool_calls={}",
request_id,
model,
parsed.finish_reason,
len(parsed.tool_calls),
)
return parsed
except Exception as e:
tb = traceback.format_exc()
write_llm_audit_event({
"event": "llm_error",
"request_id": request_id,
"provider_impl": type(self).__name__,
"gateway": self._gateway.name if self._gateway else None,
"original_model": original_model,
"resolved_model": model,
"api_base": self.api_base,
"error": summarize_exception(e),
"traceback": truncate_traceback(tb),
"message_count": len(sanitized_messages),
"messages": summarize_messages(sanitized_messages),
"tools": summarize_tools(tools),
})
logger.exception("LLM error [{}]: model={} provider call failed", request_id, model)
# Return error as content for graceful handling
return LLMResponse(
content=f"Error calling LLM: {str(e)}",