langtrace-python-sdk 2.1.29__py3-none-any.whl → 2.2.2__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.
Files changed (58) hide show
  1. examples/cohere_example/chat.py +1 -0
  2. examples/cohere_example/chat_stream.py +3 -0
  3. examples/dspy_example/math_problems_cot_parallel.py +59 -0
  4. examples/gemini_example/__init__.py +6 -0
  5. examples/gemini_example/function_tools.py +62 -0
  6. examples/gemini_example/main.py +91 -0
  7. examples/langchain_example/__init__.py +8 -0
  8. examples/langchain_example/groq_example.py +28 -15
  9. examples/ollama_example/basic.py +1 -0
  10. examples/openai_example/__init__.py +1 -0
  11. examples/openai_example/async_tool_calling_nonstreaming.py +1 -1
  12. examples/openai_example/chat_completion.py +1 -1
  13. examples/openai_example/embeddings_create.py +1 -0
  14. examples/openai_example/images_edit.py +2 -2
  15. examples/vertexai_example/__init__.py +6 -0
  16. examples/vertexai_example/main.py +214 -0
  17. langtrace_python_sdk/constants/instrumentation/common.py +2 -0
  18. langtrace_python_sdk/constants/instrumentation/gemini.py +12 -0
  19. langtrace_python_sdk/constants/instrumentation/vertexai.py +42 -0
  20. langtrace_python_sdk/instrumentation/__init__.py +4 -0
  21. langtrace_python_sdk/instrumentation/anthropic/patch.py +68 -96
  22. langtrace_python_sdk/instrumentation/chroma/patch.py +29 -29
  23. langtrace_python_sdk/instrumentation/cohere/patch.py +143 -242
  24. langtrace_python_sdk/instrumentation/dspy/instrumentation.py +2 -2
  25. langtrace_python_sdk/instrumentation/dspy/patch.py +36 -36
  26. langtrace_python_sdk/instrumentation/gemini/__init__.py +3 -0
  27. langtrace_python_sdk/instrumentation/gemini/instrumentation.py +36 -0
  28. langtrace_python_sdk/instrumentation/gemini/patch.py +186 -0
  29. langtrace_python_sdk/instrumentation/groq/patch.py +82 -125
  30. langtrace_python_sdk/instrumentation/ollama/patch.py +62 -65
  31. langtrace_python_sdk/instrumentation/openai/patch.py +190 -494
  32. langtrace_python_sdk/instrumentation/qdrant/patch.py +6 -6
  33. langtrace_python_sdk/instrumentation/vertexai/__init__.py +3 -0
  34. langtrace_python_sdk/instrumentation/vertexai/instrumentation.py +33 -0
  35. langtrace_python_sdk/instrumentation/vertexai/patch.py +131 -0
  36. langtrace_python_sdk/langtrace.py +5 -0
  37. langtrace_python_sdk/utils/__init__.py +14 -3
  38. langtrace_python_sdk/utils/llm.py +311 -6
  39. langtrace_python_sdk/version.py +1 -1
  40. {langtrace_python_sdk-2.1.29.dist-info → langtrace_python_sdk-2.2.2.dist-info}/METADATA +26 -19
  41. {langtrace_python_sdk-2.1.29.dist-info → langtrace_python_sdk-2.2.2.dist-info}/RECORD +58 -38
  42. tests/anthropic/test_anthropic.py +28 -27
  43. tests/cohere/test_cohere_chat.py +36 -36
  44. tests/cohere/test_cohere_embed.py +12 -9
  45. tests/cohere/test_cohere_rerank.py +18 -11
  46. tests/groq/cassettes/test_async_chat_completion.yaml +113 -0
  47. tests/groq/cassettes/test_async_chat_completion_streaming.yaml +2232 -0
  48. tests/groq/cassettes/test_chat_completion.yaml +114 -0
  49. tests/groq/cassettes/test_chat_completion_streaming.yaml +2512 -0
  50. tests/groq/conftest.py +33 -0
  51. tests/groq/test_groq.py +142 -0
  52. tests/openai/cassettes/test_async_chat_completion_streaming.yaml +28 -28
  53. tests/openai/test_chat_completion.py +53 -67
  54. tests/openai/test_image_generation.py +47 -24
  55. tests/utils.py +40 -5
  56. {langtrace_python_sdk-2.1.29.dist-info → langtrace_python_sdk-2.2.2.dist-info}/WHEEL +0 -0
  57. {langtrace_python_sdk-2.1.29.dist-info → langtrace_python_sdk-2.2.2.dist-info}/entry_points.txt +0 -0
  58. {langtrace_python_sdk-2.1.29.dist-info → langtrace_python_sdk-2.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -16,22 +16,32 @@ limitations under the License.
16
16
 
17
17
  import json
18
18
 
19
- from importlib_metadata import version as v
20
- from langtrace.trace_attributes import Event, LLMSpanAttributes
21
- from opentelemetry import baggage, trace
19
+ from langtrace.trace_attributes import (
20
+ LLMSpanAttributes,
21
+ SpanAttributes,
22
+ )
23
+ from langtrace_python_sdk.utils import set_span_attribute
24
+ from langtrace_python_sdk.utils.silently_fail import silently_fail
25
+ from opentelemetry import trace
22
26
  from opentelemetry.trace import SpanKind
23
27
  from opentelemetry.trace.status import Status, StatusCode
24
28
  from opentelemetry.trace.propagation import set_span_in_context
25
- from langtrace_python_sdk.constants import LANGTRACE_SDK_NAME
26
29
  from langtrace_python_sdk.constants.instrumentation.common import (
27
- LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY,
28
30
  SERVICE_PROVIDERS,
29
31
  )
30
32
  from langtrace_python_sdk.constants.instrumentation.openai import APIS
31
33
  from langtrace_python_sdk.utils.llm import (
32
34
  calculate_prompt_tokens,
33
- estimate_tokens,
35
+ get_base_url,
36
+ get_extra_attributes,
37
+ get_langtrace_attributes,
38
+ get_llm_request_attributes,
39
+ get_llm_url,
34
40
  get_tool_calls,
41
+ is_streaming,
42
+ set_event_completion,
43
+ StreamWrapper,
44
+ set_span_attributes,
35
45
  )
36
46
  from openai._types import NOT_GIVEN
37
47
 
@@ -42,44 +52,27 @@ def images_generate(original_method, version, tracer):
42
52
  """
43
53
 
44
54
  def traced_method(wrapped, instance, args, kwargs):
45
- base_url = (
46
- str(instance._client._base_url)
47
- if hasattr(instance, "_client") and hasattr(instance._client, "_base_url")
48
- else ""
49
- )
50
55
  service_provider = SERVICE_PROVIDERS["OPENAI"]
51
- extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
52
-
53
56
  span_attributes = {
54
- "langtrace.sdk.name": "langtrace-python-sdk",
55
- "langtrace.service.name": service_provider,
56
- "langtrace.service.type": "llm",
57
- "langtrace.service.version": version,
58
- "langtrace.version": v(LANGTRACE_SDK_NAME),
59
- "url.full": base_url,
60
- "llm.api": APIS["IMAGES_GENERATION"]["ENDPOINT"],
61
- "llm.model": kwargs.get("model"),
62
- "llm.stream": kwargs.get("stream"),
63
- "llm.prompts": json.dumps(
64
- [{"role": "user", "content": kwargs.get("prompt", [])}]
65
- ),
66
- **(extra_attributes if extra_attributes is not None else {}),
57
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
58
+ **get_llm_request_attributes(kwargs),
59
+ **get_llm_url(instance),
60
+ SpanAttributes.LLM_PATH: APIS["IMAGES_GENERATION"]["ENDPOINT"],
61
+ **get_extra_attributes(),
67
62
  }
68
63
 
69
64
  attributes = LLMSpanAttributes(**span_attributes)
70
65
 
71
66
  with tracer.start_as_current_span(
72
67
  APIS["IMAGES_GENERATION"]["METHOD"],
73
- kind=SpanKind.CLIENT,
68
+ kind=SpanKind.CLIENT.value,
74
69
  context=set_span_in_context(trace.get_current_span()),
75
70
  ) as span:
76
- for field, value in attributes.model_dump(by_alias=True).items():
77
- if value is not None:
78
- span.set_attribute(field, value)
71
+ set_span_attributes(span, attributes)
79
72
  try:
80
73
  # Attempt to call the original method
81
74
  result = wrapped(*args, **kwargs)
82
- if kwargs.get("stream") is False or kwargs.get("stream") is None:
75
+ if not is_streaming(kwargs):
83
76
  data = (
84
77
  result.data[0]
85
78
  if hasattr(result, "data") and len(result.data) > 0
@@ -98,7 +91,7 @@ def images_generate(original_method, version, tracer):
98
91
  },
99
92
  }
100
93
  ]
101
- span.set_attribute("llm.responses", json.dumps(response))
94
+ set_event_completion(span, response)
102
95
 
103
96
  span.set_status(StatusCode.OK)
104
97
  return result
@@ -121,45 +114,28 @@ def async_images_generate(original_method, version, tracer):
121
114
  """
122
115
 
123
116
  async def traced_method(wrapped, instance, args, kwargs):
124
- base_url = (
125
- str(instance._client._base_url)
126
- if hasattr(instance, "_client") and hasattr(instance._client, "_base_url")
127
- else ""
128
- )
129
117
  service_provider = SERVICE_PROVIDERS["OPENAI"]
130
- extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
131
118
 
132
119
  span_attributes = {
133
- "langtrace.sdk.name": "langtrace-python-sdk",
134
- "langtrace.service.name": service_provider,
135
- "langtrace.service.type": "llm",
136
- "langtrace.service.version": version,
137
- "langtrace.version": v(LANGTRACE_SDK_NAME),
138
- "url.full": base_url,
139
- "llm.api": APIS["IMAGES_GENERATION"]["ENDPOINT"],
140
- "llm.model": kwargs.get("model"),
141
- "llm.stream": kwargs.get("stream"),
142
- "llm.prompts": json.dumps(
143
- [{"role": "user", "content": kwargs.get("prompt", [])}]
144
- ),
145
- **(extra_attributes if extra_attributes is not None else {}),
120
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
121
+ **get_llm_request_attributes(kwargs),
122
+ **get_llm_url(instance),
123
+ SpanAttributes.LLM_PATH: APIS["IMAGES_GENERATION"]["ENDPOINT"],
124
+ **get_extra_attributes(),
146
125
  }
147
126
 
148
127
  attributes = LLMSpanAttributes(**span_attributes)
149
128
 
150
129
  with tracer.start_as_current_span(
151
130
  APIS["IMAGES_GENERATION"]["METHOD"],
152
- kind=SpanKind.CLIENT,
131
+ kind=SpanKind.CLIENT.value,
153
132
  context=set_span_in_context(trace.get_current_span()),
154
133
  ) as span:
155
- items = attributes.model_dump(by_alias=True).items()
156
- for field, value in items:
157
- if value is not None:
158
- span.set_attribute(field, value)
134
+ set_span_attributes(span, attributes)
159
135
  try:
160
136
  # Attempt to call the original method
161
137
  result = await wrapped(*args, **kwargs)
162
- if kwargs.get("stream") is False or kwargs.get("stream") is None:
138
+ if not is_streaming(kwargs):
163
139
  data = (
164
140
  result.data[0]
165
141
  if hasattr(result, "data") and len(result.data) > 0
@@ -178,7 +154,7 @@ def async_images_generate(original_method, version, tracer):
178
154
  },
179
155
  }
