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.
Files changed (107) hide show
  1. {tracecast-0.2.6 → tracecast-0.2.7}/PKG-INFO +1 -1
  2. {tracecast-0.2.6 → tracecast-0.2.7}/pyproject.toml +1 -1
  3. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_cost_calculator.py +51 -1
  4. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/cost_calculator.py +26 -0
  5. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/openai_inst.py +165 -0
  6. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/models/span.py +5 -3
  7. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/PKG-INFO +1 -1
  8. {tracecast-0.2.6 → tracecast-0.2.7}/LICENSE +0 -0
  9. {tracecast-0.2.6 → tracecast-0.2.7}/README.md +0 -0
  10. {tracecast-0.2.6 → tracecast-0.2.7}/setup.cfg +0 -0
  11. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_aggregator_graph.py +0 -0
  12. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_context_propagation.py +0 -0
  13. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_dashboard_mount.py +0 -0
  14. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_dashboard_pushdown.py +0 -0
  15. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_dashboard_sessions.py +0 -0
  16. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_decorators.py +0 -0
  17. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_e2e_langgraph.py +0 -0
  18. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_exporters_db.py +0 -0
  19. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_integration_frameworks.py +0 -0
  20. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_integration_langchain.py +0 -0
  21. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_json_exporter.py +0 -0
  22. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_langchain_callback.py +0 -0
  23. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_llm_wrappers.py +0 -0
  24. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_logging.py +0 -0
  25. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_mongo_exporter.py +0 -0
  26. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_prompt_dashboard.py +0 -0
  27. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_prompts.py +0 -0
  28. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_roundtrip.py +0 -0
  29. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_schema_v2.py +0 -0
  30. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_score.py +0 -0
  31. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_score_dashboard.py +0 -0
  32. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_scoring_api.py +0 -0
  33. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_serve.py +0 -0
  34. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_span.py +0 -0
  35. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_streaming.py +0 -0
  36. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_token_counter.py +0 -0
  37. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_trace.py +0 -0
  38. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_trace_span.py +0 -0
  39. {tracecast-0.2.6 → tracecast-0.2.7}/tests/test_tracer.py +0 -0
  40. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/__init__.py +0 -0
  41. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/__init__.py +0 -0
  42. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/logger.py +0 -0
  43. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/scoring.py +0 -0
  44. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/token_counter.py +0 -0
  45. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/core/tracer.py +0 -0
  46. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/__init__.py +0 -0
  47. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/aggregator.py +0 -0
  48. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/asgi_middleware.py +0 -0
  49. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/blueprint.py +0 -0
  50. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/eval_reader.py +0 -0
  51. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/prompt_reader.py +0 -0
  52. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/reader.py +0 -0
  53. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/router.py +0 -0
  54. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/score_reader.py +0 -0
  55. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/standalone.py +0 -0
  56. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-B07hiBI2.js +0 -0
  57. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-BMIlEmLS.js +0 -0
  58. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-CY5YOA8F.js +0 -0
  59. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-DllJJzmE.css +0 -0
  60. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/assets/index-Dt0awpJJ.js +0 -0
  61. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/dashboard/static/index.html +0 -0
  62. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/decorators.py +0 -0
  63. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/__init__.py +0 -0
  64. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/cli.py +0 -0
  65. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/compare.py +0 -0
  66. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/dataset.py +0 -0
  67. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/decorator.py +0 -0
  68. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/judge.py +0 -0
  69. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/metrics.py +0 -0
  70. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/models.py +0 -0
  71. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/online.py +0 -0
  72. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/runner.py +0 -0
  73. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/eval/scorers.py +0 -0
  74. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/__init__.py +0 -0
  75. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/_eval_store.py +0 -0
  76. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/base.py +0 -0
  77. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/dict_exporter.py +0 -0
  78. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/json_file.py +0 -0
  79. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/mongo.py +0 -0
  80. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/postgres.py +0 -0
  81. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/exporters/query.py +0 -0
  82. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrument.py +0 -0
  83. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/__init__.py +0 -0
  84. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/_llamaindex_handler.py +0 -0
  85. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/_streaming.py +0 -0
  86. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/anthropic_inst.py +0 -0
  87. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/base.py +0 -0
  88. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/crewai_inst.py +0 -0
  89. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/gemini_inst.py +0 -0
  90. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/langchain_inst.py +0 -0
  91. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/instrumentors/llamaindex_inst.py +0 -0
  92. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/integrations/__init__.py +0 -0
  93. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/integrations/langchain.py +0 -0
  94. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/integrations/llm.py +0 -0
  95. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/middleware.py +0 -0
  96. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/models/__init__.py +0 -0
  97. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/models/score.py +0 -0
  98. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/models/trace.py +0 -0
  99. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/prompts/__init__.py +0 -0
  100. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/prompts/client.py +0 -0
  101. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/prompts/models.py +0 -0
  102. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast/serve.py +0 -0
  103. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/SOURCES.txt +0 -0
  104. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/dependency_links.txt +0 -0
  105. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/entry_points.txt +0 -0
  106. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/requires.txt +0 -0
  107. {tracecast-0.2.6 → tracecast-0.2.7}/tracecast.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tracecast
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: LLM observability SDK — framework-agnostic tracing for AI applications
5
5
  Author: Pedro Castanheira Costa
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tracecast"
7
- version = "0.2.6"
7
+ version = "0.2.7"
8
8
  description = "LLM observability SDK — framework-agnostic tracing for AI applications"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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 = "llm"
9
- TOOL = "tool"
10
- AGENT = "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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tracecast
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: LLM observability SDK — framework-agnostic tracing for AI applications
5
5
  Author: Pedro Castanheira Costa
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes