opentelemetry-instrumentation-openai 0.34.1__py3-none-any.whl → 0.49.3__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.
Potentially problematic release.
This version of opentelemetry-instrumentation-openai might be problematic. Click here for more details.
- opentelemetry/instrumentation/openai/__init__.py +11 -6
- opentelemetry/instrumentation/openai/shared/__init__.py +167 -68
- opentelemetry/instrumentation/openai/shared/chat_wrappers.py +544 -231
- opentelemetry/instrumentation/openai/shared/completion_wrappers.py +143 -81
- opentelemetry/instrumentation/openai/shared/config.py +8 -3
- opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +91 -30
- opentelemetry/instrumentation/openai/shared/event_emitter.py +108 -0
- opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +1 -1
- opentelemetry/instrumentation/openai/shared/span_utils.py +0 -0
- opentelemetry/instrumentation/openai/utils.py +42 -9
- opentelemetry/instrumentation/openai/v0/__init__.py +32 -11
- opentelemetry/instrumentation/openai/v1/__init__.py +177 -69
- opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +208 -109
- opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +41 -19
- opentelemetry/instrumentation/openai/v1/responses_wrappers.py +1073 -0
- opentelemetry/instrumentation/openai/version.py +1 -1
- {opentelemetry_instrumentation_openai-0.34.1.dist-info → opentelemetry_instrumentation_openai-0.49.3.dist-info}/METADATA +7 -8
- opentelemetry_instrumentation_openai-0.49.3.dist-info/RECORD +21 -0
- {opentelemetry_instrumentation_openai-0.34.1.dist-info → opentelemetry_instrumentation_openai-0.49.3.dist-info}/WHEEL +1 -1
- opentelemetry_instrumentation_openai-0.34.1.dist-info/RECORD +0 -17
- {opentelemetry_instrumentation_openai-0.34.1.dist-info → opentelemetry_instrumentation_openai-0.49.3.dist-info}/entry_points.txt +0 -0
|
@@ -1,37 +1,44 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
from opentelemetry import context as context_api
|
|
4
|
-
|
|
5
|
-
from opentelemetry.semconv_ai import (
|
|
6
|
-
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
|
|
7
|
-
SpanAttributes,
|
|
8
|
-
LLMRequestTypeValues,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
|
12
|
-
from opentelemetry.instrumentation.openai.utils import _with_tracer_wrapper, dont_throw
|
|
4
|
+
from opentelemetry import trace
|
|
13
5
|
from opentelemetry.instrumentation.openai.shared import (
|
|
14
6
|
_set_client_attributes,
|
|
15
|
-
_set_request_attributes,
|
|
16
|
-
_set_span_attribute,
|
|
17
7
|
_set_functions_attributes,
|
|
8
|
+
_set_request_attributes,
|
|
18
9
|
_set_response_attributes,
|
|
10
|
+
_set_span_attribute,
|
|
11
|
+
_set_span_stream_usage,
|
|
19
12
|
is_streaming_response,
|
|
20
|
-
should_send_prompts,
|
|
21
13
|
model_as_dict,
|
|
22
|
-
should_record_stream_token_usage,
|
|
23
|
-
get_token_count_from_string,
|
|
24
|
-
_set_span_stream_usage,
|
|
25
14
|
propagate_trace_context,
|
|
26
15
|
)
|
|
27
|
-
|
|
28
|
-
from opentelemetry.
|
|
29
|
-
|
|
16
|
+
from opentelemetry.instrumentation.openai.shared.config import Config
|
|
17
|
+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
|
18
|
+
from opentelemetry.instrumentation.openai.shared.event_emitter import emit_event
|
|
19
|
+
from opentelemetry.instrumentation.openai.shared.event_models import (
|
|
20
|
+
ChoiceEvent,
|
|
21
|
+
MessageEvent,
|
|
22
|
+
)
|
|
23
|
+
from opentelemetry.instrumentation.openai.utils import (
|
|
24
|
+
_with_tracer_wrapper,
|
|
25
|
+
dont_throw,
|
|
26
|
+
is_openai_v1,
|
|
27
|
+
should_emit_events,
|
|
28
|
+
should_send_prompts,
|
|
29
|
+
)
|
|
30
|
+
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
|
31
|
+
from opentelemetry.semconv._incubating.attributes import (
|
|
32
|
+
gen_ai_attributes as GenAIAttributes,
|
|
33
|
+
)
|
|
34
|
+
from opentelemetry.semconv_ai import (
|
|
35
|
+
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
|
|
36
|
+
LLMRequestTypeValues,
|
|
37
|
+
SpanAttributes,
|
|
38
|
+
)
|
|
30
39
|
from opentelemetry.trace import SpanKind
|
|
31
40
|
from opentelemetry.trace.status import Status, StatusCode
|
|
32
41
|
|
|
33
|
-
from opentelemetry.instrumentation.openai.shared.config import Config
|
|
34
|
-
|
|
35
42
|
SPAN_NAME = "openai.completion"
|
|
36
43
|
LLM_REQUEST_TYPE = LLMRequestTypeValues.COMPLETION
|
|
37
44
|
|
|
@@ -52,17 +59,27 @@ def completion_wrapper(tracer, wrapped, instance, args, kwargs):
|
|
|
52
59
|
attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
|
|
53
60
|
)
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
# Use the span as current context to ensure events get proper trace context
|
|
63
|
+
with trace.use_span(span, end_on_exit=False):
|
|
64
|
+
_handle_request(span, kwargs, instance)
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
try:
|
|
67
|
+
response = wrapped(*args, **kwargs)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
|
70
|
+
span.record_exception(e)
|
|
71
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
72
|
+
span.end()
|
|
73
|
+
raise
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
if is_streaming_response(response):
|
|
76
|
+
# span will be closed after the generator is done
|
|
77
|
+
return _build_from_streaming_response(span, kwargs, response)
|
|
78
|
+
else:
|
|
79
|
+
_handle_response(response, span, instance)
|
|
80
|
+
|
|
81
|
+
span.end()
|
|
82
|
+
return response
|
|
66
83
|
|
|
67
84
|
|
|
68
85
|
@_with_tracer_wrapper
|
|
@@ -78,41 +95,66 @@ async def acompletion_wrapper(tracer, wrapped, instance, args, kwargs):
|
|
|
78
95
|
attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
|
|
79
96
|
)
|
|
80
97
|
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
# Use the span as current context to ensure events get proper trace context
|
|
99
|
+
with trace.use_span(span, end_on_exit=False):
|
|
100
|
+
_handle_request(span, kwargs, instance)
|
|
83
101
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
try:
|
|
103
|
+
response = await wrapped(*args, **kwargs)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
|
106
|
+
span.record_exception(e)
|
|
107
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
108
|
+
span.end()
|
|
109
|
+
raise
|
|
89
110
|
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
if is_streaming_response(response):
|
|
112
|
+
# span will be closed after the generator is done
|
|
113
|
+
return _abuild_from_streaming_response(span, kwargs, response)
|
|
114
|
+
else:
|
|
115
|
+
_handle_response(response, span, instance)
|
|
116
|
+
|
|
117
|
+
span.end()
|
|
118
|
+
return response
|
|
92
119
|
|
|
93
120
|
|
|
94
121
|
@dont_throw
|
|
95
122
|
def _handle_request(span, kwargs, instance):
|
|
96
|
-
_set_request_attributes(span, kwargs)
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
_set_request_attributes(span, kwargs, instance)
|
|
124
|
+
if should_emit_events():
|
|
125
|
+
_emit_prompts_events(kwargs)
|
|
126
|
+
else:
|
|
127
|
+
if should_send_prompts():
|
|
128
|
+
_set_prompts(span, kwargs.get("prompt"))
|
|
129
|
+
_set_functions_attributes(span, kwargs.get("functions"))
|
|
100
130
|
_set_client_attributes(span, instance)
|
|
101
131
|
if Config.enable_trace_context_propagation:
|
|
102
132
|
propagate_trace_context(span, kwargs)
|
|
103
133
|
|
|
104
134
|
|
|
135
|
+
def _emit_prompts_events(kwargs):
|
|
136
|
+
prompt = kwargs.get("prompt")
|
|
137
|
+
if isinstance(prompt, list):
|
|
138
|
+
for p in prompt:
|
|
139
|
+
emit_event(MessageEvent(content=p))
|
|
140
|
+
elif isinstance(prompt, str):
|
|
141
|
+
emit_event(MessageEvent(content=prompt))
|
|
142
|
+
|
|
143
|
+
|
|
105
144
|
@dont_throw
|
|
106
|
-
def _handle_response(response, span):
|
|
145
|
+
def _handle_response(response, span, instance=None):
|
|
107
146
|
if is_openai_v1():
|
|
108
147
|
response_dict = model_as_dict(response)
|
|
109
148
|
else:
|
|
110
149
|
response_dict = response
|
|
111
150
|
|
|
112
151
|
_set_response_attributes(span, response_dict)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
if should_emit_events():
|
|
153
|
+
for choice in response.choices:
|
|
154
|
+
emit_event(_parse_choice_event(choice))
|
|
155
|
+
else:
|
|
156
|
+
if should_send_prompts():
|
|
157
|
+
_set_completions(span, response_dict.get("choices"))
|
|
116
158
|
|
|
117
159
|
|
|
118
160
|
def _set_prompts(span, prompt):
|
|
@@ -121,7 +163,7 @@ def _set_prompts(span, prompt):
|
|
|
121
163
|
|
|
122
164
|
_set_span_attribute(
|
|
123
165
|
span,
|
|
124
|
-
f"{
|
|
166
|
+
f"{GenAIAttributes.GEN_AI_PROMPT}.0.user",
|
|
125
167
|
prompt[0] if isinstance(prompt, list) else prompt,
|
|
126
168
|
)
|
|
127
169
|
|
|
@@ -133,7 +175,7 @@ def _set_completions(span, choices):
|
|
|
133
175
|
|
|
134
176
|
for choice in choices:
|
|
135
177
|
index = choice.get("index")
|
|
136
|
-
prefix = f"{
|
|
178
|
+
prefix = f"{GenAIAttributes.GEN_AI_COMPLETION}.{index}"
|
|
137
179
|
_set_span_attribute(
|
|
138
180
|
span, f"{prefix}.finish_reason", choice.get("finish_reason")
|
|
139
181
|
)
|
|
@@ -142,7 +184,7 @@ def _set_completions(span, choices):
|
|
|
142
184
|
|
|
143
185
|
@dont_throw
|
|
144
186
|
def _build_from_streaming_response(span, request_kwargs, response):
|
|
145
|
-
complete_response = {"choices": [], "model": ""}
|
|
187
|
+
complete_response = {"choices": [], "model": "", "id": ""}
|
|
146
188
|
for item in response:
|
|
147
189
|
yield item
|
|
148
190
|
_accumulate_streaming_response(complete_response, item)
|
|
@@ -151,8 +193,11 @@ def _build_from_streaming_response(span, request_kwargs, response):
|
|
|
151
193
|
|
|
152
194
|
_set_token_usage(span, request_kwargs, complete_response)
|
|
153
195
|
|
|
154
|
-
if
|
|
155
|
-
|
|
196
|
+
if should_emit_events():
|
|
197
|
+
_emit_streaming_response_events(complete_response)
|
|
198
|
+
else:
|
|
199
|
+
if should_send_prompts():
|
|
200
|
+
_set_completions(span, complete_response.get("choices"))
|
|
156
201
|
|
|
157
202
|
span.set_status(Status(StatusCode.OK))
|
|
158
203
|
span.end()
|
|
@@ -160,7 +205,7 @@ def _build_from_streaming_response(span, request_kwargs, response):
|
|
|
160
205
|
|
|
161
206
|
@dont_throw
|
|
162
207
|
async def _abuild_from_streaming_response(span, request_kwargs, response):
|
|
163
|
-
complete_response = {"choices": [], "model": ""}
|
|
208
|
+
complete_response = {"choices": [], "model": "", "id": ""}
|
|
164
209
|
async for item in response:
|
|
165
210
|
yield item
|
|
166
211
|
_accumulate_streaming_response(complete_response, item)
|
|
@@ -169,44 +214,42 @@ async def _abuild_from_streaming_response(span, request_kwargs, response):
|
|
|
169
214
|
|
|
170
215
|
_set_token_usage(span, request_kwargs, complete_response)
|
|
171
216
|
|
|
172
|
-
if
|
|
173
|
-
|
|
217
|
+
if should_emit_events():
|
|
218
|
+
_emit_streaming_response_events(complete_response)
|
|
219
|
+
else:
|
|
220
|
+
if should_send_prompts():
|
|
221
|
+
_set_completions(span, complete_response.get("choices"))
|
|
174
222
|
|
|
175
223
|
span.set_status(Status(StatusCode.OK))
|
|
176
224
|
span.end()
|
|
177
225
|
|
|
178
226
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
prompt_content = request_kwargs.get("prompt")
|
|
189
|
-
model_name = complete_response.get("model") or None
|
|
190
|
-
|
|
191
|
-
if model_name:
|
|
192
|
-
prompt_usage = get_token_count_from_string(prompt_content, model_name)
|
|
227
|
+
def _emit_streaming_response_events(complete_response):
|
|
228
|
+
for i, choice in enumerate(complete_response["choices"]):
|
|
229
|
+
emit_event(
|
|
230
|
+
ChoiceEvent(
|
|
231
|
+
index=choice.get("index", i),
|
|
232
|
+
message={"content": choice.get("text"), "role": "assistant"},
|
|
233
|
+
finish_reason=choice.get("finish_reason", "unknown"),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
193
236
|
|
|
194
|
-
# completion_usage
|
|
195
|
-
if complete_response.get("choices"):
|
|
196
|
-
completion_content = ""
|
|
197
|
-
model_name = complete_response.get("model") or None
|
|
198
237
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
238
|
+
@dont_throw
|
|
239
|
+
def _set_token_usage(span, request_kwargs, complete_response):
|
|
240
|
+
prompt_usage = -1
|
|
241
|
+
completion_usage = -1
|
|
202
242
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
243
|
+
# Use token usage from API response only
|
|
244
|
+
if complete_response.get("usage"):
|
|
245
|
+
usage = complete_response["usage"]
|
|
246
|
+
if usage.get("prompt_tokens"):
|
|
247
|
+
prompt_usage = usage["prompt_tokens"]
|
|
248
|
+
if usage.get("completion_tokens"):
|
|
249
|
+
completion_usage = usage["completion_tokens"]
|
|
207
250
|
|
|
208
|
-
|
|
209
|
-
|
|
251
|
+
# span record
|
|
252
|
+
_set_span_stream_usage(span, prompt_usage, completion_usage)
|
|
210
253
|
|
|
211
254
|
|
|
212
255
|
@dont_throw
|
|
@@ -215,6 +258,11 @@ def _accumulate_streaming_response(complete_response, item):
|
|
|
215
258
|
item = model_as_dict(item)
|
|
216
259
|
|
|
217
260
|
complete_response["model"] = item.get("model")
|
|
261
|
+
complete_response["id"] = item.get("id")
|
|
262
|
+
|
|
263
|
+
# capture usage information from the stream chunks
|
|
264
|
+
if item.get("usage"):
|
|
265
|
+
complete_response["usage"] = item.get("usage")
|
|
218
266
|
|
|
219
267
|
for choice in item.get("choices"):
|
|
220
268
|
index = choice.get("index")
|
|
@@ -228,3 +276,17 @@ def _accumulate_streaming_response(complete_response, item):
|
|
|
228
276
|
complete_choice["text"] += choice.get("text")
|
|
229
277
|
|
|
230
278
|
return complete_response
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _parse_choice_event(choice) -> ChoiceEvent:
|
|
282
|
+
has_message = choice.text is not None
|
|
283
|
+
has_finish_reason = choice.finish_reason is not None
|
|
284
|
+
|
|
285
|
+
content = choice.text if has_message else None
|
|
286
|
+
finish_reason = choice.finish_reason if has_finish_reason else "unknown"
|
|
287
|
+
|
|
288
|
+
return ChoiceEvent(
|
|
289
|
+
index=choice.index,
|
|
290
|
+
message={"content": content, "role": "assistant"},
|
|
291
|
+
finish_reason=finish_reason,
|
|
292
|
+
)
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
from typing import Callable
|
|
1
|
+
from typing import Callable, Optional
|
|
2
|
+
|
|
3
|
+
from opentelemetry._logs import Logger
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class Config:
|
|
5
|
-
enrich_token_usage = False
|
|
6
7
|
enrich_assistant = False
|
|
7
8
|
exception_logger = None
|
|
8
9
|
get_common_metrics_attributes: Callable[[], dict] = lambda: {}
|
|
9
|
-
upload_base64_image: Callable[[str, str, str], str] =
|
|
10
|
+
upload_base64_image: Callable[[str, str, str, str], str] = (
|
|
11
|
+
lambda trace_id, span_id, image_name, base64_string: str
|
|
12
|
+
)
|
|
10
13
|
enable_trace_context_propagation: bool = True
|
|
14
|
+
use_legacy_attributes = True
|
|
15
|
+
event_logger: Optional[Logger] = None
|
|
@@ -1,39 +1,49 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
|
+
from collections.abc import Iterable
|
|
3
4
|
|
|
4
5
|
from opentelemetry import context as context_api
|
|
5
|
-
from opentelemetry.metrics import Counter, Histogram
|
|
6
|
-
from opentelemetry.semconv_ai import (
|
|
7
|
-
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
|
|
8
|
-
SpanAttributes,
|
|
9
|
-
LLMRequestTypeValues,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
|
13
|
-
from opentelemetry.instrumentation.openai.utils import (
|
|
14
|
-
dont_throw,
|
|
15
|
-
start_as_current_span_async,
|
|
16
|
-
_with_embeddings_telemetry_wrapper,
|
|
17
|
-
)
|
|
18
6
|
from opentelemetry.instrumentation.openai.shared import (
|
|
19
|
-
|
|
7
|
+
OPENAI_LLM_USAGE_TOKEN_TYPES,
|
|
8
|
+
_get_openai_base_url,
|
|
20
9
|
_set_client_attributes,
|
|
21
10
|
_set_request_attributes,
|
|
22
|
-
_set_span_attribute,
|
|
23
11
|
_set_response_attributes,
|
|
12
|
+
_set_span_attribute,
|
|
24
13
|
_token_type,
|
|
25
|
-
|
|
14
|
+
metric_shared_attributes,
|
|
26
15
|
model_as_dict,
|
|
27
|
-
_get_openai_base_url,
|
|
28
|
-
OPENAI_LLM_USAGE_TOKEN_TYPES,
|
|
29
16
|
propagate_trace_context,
|
|
30
17
|
)
|
|
31
|
-
|
|
32
18
|
from opentelemetry.instrumentation.openai.shared.config import Config
|
|
19
|
+
from opentelemetry.instrumentation.openai.shared.event_emitter import emit_event
|
|
20
|
+
from opentelemetry.instrumentation.openai.shared.event_models import (
|
|
21
|
+
ChoiceEvent,
|
|
22
|
+
MessageEvent,
|
|
23
|
+
)
|
|
24
|
+
from opentelemetry.instrumentation.openai.utils import (
|
|
25
|
+
_with_embeddings_telemetry_wrapper,
|
|
26
|
+
dont_throw,
|
|
27
|
+
is_openai_v1,
|
|
28
|
+
should_emit_events,
|
|
29
|
+
should_send_prompts,
|
|
30
|
+
start_as_current_span_async,
|
|
31
|
+
)
|
|
32
|
+
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
|
33
|
+
from opentelemetry.metrics import Counter, Histogram
|
|
34
|
+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
|
35
|
+
from opentelemetry.semconv._incubating.attributes import (
|
|
36
|
+
gen_ai_attributes as GenAIAttributes,
|
|
37
|
+
)
|
|
38
|
+
from opentelemetry.semconv_ai import (
|
|
39
|
+
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
|
|
40
|
+
LLMRequestTypeValues,
|
|
41
|
+
SpanAttributes,
|
|
42
|
+
)
|
|
43
|
+
from opentelemetry.trace import SpanKind, Status, StatusCode
|
|
33
44
|
|
|
34
|
-
from
|
|
35
|
-
|
|
36
|
-
from opentelemetry.trace import SpanKind
|
|
45
|
+
from openai._legacy_response import LegacyAPIResponse
|
|
46
|
+
from openai.types.create_embedding_response import CreateEmbeddingResponse
|
|
37
47
|
|
|
38
48
|
SPAN_NAME = "openai.embeddings"
|
|
39
49
|
LLM_REQUEST_TYPE = LLMRequestTypeValues.EMBEDDING
|
|
@@ -83,7 +93,12 @@ def embeddings_wrapper(
|
|
|
83
93
|
if exception_counter:
|
|
84
94
|
exception_counter.add(1, attributes=attributes)
|
|
85
95
|
|
|
86
|
-
|
|
96
|
+
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
|
97
|
+
span.record_exception(e)
|
|
98
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
99
|
+
span.end()
|
|
100
|
+
|
|
101
|
+
raise
|
|
87
102
|
|
|
88
103
|
duration = end_time - start_time
|
|
89
104
|
|
|
@@ -124,6 +139,7 @@ async def aembeddings_wrapper(
|
|
|
124
139
|
attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
|
|
125
140
|
) as span:
|
|
126
141
|
_handle_request(span, kwargs, instance)
|
|
142
|
+
|
|
127
143
|
try:
|
|
128
144
|
# record time for duration
|
|
129
145
|
start_time = time.time()
|
|
@@ -142,9 +158,15 @@ async def aembeddings_wrapper(
|
|
|
142
158
|
if exception_counter:
|
|
143
159
|
exception_counter.add(1, attributes=attributes)
|
|
144
160
|
|
|
145
|
-
|
|
161
|
+
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
|
162
|
+
span.record_exception(e)
|
|
163
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
164
|
+
span.end()
|
|
165
|
+
|
|
166
|
+
raise
|
|
146
167
|
|
|
147
168
|
duration = end_time - start_time
|
|
169
|
+
|
|
148
170
|
_handle_response(
|
|
149
171
|
response,
|
|
150
172
|
span,
|
|
@@ -160,10 +182,16 @@ async def aembeddings_wrapper(
|
|
|
160
182
|
|
|
161
183
|
@dont_throw
|
|
162
184
|
def _handle_request(span, kwargs, instance):
|
|
163
|
-
_set_request_attributes(span, kwargs)
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
_set_request_attributes(span, kwargs, instance)
|
|
186
|
+
|
|
187
|
+
if should_emit_events():
|
|
188
|
+
_emit_embeddings_message_event(kwargs.get("input"))
|
|
189
|
+
else:
|
|
190
|
+
if should_send_prompts():
|
|
191
|
+
_set_prompts(span, kwargs.get("input"))
|
|
192
|
+
|
|
166
193
|
_set_client_attributes(span, instance)
|
|
194
|
+
|
|
167
195
|
if Config.enable_trace_context_propagation:
|
|
168
196
|
propagate_trace_context(span, kwargs)
|
|
169
197
|
|
|
@@ -194,6 +222,10 @@ def _handle_response(
|
|
|
194
222
|
# span attributes
|
|
195
223
|
_set_response_attributes(span, response_dict)
|
|
196
224
|
|
|
225
|
+
# emit events
|
|
226
|
+
if should_emit_events():
|
|
227
|
+
_emit_embeddings_choice_event(response)
|
|
228
|
+
|
|
197
229
|
|
|
198
230
|
def _set_embeddings_metrics(
|
|
199
231
|
instance,
|
|
@@ -219,7 +251,7 @@ def _set_embeddings_metrics(
|
|
|
219
251
|
continue
|
|
220
252
|
attributes_with_token_type = {
|
|
221
253
|
**shared_attributes,
|
|
222
|
-
|
|
254
|
+
GenAIAttributes.GEN_AI_TOKEN_TYPE: _token_type(name),
|
|
223
255
|
}
|
|
224
256
|
token_counter.record(val, attributes=attributes_with_token_type)
|
|
225
257
|
|
|
@@ -241,10 +273,39 @@ def _set_prompts(span, prompt):
|
|
|
241
273
|
|
|
242
274
|
if isinstance(prompt, list):
|
|
243
275
|
for i, p in enumerate(prompt):
|
|
244
|
-
_set_span_attribute(span, f"{
|
|
276
|
+
_set_span_attribute(span, f"{GenAIAttributes.GEN_AI_PROMPT}.{i}.content", p)
|
|
245
277
|
else:
|
|
246
278
|
_set_span_attribute(
|
|
247
279
|
span,
|
|
248
|
-
f"{
|
|
280
|
+
f"{GenAIAttributes.GEN_AI_PROMPT}.0.content",
|
|
249
281
|
prompt,
|
|
250
282
|
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _emit_embeddings_message_event(embeddings) -> None:
|
|
286
|
+
if isinstance(embeddings, str):
|
|
287
|
+
emit_event(MessageEvent(content=embeddings))
|
|
288
|
+
elif isinstance(embeddings, Iterable):
|
|
289
|
+
for i in embeddings:
|
|
290
|
+
emit_event(MessageEvent(content=i))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _emit_embeddings_choice_event(response) -> None:
|
|
294
|
+
if isinstance(response, CreateEmbeddingResponse):
|
|
295
|
+
for embedding in response.data:
|
|
296
|
+
emit_event(
|
|
297
|
+
ChoiceEvent(
|
|
298
|
+
index=embedding.index,
|
|
299
|
+
message={"content": embedding.embedding, "role": "assistant"},
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
elif isinstance(response, LegacyAPIResponse):
|
|
304
|
+
parsed_response = response.parse()
|
|
305
|
+
for embedding in parsed_response.data:
|
|
306
|
+
emit_event(
|
|
307
|
+
ChoiceEvent(
|
|
308
|
+
index=embedding.index,
|
|
309
|
+
message={"content": embedding.embedding, "role": "assistant"},
|
|
310
|
+
)
|
|
311
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from opentelemetry._logs import LogRecord
|
|
6
|
+
from opentelemetry.instrumentation.openai.shared.event_models import (
|
|
7
|
+
ChoiceEvent,
|
|
8
|
+
MessageEvent,
|
|
9
|
+
)
|
|
10
|
+
from opentelemetry.instrumentation.openai.utils import (
|
|
11
|
+
should_emit_events,
|
|
12
|
+
should_send_prompts,
|
|
13
|
+
)
|
|
14
|
+
from opentelemetry.semconv._incubating.attributes import (
|
|
15
|
+
gen_ai_attributes as GenAIAttributes,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .config import Config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Roles(Enum):
|
|
22
|
+
USER = "user"
|
|
23
|
+
ASSISTANT = "assistant"
|
|
24
|
+
SYSTEM = "system"
|
|
25
|
+
TOOL = "tool"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
VALID_MESSAGE_ROLES = {role.value for role in Roles}
|
|
29
|
+
"""The valid roles for naming the message event."""
|
|
30
|
+
|
|
31
|
+
EVENT_ATTRIBUTES = {
|
|
32
|
+
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value
|
|
33
|
+
}
|
|
34
|
+
"""The attributes to be used for the event."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def emit_event(event: Union[MessageEvent, ChoiceEvent]) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Emit an event to the OpenTelemetry SDK.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
event: The event to emit.
|
|
43
|
+
"""
|
|
44
|
+
if not should_emit_events():
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
if isinstance(event, MessageEvent):
|
|
48
|
+
_emit_message_event(event)
|
|
49
|
+
elif isinstance(event, ChoiceEvent):
|
|
50
|
+
_emit_choice_event(event)
|
|
51
|
+
else:
|
|
52
|
+
raise TypeError("Unsupported event type")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _emit_message_event(event: MessageEvent) -> None:
|
|
56
|
+
body = asdict(event)
|
|
57
|
+
|
|
58
|
+
if event.role in VALID_MESSAGE_ROLES:
|
|
59
|
+
name = "gen_ai.{}.message".format(event.role)
|
|
60
|
+
# According to the semantic conventions, the role is conditionally required if available
|
|
61
|
+
# and not equal to the "role" in the message name. So, remove the role from the body if
|
|
62
|
+
# it is the same as the in the event name.
|
|
63
|
+
body.pop("role", None)
|
|
64
|
+
else:
|
|
65
|
+
name = "gen_ai.user.message"
|
|
66
|
+
|
|
67
|
+
# According to the semantic conventions, only the assistant role has tool call
|
|
68
|
+
if event.role != Roles.ASSISTANT.value and event.tool_calls is not None:
|
|
69
|
+
del body["tool_calls"]
|
|
70
|
+
elif event.tool_calls is None:
|
|
71
|
+
del body["tool_calls"]
|
|
72
|
+
|
|
73
|
+
if not should_send_prompts():
|
|
74
|
+
del body["content"]
|
|
75
|
+
if body.get("tool_calls") is not None:
|
|
76
|
+
for tool_call in body["tool_calls"]:
|
|
77
|
+
tool_call["function"].pop("arguments", None)
|
|
78
|
+
|
|
79
|
+
log_record = LogRecord(
|
|
80
|
+
body=body,
|
|
81
|
+
attributes=EVENT_ATTRIBUTES,
|
|
82
|
+
event_name=name
|
|
83
|
+
)
|
|
84
|
+
Config.event_logger.emit(log_record)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _emit_choice_event(event: ChoiceEvent) -> None:
|
|
88
|
+
body = asdict(event)
|
|
89
|
+
if event.message["role"] == Roles.ASSISTANT.value:
|
|
90
|
+
# According to the semantic conventions, the role is conditionally required if available
|
|
91
|
+
# and not equal to "assistant", so remove the role from the body if it is "assistant".
|
|
92
|
+
body["message"].pop("role", None)
|
|
93
|
+
|
|
94
|
+
if event.tool_calls is None:
|
|
95
|
+
del body["tool_calls"]
|
|
96
|
+
|
|
97
|
+
if not should_send_prompts():
|
|
98
|
+
body["message"].pop("content", None)
|
|
99
|
+
if body.get("tool_calls") is not None:
|
|
100
|
+
for tool_call in body["tool_calls"]:
|
|
101
|
+
tool_call["function"].pop("arguments", None)
|
|
102
|
+
|
|
103
|
+
log_record = LogRecord(
|
|
104
|
+
body=body,
|
|
105
|
+
attributes=EVENT_ATTRIBUTES,
|
|
106
|
+
event_name="gen_ai.choice"
|
|
107
|
+
)
|
|
108
|
+
Config.event_logger.emit(log_record)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, List, Literal, Optional, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class _FunctionToolCall(TypedDict):
|
|
6
|
+
function_name: str
|
|
7
|
+
arguments: Optional[dict[str, Any]]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolCall(TypedDict):
|
|
11
|
+
"""Represents a tool call in the AI model."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
function: _FunctionToolCall
|
|
15
|
+
type: Literal["function"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CompletionMessage(TypedDict):
|
|
19
|
+
"""Represents a message in the AI model."""
|
|
20
|
+
|
|
21
|
+
content: Any
|
|
22
|
+
role: str = "assistant"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class MessageEvent:
|
|
27
|
+
"""Represents an input event for the AI model."""
|
|
28
|
+
|
|
29
|
+
content: Any
|
|
30
|
+
role: str = "user"
|
|
31
|
+
tool_calls: Optional[List[ToolCall]] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ChoiceEvent:
|
|
36
|
+
"""Represents a completion event for the AI model."""
|
|
37
|
+
|
|
38
|
+
index: int
|
|
39
|
+
message: CompletionMessage
|
|
40
|
+
finish_reason: str = "unknown"
|
|
41
|
+
tool_calls: Optional[List[ToolCall]] = None
|