180
156
  ]
181
- span.set_attribute("llm.responses", json.dumps(response))
157
+ set_event_completion(span, response)
182
158
 
183
159
  span.set_status(StatusCode.OK)
184
160
  return result
@@ -201,47 +177,26 @@ def images_edit(original_method, version, tracer):
201
177
  """
202
178
 
203
179
  def traced_method(wrapped, instance, args, kwargs):
204
- base_url = (
205
- str(instance._client._base_url)
206
- if hasattr(instance, "_client") and hasattr(instance._client, "_base_url")
207
- else ""
208
- )
209
180
  service_provider = SERVICE_PROVIDERS["OPENAI"]
210
- extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
211
181
 
212
182
  span_attributes = {
213
- "langtrace.sdk.name": "langtrace-python-sdk",
214
- "langtrace.service.name": service_provider,
215
- "langtrace.service.type": "llm",
216
- "langtrace.service.version": version,
217
- "langtrace.version": v(LANGTRACE_SDK_NAME),
218
- "url.full": base_url,
219
- "llm.api": APIS["IMAGES_EDIT"]["ENDPOINT"],
220
- "llm.model": kwargs.get("model"),
221
- "llm.response_format": kwargs.get("response_format"),
222
- "llm.image.size": kwargs.get("size"),
223
- "llm.prompts": json.dumps(
224
- [
225
- {
226
- "role": kwargs.get("user", "user"),
227
- "content": kwargs.get("prompt", []),
228
- }
229
- ]
230
- ),
231
- "llm.top_k": kwargs.get("n"),
232
- **(extra_attributes if extra_attributes is not None else {}),
183
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
184
+ **get_llm_request_attributes(kwargs),
185
+ **get_llm_url(instance),
186
+ SpanAttributes.LLM_PATH: APIS["IMAGES_EDIT"]["ENDPOINT"],
187
+ SpanAttributes.LLM_RESPONSE_FORMAT: kwargs.get("response_format"),
188
+ SpanAttributes.LLM_IMAGE_SIZE: kwargs.get("size"),
189
+ **get_extra_attributes(),
233
190
  }
234
191
 
235
192
  attributes = LLMSpanAttributes(**span_attributes)
236
193
 
237
194
  with tracer.start_as_current_span(
238
195
  APIS["IMAGES_EDIT"]["METHOD"],
239
- kind=SpanKind.CLIENT,
196
+ kind=SpanKind.CLIENT.value,
240
197
  context=set_span_in_context(trace.get_current_span()),
241
198
  ) as span:
242
- for field, value in attributes.model_dump(by_alias=True).items():
243
- if value is not None:
244
- span.set_attribute(field, value)
199
+ set_span_attributes(span, attributes)
245
200
  try:
246
201
  # Attempt to call the original method
247
202
  result = wrapped(*args, **kwargs)
@@ -260,10 +215,7 @@ def images_edit(original_method, version, tracer):
260
215
  }
261
216
  )
262
217
 
263
- span.add_event(
264
- name="response",
265
- attributes={"llm.responses": json.dumps(response)},
266
- )
218
+ set_event_completion(span, response)
267
219
 
268
220
  span.set_status(StatusCode.OK)
269
221
  return result
@@ -280,158 +232,17 @@ def images_edit(original_method, version, tracer):
280
232
  return traced_method
281
233
 
282
234
 
283
- class StreamWrapper:
284
- def __init__(
285
- self, stream, span, prompt_tokens, function_call=False, tool_calls=False
286
- ):
287
- self.stream = stream
288
- self.span = span
289
- self.prompt_tokens = prompt_tokens
290
- self.function_call = function_call
291
- self.tool_calls = tool_calls
292
- self.result_content = []
293
- self.completion_tokens = 0
294
- self._span_started = False
295
- self._start_span()
296
-
297
- def _start_span(self):
298
- if not self._span_started:
299
- self.span.add_event(Event.STREAM_START.value)
300
- self._span_started = True
301
-
302
- def _end_span(self):
303
- if self._span_started:
304
- self.span.add_event(Event.STREAM_END.value)
305
- self.span.set_attribute(
306
- "llm.token.counts",
307
- json.dumps(
308
- {
309
- "input_tokens": self.prompt_tokens,
310
- "output_tokens": self.completion_tokens,
311
- "total_tokens": self.prompt_tokens + self.completion_tokens,
312
- }
313
- ),
314
- )
315
- self.span.set_attribute(
316
- "llm.responses",
317
- json.dumps(
318
- [
319
- {
320
- "role": "assistant",
321
- "content": "".join(self.result_content),
322
- }
323
- ]
324
- ),
325
- )
326
- self.span.set_status(StatusCode.OK)
327
- self.span.end()
328
- self._span_started = False
329
-
330
- def __enter__(self):
331
- self._start_span()
332
- return self
333
-
334
- def __exit__(self, exc_type, exc_val, exc_tb):
335
- self._end_span()
336
-
337
- def __iter__(self):
338
- self._start_span()
339
- return self
340
-
341
- def __aiter__(self):
342
- self._start_span()
343
- return self
344
-
345
- async def __anext__(self):
346
- try:
347
- chunk = await self.stream.__anext__()
348
- self.process_chunk(chunk)
349
- return chunk
350
- except StopIteration:
351
- self._end_span()
352
- raise
353
-
354
- def __next__(self):
355
- try:
356
- chunk = next(self.stream)
357
- self.process_chunk(chunk)
358
- return chunk
359
- except StopIteration:
360
- self._end_span()
361
- raise
362
-
363
- def process_chunk(self, chunk):
364
- if hasattr(chunk, "model") and chunk.model is not None:
365
- self.span.set_attribute("llm.model", chunk.model)
366
- if hasattr(chunk, "choices") and chunk.choices is not None:
367
- content = []
368
- if not self.function_call and not self.tool_calls:
369
- for choice in chunk.choices:
370
- if choice.delta and choice.delta.content is not None:
371
- token_counts = estimate_tokens(choice.delta.content)
372
- self.completion_tokens += token_counts
373
- content = [choice.delta.content]
374
- elif self.function_call:
375
- for choice in chunk.choices:
376
- if (
377
- choice.delta
378
- and choice.delta.function_call is not None
379
- and choice.delta.function_call.arguments is not None
380
- ):
381
- token_counts = estimate_tokens(
382
- choice.delta.function_call.arguments
383
- )
384
- self.completion_tokens += token_counts
385
- content = [choice.delta.function_call.arguments]
386
- elif self.tool_calls:
387
- for choice in chunk.choices:
388
- if choice.delta and choice.delta.tool_calls is not None:
389
- toolcalls = choice.delta.tool_calls
390
- content = []
391
- for tool_call in toolcalls:
392
- if (
393
- tool_call
394
- and tool_call.function is not None
395
- and tool_call.function.arguments is not None
396
- ):
397
- token_counts = estimate_tokens(
398
- tool_call.function.arguments
399
- )
400
- self.completion_tokens += token_counts
401
- content.append(tool_call.function.arguments)
402
- self.span.add_event(
403
- Event.STREAM_OUTPUT.value,
404
- {
405
- "response": (
406
- "".join(content)
407
- if len(content) > 0 and content[0] is not None
408
- else ""
409
- )
410
- },
411
- )
412
- if content:
413
- self.result_content.append(content[0])
414
-
415
-
416
235
  def chat_completions_create(original_method, version, tracer):
417
236
  """Wrap the `create` method of the `ChatCompletion` class to trace it."""
418
237
 
419
238
  def traced_method(wrapped, instance, args, kwargs):
420
- base_url = (
421
- str(instance._client._base_url)
422
- if hasattr(instance, "_client") and hasattr(instance._client, "_base_url")
423
- else ""
424
- )
425
239
  service_provider = SERVICE_PROVIDERS["OPENAI"]
426
- # If base url contains perplexity or azure, set the service provider accordingly
427
- if "perplexity" in base_url:
240
+ if "perplexity" in get_base_url(instance):
428
241
  service_provider = SERVICE_PROVIDERS["PPLX"]
429
- elif "azure" in base_url:
242
+ elif "azure" in get_base_url(instance):
430
243
  service_provider = SERVICE_PROVIDERS["AZURE"]
431
-
432
- extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
433
-
434
- # handle tool calls in the kwargs
244
+ elif "groq" in get_base_url(instance):
245
+ service_provider = SERVICE_PROVIDERS["GROQ"]
435
246
  llm_prompts = []
436
247
  for item in kwargs.get("messages", []):
437
248
  tools = get_tool_calls(item)
@@ -461,113 +272,31 @@ def chat_completions_create(original_method, version, tracer):
461
272
  llm_prompts.append(item)
462
273
 
463
274
  span_attributes = {
464
- "langtrace.sdk.name": "langtrace-python-sdk",
465
- "langtrace.service.name": service_provider,
466
- "langtrace.service.type": "llm",
467
- "langtrace.service.version": version,
468
- "langtrace.version": v(LANGTRACE_SDK_NAME),
469
- "url.full": base_url,
470
- "llm.api": APIS["CHAT_COMPLETION"]["ENDPOINT"],
471
- "llm.prompts": json.dumps(llm_prompts),
472
- "llm.stream": kwargs.get("stream"),
473
- **(extra_attributes if extra_attributes is not None else {}),
275
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
276
+ **get_llm_request_attributes(kwargs, prompts=llm_prompts),
277
+ **get_llm_url(instance),
278
+ SpanAttributes.LLM_PATH: APIS["CHAT_COMPLETION"]["ENDPOINT"],
279
+ **get_extra_attributes(),
474
280
  }
475
281
 
476
282
  attributes = LLMSpanAttributes(**span_attributes)
477
283
 
478
- tools = []
479
- if (
480
- kwargs.get("temperature") is not None
481
- and kwargs.get("temperature") != NOT_GIVEN
482
- ):
483
- attributes.llm_temperature = kwargs.get("temperature")
484
- if kwargs.get("top_p") is not None and kwargs.get("top_p") != NOT_GIVEN:
485
- attributes.llm_top_p = kwargs.get("top_p")
486
- if kwargs.get("user") is not None and kwargs.get("user") != NOT_GIVEN:
487
- attributes.llm_user = kwargs.get("user")
488
- if kwargs.get("functions") is not None and kwargs.get("functions") != NOT_GIVEN:
489
- for function in kwargs.get("functions"):
490
- tools.append(json.dumps({"type": "function", "function": function}))
491
- if kwargs.get("tools") is not None and kwargs.get("tools") != NOT_GIVEN:
492
- tools.append(json.dumps(kwargs.get("tools")))
493
- if len(tools) > 0:
494
- attributes.llm_tools = json.dumps(tools)
495
-
496
- # TODO(Karthik): Gotta figure out how to handle streaming with context
497
- # with tracer.start_as_current_span(APIS["CHAT_COMPLETION"]["METHOD"],
498
- # kind=SpanKind.CLIENT) as span:
499
284
  span = tracer.start_span(
500
285
  APIS["CHAT_COMPLETION"]["METHOD"],
501
- kind=SpanKind.CLIENT,
286
+ kind=SpanKind.CLIENT.value,
502
287
  context=set_span_in_context(trace.get_current_span()),
503
288
  )
504
- for field, value in attributes.model_dump(by_alias=True).items():
505
- if value is not None:
506
- span.set_attribute(field, value)
289
+ _set_input_attributes(span, kwargs, attributes)
290
+
507
291
  try:
508
- # Attempt to call the original method
509
292
  result = wrapped(*args, **kwargs)
510
- if (
511
- kwargs.get("stream") is False
512
- or kwargs.get("stream") is None
513
- or kwargs.get("stream") == NOT_GIVEN
514
- ):
515
- span.set_attribute("llm.model", result.model)
516
- if hasattr(result, "choices") and result.choices is not None:
517
- responses = [
518
- {
519
- "role": (
520
- choice.message.role
521
- if choice.message and choice.message.role
522
- else "assistant"
523
- ),
524
- "content": extract_content(choice),
525
- **(
526
- {
527
- "content_filter_results": choice[
528
- "content_filter_results"
529
- ]
530
- }
531
- if "content_filter_results" in choice
532
- else {}
533
- ),
534
- }
535
- for choice in result.choices
536
- ]
537
- span.set_attribute("llm.responses", json.dumps(responses))
538
- else:
539
- responses = []
540
- span.set_attribute("llm.responses", json.dumps(responses))
541
- if (
542
- hasattr(result, "system_fingerprint")
543
- and result.system_fingerprint is not None
544
- and result.system_fingerprint != NOT_GIVEN
545
- ):
546
- span.set_attribute(
547
- "llm.system.fingerprint", result.system_fingerprint
548
- )
549
- # Get the usage
550
- if hasattr(result, "usage") and result.usage is not None:
551
- usage = result.usage
552
- if usage is not None:
553
- usage_dict = {
554
- "input_tokens": result.usage.prompt_tokens,
555
- "output_tokens": usage.completion_tokens,
556
- "total_tokens": usage.total_tokens,
557
- }
558
- span.set_attribute("llm.token.counts", json.dumps(usage_dict))
559
- span.set_status(StatusCode.OK)
560
- span.end()
561
- return result
562
- else:
563
- # iterate over kwargs.get("messages", {}) and calculate the prompt tokens
293
+ if is_streaming(kwargs):
564
294
  prompt_tokens = 0
565
295
  for message in kwargs.get("messages", {}):
566
296
  prompt_tokens += calculate_prompt_tokens(
567
297
  json.dumps(message), kwargs.get("model")
568
298
  )
569
299
 
570
- # iterate over kwargs.get("functions") and calculate the prompt tokens
571
300
  if (
572
301
  kwargs.get("functions") is not None
573
302
  and kwargs.get("functions") != NOT_GIVEN
@@ -584,6 +313,11 @@ def chat_completions_create(original_method, version, tracer):
584
313
  function_call=kwargs.get("functions") is not None,
585
314
  tool_calls=kwargs.get("tools") is not None,
586
315
  )
316
+ else:
317
+ _set_response_attributes(span, kwargs, result)
318
+ span.set_status(StatusCode.OK)
319
+ span.end()
320
+ return result
587
321
 
588
322
  except Exception as error:
589
323
  span.record_exception(error)
@@ -591,7 +325,6 @@ def chat_completions_create(original_method, version, tracer):
591
325
  span.end()
592
326
  raise
593
327
 
594
- # return the wrapped method
595
328
  return traced_method
596
329
 
597
330
 
@@ -599,21 +332,11 @@ def async_chat_completions_create(original_method, version, tracer):
599
332
  """Wrap the `create` method of the `ChatCompletion` class to trace it."""
600
333
 
601
334
  async def traced_method(wrapped, instance, args, kwargs):
602
- base_url = (
603
- str(instance._client._base_url)
604
- if hasattr(instance, "_client") and hasattr(instance._client, "_base_url")
605
- else ""
606
- )
607
335
  service_provider = SERVICE_PROVIDERS["OPENAI"]
608
- # If base url contains perplexity or azure, set the service provider accordingly
609
- if "perplexity" in base_url:
336
+ if "perplexity" in get_base_url(instance):
610
337
  service_provider = SERVICE_PROVIDERS["PPLX"]
611
- elif "azure" in base_url:
338
+ elif "azure" in get_base_url(instance):
612
339
  service_provider = SERVICE_PROVIDERS["AZURE"]
613
-
614
- extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
615
-
616
- # handle tool calls in the kwargs
617
340
  llm_prompts = []
618
341
  for item in kwargs.get("messages", []):
619
342
  tools = get_tool_calls(item)
@@ -637,117 +360,37 @@ def async_chat_completions_create(original_method, version, tracer):
637
360
  else ""
638
361
  ),
639
362
  }
640
- tool_calls.append(tool_call_dict)
363
+ tool_calls.append(json.dumps(tool_call_dict))
641
364
  llm_prompts.append(tool_calls)
642
365
  else:
643
366
  llm_prompts.append(item)
644
367
 
645
368
  span_attributes = {
646
- "langtrace.sdk.name": "langtrace-python-sdk",
647
- "langtrace.service.name": service_provider,
648
- "langtrace.service.type": "llm",
649
- "langtrace.service.version": version,
650
- "langtrace.version": v(LANGTRACE_SDK_NAME),
651
- "url.full": base_url,
652
- "llm.api": APIS["CHAT_COMPLETION"]["ENDPOINT"],
653
- "llm.prompts": json.dumps(llm_prompts),
654
- "llm.stream": kwargs.get("stream"),
655
- **(extra_attributes if extra_attributes is not None else {}),
369
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
370
+ **get_llm_request_attributes(kwargs, prompts=llm_prompts),
371
+ **get_llm_url(instance),
372
+ SpanAttributes.LLM_PATH: APIS["CHAT_COMPLETION"]["ENDPOINT"],
373
+ **get_extra_attributes(),
656
374
  }
657
375
 
658
376
  attributes = LLMSpanAttributes(**span_attributes)
659
377
 
660
- tools = []
661
- if (
662
- kwargs.get("temperature") is not None
663
- and kwargs.get("temperature") != NOT_GIVEN
664
- ):
665
- attributes.llm_temperature = kwargs.get("temperature")
666
- if kwargs.get("top_p") is not None and kwargs.get("top_p") != NOT_GIVEN:
667
- attributes.llm_top_p = kwargs.get("top_p")
668
- if kwargs.get("user") is not None and kwargs.get("user") != NOT_GIVEN:
669
- attributes.llm_user = kwargs.get("user")
670
- if kwargs.get("functions") is not None and kwargs.get("functions") != NOT_GIVEN:
671
- for function in kwargs.get("functions"):
672
- tools.append(json.dumps({"type": "function", "function": function}))
673
- if kwargs.get("tools") is not None and kwargs.get("tools") != NOT_GIVEN:
674
- tools.append(json.dumps(kwargs.get("tools")))
675
- if len(tools) > 0:
676
- attributes.llm_tools = json.dumps(tools)
677
-
678
- # TODO(Karthik): Gotta figure out how to handle streaming with context
679
- # with tracer.start_as_current_span(APIS["CHAT_COMPLETION"]["METHOD"],
680
- # kind=SpanKind.CLIENT) as span:
681
378
  span = tracer.start_span(
682
- APIS["CHAT_COMPLETION"]["METHOD"], kind=SpanKind.CLIENT
379
+ APIS["CHAT_COMPLETION"]["METHOD"],
380
+ kind=SpanKind.CLIENT.value,
381
+ context=set_span_in_context(trace.get_current_span()),
683
382
  )
684
- for field, value in attributes.model_dump(by_alias=True).items():
685
- if value is not None:
686
- span.set_attribute(field, value)
383
+ _set_input_attributes(span, kwargs, attributes)
384
+
687
385
  try:
688
- # Attempt to call the original method
689
386
  result = await wrapped(*args, **kwargs)
690
- if (
691
- kwargs.get("stream") is False
692
- or kwargs.get("stream") is None
693
- or kwargs.get("stream") == NOT_GIVEN
694
- ):
695
- span.set_attribute("llm.model", result.model)
696
- if hasattr(result, "choices") and result.choices is not None:
697
- responses = [
698
- {
699
- "role": (
700
- choice.message.role
701
- if choice.message and choice.message.role
702
- else "assistant"
703
- ),
704
- "content": extract_content(choice),
705
- **(
706
- {
707
- "content_filter_results": choice[
708
- "content_filter_results"
709
- ]
710
- }
711
- if "content_filter_results" in choice
712
- else {}
713
- ),
714
- }
715
- for choice in result.choices
716
- ]
717
- span.set_attribute("llm.responses", json.dumps(responses))
718
- else:
719
- responses = []
720
- span.set_attribute("llm.responses", json.dumps(responses))
721
- if (
722
- hasattr(result, "system_fingerprint")
723
- and result.system_fingerprint is not None
724
- and result.system_fingerprint != NOT_GIVEN
725
- ):
726
- span.set_attribute(
727
- "llm.system.fingerprint", result.system_fingerprint
728
- )
729
- # Get the usage
730
- if hasattr(result, "usage") and result.usage is not None:
731
- usage = result.usage
732
- if usage is not None:
733
- usage_dict = {
734
- "input_tokens": result.usage.prompt_tokens,
735
- "output_tokens": usage.completion_tokens,
736
- "total_tokens": usage.total_tokens,
737
- }
738
- span.set_attribute("llm.token.counts", json.dumps(usage_dict))
739
- span.set_status(StatusCode.OK)
740
- span.end()
741
- return result
742
- else:
743
- # iterate over kwargs.get("messages", {}) and calculate the prompt tokens
387
+ if is_streaming(kwargs):
744
388
  prompt_tokens = 0
745
389
  for message in kwargs.get("messages", {}):
746
390
  prompt_tokens += calculate_prompt_tokens(
747
391
  json.dumps(message), kwargs.get("model")
748
392
  )
749
393
 
750
- # iterate over kwargs.get("functions") and calculate the prompt tokens
751
394
  if (
752
395
  kwargs.get("functions") is not None
753
396
  and kwargs.get("functions") != NOT_GIVEN
@@ -764,6 +407,11 @@ def async_chat_completions_create(original_method, version, tracer):
764
407
  function_call=kwargs.get("functions") is not None,
765
408
  tool_calls=kwargs.get("tools") is not None,
766
409
  )
410
+ else:
411
+ _set_response_attributes(span, kwargs, result)
412
+ span.set_status(StatusCode.OK)
413
+ span.end()
414
+ return result
767
415
 
768
416
  except Exception as error:
769
417
  span.record_exception(error)
@@ -771,7 +419,6 @@ def async_chat_completions_create(original_method, version, tracer):
771
419
  span.end()
772
420
  raise
773
421
 
774
- # return the wrapped method
775
422
  return traced_method
776
423
 
777
424
 
@@ -781,51 +428,39 @@ def embeddings_create(original_method, version, tracer):
781
428
  """
