tracecast 0.2.6__tar.gz → 0.2.7__tar.gz
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.
- {tracecast-0.2.6 → tracecast-0.2.7}/PKG-INFO +1 -1
- {tracecast-0.2.6 → tracecast-0.2.7}/pyproject.toml +1 -1
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_cost_calculator.py +51 -1
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/cost_calculator.py +26 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/openai_inst.py +165 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/models/span.py +5 -3
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/PKG-INFO +1 -1
- {tracecast-0.2.6 → tracecast-0.2.7}/LICENSE +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/README.md +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/setup.cfg +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_aggregator_graph.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_context_propagation.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_dashboard_mount.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_dashboard_pushdown.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_dashboard_sessions.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_decorators.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_e2e_langgraph.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_exporters_db.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_integration_frameworks.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_integration_langchain.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_json_exporter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_langchain_callback.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_llm_wrappers.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_logging.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_mongo_exporter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_prompt_dashboard.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_prompts.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_roundtrip.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_schema_v2.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_score.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_score_dashboard.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_scoring_api.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_serve.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_span.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_streaming.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_token_counter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_trace.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_trace_span.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_tracer.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/logger.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/scoring.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/token_counter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/tracer.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/aggregator.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/asgi_middleware.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/blueprint.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/eval_reader.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/prompt_reader.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/reader.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/router.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/score_reader.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/standalone.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-B07hiBI2.js +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-BMIlEmLS.js +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-CY5YOA8F.js +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-DllJJzmE.css +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-Dt0awpJJ.js +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/index.html +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/decorators.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/cli.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/compare.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/dataset.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/decorator.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/judge.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/metrics.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/models.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/online.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/runner.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/scorers.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/_eval_store.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/base.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/dict_exporter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/json_file.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/mongo.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/postgres.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/query.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrument.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/_llamaindex_handler.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/_streaming.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/anthropic_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/base.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/crewai_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/gemini_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/langchain_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/llamaindex_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/integrations/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/integrations/langchain.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/integrations/llm.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/middleware.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/models/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/models/score.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/models/trace.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/prompts/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/prompts/client.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/prompts/models.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/serve.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/SOURCES.txt +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/dependency_links.txt +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/entry_points.txt +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/requires.txt +0 -0
- {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pytest
|
|
2
|
-
from tracecast.core.cost_calculator import calculate_cost, PRICE_TABLE
|
|
2
|
+
from tracecast.core.cost_calculator import calculate_cost, calculate_audio_cost, PRICE_TABLE
|
|
3
3
|
|
|
4
4
|
@pytest.mark.parametrize("model", [
|
|
5
5
|
"gpt-4o", "gpt-4o-mini", "gpt-4-turbo",
|
|
@@ -151,3 +151,53 @@ def test_modelos_sem_suporte_a_cache_nao_tem_chave_cached():
|
|
|
151
151
|
modelos_sem_cache = ["gpt-4-turbo", "gemini-2.5-flash", "llama-4-scout"]
|
|
152
152
|
for model in modelos_sem_cache:
|
|
153
153
|
assert "cached" not in PRICE_TABLE[model], f"{model} nao deveria ter preco de cache"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# --- gpt-4.1-nano (bug: ausente da tabela, custo virava $0 silenciosamente) ---
|
|
157
|
+
|
|
158
|
+
def test_gpt_4_1_nano_tem_preco():
|
|
159
|
+
assert "gpt-4.1-nano" in PRICE_TABLE
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_calcula_custo_gpt_4_1_nano():
|
|
163
|
+
# $0.10 / $0.40 por 1M tokens = 0.00010 / 0.00040 por 1k
|
|
164
|
+
cost = calculate_cost("gpt-4.1-nano", tokens_in=1000, tokens_out=1000)
|
|
165
|
+
assert abs(cost - (0.00010 + 0.00040)) < 1e-9
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_gpt_4_1_nano_cached_e_25_pct_do_input():
|
|
169
|
+
# mesmo padrao dos irmaos gpt-4.1 / gpt-4.1-mini: cached = 25% do input
|
|
170
|
+
entry = PRICE_TABLE["gpt-4.1-nano"]
|
|
171
|
+
assert abs(entry["cached"] - entry["input"] * 0.25) < 1e-9
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --- embeddings (bug: nunca precificados, custo $0 mesmo se instrumentados) ---
|
|
175
|
+
|
|
176
|
+
@pytest.mark.parametrize("model,expected_input", [
|
|
177
|
+
("text-embedding-3-small", 0.00002),
|
|
178
|
+
("text-embedding-3-large", 0.00013),
|
|
179
|
+
("text-embedding-ada-002", 0.00010),
|
|
180
|
+
])
|
|
181
|
+
def test_calcula_custo_embeddings(model, expected_input):
|
|
182
|
+
cost = calculate_cost(model, tokens_in=1000, tokens_out=0)
|
|
183
|
+
assert abs(cost - expected_input) < 1e-9
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --- audio (bug: whisper/tts nunca precificados nem instrumentados) ---
|
|
187
|
+
|
|
188
|
+
def test_calculate_audio_cost_whisper_por_minuto():
|
|
189
|
+
cost = calculate_audio_cost("whisper-1", minutes=2.5)
|
|
190
|
+
assert abs(cost - (2.5 * 0.006)) < 1e-9
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_calculate_audio_cost_tts_por_caractere():
|
|
194
|
+
cost = calculate_audio_cost("gpt-4o-mini-tts", chars=1000)
|
|
195
|
+
assert cost > 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_calculate_audio_cost_modelo_desconhecido_retorna_zero():
|
|
199
|
+
assert calculate_audio_cost("modelo-inexistente", minutes=10) == 0.0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_calculate_audio_cost_sem_uso_retorna_zero():
|
|
203
|
+
assert calculate_audio_cost("whisper-1") == 0.0
|
|
@@ -8,9 +8,14 @@ PRICE_TABLE: Dict[str, Dict[str, float]] = {
|
|
|
8
8
|
"gpt-4-turbo": {"input": 0.01000, "output": 0.03000},
|
|
9
9
|
"gpt-4.1": {"input": 0.00200, "output": 0.00800, "cached": 0.000500},
|
|
10
10
|
"gpt-4.1-mini": {"input": 0.00040, "output": 0.00160, "cached": 0.000100},
|
|
11
|
+
"gpt-4.1-nano": {"input": 0.00010, "output": 0.00040, "cached": 0.000025},
|
|
11
12
|
"o3": {"input": 0.00200, "output": 0.00800, "cached": 0.000500},
|
|
12
13
|
"o4-mini": {"input": 0.00110, "output": 0.00440, "cached": 0.000275},
|
|
13
14
|
|
|
15
|
+
"text-embedding-3-small": {"input": 0.00002, "output": 0.0},
|
|
16
|
+
"text-embedding-3-large": {"input": 0.00013, "output": 0.0},
|
|
17
|
+
"text-embedding-ada-002": {"input": 0.00010, "output": 0.0},
|
|
18
|
+
|
|
14
19
|
"claude-opus-4-6": {"input": 0.00500, "output": 0.02500, "cached": 0.000500},
|
|
15
20
|
"claude-sonnet-4-6": {"input": 0.00300, "output": 0.01500, "cached": 0.000300},
|
|
16
21
|
"claude-haiku-4-5": {"input": 0.00100, "output": 0.00500, "cached": 0.000100},
|
|
@@ -62,3 +67,24 @@ def calculate_cost(
|
|
|
62
67
|
def _prefix_match(model: str, prices: Dict) -> Optional[Dict]:
|
|
63
68
|
provider = model.split("/")[0] + "/*"
|
|
64
69
|
return prices.get(provider)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Audio isn't token-billed: transcription is per-minute of input audio, TTS is
|
|
73
|
+
# per-character of input text. gpt-4o-mini-tts's rate is an approximation
|
|
74
|
+
# (not published on OpenAI's current pricing page as of 2026-07) — verify
|
|
75
|
+
# against an actual invoice before trusting it for reconciliation.
|
|
76
|
+
AUDIO_PRICE_TABLE: Dict[str, Dict[str, float]] = {
|
|
77
|
+
"whisper-1": {"unit": "minute", "price": 0.006},
|
|
78
|
+
"gpt-4o-mini-tts": {"unit": "char", "price": 0.000015},
|
|
79
|
+
"tts-1": {"unit": "char", "price": 0.000015},
|
|
80
|
+
"tts-1-hd": {"unit": "char", "price": 0.00003},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def calculate_audio_cost(model: str, *, minutes: float = 0.0, chars: int = 0) -> float:
|
|
85
|
+
entry = AUDIO_PRICE_TABLE.get(model)
|
|
86
|
+
if not entry:
|
|
87
|
+
return 0.0
|
|
88
|
+
if entry["unit"] == "minute":
|
|
89
|
+
return minutes * entry["price"]
|
|
90
|
+
return chars * entry["price"]
|
|
@@ -27,6 +27,9 @@ class OpenAIInstrumentor(BaseInstrumentor):
|
|
|
27
27
|
def __init__(self) -> None:
|
|
28
28
|
self._original_create: Optional[Any] = None
|
|
29
29
|
self._original_acreate: Optional[Any] = None
|
|
30
|
+
self._original_embeddings_create: Optional[Any] = None
|
|
31
|
+
self._original_transcriptions_create: Optional[Any] = None
|
|
32
|
+
self._original_speech_create: Optional[Any] = None
|
|
30
33
|
self._patched: bool = False
|
|
31
34
|
|
|
32
35
|
def patch(self) -> None:
|
|
@@ -54,8 +57,54 @@ class OpenAIInstrumentor(BaseInstrumentor):
|
|
|
54
57
|
|
|
55
58
|
mod.AsyncCompletions.create = patched_acreate
|
|
56
59
|
|
|
60
|
+
self._patch_embeddings()
|
|
61
|
+
self._patch_audio()
|
|
62
|
+
|
|
57
63
|
self._patched = True
|
|
58
64
|
|
|
65
|
+
def _patch_embeddings(self) -> None:
|
|
66
|
+
try:
|
|
67
|
+
import openai.resources.embeddings as emb_mod
|
|
68
|
+
except ImportError:
|
|
69
|
+
return
|
|
70
|
+
self._original_embeddings_create = emb_mod.Embeddings.create
|
|
71
|
+
self_ref = self
|
|
72
|
+
_orig = self._original_embeddings_create
|
|
73
|
+
|
|
74
|
+
def patched(client_self, *args, **kwargs):
|
|
75
|
+
return self_ref._intercept_embeddings(client_self, args, kwargs, _orig)
|
|
76
|
+
|
|
77
|
+
emb_mod.Embeddings.create = patched
|
|
78
|
+
|
|
79
|
+
def _patch_audio(self) -> None:
|
|
80
|
+
try:
|
|
81
|
+
import openai.resources.audio.transcriptions as tr_mod
|
|
82
|
+
except ImportError:
|
|
83
|
+
tr_mod = None
|
|
84
|
+
if tr_mod is not None:
|
|
85
|
+
self._original_transcriptions_create = tr_mod.Transcriptions.create
|
|
86
|
+
self_ref = self
|
|
87
|
+
_orig = self._original_transcriptions_create
|
|
88
|
+
|
|
89
|
+
def patched_tr(client_self, *args, **kwargs):
|
|
90
|
+
return self_ref._intercept_transcription(client_self, args, kwargs, _orig)
|
|
91
|
+
|
|
92
|
+
tr_mod.Transcriptions.create = patched_tr
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
import openai.resources.audio.speech as sp_mod
|
|
96
|
+
except ImportError:
|
|
97
|
+
sp_mod = None
|
|
98
|
+
if sp_mod is not None:
|
|
99
|
+
self._original_speech_create = sp_mod.Speech.create
|
|
100
|
+
self_ref = self
|
|
101
|
+
_orig_sp = self._original_speech_create
|
|
102
|
+
|
|
103
|
+
def patched_sp(client_self, *args, **kwargs):
|
|
104
|
+
return self_ref._intercept_speech(client_self, args, kwargs, _orig_sp)
|
|
105
|
+
|
|
106
|
+
sp_mod.Speech.create = patched_sp
|
|
107
|
+
|
|
59
108
|
def unpatch(self) -> None:
|
|
60
109
|
if not self._patched:
|
|
61
110
|
return
|
|
@@ -65,6 +114,22 @@ class OpenAIInstrumentor(BaseInstrumentor):
|
|
|
65
114
|
mod.AsyncCompletions.create = self._original_acreate
|
|
66
115
|
self._original_create = None
|
|
67
116
|
self._original_acreate = None
|
|
117
|
+
|
|
118
|
+
if self._original_embeddings_create is not None:
|
|
119
|
+
import openai.resources.embeddings as emb_mod
|
|
120
|
+
emb_mod.Embeddings.create = self._original_embeddings_create
|
|
121
|
+
self._original_embeddings_create = None
|
|
122
|
+
|
|
123
|
+
if self._original_transcriptions_create is not None:
|
|
124
|
+
import openai.resources.audio.transcriptions as tr_mod
|
|
125
|
+
tr_mod.Transcriptions.create = self._original_transcriptions_create
|
|
126
|
+
self._original_transcriptions_create = None
|
|
127
|
+
|
|
128
|
+
if self._original_speech_create is not None:
|
|
129
|
+
import openai.resources.audio.speech as sp_mod
|
|
130
|
+
sp_mod.Speech.create = self._original_speech_create
|
|
131
|
+
self._original_speech_create = None
|
|
132
|
+
|
|
68
133
|
self._patched = False
|
|
69
134
|
|
|
70
135
|
def is_patched(self) -> bool:
|
|
@@ -172,3 +237,103 @@ class OpenAIInstrumentor(BaseInstrumentor):
|
|
|
172
237
|
span.output = extract_content(response, "openai")
|
|
173
238
|
trace.spans.append(span)
|
|
174
239
|
return response
|
|
240
|
+
|
|
241
|
+
def _intercept_embeddings(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
242
|
+
from ..core.tracer import Tracer
|
|
243
|
+
trace = Tracer.current()
|
|
244
|
+
if trace is None or _handled_by_langchain():
|
|
245
|
+
return original_fn(client_self, *args, **kwargs)
|
|
246
|
+
|
|
247
|
+
from ..models.span import Span, SpanType
|
|
248
|
+
from ..core.cost_calculator import calculate_cost
|
|
249
|
+
|
|
250
|
+
model = kwargs.get("model", "unknown")
|
|
251
|
+
span = Span(
|
|
252
|
+
span_id=str(uuid.uuid4()),
|
|
253
|
+
parent_span_id=active_parent_id(),
|
|
254
|
+
type=SpanType.EMBEDDING,
|
|
255
|
+
name=f"embedding:{model}",
|
|
256
|
+
model=model,
|
|
257
|
+
started_at=datetime.now(timezone.utc),
|
|
258
|
+
)
|
|
259
|
+
try:
|
|
260
|
+
response = original_fn(client_self, *args, **kwargs)
|
|
261
|
+
except Exception as exc:
|
|
262
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
263
|
+
span.mark_error(exc)
|
|
264
|
+
trace.spans.append(span)
|
|
265
|
+
raise
|
|
266
|
+
|
|
267
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
268
|
+
usage = getattr(response, "usage", None)
|
|
269
|
+
span.tokens_in = int(getattr(usage, "prompt_tokens", 0) or 0) if usage else 0
|
|
270
|
+
span.cost_usd = calculate_cost(model, span.tokens_in, 0)
|
|
271
|
+
trace.spans.append(span)
|
|
272
|
+
return response
|
|
273
|
+
|
|
274
|
+
def _intercept_transcription(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
275
|
+
from ..core.tracer import Tracer
|
|
276
|
+
trace = Tracer.current()
|
|
277
|
+
if trace is None:
|
|
278
|
+
return original_fn(client_self, *args, **kwargs)
|
|
279
|
+
|
|
280
|
+
from ..models.span import Span, SpanType
|
|
281
|
+
from ..core.cost_calculator import calculate_audio_cost
|
|
282
|
+
|
|
283
|
+
model = kwargs.get("model", "unknown")
|
|
284
|
+
span = Span(
|
|
285
|
+
span_id=str(uuid.uuid4()),
|
|
286
|
+
parent_span_id=active_parent_id(),
|
|
287
|
+
type=SpanType.AUDIO,
|
|
288
|
+
name=f"audio:{model}",
|
|
289
|
+
model=model,
|
|
290
|
+
started_at=datetime.now(timezone.utc),
|
|
291
|
+
)
|
|
292
|
+
try:
|
|
293
|
+
response = original_fn(client_self, *args, **kwargs)
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
296
|
+
span.mark_error(exc)
|
|
297
|
+
trace.spans.append(span)
|
|
298
|
+
raise
|
|
299
|
+
|
|
300
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
301
|
+
duration = getattr(response, "duration", None)
|
|
302
|
+
if duration is not None:
|
|
303
|
+
span.metadata["duration_seconds"] = duration
|
|
304
|
+
span.cost_usd = calculate_audio_cost(model, minutes=duration / 60)
|
|
305
|
+
trace.spans.append(span)
|
|
306
|
+
return response
|
|
307
|
+
|
|
308
|
+
def _intercept_speech(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
309
|
+
from ..core.tracer import Tracer
|
|
310
|
+
trace = Tracer.current()
|
|
311
|
+
if trace is None:
|
|
312
|
+
return original_fn(client_self, *args, **kwargs)
|
|
313
|
+
|
|
314
|
+
from ..models.span import Span, SpanType
|
|
315
|
+
from ..core.cost_calculator import calculate_audio_cost
|
|
316
|
+
|
|
317
|
+
model = kwargs.get("model", "unknown")
|
|
318
|
+
char_count = len(kwargs.get("input", "") or "")
|
|
319
|
+
span = Span(
|
|
320
|
+
span_id=str(uuid.uuid4()),
|
|
321
|
+
parent_span_id=active_parent_id(),
|
|
322
|
+
type=SpanType.AUDIO,
|
|
323
|
+
name=f"audio:{model}",
|
|
324
|
+
model=model,
|
|
325
|
+
started_at=datetime.now(timezone.utc),
|
|
326
|
+
metadata={"char_count": char_count},
|
|
327
|
+
)
|
|
328
|
+
try:
|
|
329
|
+
response = original_fn(client_self, *args, **kwargs)
|
|
330
|
+
except Exception as exc:
|
|
331
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
332
|
+
span.mark_error(exc)
|
|
333
|
+
trace.spans.append(span)
|
|
334
|
+
raise
|
|
335
|
+
|
|
336
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
337
|
+
span.cost_usd = calculate_audio_cost(model, chars=char_count)
|
|
338
|
+
trace.spans.append(span)
|
|
339
|
+
return response
|
|
@@ -5,9 +5,11 @@ from typing import Optional, Dict, Any
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class SpanType(str, Enum):
|
|
8
|
-
LLM
|
|
9
|
-
TOOL
|
|
10
|
-
AGENT
|
|
8
|
+
LLM = "llm"
|
|
9
|
+
TOOL = "tool"
|
|
10
|
+
AGENT = "agent"
|
|
11
|
+
EMBEDDING = "embedding"
|
|
12
|
+
AUDIO = "audio"
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class SpanStatus(str, Enum):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|