Add persisted LLM audit logging
This commit is contained in:
@ -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)}",
|
||||
|
||||
Reference in New Issue
Block a user