782
429
 
783
430
  def traced_method(wrapped, instance, args, kwargs):
784
- base_url = (
785
- str(instance._client._base_url)
786
- if hasattr(instance, "_client") and hasattr(instance._client, "_base_url")
787
- else ""
788
- )
789
-
790
431
  service_provider = SERVICE_PROVIDERS["OPENAI"]
791
- extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
792
432
 
793
433
  span_attributes = {
794
- "langtrace.sdk.name": "langtrace-python-sdk",
795
- "langtrace.service.name": service_provider,
796
- "langtrace.service.type": "llm",
797
- "langtrace.service.version": version,
798
- "langtrace.version": v(LANGTRACE_SDK_NAME),
799
- "url.full": base_url,
800
- "llm.api": APIS["EMBEDDINGS_CREATE"]["ENDPOINT"],
801
- "llm.model": kwargs.get("model"),
802
- "llm.prompts": "",
803
- "llm.embedding_inputs": json.dumps([kwargs.get("input", "")]),
804
- **(extra_attributes if extra_attributes is not None else {}),
434
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
435
+ **get_llm_request_attributes(kwargs),
436
+ **get_llm_url(instance),
437
+ SpanAttributes.LLM_PATH: APIS["EMBEDDINGS_CREATE"]["ENDPOINT"],
438
+ SpanAttributes.LLM_REQUEST_DIMENSIONS: kwargs.get("dimensions"),
439
+ **get_extra_attributes(),
805
440
  }
