2026-07-05 22:18:00 +02:00
|
|
|
"""LiteLLM-Wrapper mit Token-Tracking."""
|
|
|
|
|
import litellm
|
|
|
|
|
from typing import List, Dict, Optional
|
|
|
|
|
from config import settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if settings.LLM_API_KEY:
|
|
|
|
|
litellm.api_key = settings.LLM_API_KEY
|
|
|
|
|
if settings.LLM_API_BASE:
|
|
|
|
|
litellm.api_base = settings.LLM_API_BASE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LLMResponse:
|
|
|
|
|
def __init__(self, content: str, tool_calls: Optional[List[Dict]] = None, usage: Optional[Dict] = None):
|
|
|
|
|
self.content = content
|
|
|
|
|
self.tool_calls = tool_calls or []
|
|
|
|
|
self.usage = usage or {}
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def input_tokens(self) -> int:
|
|
|
|
|
return self.usage.get("prompt_tokens", 0)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def output_tokens(self) -> int:
|
|
|
|
|
return self.usage.get("completion_tokens", 0)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def total_tokens(self) -> int:
|
|
|
|
|
return self.input_tokens + self.output_tokens
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def finish_reason(self) -> str:
|
|
|
|
|
return self.usage.get("finish_reason", "stop")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def chat(
|
|
|
|
|
messages: List[Dict[str, str]],
|
|
|
|
|
tools: Optional[List[Dict]] = None,
|
|
|
|
|
model: Optional[str] = None,
|
|
|
|
|
temperature: float = 0.7,
|
|
|
|
|
max_tokens: int = 2000,
|
|
|
|
|
) -> LLMResponse:
|
|
|
|
|
"""LLM-Call via LiteLLM.
|
|
|
|
|
|
|
|
|
|
messages: Liste von {role, content} Dicts.
|
|
|
|
|
tools: Optional, MCP-Tool-Definitionen für Function-Calling.
|
|
|
|
|
"""
|
|
|
|
|
model = model or settings.LLM_MODEL
|
|
|
|
|
|
|
|
|
|
kwargs = {
|
|
|
|
|
"model": model,
|
|
|
|
|
"messages": messages,
|
|
|
|
|
"temperature": temperature,
|
|
|
|
|
"max_tokens": max_tokens,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tools:
|
|
|
|
|
kwargs["tools"] = tools
|
|
|
|
|
|
|
|
|
|
response = await litellm.acompletion(**kwargs)
|
|
|
|
|
|
|
|
|
|
message = response.choices[0].message
|
|
|
|
|
|
|
|
|
|
tool_calls = []
|
|
|
|
|
if hasattr(message, "tool_calls") and message.tool_calls:
|
|
|
|
|
for tc in message.tool_calls:
|
|
|
|
|
tool_calls.append({
|
|
|
|
|
"id": tc.id,
|
|
|
|
|
"name": tc.function.name,
|
|
|
|
|
"arguments": tc.function.arguments,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
usage = {}
|
|
|
|
|
if hasattr(response, "usage") and response.usage:
|
2026-07-06 07:33:38 +02:00
|
|
|
# LiteLLM exposes cached prompt tokens via prompt_tokens_details.cached_tokens
|
|
|
|
|
# (see litellm.types.utils.Usage). Providers like Anthropic/OpenAI surface this
|
|
|
|
|
# when prompt caching is active; surfacing it here flows through LLMResponse.usage
|
|
|
|
|
# to any audit log that serialises the usage dict.
|
|
|
|
|
cached_tokens = 0
|
|
|
|
|
details = getattr(response.usage, "prompt_tokens_details", None)
|
|
|
|
|
if details is not None:
|
|
|
|
|
cached_tokens = getattr(details, "cached_tokens", None) or 0
|
|
|
|
|
|
2026-07-05 22:18:00 +02:00
|
|
|
usage = {
|
|
|
|
|
"prompt_tokens": response.usage.prompt_tokens or 0,
|
|
|
|
|
"completion_tokens": response.usage.completion_tokens or 0,
|
|
|
|
|
"total_tokens": response.usage.total_tokens or 0,
|
2026-07-06 07:33:38 +02:00
|
|
|
"cached_tokens": cached_tokens,
|
2026-07-05 22:18:00 +02:00
|
|
|
"finish_reason": response.choices[0].finish_reason or "stop",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return LLMResponse(
|
|
|
|
|
content=message.content or "",
|
|
|
|
|
tool_calls=tool_calls,
|
|
|
|
|
usage=usage,
|
|
|
|
|
)
|