unique_toolkit 2026.36.0.dev4__py3-none-any.whl → 2026.36.0.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unique_toolkit/language_model/invocation_stats.py +97 -1
- {unique_toolkit-2026.36.0.dev4.dist-info → unique_toolkit-2026.36.0.dev5.dist-info}/METADATA +1 -1
- {unique_toolkit-2026.36.0.dev4.dist-info → unique_toolkit-2026.36.0.dev5.dist-info}/RECORD +4 -4
- {unique_toolkit-2026.36.0.dev4.dist-info → unique_toolkit-2026.36.0.dev5.dist-info}/WHEEL +0 -0
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
Lives in its own module (not ``schemas.py``) because it needs
|
|
4
4
|
``LanguageModelName`` from ``infos.py``, which itself imports from
|
|
5
5
|
``schemas.py``.
|
|
6
|
+
|
|
7
|
+
:class:`InvocationStatsCollector` collects those stats for one namespaced
|
|
8
|
+
run. ``record_token_usage`` swallows all exceptions around parsing and
|
|
9
|
+
:meth:`LanguageModelInvocationStats.from_usage`: usage is optional
|
|
10
|
+
billing metadata and must not abort a tool run.
|
|
6
11
|
"""
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
import logging
|
|
14
|
+
from collections.abc import Iterable, Iterator
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from contextvars import ContextVar, Token
|
|
17
|
+
from typing import Annotated, Any, Self
|
|
9
18
|
|
|
10
19
|
from humps import camelize
|
|
11
20
|
from pydantic import BaseModel, BeforeValidator, ConfigDict
|
|
@@ -14,6 +23,8 @@ from unique_toolkit.language_model.infos import LanguageModelName
|
|
|
14
23
|
from unique_toolkit.language_model.model_costs import calculate_invocation_cost_usd
|
|
15
24
|
from unique_toolkit.language_model.schemas import LanguageModelTokenUsage
|
|
16
25
|
|
|
26
|
+
_LOGGER = logging.getLogger(__name__)
|
|
27
|
+
|
|
17
28
|
# `protected_namespaces=()` allows the `model_name` field name.
|
|
18
29
|
model_config = ConfigDict(
|
|
19
30
|
alias_generator=camelize,
|
|
@@ -75,3 +86,88 @@ class LanguageModelInvocationStats(BaseModel):
|
|
|
75
86
|
source=source,
|
|
76
87
|
cost_usd=calculate_invocation_cost_usd(str(model_name), token_usage),
|
|
77
88
|
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class InvocationStatsCollector:
|
|
92
|
+
"""Run-scoped collector with a namespaced ContextVar so nested tools don't mix.
|
|
93
|
+
|
|
94
|
+
Instantiate once per namespace and share that instance.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, namespace: str) -> None:
|
|
98
|
+
self._namespace = namespace
|
|
99
|
+
self._var: ContextVar[list[LanguageModelInvocationStats] | None] = ContextVar(
|
|
100
|
+
f"{namespace}_invocation_stats", default=None
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@contextmanager
|
|
104
|
+
def scope(self) -> Iterator[list[LanguageModelInvocationStats]]:
|
|
105
|
+
"""Collect stats for one run and isolate concurrent runs."""
|
|
106
|
+
invocation_stats: list[LanguageModelInvocationStats] = []
|
|
107
|
+
token: Token[list[LanguageModelInvocationStats] | None] = self._var.set(
|
|
108
|
+
invocation_stats
|
|
109
|
+
)
|
|
110
|
+
try:
|
|
111
|
+
yield invocation_stats
|
|
112
|
+
finally:
|
|
113
|
+
self._var.reset(token)
|
|
114
|
+
|
|
115
|
+
def record_invocation_stats(
|
|
116
|
+
self,
|
|
117
|
+
invocation_stats: Iterable[LanguageModelInvocationStats],
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Record already-built stats when a run scope is active. No-op otherwise."""
|
|
120
|
+
current_invocation_stats = self._var.get()
|
|
121
|
+
if current_invocation_stats is not None:
|
|
122
|
+
current_invocation_stats.extend(invocation_stats)
|
|
123
|
+
|
|
124
|
+
def record_token_usage(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
model_name: str,
|
|
128
|
+
usage: Any,
|
|
129
|
+
source: str,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Record provider usage when a run scope and usage are available.
|
|
132
|
+
|
|
133
|
+
Swallows all exceptions: this metadata is optional and must not abort
|
|
134
|
+
the calling tool.
|
|
135
|
+
"""
|
|
136
|
+
invocation_stats = self._var.get()
|
|
137
|
+
if invocation_stats is None or usage is None:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
token_usage = LanguageModelTokenUsage.model_validate(usage)
|
|
142
|
+
invocation_stats.append(
|
|
143
|
+
LanguageModelInvocationStats.from_usage(
|
|
144
|
+
model_name=model_name,
|
|
145
|
+
token_usage=token_usage,
|
|
146
|
+
source=source,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
except Exception:
|
|
150
|
+
_LOGGER.warning(
|
|
151
|
+
"Unable to parse %s token usage for %s",
|
|
152
|
+
self._namespace,
|
|
153
|
+
source,
|
|
154
|
+
exc_info=True,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def record_language_model_response(
|
|
158
|
+
self,
|
|
159
|
+
*,
|
|
160
|
+
model_name: str,
|
|
161
|
+
response: Any,
|
|
162
|
+
source: str,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Record usage from ``.usage``, ``.usage_metadata``, or ``response_metadata``."""
|
|
165
|
+
usage = getattr(response, "usage", None)
|
|
166
|
+
if usage is None:
|
|
167
|
+
usage = getattr(response, "usage_metadata", None)
|
|
168
|
+
if usage is None:
|
|
169
|
+
response_metadata = getattr(response, "response_metadata", None)
|
|
170
|
+
if isinstance(response_metadata, dict):
|
|
171
|
+
usage = response_metadata.get("token_usage")
|
|
172
|
+
|
|
173
|
+
self.record_token_usage(model_name=model_name, usage=usage, source=source)
|
{unique_toolkit-2026.36.0.dev4.dist-info → unique_toolkit-2026.36.0.dev5.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-toolkit
|
|
3
|
-
Version: 2026.36.0.
|
|
3
|
+
Version: 2026.36.0.dev5
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
|
|
6
6
|
Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
|
|
@@ -394,7 +394,7 @@ unique_toolkit/language_model/constants.py,sha256=B-topqW0r83dkC_25DeQfnPk3n53qz
|
|
|
394
394
|
unique_toolkit/language_model/default_language_model.py,sha256=mcyuFs_V6tp52C8xJSHiEHO4kFGj6xed1gvozvCRD7k,1181
|
|
395
395
|
unique_toolkit/language_model/functions.py,sha256=W6JC4LUh-LZ9waDgph--xVmKeQ7Ls6NRAqDx9DwQdeY,26691
|
|
396
396
|
unique_toolkit/language_model/infos.py,sha256=zs0UFhds4UkSRr3zxyTJ-jJ9u2O2x9VgZx3a9d4zbYU,146221
|
|
397
|
-
unique_toolkit/language_model/invocation_stats.py,sha256=
|
|
397
|
+
unique_toolkit/language_model/invocation_stats.py,sha256=CQbpUz394IsVd8eNTB1gPSMt5f0UXmkN1amNsqtAW6U,5857
|
|
398
398
|
unique_toolkit/language_model/model_costs.py,sha256=WM1u2fWAKpPB_creS-ew8YAzc1V8oiaCbvhpSJaO8B0,3789
|
|
399
399
|
unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
|
|
400
400
|
unique_toolkit/language_model/reference.py,sha256=T3wr2jP_uRg5wKVIbKqLbxJlcgxfbZQGq3XdjbQ2kSo,6984
|
|
@@ -421,6 +421,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
|
|
|
421
421
|
unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
422
422
|
unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
|
|
423
423
|
unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
|
|
424
|
-
unique_toolkit-2026.36.0.
|
|
425
|
-
unique_toolkit-2026.36.0.
|
|
426
|
-
unique_toolkit-2026.36.0.
|
|
424
|
+
unique_toolkit-2026.36.0.dev5.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
425
|
+
unique_toolkit-2026.36.0.dev5.dist-info/METADATA,sha256=XnRiUVEB6mURHDnoA_QevRw7qH1IadNMN1XBKSUgiLg,7821
|
|
426
|
+
unique_toolkit-2026.36.0.dev5.dist-info/RECORD,,
|
|
File without changes
|