spanforge 2.0.0__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.
- spanforge/__init__.py +695 -0
- spanforge/_batch_exporter.py +322 -0
- spanforge/_cli.py +3081 -0
- spanforge/_hooks.py +340 -0
- spanforge/_server.py +953 -0
- spanforge/_span.py +1015 -0
- spanforge/_store.py +287 -0
- spanforge/_stream.py +654 -0
- spanforge/_trace.py +334 -0
- spanforge/_tracer.py +253 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +464 -0
- spanforge/auto.py +181 -0
- spanforge/baseline.py +336 -0
- spanforge/config.py +460 -0
- spanforge/consent.py +227 -0
- spanforge/consumer.py +379 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1060 -0
- spanforge/cost.py +597 -0
- spanforge/debug.py +514 -0
- spanforge/drift.py +488 -0
- spanforge/egress.py +63 -0
- spanforge/eval.py +575 -0
- spanforge/event.py +1052 -0
- spanforge/exceptions.py +246 -0
- spanforge/explain.py +181 -0
- spanforge/export/__init__.py +50 -0
- spanforge/export/append_only.py +342 -0
- spanforge/export/cloud.py +349 -0
- spanforge/export/datadog.py +495 -0
- spanforge/export/grafana.py +331 -0
- spanforge/export/jsonl.py +198 -0
- spanforge/export/otel_bridge.py +291 -0
- spanforge/export/otlp.py +817 -0
- spanforge/export/otlp_bridge.py +231 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/webhook.py +302 -0
- spanforge/exporters/__init__.py +29 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/hitl.py +297 -0
- spanforge/inspect.py +429 -0
- spanforge/integrations/__init__.py +39 -0
- spanforge/integrations/_pricing.py +277 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/bedrock.py +306 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +349 -0
- spanforge/integrations/groq.py +444 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/llamaindex.py +370 -0
- spanforge/integrations/ollama.py +286 -0
- spanforge/integrations/openai.py +370 -0
- spanforge/integrations/together.py +485 -0
- spanforge/metrics.py +393 -0
- spanforge/metrics_export.py +342 -0
- spanforge/migrate.py +278 -0
- spanforge/model_registry.py +282 -0
- spanforge/models.py +407 -0
- spanforge/namespaces/__init__.py +215 -0
- spanforge/namespaces/audit.py +253 -0
- spanforge/namespaces/cache.py +209 -0
- spanforge/namespaces/chain.py +74 -0
- spanforge/namespaces/confidence.py +69 -0
- spanforge/namespaces/consent.py +85 -0
- spanforge/namespaces/cost.py +175 -0
- spanforge/namespaces/decision.py +135 -0
- spanforge/namespaces/diff.py +146 -0
- spanforge/namespaces/drift.py +79 -0
- spanforge/namespaces/eval_.py +232 -0
- spanforge/namespaces/fence.py +180 -0
- spanforge/namespaces/guard.py +104 -0
- spanforge/namespaces/hitl.py +92 -0
- spanforge/namespaces/latency.py +69 -0
- spanforge/namespaces/prompt.py +185 -0
- spanforge/namespaces/redact.py +172 -0
- spanforge/namespaces/template.py +197 -0
- spanforge/namespaces/tool_call.py +76 -0
- spanforge/namespaces/trace.py +1006 -0
- spanforge/normalizer.py +183 -0
- spanforge/presidio_backend.py +149 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +415 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +780 -0
- spanforge/sampling.py +500 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/signing.py +1152 -0
- spanforge/stream.py +559 -0
- spanforge/testing.py +376 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +304 -0
- spanforge/validate.py +383 -0
- spanforge-2.0.0.dist-info/METADATA +1777 -0
- spanforge-2.0.0.dist-info/RECORD +101 -0
- spanforge-2.0.0.dist-info/WHEEL +4 -0
- spanforge-2.0.0.dist-info/entry_points.txt +5 -0
- spanforge-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""spanforge.integrations.together — Auto-instrumentation for the Together AI Python SDK.
|
|
2
|
+
|
|
3
|
+
This module monkey-patches the Together AI client so every
|
|
4
|
+
``client.chat.completions.create(...)`` call automatically populates the
|
|
5
|
+
active :class:`~spanforge._span.Span` with:
|
|
6
|
+
|
|
7
|
+
* :class:`~spanforge.namespaces.trace.TokenUsage` (input / output token counts)
|
|
8
|
+
* :class:`~spanforge.namespaces.trace.ModelInfo` (provider = ``together_ai``,
|
|
9
|
+
normalized name from response)
|
|
10
|
+
* :class:`~spanforge.namespaces.trace.CostBreakdown` (computed from the static
|
|
11
|
+
pricing table below)
|
|
12
|
+
|
|
13
|
+
Together AI model names include an organization prefix separated by ``/``
|
|
14
|
+
(e.g. ``"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"``). This module
|
|
15
|
+
normalizes the name by stripping the org prefix so ``ModelInfo.name`` is
|
|
16
|
+
``"Meta-Llama-3.1-8B-Instruct-Turbo"``. The full identifier (with prefix)
|
|
17
|
+
is retained as :attr:`~spanforge.namespaces.trace.ModelInfo.name` when the
|
|
18
|
+
normalized name is not found in the pricing table, to preserve observability
|
|
19
|
+
accuracy.
|
|
20
|
+
|
|
21
|
+
Usage::
|
|
22
|
+
|
|
23
|
+
from spanforge.integrations import together as together_integration
|
|
24
|
+
together_integration.patch()
|
|
25
|
+
|
|
26
|
+
from together import Together
|
|
27
|
+
client = Together()
|
|
28
|
+
|
|
29
|
+
import spanforge
|
|
30
|
+
spanforge.configure(exporter="console")
|
|
31
|
+
|
|
32
|
+
with spanforge.span("together-chat") as span:
|
|
33
|
+
resp = client.chat.completions.create(
|
|
34
|
+
model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
|
|
35
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
36
|
+
)
|
|
37
|
+
# → span.token_usage and span.cost auto-populated on exit
|
|
38
|
+
|
|
39
|
+
Calling ``patch()`` is **idempotent** — calling it multiple times has no
|
|
40
|
+
effect. Call :func:`unpatch` to restore the original methods.
|
|
41
|
+
|
|
42
|
+
Install with::
|
|
43
|
+
|
|
44
|
+
pip install "spanforge[together]"
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import functools
|
|
50
|
+
from typing import Any
|
|
51
|
+
|
|
52
|
+
from spanforge.namespaces.trace import (
|
|
53
|
+
CostBreakdown,
|
|
54
|
+
GenAISystem,
|
|
55
|
+
ModelInfo,
|
|
56
|
+
TokenUsage,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"is_patched",
|
|
61
|
+
"normalize_model_name",
|
|
62
|
+
"normalize_response",
|
|
63
|
+
"patch",
|
|
64
|
+
"unpatch",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Static pricing table (USD per million tokens, effective 2026-03-04)
|
|
69
|
+
# Keys are the *full* model identifiers as returned by the Together AI API.
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
PRICING_DATE: str = "2026-03-04"
|
|
73
|
+
|
|
74
|
+
#: Together AI model pricing — USD per million tokens.
|
|
75
|
+
#: Keys use the full ``org/model`` identifier from the API.
|
|
76
|
+
TOGETHER_PRICING: dict[str, dict[str, float]] = {
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Meta LLaMA 3.3
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo": {
|
|
81
|
+
"input": 0.88,
|
|
82
|
+
"output": 0.88,
|
|
83
|
+
},
|
|
84
|
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo-Free": {
|
|
85
|
+
"input": 0.00,
|
|
86
|
+
"output": 0.00,
|
|
87
|
+
},
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
# Meta LLaMA 3.2
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
"meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo": {
|
|
92
|
+
"input": 1.20,
|
|
93
|
+
"output": 1.20,
|
|
94
|
+
},
|
|
95
|
+
"meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo": {
|
|
96
|
+
"input": 0.18,
|
|
97
|
+
"output": 0.18,
|
|
98
|
+
},
|
|
99
|
+
"meta-llama/Llama-3.2-3B-Instruct-Turbo": {
|
|
100
|
+
"input": 0.06,
|
|
101
|
+
"output": 0.06,
|
|
102
|
+
},
|
|
103
|
+
"meta-llama/Llama-3.2-1B-Instruct-Turbo": {
|
|
104
|
+
"input": 0.04,
|
|
105
|
+
"output": 0.04,
|
|
106
|
+
},
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Meta LLaMA 3.1
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo": {
|
|
111
|
+
"input": 3.50,
|
|
112
|
+
"output": 3.50,
|
|
113
|
+
},
|
|
114
|
+
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": {
|
|
115
|
+
"input": 0.88,
|
|
116
|
+
"output": 0.88,
|
|
117
|
+
},
|
|
118
|
+
"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo": {
|
|
119
|
+
"input": 0.18,
|
|
120
|
+
"output": 0.18,
|
|
121
|
+
},
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
# Meta LLaMA 3
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
"meta-llama/Meta-Llama-3-70B-Instruct-Turbo": {
|
|
126
|
+
"input": 0.88,
|
|
127
|
+
"output": 0.88,
|
|
128
|
+
},
|
|
129
|
+
"meta-llama/Meta-Llama-3-8B-Instruct-Turbo": {
|
|
130
|
+
"input": 0.18,
|
|
131
|
+
"output": 0.18,
|
|
132
|
+
},
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Qwen
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
"Qwen/Qwen2.5-72B-Instruct-Turbo": {
|
|
137
|
+
"input": 1.20,
|
|
138
|
+
"output": 1.20,
|
|
139
|
+
},
|
|
140
|
+
"Qwen/Qwen2.5-7B-Instruct-Turbo": {
|
|
141
|
+
"input": 0.30,
|
|
142
|
+
"output": 0.30,
|
|
143
|
+
},
|
|
144
|
+
"Qwen/QwQ-32B-Preview": {
|
|
145
|
+
"input": 1.20,
|
|
146
|
+
"output": 1.20,
|
|
147
|
+
},
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# Mistral / Mixtral
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
"mistralai/Mixtral-8x7B-Instruct-v0.1": {
|
|
152
|
+
"input": 0.60,
|
|
153
|
+
"output": 0.60,
|
|
154
|
+
},
|
|
155
|
+
"mistralai/Mixtral-8x22B-Instruct-v0.1": {
|
|
156
|
+
"input": 1.20,
|
|
157
|
+
"output": 1.20,
|
|
158
|
+
},
|
|
159
|
+
"mistralai/Mistral-7B-Instruct-v0.3": {
|
|
160
|
+
"input": 0.20,
|
|
161
|
+
"output": 0.20,
|
|
162
|
+
},
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
# DeepSeek
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
"deepseek-ai/DeepSeek-V3": {
|
|
167
|
+
"input": 1.25,
|
|
168
|
+
"output": 1.25,
|
|
169
|
+
},
|
|
170
|
+
"deepseek-ai/DeepSeek-R1": {
|
|
171
|
+
"input": 2.19,
|
|
172
|
+
"output": 7.89,
|
|
173
|
+
},
|
|
174
|
+
"deepseek-ai/DeepSeek-R1-Distill-Llama-70B": {
|
|
175
|
+
"input": 2.19,
|
|
176
|
+
"output": 2.19,
|
|
177
|
+
},
|
|
178
|
+
"deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B": {
|
|
179
|
+
"input": 0.18,
|
|
180
|
+
"output": 0.18,
|
|
181
|
+
},
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
# Google Gemma
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
"google/gemma-2-27b-it": {
|
|
186
|
+
"input": 0.80,
|
|
187
|
+
"output": 0.80,
|
|
188
|
+
},
|
|
189
|
+
"google/gemma-2-9b-it": {
|
|
190
|
+
"input": 0.30,
|
|
191
|
+
"output": 0.30,
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Sentinel attribute set on the together module to prevent double-patching.
|
|
196
|
+
_PATCH_FLAG = "_spanforge_patched"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Public API
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def patch() -> None:
|
|
205
|
+
"""Monkey-patch the Together AI client to auto-instrument all chat completions.
|
|
206
|
+
|
|
207
|
+
Wraps both the sync and async ``create`` methods on the Completions
|
|
208
|
+
resource. The wrapper calls :func:`normalize_response` on the result
|
|
209
|
+
and, if a span is currently active on this thread, updates it.
|
|
210
|
+
|
|
211
|
+
This function is **idempotent** — safe to call multiple times.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ImportError: If the ``together`` package is not installed.
|
|
215
|
+
"""
|
|
216
|
+
together_mod = _require_together()
|
|
217
|
+
|
|
218
|
+
if getattr(together_mod, _PATCH_FLAG, False):
|
|
219
|
+
return # already patched
|
|
220
|
+
|
|
221
|
+
# --- sync ----------------------------------------------------------------
|
|
222
|
+
try:
|
|
223
|
+
from together.resources.chat.completions import ( # noqa: PLC0415
|
|
224
|
+
Completions, # type: ignore[import-untyped]
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
_orig_sync = Completions.create # type: ignore[attr-defined]
|
|
228
|
+
|
|
229
|
+
@functools.wraps(_orig_sync)
|
|
230
|
+
def _patched_sync(self: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
231
|
+
response = _orig_sync(self, *args, **kwargs)
|
|
232
|
+
_auto_populate_span(response)
|
|
233
|
+
return response
|
|
234
|
+
|
|
235
|
+
Completions.create = _patched_sync # type: ignore[method-assign]
|
|
236
|
+
Completions._spanforge_orig_create = _orig_sync # type: ignore[attr-defined]
|
|
237
|
+
except (ImportError, AttributeError): # pragma: no cover
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
# --- async ---------------------------------------------------------------
|
|
241
|
+
try:
|
|
242
|
+
from together.resources.chat.completions import ( # noqa: PLC0415
|
|
243
|
+
AsyncCompletions, # type: ignore[import-untyped]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
_orig_async = AsyncCompletions.create # type: ignore[attr-defined]
|
|
247
|
+
|
|
248
|
+
@functools.wraps(_orig_async)
|
|
249
|
+
async def _patched_async(self: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
250
|
+
response = await _orig_async(self, *args, **kwargs)
|
|
251
|
+
_auto_populate_span(response)
|
|
252
|
+
return response
|
|
253
|
+
|
|
254
|
+
AsyncCompletions.create = _patched_async # type: ignore[method-assign]
|
|
255
|
+
AsyncCompletions._spanforge_orig_create = _orig_async # type: ignore[attr-defined]
|
|
256
|
+
except (ImportError, AttributeError): # pragma: no cover
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
together_mod._spanforge_patched = True # type: ignore[attr-defined]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def unpatch() -> None:
|
|
263
|
+
"""Restore the original Together AI methods and remove the patch flag.
|
|
264
|
+
|
|
265
|
+
Safe to call even if :func:`patch` was never called.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
ImportError: If the ``together`` package is not installed.
|
|
269
|
+
"""
|
|
270
|
+
together_mod = _require_together()
|
|
271
|
+
|
|
272
|
+
if not getattr(together_mod, _PATCH_FLAG, False):
|
|
273
|
+
return # nothing to do
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
from together.resources.chat.completions import ( # noqa: PLC0415
|
|
277
|
+
Completions, # type: ignore[import-untyped]
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
Completions.create = Completions._spanforge_orig_create # type: ignore[attr-defined,method-assign]
|
|
281
|
+
del Completions._spanforge_orig_create # type: ignore[attr-defined]
|
|
282
|
+
except (ImportError, AttributeError): # pragma: no cover
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
from together.resources.chat.completions import ( # noqa: PLC0415
|
|
287
|
+
AsyncCompletions, # type: ignore[import-untyped]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
AsyncCompletions.create = AsyncCompletions._spanforge_orig_create # type: ignore[attr-defined,method-assign]
|
|
291
|
+
del AsyncCompletions._spanforge_orig_create # type: ignore[attr-defined]
|
|
292
|
+
except (ImportError, AttributeError): # pragma: no cover
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
try: # noqa: SIM105
|
|
296
|
+
del together_mod._spanforge_patched # type: ignore[attr-defined]
|
|
297
|
+
except AttributeError: # pragma: no cover
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def is_patched() -> bool:
|
|
302
|
+
"""Return ``True`` if the Together AI client has been patched by spanforge.
|
|
303
|
+
|
|
304
|
+
Returns ``False`` if the ``together`` package is not installed.
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
together_mod = _require_together()
|
|
308
|
+
return bool(getattr(together_mod, _PATCH_FLAG, False))
|
|
309
|
+
except ImportError:
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def normalize_model_name(raw_name: str) -> str:
|
|
314
|
+
"""Normalize a Together AI model name by stripping the organization prefix.
|
|
315
|
+
|
|
316
|
+
Together AI uses ``org/model-name`` identifiers. This function strips
|
|
317
|
+
the ``org/`` prefix so the returned name contains only the model
|
|
318
|
+
component. If no ``/`` is present the name is returned unchanged.
|
|
319
|
+
|
|
320
|
+
Examples::
|
|
321
|
+
|
|
322
|
+
>>> normalize_model_name("meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo")
|
|
323
|
+
'Meta-Llama-3.1-8B-Instruct-Turbo'
|
|
324
|
+
>>> normalize_model_name("gpt-4o")
|
|
325
|
+
'gpt-4o'
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
raw_name: Model identifier as returned by the Together AI API.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Model name without the organization prefix.
|
|
332
|
+
"""
|
|
333
|
+
if "/" in raw_name:
|
|
334
|
+
return raw_name.split("/", 1)[1]
|
|
335
|
+
return raw_name
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def normalize_response(
|
|
339
|
+
response: Any, # noqa: ANN401
|
|
340
|
+
) -> tuple[TokenUsage, ModelInfo, CostBreakdown]:
|
|
341
|
+
"""Extract structured observability data from a Together AI chat completion.
|
|
342
|
+
|
|
343
|
+
Together AI mirrors the OpenAI response format for token fields, but
|
|
344
|
+
model names use an ``org/model`` scheme. The model name is stored with
|
|
345
|
+
the full identifier preserved for unique identification, while the
|
|
346
|
+
normalized (org-stripped) name is available via :func:`normalize_model_name`.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
response: A Together AI ``ChatCompletion`` (or compatible object).
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
A 3-tuple of ``(TokenUsage, ModelInfo, CostBreakdown)``.
|
|
353
|
+
|
|
354
|
+
Field mapping:
|
|
355
|
+
|
|
356
|
+
+--------------------------------------------+---------------------------+
|
|
357
|
+
| Together AI field | SpanForge field |
|
|
358
|
+
+============================================+===========================+
|
|
359
|
+
| ``response.model`` | ``ModelInfo.name`` (full) |
|
|
360
|
+
| ``usage.prompt_tokens`` | ``TokenUsage.input_tokens``|
|
|
361
|
+
| ``usage.completion_tokens`` | ``TokenUsage.output_tokens``|
|
|
362
|
+
| ``usage.total_tokens`` | ``TokenUsage.total_tokens``|
|
|
363
|
+
+--------------------------------------------+---------------------------+
|
|
364
|
+
"""
|
|
365
|
+
# ------------------------------------------------------------------ usage
|
|
366
|
+
usage = getattr(response, "usage", None)
|
|
367
|
+
input_tokens: int = 0
|
|
368
|
+
output_tokens: int = 0
|
|
369
|
+
total_tokens: int = 0
|
|
370
|
+
|
|
371
|
+
if usage is not None:
|
|
372
|
+
input_tokens = int(getattr(usage, "prompt_tokens", 0) or 0)
|
|
373
|
+
output_tokens = int(getattr(usage, "completion_tokens", 0) or 0)
|
|
374
|
+
total_tokens = int(
|
|
375
|
+
getattr(usage, "total_tokens", input_tokens + output_tokens) or 0
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
token_usage = TokenUsage(
|
|
379
|
+
input_tokens=input_tokens,
|
|
380
|
+
output_tokens=output_tokens,
|
|
381
|
+
total_tokens=total_tokens,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# ---------------------------------------------------------------- model
|
|
385
|
+
# Keep the full ``org/model`` identifier for unique model identification.
|
|
386
|
+
model_name: str = getattr(response, "model", None) or "unknown"
|
|
387
|
+
model_info = ModelInfo(system=GenAISystem.TOGETHER_AI, name=model_name)
|
|
388
|
+
|
|
389
|
+
# ----------------------------------------------------------------- cost
|
|
390
|
+
cost = _compute_cost(model_name, input_tokens, output_tokens)
|
|
391
|
+
|
|
392
|
+
return token_usage, model_info, cost
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def list_models() -> list[str]:
|
|
396
|
+
"""Return a sorted list of all Together AI model identifiers in the pricing table."""
|
|
397
|
+
return sorted(TOGETHER_PRICING.keys())
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
# Internal helpers
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _require_together() -> Any: # noqa: ANN401
|
|
406
|
+
"""Import and return the ``together`` module, raising ``ImportError`` if absent."""
|
|
407
|
+
try:
|
|
408
|
+
import together # type: ignore[import-untyped] # noqa: PLC0415
|
|
409
|
+
except ImportError as exc:
|
|
410
|
+
raise ImportError(
|
|
411
|
+
"The 'together' package is required for spanforge Together AI integration.\n"
|
|
412
|
+
"Install it with: pip install 'spanforge[together]'"
|
|
413
|
+
) from exc
|
|
414
|
+
else:
|
|
415
|
+
return together
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _get_pricing(model: str) -> dict[str, float] | None:
|
|
419
|
+
"""Return the pricing entry for *model*, or ``None`` if unknown.
|
|
420
|
+
|
|
421
|
+
Tries the full ``org/model`` key first, then falls back to the
|
|
422
|
+
normalized (org-stripped) name.
|
|
423
|
+
"""
|
|
424
|
+
if model in TOGETHER_PRICING:
|
|
425
|
+
return TOGETHER_PRICING[model]
|
|
426
|
+
|
|
427
|
+
# Try without org prefix
|
|
428
|
+
normalized = normalize_model_name(model)
|
|
429
|
+
if normalized in TOGETHER_PRICING:
|
|
430
|
+
return TOGETHER_PRICING[normalized]
|
|
431
|
+
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _compute_cost(
|
|
436
|
+
model_name: str,
|
|
437
|
+
input_tokens: int,
|
|
438
|
+
output_tokens: int,
|
|
439
|
+
) -> CostBreakdown:
|
|
440
|
+
"""Compute :class:`~spanforge.namespaces.trace.CostBreakdown` from token counts."""
|
|
441
|
+
pricing = _get_pricing(model_name)
|
|
442
|
+
if pricing is None:
|
|
443
|
+
return CostBreakdown.zero()
|
|
444
|
+
|
|
445
|
+
input_cost = input_tokens * pricing["input"] / 1_000_000.0
|
|
446
|
+
output_cost = output_tokens * pricing["output"] / 1_000_000.0
|
|
447
|
+
total = input_cost + output_cost
|
|
448
|
+
|
|
449
|
+
return CostBreakdown(
|
|
450
|
+
input_cost_usd=input_cost,
|
|
451
|
+
output_cost_usd=output_cost,
|
|
452
|
+
total_cost_usd=total,
|
|
453
|
+
pricing_date=PRICING_DATE,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _auto_populate_span(response: Any) -> None: # noqa: ANN401
|
|
458
|
+
"""If there is an active span on this thread, populate it from *response*.
|
|
459
|
+
|
|
460
|
+
Silently does nothing if:
|
|
461
|
+
|
|
462
|
+
* There is no active span.
|
|
463
|
+
* ``normalize_response`` raises (malformed response).
|
|
464
|
+
* The span already has ``token_usage`` set (don't overwrite manual data).
|
|
465
|
+
"""
|
|
466
|
+
try:
|
|
467
|
+
from spanforge._span import _span_stack # noqa: PLC0415
|
|
468
|
+
|
|
469
|
+
stack = _span_stack()
|
|
470
|
+
if not stack:
|
|
471
|
+
return
|
|
472
|
+
span = stack[-1]
|
|
473
|
+
|
|
474
|
+
if span.token_usage is not None:
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
token_usage, model_info, cost = normalize_response(response)
|
|
478
|
+
span.token_usage = token_usage
|
|
479
|
+
span.cost = cost
|
|
480
|
+
|
|
481
|
+
if span.model is None:
|
|
482
|
+
span.model = model_info.name
|
|
483
|
+
|
|
484
|
+
except Exception: # noqa: S110 # NOSONAR
|
|
485
|
+
pass
|