806
441
 
807
- if kwargs.get("encoding_format") is not None:
808
- span_attributes["llm.encoding.formats"] = json.dumps(
809
- [kwargs.get("encoding_format")]
442
+ encoding_format = kwargs.get("encoding_format")
443
+ if encoding_format is not None:
444
+ if not isinstance(encoding_format, list):
445
+ encoding_format = [encoding_format]
446
+ span_attributes[SpanAttributes.LLM_REQUEST_ENCODING_FORMATS] = (
447
+ encoding_format
810
448
  )
811
449
 
812
- attributes = LLMSpanAttributes(**span_attributes)
813
- kwargs.get("encoding_format")
450
+ if kwargs.get("input") is not None:
451
+ span_attributes[SpanAttributes.LLM_REQUEST_EMBEDDING_INPUTS] = json.dumps(
452
+ [kwargs.get("input", "")]
453
+ )
814
454
 
815
- if kwargs.get("dimensions") is not None:
816
- attributes["llm.dimensions"] = kwargs.get("dimensions")
817
- if kwargs.get("user") is not None:
818
- attributes["llm.user"] = kwargs.get("user")
455
+ attributes = LLMSpanAttributes(**span_attributes)
819
456
 
820
457
  with tracer.start_as_current_span(
821
458
  APIS["EMBEDDINGS_CREATE"]["METHOD"],
822
- kind=SpanKind.CLIENT,
459
+ kind=SpanKind.CLIENT.value,
823
460
  context=set_span_in_context(trace.get_current_span()),
824
461
  ) as span:
825
462
 
826
- for field, value in attributes.model_dump(by_alias=True).items():
827
- if value is not None:
828
- span.set_attribute(field, value)
463
+ set_span_attributes(span, attributes)
829
464
  try:
830
465
  # Attempt to call the original method
831
466
  result = wrapped(*args, **kwargs)
@@ -850,49 +485,39 @@ def async_embeddings_create(original_method, version, tracer):
850
485
  """
851
486
 
852
487
  async def traced_method(wrapped, instance, args, kwargs):
853
- base_url = (
854
- str(instance._client._base_url)
855
- if hasattr(instance, "_client") and hasattr(instance._client, "_base_url")
856
- else ""
857
- )
858
488
 
859
489
  service_provider = SERVICE_PROVIDERS["OPENAI"]
860
- extra_attributes = baggage.get_baggage(LANGTRACE_ADDITIONAL_SPAN_ATTRIBUTES_KEY)
861
490
 
862
491
  span_attributes = {
863
- "langtrace.sdk.name": "langtrace-python-sdk",
864
- "langtrace.service.name": service_provider,
865
- "langtrace.service.type": "llm",
866
- "langtrace.service.version": version,
867
- "langtrace.version": v(LANGTRACE_SDK_NAME),
868
- "url.full": base_url,
869
- "llm.api": APIS["EMBEDDINGS_CREATE"]["ENDPOINT"],
870
- "llm.model": kwargs.get("model"),
871
- "llm.prompts": json.dumps(
872
- [{"role": "user", "content": kwargs.get("input", "")}]
873
- ),
874
- **(extra_attributes if extra_attributes is not None else {}),
492
+ **get_langtrace_attributes(version, service_provider, vendor_type="llm"),
493
+ **get_llm_request_attributes(kwargs),
494
+ SpanAttributes.LLM_PATH: APIS["EMBEDDINGS_CREATE"]["ENDPOINT"],
495
+ SpanAttributes.LLM_REQUEST_DIMENSIONS: kwargs.get("dimensions"),
496
+ **get_extra_attributes(),
875
497
  }
876
498
 
877
499
  attributes = LLMSpanAttributes(**span_attributes)
878
- kwargs.get("encoding_format")
879
500
 
880
- if kwargs.get("encoding_format") is not None:
881
- attributes.llm_encoding_format = kwargs.get("encoding_format")
882
- if kwargs.get("dimensions") is not None:
883
- attributes["llm.dimensions"] = kwargs.get("dimensions")
884
- if kwargs.get("user") is not None:
885
- attributes["llm.user"] = kwargs.get("user")
501
+ encoding_format = kwargs.get("encoding_format")
502
+ if encoding_format is not None:
503
+ if not isinstance(encoding_format, list):
504
+ encoding_format = [encoding_format]
505
+ span_attributes[SpanAttributes.LLM_REQUEST_ENCODING_FORMATS] = (
506
+ encoding_format
507
+ )
508
+
509
+ if kwargs.get("input") is not None:
510
+ span_attributes[SpanAttributes.LLM_REQUEST_EMBEDDING_INPUTS] = json.dumps(
511
+ [kwargs.get("input", "")]
512
+ )
886
513
 
887
514
  with tracer.start_as_current_span(
888
515
  APIS["EMBEDDINGS_CREATE"]["METHOD"],
889
- kind=SpanKind.CLIENT,
516
+ kind=SpanKind.CLIENT.value,
890
517
  context=set_span_in_context(trace.get_current_span()),
891
518
  ) as span:
892
519
 
893
- async for field, value in attributes.model_dump(by_alias=True).items():
894
- if value is not None:
895
- span.set_attribute(field, value)
520
+ set_span_attributes(span, attributes)
896
521
  try:
897
522
  # Attempt to call the original method
898
523
  result = await wrapped(*args, **kwargs)
@@ -953,3 +578,74 @@ def extract_content(choice):
953
578
  # Return an empty string if none of the above conditions are met
954
579
  else:
955
580
  return ""
581
+
582
+
583
+ @silently_fail
584
+ def _set_input_attributes(span, kwargs, attributes):
585
+
586
+ for field, value in attributes.model_dump(by_alias=True).items():
587
+ set_span_attribute(span, field, value)
588
+
589
+ if kwargs.get("functions") is not None and kwargs.get("functions") != NOT_GIVEN:
590
+ tools = []
591
+ for function in kwargs.get("functions"):
592
+ tools.append(json.dumps({"type": "function", "function": function}))
593
+
594
+ if kwargs.get("tools") is not None and kwargs.get("tools") != NOT_GIVEN:
595
+ tools.append(json.dumps(kwargs.get("tools")))
596
+
597
+ if tools:
598
+ set_span_attribute(span, SpanAttributes.LLM_TOOLS, json.dumps(tools))
599
+
600
+
601
+ @silently_fail
602
+ def _set_response_attributes(span, kwargs, result):
603
+ set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, result.model)
604
+ if hasattr(result, "choices") and result.choices is not None:
605
+ responses = [
606
+ {
607
+ "role": (
608
+ choice.message.role
609
+ if choice.message and choice.message.role
610
+ else "assistant"
611
+ ),
612
+ "content": extract_content(choice),
613
+ **(
614
+ {"content_filter_results": choice["content_filter_results"]}
615
+ if "content_filter_results" in choice
616
+ else {}
617
+ ),
618
+ }
619
+ for choice in result.choices
620
+ ]
621
+ set_event_completion(span, responses)
622
+
623
+ if (
624
+ hasattr(result, "system_fingerprint")
625
+ and result.system_fingerprint is not None
626
+ and result.system_fingerprint != NOT_GIVEN
627
+ ):
628
+ set_span_attribute(
629
+ span,
630
+ SpanAttributes.LLM_SYSTEM_FINGERPRINT,
631
+ result.system_fingerprint,
632
+ )
633
+ # Get the usage
634
+ if hasattr(result, "usage") and result.usage is not None:
635
+ usage = result.usage
636
+ if usage is not None:
637
+ set_span_attribute(
638
+ span,
639
+ SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
640
+ result.usage.prompt_tokens,
641
+ )
642
+ set_span_attribute(
643
+ span,
644
+ SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
645
+ result.usage.completion_tokens,
646
+ )
647
+ set_span_attribute(
648
+ span,
649
+ SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
650
+ result.usage.total_tokens,
651
+ )