unique_toolkit 2026.36.0.dev4__py3-none-any.whl → 2026.36.0.dev6__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.
@@ -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
- from typing import Annotated, Self
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.36.0.dev4
3
+ Version: 2026.36.0.dev6
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>
@@ -16,7 +16,7 @@ Requires-Dist: sseclient>=0.0.27,<0.0.28
16
16
  Requires-Dist: openai>=1.105.0,<3
17
17
  Requires-Dist: platformdirs>=4.0.0,<5
18
18
  Requires-Dist: pillow>=12.1.1
19
- Requires-Dist: unique-sdk>=2026.36.0.dev0,<2026.36.0rc0
19
+ Requires-Dist: unique-sdk>=2026.36.0.dev1,<2026.36.0rc0
20
20
  Requires-Dist: jambo>=0.1.2,<0.2
21
21
  Requires-Dist: jinja2>=3.1.6,<4
22
22
  Requires-Dist: docxtpl>=0.20.1,<0.21
@@ -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=XAM_qckI-BoMBSgDpVcxFD1cFSdi1-GHZToHz_jLynM,2491
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.dev4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
425
- unique_toolkit-2026.36.0.dev4.dist-info/METADATA,sha256=cFB1qZ_frQUp2w6_eMAL5auiBr8ELgaGhfLApCzNywI,7821
426
- unique_toolkit-2026.36.0.dev4.dist-info/RECORD,,
424
+ unique_toolkit-2026.36.0.dev6.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
425
+ unique_toolkit-2026.36.0.dev6.dist-info/METADATA,sha256=pZseZiZpalFBDEY17j10JOFZrZ51yy8Y9ByMQ5LEdAw,7821
426
+ unique_toolkit-2026.36.0.dev6.dist-info/RECORD,,