forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathusage_tracker.py
More file actions
223 lines (171 loc) · 7.1 KB
/
Copy pathusage_tracker.py
File metadata and controls
223 lines (171 loc) · 7.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"""
LLM Usage Tracker - Feature-level token usage tracking.
Uses LangChain callbacks and contextvars to track which features consume LLM tokens.
"""
from __future__ import annotations
import contextvars
import threading
from contextlib import contextmanager
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Callable, Dict, Iterator, Mapping, Optional, cast
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.outputs import LLMResult
from database.llm_usage import record_llm_usage
# Context variable for tracking current feature
_usage_context: contextvars.ContextVar[Optional["UsageContext"]] = contextvars.ContextVar(
"llm_usage_context", default=None
)
# Thread-safe buffer for batching writes
_buffer_lock = threading.Lock()
_usage_buffer: Dict[str, Dict[str, int]] = {}
UsageContextToken = contextvars.Token[Optional["UsageContext"]]
@dataclass(frozen=True)
class UsageContext:
"""Context for LLM usage tracking."""
uid: str
feature: str
@dataclass
class UsageRecord:
"""A single usage record."""
uid: str
feature: str
model: str
input_tokens: int
output_tokens: int
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
class LLMUsageCallback(BaseCallbackHandler):
"""LangChain callback handler for tracking LLM token usage by feature."""
def __init__(self, flush_fn: Optional[Callable[[str, str, str, int, int], None]] = None) -> None:
"""
Initialize the callback.
Args:
flush_fn: Optional function to call for each usage record.
Signature: (uid, feature, model, input_tokens, output_tokens) -> None
"""
self._flush_fn = flush_fn
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
"""Called when LLM call ends. Records token usage."""
ctx = _usage_context.get()
if not ctx:
ctx = UsageContext(uid="unknown", feature=Features.OTHER)
# Extract token usage from response
token_usage: Mapping[str, object] = {}
model = "unknown"
llm_output = _mapping_or_empty(getattr(response, "llm_output", None))
if llm_output:
token_usage = _mapping_or_empty(llm_output.get("token_usage"))
model = _string_or_default(llm_output.get("model_name") or token_usage.get("model_name"), model)
input_tokens = _int_or_zero(token_usage.get("prompt_tokens"))
output_tokens = _int_or_zero(token_usage.get("completion_tokens"))
# Also try to get model from response metadata
if model == "unknown" and response.generations:
for gen_list in response.generations:
for gen in gen_list:
if hasattr(gen, "generation_info") and gen.generation_info:
generation_info = cast(Mapping[str, object], gen.generation_info)
model = _string_or_default(generation_info.get("model_name"), model)
break
if input_tokens > 0 or output_tokens > 0:
if self._flush_fn:
# Write immediately - skip buffering to avoid unbounded growth
self._flush_fn(ctx.uid, ctx.feature, model, input_tokens, output_tokens)
else:
# Buffer for batch writing when no flush_fn provided
_buffer_usage(ctx.uid, ctx.feature, model, input_tokens, output_tokens)
def _mapping_or_empty(value: object) -> Mapping[str, object]:
if isinstance(value, Mapping):
return cast(Mapping[str, object], value)
return {}
def _string_or_default(value: object, default: str) -> str:
if isinstance(value, str) and value:
return value
return default
def _int_or_zero(value: object) -> int:
if isinstance(value, int):
return value
if isinstance(value, float):
return int(value)
return 0
def _buffer_usage(uid: str, feature: str, model: str, input_tokens: int, output_tokens: int) -> None:
"""Buffer usage data for batch writing."""
key = f"{uid}:{feature}:{model}"
with _buffer_lock:
if key not in _usage_buffer:
_usage_buffer[key] = {"input_tokens": 0, "output_tokens": 0}
_usage_buffer[key]["input_tokens"] += input_tokens
_usage_buffer[key]["output_tokens"] += output_tokens
def get_and_clear_buffer() -> Dict[str, Dict[str, int]]:
"""Get buffered usage data and clear the buffer."""
with _buffer_lock:
data = _usage_buffer.copy()
_usage_buffer.clear()
return data
@contextmanager
def track_usage(uid: str, feature: str) -> Iterator[UsageContext]:
"""
Context manager to track LLM usage for a specific feature.
Usage:
with track_usage(uid, "chat"):
response = llm.invoke(prompt)
Args:
uid: User ID
feature: Feature name (e.g., "chat", "conversation_processing", "rag")
"""
ctx = UsageContext(uid=uid, feature=feature)
token = _usage_context.set(ctx)
try:
yield ctx
finally:
_usage_context.reset(token)
def set_usage_context(uid: str, feature: str) -> UsageContextToken:
"""
Set the usage context manually (for cases where context manager isn't suitable).
Returns a token that should be used with reset_usage_context().
"""
ctx = UsageContext(uid=uid, feature=feature)
return _usage_context.set(ctx)
def reset_usage_context(token: UsageContextToken) -> None:
"""Reset the usage context using the token from set_usage_context()."""
_usage_context.reset(token)
def get_current_context() -> Optional[UsageContext]:
"""Get the current usage context, if any."""
return _usage_context.get()
# Singleton callback instance
_callback_instance: Optional[LLMUsageCallback] = None
def get_usage_callback() -> LLMUsageCallback:
"""Get the singleton usage callback instance."""
global _callback_instance
if _callback_instance is None:
_callback_instance = LLMUsageCallback(flush_fn=record_llm_usage)
return _callback_instance
# Feature constants for consistency
class Features:
CHAT = "chat"
CONVERSATION_PROCESSING = "conversation_processing"
RAG = "rag"
NOTIFICATIONS = "notifications"
APP_INTEGRATIONS = "app_integrations"
GOALS = "goals"
TRENDS = "trends"
PERSONA = "persona"
MEMORIES = "memories"
TRANSCRIBE = "transcribe"
REALTIME_INTEGRATIONS = "realtime_integrations"
DAILY_SUMMARY = "daily_summary"
SUBSCRIPTION_NOTIFICATION = "subscription_notification"
KNOWLEDGE_GRAPH = "knowledge_graph"
OTHER = "other"
PROACTIVE_NOTIFICATION = "proactive_notification"
FOLLOWUP = "followup"
OPENGLASS = "openglass"
APP_GENERATOR = "app_generator"
ONBOARDING = "onboarding"
SCREEN_FRAME_JUDGE = "screen_frame_judge"
# Conversation processing sub-features (granular cost tracking)
CONVERSATION_DISCARD = "conv_discard"
CONVERSATION_STRUCTURE = "conv_structure"
CONVERSATION_ACTION_ITEMS = "conv_action_items"
WAKE_WORD_ADJUDICATION = "wake_word_adjudication"
CONVERSATION_FOLDER = "conv_folder"
CONVERSATION_APPS = "conv_apps"