tracecast 0.2.6__tar.gz → 0.2.8__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.8}/PKG-INFO +1 -1
- {tracecast-0.2.6 → tracecast-0.2.8}/pyproject.toml +1 -1
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_cost_calculator.py +51 -1
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/__init__.py +2 -2
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/core/cost_calculator.py +26 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrument.py +18 -0
- tracecast-0.2.8/tracecast/instrumentors/openai_inst.py +351 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/models/span.py +5 -3
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast.egg-info/PKG-INFO +1 -1
- tracecast-0.2.6/tracecast/instrumentors/openai_inst.py +0 -174
- {tracecast-0.2.6 → tracecast-0.2.8}/LICENSE +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/README.md +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/setup.cfg +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_aggregator_graph.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_context_propagation.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_dashboard_mount.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_dashboard_pushdown.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_dashboard_sessions.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_decorators.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_e2e_langgraph.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_exporters_db.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_integration_frameworks.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_integration_langchain.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_json_exporter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_langchain_callback.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_llm_wrappers.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_logging.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_mongo_exporter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_prompt_dashboard.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_prompts.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_roundtrip.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_schema_v2.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_score.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_score_dashboard.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_scoring_api.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_serve.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_span.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_streaming.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_token_counter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_trace.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_trace_span.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tests/test_tracer.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/core/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/core/logger.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/core/scoring.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/core/token_counter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/core/tracer.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/aggregator.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/asgi_middleware.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/blueprint.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/eval_reader.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/prompt_reader.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/reader.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/router.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/score_reader.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/standalone.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/static/assets/index-B07hiBI2.js +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/static/assets/index-BMIlEmLS.js +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/static/assets/index-CY5YOA8F.js +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/static/assets/index-DllJJzmE.css +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/static/assets/index-Dt0awpJJ.js +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/dashboard/static/index.html +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/decorators.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/cli.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/compare.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/dataset.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/decorator.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/judge.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/metrics.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/models.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/online.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/runner.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/eval/scorers.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/exporters/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/exporters/_eval_store.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/exporters/base.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/exporters/dict_exporter.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/exporters/json_file.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/exporters/mongo.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/exporters/postgres.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/exporters/query.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/_llamaindex_handler.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/_streaming.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/anthropic_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/base.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/crewai_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/gemini_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/langchain_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/instrumentors/llamaindex_inst.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/integrations/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/integrations/langchain.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/integrations/llm.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/middleware.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/models/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/models/score.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/models/trace.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/prompts/__init__.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/prompts/client.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/prompts/models.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast/serve.py +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast.egg-info/SOURCES.txt +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast.egg-info/dependency_links.txt +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast.egg-info/entry_points.txt +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/tracecast.egg-info/requires.txt +0 -0
- {tracecast-0.2.6 → tracecast-0.2.8}/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,7 +8,7 @@ from .decorators import trace_cast, trace_span, set_default_tracer
|
|
|
8
8
|
from .core.scoring import score
|
|
9
9
|
from .prompts import create_prompt, get_prompt, set_label, PromptVersion
|
|
10
10
|
from .integrations.llm import trace_llm_call, wrap_openai, wrap_anthropic
|
|
11
|
-
from .instrument import auto_instrument
|
|
11
|
+
from .instrument import auto_instrument, instrument_openai
|
|
12
12
|
from .exporters.dict_exporter import DictExporter
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
@@ -18,5 +18,5 @@ __all__ = [
|
|
|
18
18
|
"trace_cast", "trace_span", "set_default_tracer", "score",
|
|
19
19
|
"create_prompt", "get_prompt", "set_label", "PromptVersion",
|
|
20
20
|
"trace_llm_call", "wrap_openai", "wrap_anthropic",
|
|
21
|
-
"auto_instrument", "DictExporter",
|
|
21
|
+
"auto_instrument", "instrument_openai", "DictExporter",
|
|
22
22
|
]
|
|
@@ -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"]
|
|
@@ -34,6 +34,24 @@ def auto_instrument(tracer: Optional[Tracer] = None) -> None:
|
|
|
34
34
|
_instrumented = True
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def instrument_openai(*, chat: bool = True, embeddings: bool = True, audio: bool = True) -> None:
|
|
38
|
+
"""Patch only the requested OpenAI API surfaces, independent of auto_instrument().
|
|
39
|
+
|
|
40
|
+
Use `chat=False` when chat completions are already traced through another
|
|
41
|
+
mechanism (e.g. a manually-wired LangChain callback) and you only want to
|
|
42
|
+
add embeddings/audio coverage — re-patching chat.completions in that case
|
|
43
|
+
would produce duplicate spans for every chat call.
|
|
44
|
+
"""
|
|
45
|
+
from .instrumentors.openai_inst import OpenAIInstrumentor
|
|
46
|
+
with _lock:
|
|
47
|
+
inst = _registry.get("openai")
|
|
48
|
+
if inst is None:
|
|
49
|
+
inst = OpenAIInstrumentor()
|
|
50
|
+
_registry["openai"] = inst
|
|
51
|
+
if not inst.is_patched():
|
|
52
|
+
inst.patch(chat=chat, embeddings=embeddings, audio=audio)
|
|
53
|
+
|
|
54
|
+
|
|
37
55
|
def _reset() -> None:
|
|
38
56
|
global _instrumented
|
|
39
57
|
with _lock:
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
from .base import BaseInstrumentor, active_parent_id
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _handled_by_langchain() -> bool:
|
|
9
|
+
from ..instrument import _registry
|
|
10
|
+
|
|
11
|
+
inst = _registry.get("langchain")
|
|
12
|
+
if inst is None or not inst.is_patched():
|
|
13
|
+
return False
|
|
14
|
+
frame = sys._getframe(2)
|
|
15
|
+
depth = 0
|
|
16
|
+
while frame is not None and depth < 60:
|
|
17
|
+
module = frame.f_globals.get("__name__", "")
|
|
18
|
+
if module.startswith("langchain"):
|
|
19
|
+
return True
|
|
20
|
+
frame = frame.f_back
|
|
21
|
+
depth += 1
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OpenAIInstrumentor(BaseInstrumentor):
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._original_create: Optional[Any] = None
|
|
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
|
|
33
|
+
self._patched: bool = False
|
|
34
|
+
|
|
35
|
+
def patch(self, *, chat: bool = True, embeddings: bool = True, audio: bool = True) -> None:
|
|
36
|
+
"""Patch the requested OpenAI API surfaces.
|
|
37
|
+
|
|
38
|
+
`chat=False` lets callers add embeddings/audio coverage without
|
|
39
|
+
re-patching chat.completions — needed when chat completions are
|
|
40
|
+
already traced through another mechanism (e.g. a manually-wired
|
|
41
|
+
LangChain callback), where re-patching would double-count spans.
|
|
42
|
+
"""
|
|
43
|
+
if self._patched:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if chat:
|
|
47
|
+
import openai.resources.chat.completions as mod
|
|
48
|
+
|
|
49
|
+
# Sync
|
|
50
|
+
self._original_create = mod.Completions.create
|
|
51
|
+
self_ref = self
|
|
52
|
+
_orig_create = self._original_create
|
|
53
|
+
|
|
54
|
+
def patched_create(client_self, *args, **kwargs):
|
|
55
|
+
return self_ref._intercept(client_self, args, kwargs, _orig_create)
|
|
56
|
+
|
|
57
|
+
mod.Completions.create = patched_create
|
|
58
|
+
|
|
59
|
+
# Async
|
|
60
|
+
if hasattr(mod, "AsyncCompletions"):
|
|
61
|
+
self._original_acreate = mod.AsyncCompletions.create
|
|
62
|
+
_orig_acreate = self._original_acreate
|
|
63
|
+
|
|
64
|
+
async def patched_acreate(client_self, *args, **kwargs):
|
|
65
|
+
return await self_ref._async_intercept(client_self, args, kwargs, _orig_acreate)
|
|
66
|
+
|
|
67
|
+
mod.AsyncCompletions.create = patched_acreate
|
|
68
|
+
|
|
69
|
+
if embeddings:
|
|
70
|
+
self._patch_embeddings()
|
|
71
|
+
if audio:
|
|
72
|
+
self._patch_audio()
|
|
73
|
+
|
|
74
|
+
self._patched = True
|
|
75
|
+
|
|
76
|
+
def _patch_embeddings(self) -> None:
|
|
77
|
+
try:
|
|
78
|
+
import openai.resources.embeddings as emb_mod
|
|
79
|
+
except ImportError:
|
|
80
|
+
return
|
|
81
|
+
self._original_embeddings_create = emb_mod.Embeddings.create
|
|
82
|
+
self_ref = self
|
|
83
|
+
_orig = self._original_embeddings_create
|
|
84
|
+
|
|
85
|
+
def patched(client_self, *args, **kwargs):
|
|
86
|
+
return self_ref._intercept_embeddings(client_self, args, kwargs, _orig)
|
|
87
|
+
|
|
88
|
+
emb_mod.Embeddings.create = patched
|
|
89
|
+
|
|
90
|
+
def _patch_audio(self) -> None:
|
|
91
|
+
try:
|
|
92
|
+
import openai.resources.audio.transcriptions as tr_mod
|
|
93
|
+
except ImportError:
|
|
94
|
+
tr_mod = None
|
|
95
|
+
if tr_mod is not None:
|
|
96
|
+
self._original_transcriptions_create = tr_mod.Transcriptions.create
|
|
97
|
+
self_ref = self
|
|
98
|
+
_orig = self._original_transcriptions_create
|
|
99
|
+
|
|
100
|
+
def patched_tr(client_self, *args, **kwargs):
|
|
101
|
+
return self_ref._intercept_transcription(client_self, args, kwargs, _orig)
|
|
102
|
+
|
|
103
|
+
tr_mod.Transcriptions.create = patched_tr
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
import openai.resources.audio.speech as sp_mod
|
|
107
|
+
except ImportError:
|
|
108
|
+
sp_mod = None
|
|
109
|
+
if sp_mod is not None:
|
|
110
|
+
self._original_speech_create = sp_mod.Speech.create
|
|
111
|
+
self_ref = self
|
|
112
|
+
_orig_sp = self._original_speech_create
|
|
113
|
+
|
|
114
|
+
def patched_sp(client_self, *args, **kwargs):
|
|
115
|
+
return self_ref._intercept_speech(client_self, args, kwargs, _orig_sp)
|
|
116
|
+
|
|
117
|
+
sp_mod.Speech.create = patched_sp
|
|
118
|
+
|
|
119
|
+
def unpatch(self) -> None:
|
|
120
|
+
if not self._patched:
|
|
121
|
+
return
|
|
122
|
+
if self._original_create is not None:
|
|
123
|
+
import openai.resources.chat.completions as mod
|
|
124
|
+
mod.Completions.create = self._original_create
|
|
125
|
+
if self._original_acreate is not None:
|
|
126
|
+
mod.AsyncCompletions.create = self._original_acreate
|
|
127
|
+
self._original_create = None
|
|
128
|
+
self._original_acreate = None
|
|
129
|
+
|
|
130
|
+
if self._original_embeddings_create is not None:
|
|
131
|
+
import openai.resources.embeddings as emb_mod
|
|
132
|
+
emb_mod.Embeddings.create = self._original_embeddings_create
|
|
133
|
+
self._original_embeddings_create = None
|
|
134
|
+
|
|
135
|
+
if self._original_transcriptions_create is not None:
|
|
136
|
+
import openai.resources.audio.transcriptions as tr_mod
|
|
137
|
+
tr_mod.Transcriptions.create = self._original_transcriptions_create
|
|
138
|
+
self._original_transcriptions_create = None
|
|
139
|
+
|
|
140
|
+
if self._original_speech_create is not None:
|
|
141
|
+
import openai.resources.audio.speech as sp_mod
|
|
142
|
+
sp_mod.Speech.create = self._original_speech_create
|
|
143
|
+
self._original_speech_create = None
|
|
144
|
+
|
|
145
|
+
self._patched = False
|
|
146
|
+
|
|
147
|
+
def is_patched(self) -> bool:
|
|
148
|
+
return self._patched
|
|
149
|
+
|
|
150
|
+
def _intercept(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
151
|
+
from ..core.tracer import Tracer
|
|
152
|
+
trace = Tracer.current()
|
|
153
|
+
if trace is None or _handled_by_langchain():
|
|
154
|
+
return original_fn(client_self, *args, **kwargs)
|
|
155
|
+
return self._capture(client_self, args, kwargs, original_fn, trace)
|
|
156
|
+
|
|
157
|
+
def _capture(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any, trace: Any) -> Any:
|
|
158
|
+
from ..models.span import Span, SpanType
|
|
159
|
+
from ..core.token_counter import extract_tokens, extract_content, extract_input_text
|
|
160
|
+
from ..core.cost_calculator import calculate_cost
|
|
161
|
+
|
|
162
|
+
model = kwargs.get("model", "unknown")
|
|
163
|
+
input_text = extract_input_text(kwargs, "openai")
|
|
164
|
+
|
|
165
|
+
span = Span(
|
|
166
|
+
span_id=str(uuid.uuid4()),
|
|
167
|
+
parent_span_id=active_parent_id(),
|
|
168
|
+
type=SpanType.LLM,
|
|
169
|
+
name=f"llm:{model}",
|
|
170
|
+
model=model,
|
|
171
|
+
started_at=datetime.now(timezone.utc),
|
|
172
|
+
input=input_text,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if kwargs.get("stream"):
|
|
176
|
+
from ._streaming import stream_openai
|
|
177
|
+
kwargs.setdefault("stream_options", {"include_usage": True})
|
|
178
|
+
raw = original_fn(client_self, *args, **kwargs)
|
|
179
|
+
return stream_openai(raw, span, trace, model)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
response = original_fn(client_self, *args, **kwargs)
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
185
|
+
span.mark_error(exc)
|
|
186
|
+
trace.spans.append(span)
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
190
|
+
tokens = extract_tokens(response, "openai")
|
|
191
|
+
span.tokens_in = int(tokens["input"])
|
|
192
|
+
span.tokens_out = int(tokens["output"])
|
|
193
|
+
span.tokens_in_cached = int(tokens.get("cached", 0))
|
|
194
|
+
span.cost_usd = calculate_cost(
|
|
195
|
+
model, span.tokens_in, span.tokens_out, tokens_in_cached=span.tokens_in_cached
|
|
196
|
+
)
|
|
197
|
+
span.output = extract_content(response, "openai")
|
|
198
|
+
trace.spans.append(span)
|
|
199
|
+
|
|
200
|
+
return response
|
|
201
|
+
|
|
202
|
+
async def _async_intercept(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
203
|
+
from ..core.tracer import Tracer
|
|
204
|
+
trace = Tracer.current()
|
|
205
|
+
if trace is None or _handled_by_langchain():
|
|
206
|
+
return await original_fn(client_self, *args, **kwargs)
|
|
207
|
+
return await self._async_capture(client_self, args, kwargs, original_fn, trace)
|
|
208
|
+
|
|
209
|
+
async def _async_capture(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any, trace: Any) -> Any:
|
|
210
|
+
from ..models.span import Span, SpanType
|
|
211
|
+
from ..core.token_counter import extract_tokens, extract_content, extract_input_text
|
|
212
|
+
from ..core.cost_calculator import calculate_cost
|
|
213
|
+
|
|
214
|
+
model = kwargs.get("model", "unknown")
|
|
215
|
+
input_text = extract_input_text(kwargs, "openai")
|
|
216
|
+
|
|
217
|
+
span = Span(
|
|
218
|
+
span_id=str(uuid.uuid4()),
|
|
219
|
+
parent_span_id=active_parent_id(),
|
|
220
|
+
type=SpanType.LLM,
|
|
221
|
+
name=f"llm:{model}",
|
|
222
|
+
model=model,
|
|
223
|
+
started_at=datetime.now(timezone.utc),
|
|
224
|
+
input=input_text,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if kwargs.get("stream"):
|
|
228
|
+
from ._streaming import astream_openai
|
|
229
|
+
kwargs.setdefault("stream_options", {"include_usage": True})
|
|
230
|
+
raw = await original_fn(client_self, *args, **kwargs)
|
|
231
|
+
return astream_openai(raw, span, trace, model)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
response = await original_fn(client_self, *args, **kwargs)
|
|
235
|
+
except Exception as exc:
|
|
236
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
237
|
+
span.mark_error(exc)
|
|
238
|
+
trace.spans.append(span)
|
|
239
|
+
raise
|
|
240
|
+
|
|
241
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
242
|
+
tokens = extract_tokens(response, "openai")
|
|
243
|
+
span.tokens_in = int(tokens["input"])
|
|
244
|
+
span.tokens_out = int(tokens["output"])
|
|
245
|
+
span.tokens_in_cached = int(tokens.get("cached", 0))
|
|
246
|
+
span.cost_usd = calculate_cost(
|
|
247
|
+
model, span.tokens_in, span.tokens_out, tokens_in_cached=span.tokens_in_cached
|
|
248
|
+
)
|
|
249
|
+
span.output = extract_content(response, "openai")
|
|
250
|
+
trace.spans.append(span)
|
|
251
|
+
return response
|
|
252
|
+
|
|
253
|
+
def _intercept_embeddings(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
254
|
+
from ..core.tracer import Tracer
|
|
255
|
+
trace = Tracer.current()
|
|
256
|
+
if trace is None or _handled_by_langchain():
|
|
257
|
+
return original_fn(client_self, *args, **kwargs)
|
|
258
|
+
|
|
259
|
+
from ..models.span import Span, SpanType
|
|
260
|
+
from ..core.cost_calculator import calculate_cost
|
|
261
|
+
|
|
262
|
+
model = kwargs.get("model", "unknown")
|
|
263
|
+
span = Span(
|
|
264
|
+
span_id=str(uuid.uuid4()),
|
|
265
|
+
parent_span_id=active_parent_id(),
|
|
266
|
+
type=SpanType.EMBEDDING,
|
|
267
|
+
name=f"embedding:{model}",
|
|
268
|
+
model=model,
|
|
269
|
+
started_at=datetime.now(timezone.utc),
|
|
270
|
+
)
|
|
271
|
+
try:
|
|
272
|
+
response = original_fn(client_self, *args, **kwargs)
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
275
|
+
span.mark_error(exc)
|
|
276
|
+
trace.spans.append(span)
|
|
277
|
+
raise
|
|
278
|
+
|
|
279
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
280
|
+
usage = getattr(response, "usage", None)
|
|
281
|
+
span.tokens_in = int(getattr(usage, "prompt_tokens", 0) or 0) if usage else 0
|
|
282
|
+
span.cost_usd = calculate_cost(model, span.tokens_in, 0)
|
|
283
|
+
trace.spans.append(span)
|
|
284
|
+
return response
|
|
285
|
+
|
|
286
|
+
def _intercept_transcription(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
287
|
+
from ..core.tracer import Tracer
|
|
288
|
+
trace = Tracer.current()
|
|
289
|
+
if trace is None:
|
|
290
|
+
return original_fn(client_self, *args, **kwargs)
|
|
291
|
+
|
|
292
|
+
from ..models.span import Span, SpanType
|
|
293
|
+
from ..core.cost_calculator import calculate_audio_cost
|
|
294
|
+
|
|
295
|
+
model = kwargs.get("model", "unknown")
|
|
296
|
+
span = Span(
|
|
297
|
+
span_id=str(uuid.uuid4()),
|
|
298
|
+
parent_span_id=active_parent_id(),
|
|
299
|
+
type=SpanType.AUDIO,
|
|
300
|
+
name=f"audio:{model}",
|
|
301
|
+
model=model,
|
|
302
|
+
started_at=datetime.now(timezone.utc),
|
|
303
|
+
)
|
|
304
|
+
try:
|
|
305
|
+
response = original_fn(client_self, *args, **kwargs)
|
|
306
|
+
except Exception as exc:
|
|
307
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
308
|
+
span.mark_error(exc)
|
|
309
|
+
trace.spans.append(span)
|
|
310
|
+
raise
|
|
311
|
+
|
|
312
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
313
|
+
duration = getattr(response, "duration", None)
|
|
314
|
+
if duration is not None:
|
|
315
|
+
span.metadata["duration_seconds"] = duration
|
|
316
|
+
span.cost_usd = calculate_audio_cost(model, minutes=duration / 60)
|
|
317
|
+
trace.spans.append(span)
|
|
318
|
+
return response
|
|
319
|
+
|
|
320
|
+
def _intercept_speech(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
321
|
+
from ..core.tracer import Tracer
|
|
322
|
+
trace = Tracer.current()
|
|
323
|
+
if trace is None:
|
|
324
|
+
return original_fn(client_self, *args, **kwargs)
|
|
325
|
+
|
|
326
|
+
from ..models.span import Span, SpanType
|
|
327
|
+
from ..core.cost_calculator import calculate_audio_cost
|
|
328
|
+
|
|
329
|
+
model = kwargs.get("model", "unknown")
|
|
330
|
+
char_count = len(kwargs.get("input", "") or "")
|
|
331
|
+
span = Span(
|
|
332
|
+
span_id=str(uuid.uuid4()),
|
|
333
|
+
parent_span_id=active_parent_id(),
|
|
334
|
+
type=SpanType.AUDIO,
|
|
335
|
+
name=f"audio:{model}",
|
|
336
|
+
model=model,
|
|
337
|
+
started_at=datetime.now(timezone.utc),
|
|
338
|
+
metadata={"char_count": char_count},
|
|
339
|
+
)
|
|
340
|
+
try:
|
|
341
|
+
response = original_fn(client_self, *args, **kwargs)
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
344
|
+
span.mark_error(exc)
|
|
345
|
+
trace.spans.append(span)
|
|
346
|
+
raise
|
|
347
|
+
|
|
348
|
+
span.finished_at = datetime.now(timezone.utc)
|
|
349
|
+
span.cost_usd = calculate_audio_cost(model, chars=char_count)
|
|
350
|
+
trace.spans.append(span)
|
|
351
|
+
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):
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import uuid
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
from typing import Any, Optional
|
|
5
|
-
from .base import BaseInstrumentor, active_parent_id
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _handled_by_langchain() -> bool:
|
|
9
|
-
from ..instrument import _registry
|
|
10
|
-
|
|
11
|
-
inst = _registry.get("langchain")
|
|
12
|
-
if inst is None or not inst.is_patched():
|
|
13
|
-
return False
|
|
14
|
-
frame = sys._getframe(2)
|
|
15
|
-
depth = 0
|
|
16
|
-
while frame is not None and depth < 60:
|
|
17
|
-
module = frame.f_globals.get("__name__", "")
|
|
18
|
-
if module.startswith("langchain"):
|
|
19
|
-
return True
|
|
20
|
-
frame = frame.f_back
|
|
21
|
-
depth += 1
|
|
22
|
-
return False
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class OpenAIInstrumentor(BaseInstrumentor):
|
|
26
|
-
|
|
27
|
-
def __init__(self) -> None:
|
|
28
|
-
self._original_create: Optional[Any] = None
|
|
29
|
-
self._original_acreate: Optional[Any] = None
|
|
30
|
-
self._patched: bool = False
|
|
31
|
-
|
|
32
|
-
def patch(self) -> None:
|
|
33
|
-
if self._patched:
|
|
34
|
-
return
|
|
35
|
-
import openai.resources.chat.completions as mod
|
|
36
|
-
|
|
37
|
-
# Sync
|
|
38
|
-
self._original_create = mod.Completions.create
|
|
39
|
-
self_ref = self
|
|
40
|
-
_orig_create = self._original_create
|
|
41
|
-
|
|
42
|
-
def patched_create(client_self, *args, **kwargs):
|
|
43
|
-
return self_ref._intercept(client_self, args, kwargs, _orig_create)
|
|
44
|
-
|
|
45
|
-
mod.Completions.create = patched_create
|
|
46
|
-
|
|
47
|
-
# Async
|
|
48
|
-
if hasattr(mod, "AsyncCompletions"):
|
|
49
|
-
self._original_acreate = mod.AsyncCompletions.create
|
|
50
|
-
_orig_acreate = self._original_acreate
|
|
51
|
-
|
|
52
|
-
async def patched_acreate(client_self, *args, **kwargs):
|
|
53
|
-
return await self_ref._async_intercept(client_self, args, kwargs, _orig_acreate)
|
|
54
|
-
|
|
55
|
-
mod.AsyncCompletions.create = patched_acreate
|
|
56
|
-
|
|
57
|
-
self._patched = True
|
|
58
|
-
|
|
59
|
-
def unpatch(self) -> None:
|
|
60
|
-
if not self._patched:
|
|
61
|
-
return
|
|
62
|
-
import openai.resources.chat.completions as mod
|
|
63
|
-
mod.Completions.create = self._original_create
|
|
64
|
-
if self._original_acreate is not None:
|
|
65
|
-
mod.AsyncCompletions.create = self._original_acreate
|
|
66
|
-
self._original_create = None
|
|
67
|
-
self._original_acreate = None
|
|
68
|
-
self._patched = False
|
|
69
|
-
|
|
70
|
-
def is_patched(self) -> bool:
|
|
71
|
-
return self._patched
|
|
72
|
-
|
|
73
|
-
def _intercept(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
74
|
-
from ..core.tracer import Tracer
|
|
75
|
-
trace = Tracer.current()
|
|
76
|
-
if trace is None or _handled_by_langchain():
|
|
77
|
-
return original_fn(client_self, *args, **kwargs)
|
|
78
|
-
return self._capture(client_self, args, kwargs, original_fn, trace)
|
|
79
|
-
|
|
80
|
-
def _capture(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any, trace: Any) -> Any:
|
|
81
|
-
from ..models.span import Span, SpanType
|
|
82
|
-
from ..core.token_counter import extract_tokens, extract_content, extract_input_text
|
|
83
|
-
from ..core.cost_calculator import calculate_cost
|
|
84
|
-
|
|
85
|
-
model = kwargs.get("model", "unknown")
|
|
86
|
-
input_text = extract_input_text(kwargs, "openai")
|
|
87
|
-
|
|
88
|
-
span = Span(
|
|
89
|
-
span_id=str(uuid.uuid4()),
|
|
90
|
-
parent_span_id=active_parent_id(),
|
|
91
|
-
type=SpanType.LLM,
|
|
92
|
-
name=f"llm:{model}",
|
|
93
|
-
model=model,
|
|
94
|
-
started_at=datetime.now(timezone.utc),
|
|
95
|
-
input=input_text,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
if kwargs.get("stream"):
|
|
99
|
-
from ._streaming import stream_openai
|
|
100
|
-
kwargs.setdefault("stream_options", {"include_usage": True})
|
|
101
|
-
raw = original_fn(client_self, *args, **kwargs)
|
|
102
|
-
return stream_openai(raw, span, trace, model)
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
response = original_fn(client_self, *args, **kwargs)
|
|
106
|
-
except Exception as exc:
|
|
107
|
-
span.finished_at = datetime.now(timezone.utc)
|
|
108
|
-
span.mark_error(exc)
|
|
109
|
-
trace.spans.append(span)
|
|
110
|
-
raise
|
|
111
|
-
|
|
112
|
-
span.finished_at = datetime.now(timezone.utc)
|
|
113
|
-
tokens = extract_tokens(response, "openai")
|
|
114
|
-
span.tokens_in = int(tokens["input"])
|
|
115
|
-
span.tokens_out = int(tokens["output"])
|
|
116
|
-
span.tokens_in_cached = int(tokens.get("cached", 0))
|
|
117
|
-
span.cost_usd = calculate_cost(
|
|
118
|
-
model, span.tokens_in, span.tokens_out, tokens_in_cached=span.tokens_in_cached
|
|
119
|
-
)
|
|
120
|
-
span.output = extract_content(response, "openai")
|
|
121
|
-
trace.spans.append(span)
|
|
122
|
-
|
|
123
|
-
return response
|
|
124
|
-
|
|
125
|
-
async def _async_intercept(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any) -> Any:
|
|
126
|
-
from ..core.tracer import Tracer
|
|
127
|
-
trace = Tracer.current()
|
|
128
|
-
if trace is None or _handled_by_langchain():
|
|
129
|
-
return await original_fn(client_self, *args, **kwargs)
|
|
130
|
-
return await self._async_capture(client_self, args, kwargs, original_fn, trace)
|
|
131
|
-
|
|
132
|
-
async def _async_capture(self, client_self: Any, args: tuple, kwargs: dict, original_fn: Any, trace: Any) -> Any:
|
|
133
|
-
from ..models.span import Span, SpanType
|
|
134
|
-
from ..core.token_counter import extract_tokens, extract_content, extract_input_text
|
|
135
|
-
from ..core.cost_calculator import calculate_cost
|
|
136
|
-
|
|
137
|
-
model = kwargs.get("model", "unknown")
|
|
138
|
-
input_text = extract_input_text(kwargs, "openai")
|
|
139
|
-
|
|
140
|
-
span = Span(
|
|
141
|
-
span_id=str(uuid.uuid4()),
|
|
142
|
-
parent_span_id=active_parent_id(),
|
|
143
|
-
type=SpanType.LLM,
|
|
144
|
-
name=f"llm:{model}",
|
|
145
|
-
model=model,
|
|
146
|
-
started_at=datetime.now(timezone.utc),
|
|
147
|
-
input=input_text,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
if kwargs.get("stream"):
|
|
151
|
-
from ._streaming import astream_openai
|
|
152
|
-
kwargs.setdefault("stream_options", {"include_usage": True})
|
|
153
|
-
raw = await original_fn(client_self, *args, **kwargs)
|
|
154
|
-
return astream_openai(raw, span, trace, model)
|
|
155
|
-
|
|
156
|
-
try:
|
|
157
|
-
response = await original_fn(client_self, *args, **kwargs)
|
|
158
|
-
except Exception as exc:
|
|
159
|
-
span.finished_at = datetime.now(timezone.utc)
|
|
160
|
-
span.mark_error(exc)
|
|
161
|
-
trace.spans.append(span)
|
|
162
|
-
raise
|
|
163
|
-
|
|
164
|
-
span.finished_at = datetime.now(timezone.utc)
|
|
165
|
-
tokens = extract_tokens(response, "openai")
|
|
166
|
-
span.tokens_in = int(tokens["input"])
|
|
167
|
-
span.tokens_out = int(tokens["output"])
|
|
168
|
-
span.tokens_in_cached = int(tokens.get("cached", 0))
|
|
169
|
-
span.cost_usd = calculate_cost(
|
|
170
|
-
model, span.tokens_in, span.tokens_out, tokens_in_cached=span.tokens_in_cached
|
|
171
|
-
)
|
|
172
|
-
span.output = extract_content(response, "openai")
|
|
173
|
-
trace.spans.append(span)
|
|
174
|
-
return response
|
|
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
|