paid-python 0.3.6__py3-none-any.whl → 0.4.1__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 (30) hide show
  1. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/__init__.py +6 -6
  2. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/__init__.py +2 -2
  3. opentelemetry/instrumentation/openai/shared/audio_wrappers.py +247 -0
  4. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/chat_wrappers.py +5 -5
  5. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/completion_wrappers.py +5 -5
  6. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/embeddings_wrappers.py +5 -5
  7. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/event_emitter.py +2 -2
  8. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/image_gen_wrappers.py +3 -3
  9. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/utils.py +24 -1
  10. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v0/__init__.py +6 -6
  11. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/__init__.py +45 -9
  12. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/assistant_wrappers.py +6 -6
  13. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/event_handler_wrapper.py +4 -4
  14. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/responses_wrappers.py +186 -69
  15. opentelemetry/instrumentation/openai/version.py +1 -0
  16. paid/client.py +23 -2
  17. paid/tracing/__init__.py +2 -1
  18. paid/tracing/autoinstrumentation.py +1 -2
  19. paid/tracing/tracing.py +16 -0
  20. {paid_python-0.3.6.dist-info → paid_python-0.4.1.dist-info}/METADATA +9 -1
  21. {paid_python-0.3.6.dist-info → paid_python-0.4.1.dist-info}/RECORD +26 -28
  22. paid/_vendor/__init__.py +0 -0
  23. paid/_vendor/opentelemetry/__init__.py +0 -0
  24. paid/_vendor/opentelemetry/instrumentation/__init__.py +0 -0
  25. paid/_vendor/opentelemetry/instrumentation/openai/version.py +0 -1
  26. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/config.py +0 -0
  27. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/event_models.py +0 -0
  28. {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/span_utils.py +0 -0
  29. {paid_python-0.3.6.dist-info → paid_python-0.4.1.dist-info}/LICENSE +0 -0
  30. {paid_python-0.3.6.dist-info → paid_python-0.4.1.dist-info}/WHEEL +0 -0
@@ -3,10 +3,58 @@ import pydantic
3
3
  import re
4
4
  import threading
5
5
  import time
6
+ from typing import Any, Optional, Union
6
7
 
7
8
  from openai import AsyncStream, Stream
9
+ from openai._legacy_response import LegacyAPIResponse
10
+ from opentelemetry import context as context_api
11
+ from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
12
+ from opentelemetry.semconv._incubating.attributes import (
13
+ gen_ai_attributes as GenAIAttributes,
14
+ openai_attributes as OpenAIAttributes,
15
+ )
16
+ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
17
+ from opentelemetry.semconv_ai import SpanAttributes
18
+ from opentelemetry.trace import SpanKind, Span, StatusCode, Tracer
19
+ from typing_extensions import NotRequired
8
20
  from wrapt import ObjectProxy
9
21
 
22
+ from opentelemetry.instrumentation.openai.shared import (
23
+ _extract_model_name_from_provider_format,
24
+ _set_request_attributes,
25
+ _set_span_attribute,
26
+ model_as_dict,
27
+ )
28
+ from opentelemetry.instrumentation.openai.utils import (
29
+ _with_tracer_wrapper,
30
+ dont_throw,
31
+ should_send_prompts,
32
+ )
33
+
34
+
35
+ def _get_openai_sentinel_types() -> tuple:
36
+ """Dynamically discover OpenAI sentinel types available in this SDK version.
37
+
38
+ OpenAI SDK uses sentinel objects (NOT_GIVEN, Omit) for unset optional parameters.
39
+ These types may not exist in older SDK versions, so we discover them at runtime.
40
+ """
41
+ sentinel_types = []
42
+ try:
43
+ from openai import NotGiven
44
+ sentinel_types.append(NotGiven)
45
+ except ImportError:
46
+ pass
47
+ try:
48
+ from openai import Omit
49
+ sentinel_types.append(Omit)
50
+ except ImportError:
51
+ pass
52
+ return tuple(sentinel_types)
53
+
54
+
55
+ # Tuple of OpenAI sentinel types for isinstance() checks (empty if none available)
56
+ _OPENAI_SENTINEL_TYPES: tuple = _get_openai_sentinel_types()
57
+
10
58
  # Conditional imports for backward compatibility
11
59
  try:
12
60
  from openai.types.responses import (
@@ -24,7 +72,7 @@ try:
24
72
  RESPONSES_AVAILABLE = True
25
73
  except ImportError:
26
74
  # Fallback types for older OpenAI SDK versions
27
- from typing import Any, Dict, List, Union
75
+ from typing import Dict, List
28
76
 
29
77
  # Create basic fallback types
30
78
  FunctionToolParam = Dict[str, Any]
@@ -37,31 +85,25 @@ except ImportError:
37
85
  ResponseOutputMessageParam = Dict[str, Any]
38
86
  RESPONSES_AVAILABLE = False
39
87
 
40
- from openai._legacy_response import LegacyAPIResponse
41
- from opentelemetry import context as context_api
42
- from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
43
- from opentelemetry.semconv._incubating.attributes import (
44
- gen_ai_attributes as GenAIAttributes,
45
- openai_attributes as OpenAIAttributes,
46
- )
47
- from opentelemetry.semconv_ai import SpanAttributes
48
- from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
49
- from opentelemetry.trace import SpanKind, Span, StatusCode, Tracer
50
- from typing import Any, Optional, Union, Literal
51
- from typing_extensions import NotRequired
88
+ SPAN_NAME = "openai.response"
52
89
 
53
- from paid._vendor.opentelemetry.instrumentation.openai.shared import (
54
- _set_span_attribute,
55
- model_as_dict,
56
- )
57
90
 
58
- from paid._vendor.opentelemetry.instrumentation.openai.utils import (
59
- _with_tracer_wrapper,
60
- dont_throw,
61
- should_send_prompts,
62
- )
91
+ def _sanitize_sentinel_values(kwargs: dict) -> dict:
92
+ """Remove OpenAI sentinel values (NOT_GIVEN, Omit) from kwargs.
63
93
 
64
- SPAN_NAME = "openai.response"
94
+ OpenAI SDK uses sentinel objects for unset optional parameters.
95
+ These don't have dict methods like .get(), causing errors when
96
+ code chains calls like kwargs.get("reasoning", {}).get("summary").
97
+
98
+ This removes sentinel values so the default (e.g., {}) is used instead
99
+ when calling .get() on the sanitized dict.
100
+
101
+ If no sentinel types are available (older SDK), returns kwargs unchanged.
102
+ """
103
+ if not _OPENAI_SENTINEL_TYPES:
104
+ return kwargs
105
+ return {k: v for k, v in kwargs.items()
106
+ if not isinstance(v, _OPENAI_SENTINEL_TYPES)}
65
107
 
66
108
 
67
109
  def prepare_input_param(input_param: ResponseInputItemParam) -> ResponseInputItemParam:
@@ -134,8 +176,14 @@ class TracedData(pydantic.BaseModel):
134
176
  response_reasoning_effort: Optional[str] = pydantic.Field(default=None)
135
177
 
136
178
  # OpenAI service tier
137
- request_service_tier: Optional[Literal["auto", "default", "flex", "scale", "priority"]] = pydantic.Field(default=None)
138
- response_service_tier: Optional[Literal["auto", "default", "flex", "scale", "priority"]] = pydantic.Field(default=None)
179
+ request_service_tier: Optional[str] = pydantic.Field(default=None)
180
+ response_service_tier: Optional[str] = pydantic.Field(default=None)
181
+
182
+ # Trace context - to maintain trace continuity across async operations
183
+ trace_context: Any = pydantic.Field(default=None)
184
+
185
+ class Config:
186
+ arbitrary_types_allowed = True
139
187
 
140
188
 
141
189
  responses: dict[str, TracedData] = {}
@@ -189,12 +237,26 @@ def process_content_block(
189
237
 
190
238
 
191
239
  @dont_throw
240
+ def prepare_kwargs_for_shared_attributes(kwargs):
241
+ """
242
+ Prepare kwargs for the shared _set_request_attributes function.
243
+ Maps responses API specific parameters to the common format.
244
+ """
245
+ prepared_kwargs = kwargs.copy()
246
+
247
+ # Map max_output_tokens to max_tokens for the shared function
248
+ if "max_output_tokens" in kwargs:
249
+ prepared_kwargs["max_tokens"] = kwargs["max_output_tokens"]
250
+
251
+ return prepared_kwargs
252
+
253
+
192
254
  def set_data_attributes(traced_response: TracedData, span: Span):
193
- _set_span_attribute(span, GenAIAttributes.GEN_AI_SYSTEM, "openai")
194
- _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MODEL, traced_response.request_model)
195
255
  _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_ID, traced_response.response_id)
196
- _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, traced_response.response_model)
197
- _set_span_attribute(span, OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER, traced_response.request_service_tier)
256
+
257
+ response_model = _extract_model_name_from_provider_format(traced_response.response_model)
258
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response_model)
259
+
198
260
  _set_span_attribute(span, OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER, traced_response.response_service_tier)
199
261
  if usage := traced_response.usage:
200
262
  _set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens)
@@ -437,24 +499,31 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
437
499
  return wrapped(*args, **kwargs)
438
500
  start_time = time.time_ns()
439
501
 
502
+ # Remove OpenAI sentinel values (NOT_GIVEN, Omit) to allow chained .get() calls
503
+ non_sentinel_kwargs = _sanitize_sentinel_values(kwargs)
504
+
440
505
  try:
441
506
  response = wrapped(*args, **kwargs)
442
507
  if isinstance(response, Stream):
508
+ # Capture current trace context to maintain trace continuity
509
+ ctx = context_api.get_current()
443
510
  span = tracer.start_span(
444
511
  SPAN_NAME,
445
512
  kind=SpanKind.CLIENT,
446
513
  start_time=start_time,
514
+ context=ctx,
447
515
  )
516
+ _set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
448
517
 
449
518
  return ResponseStream(
450
519
  span=span,
451
520
  response=response,
452
521
  start_time=start_time,
453
- request_kwargs=kwargs,
522
+ request_kwargs=non_sentinel_kwargs,
454
523
  tracer=tracer,
455
524
  )
456
525
  except Exception as e:
457
- response_id = kwargs.get("response_id")
526
+ response_id = non_sentinel_kwargs.get("response_id")
458
527
  existing_data = {}
459
528
  if response_id and response_id in responses:
460
529
  existing_data = responses[response_id].model_dump()
@@ -463,46 +532,53 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
463
532
  start_time=existing_data.get("start_time", start_time),
464
533
  response_id=response_id or "",
465
534
  input=process_input(
466
- kwargs.get("input", existing_data.get("input", []))
535
+ non_sentinel_kwargs.get("input", existing_data.get("input", []))
467
536
  ),
468
- instructions=kwargs.get(
537
+ instructions=non_sentinel_kwargs.get(
469
538
  "instructions", existing_data.get("instructions")
470
539
  ),
471
- tools=get_tools_from_kwargs(kwargs) or existing_data.get("tools", []),
540
+ tools=get_tools_from_kwargs(non_sentinel_kwargs) or existing_data.get("tools", []),
472
541
  output_blocks=existing_data.get("output_blocks", {}),
473
542
  usage=existing_data.get("usage"),
474
- output_text=kwargs.get(
543
+ output_text=non_sentinel_kwargs.get(
475
544
  "output_text", existing_data.get("output_text", "")
476
545
  ),
477
- request_model=kwargs.get(
546
+ request_model=non_sentinel_kwargs.get(
478
547
  "model", existing_data.get("request_model", "")
479
548
  ),
480
549
  response_model=existing_data.get("response_model", ""),
481
550
  # Reasoning attributes
482
551
  request_reasoning_summary=(
483
- kwargs.get("reasoning", {}).get(
552
+ non_sentinel_kwargs.get("reasoning", {}).get(
484
553
  "summary", existing_data.get("request_reasoning_summary")
485
554
  )
486
555
  ),
487
556
  request_reasoning_effort=(
488
- kwargs.get("reasoning", {}).get(
557
+ non_sentinel_kwargs.get("reasoning", {}).get(
489
558
  "effort", existing_data.get("request_reasoning_effort")
490
559
  )
491
560
  ),
492
- response_reasoning_effort=kwargs.get("reasoning", {}).get("effort"),
493
- request_service_tier=kwargs.get("service_tier"),
561
+ response_reasoning_effort=non_sentinel_kwargs.get("reasoning", {}).get("effort"),
562
+ request_service_tier=non_sentinel_kwargs.get("service_tier"),
494
563
  response_service_tier=existing_data.get("response_service_tier"),
564
+ # Capture trace context to maintain continuity
565
+ trace_context=existing_data.get("trace_context", context_api.get_current()),
495
566
  )
496
567
  except Exception:
497
568
  traced_data = None
498
569
 
570
+ # Restore the original trace context to maintain trace continuity
571
+ ctx = (traced_data.trace_context if traced_data and traced_data.trace_context
572
+ else context_api.get_current())
499
573
  span = tracer.start_span(
500
574
  SPAN_NAME,
501
575
  kind=SpanKind.CLIENT,
502
576
  start_time=(
503
577
  start_time if traced_data is None else int(traced_data.start_time)
504
578
  ),
579
+ context=ctx,
505
580
  )
581
+ _set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
506
582
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
507
583
  span.record_exception(e)
508
584
  span.set_status(StatusCode.ERROR, str(e))
@@ -518,7 +594,7 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
518
594
  else:
519
595
  existing_data = existing_data.model_dump()
520
596
 
521
- request_tools = get_tools_from_kwargs(kwargs)
597
+ request_tools = get_tools_from_kwargs(non_sentinel_kwargs)
522
598
 
523
599
  merged_tools = existing_data.get("tools", []) + request_tools
524
600
 
@@ -534,40 +610,46 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
534
610
  traced_data = TracedData(
535
611
  start_time=existing_data.get("start_time", start_time),
536
612
  response_id=parsed_response.id,
537
- input=process_input(existing_data.get("input", kwargs.get("input"))),
538
- instructions=existing_data.get("instructions", kwargs.get("instructions")),
613
+ input=process_input(existing_data.get("input", non_sentinel_kwargs.get("input"))),
614
+ instructions=existing_data.get("instructions", non_sentinel_kwargs.get("instructions")),
539
615
  tools=merged_tools if merged_tools else None,
540
616
  output_blocks={block.id: block for block in parsed_response.output}
541
617
  | existing_data.get("output_blocks", {}),
542
618
  usage=existing_data.get("usage", parsed_response.usage),
543
619
  output_text=existing_data.get("output_text", parsed_response_output_text),
544
- request_model=existing_data.get("request_model", kwargs.get("model")),
620
+ request_model=existing_data.get("request_model", non_sentinel_kwargs.get("model")),
545
621
  response_model=existing_data.get("response_model", parsed_response.model),
546
622
  # Reasoning attributes
547
623
  request_reasoning_summary=(
548
- kwargs.get("reasoning", {}).get(
624
+ non_sentinel_kwargs.get("reasoning", {}).get(
549
625
  "summary", existing_data.get("request_reasoning_summary")
550
626
  )
551
627
  ),
552
628
  request_reasoning_effort=(
553
- kwargs.get("reasoning", {}).get(
629
+ non_sentinel_kwargs.get("reasoning", {}).get(
554
630
  "effort", existing_data.get("request_reasoning_effort")
555
631
  )
556
632
  ),
557
- response_reasoning_effort=kwargs.get("reasoning", {}).get("effort"),
558
- request_service_tier=existing_data.get("request_service_tier", kwargs.get("service_tier")),
633
+ response_reasoning_effort=non_sentinel_kwargs.get("reasoning", {}).get("effort"),
634
+ request_service_tier=existing_data.get("request_service_tier", non_sentinel_kwargs.get("service_tier")),
559
635
  response_service_tier=existing_data.get("response_service_tier", parsed_response.service_tier),
636
+ # Capture trace context to maintain continuity across async operations
637
+ trace_context=existing_data.get("trace_context", context_api.get_current()),
560
638
  )
561
639
  responses[parsed_response.id] = traced_data
562
640
  except Exception:
563
641
  return response
564
642
 
565
643
  if parsed_response.status == "completed":
644
+ # Restore the original trace context to maintain trace continuity
645
+ ctx = traced_data.trace_context if traced_data.trace_context else context_api.get_current()
566
646
  span = tracer.start_span(
567
647
  SPAN_NAME,
568
648
  kind=SpanKind.CLIENT,
569
649
  start_time=int(traced_data.start_time),
650
+ context=ctx,
570
651
  )
652
+ _set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
571
653
  set_data_attributes(traced_data, span)
572
654
  span.end()
573
655
 
@@ -583,24 +665,31 @@ async def async_responses_get_or_create_wrapper(
583
665
  return await wrapped(*args, **kwargs)
584
666
  start_time = time.time_ns()
585
667
 
668
+ # Remove OpenAI sentinel values (NOT_GIVEN, Omit) to allow chained .get() calls
669
+ non_sentinel_kwargs = _sanitize_sentinel_values(kwargs)
670
+
586
671
  try:
587
672
  response = await wrapped(*args, **kwargs)
588
673
  if isinstance(response, (Stream, AsyncStream)):
674
+ # Capture current trace context to maintain trace continuity
675
+ ctx = context_api.get_current()
589
676
  span = tracer.start_span(
590
677
  SPAN_NAME,
591
678
  kind=SpanKind.CLIENT,
592
679
  start_time=start_time,
680
+ context=ctx,
593
681
  )
682
+ _set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
594
683
 
595
684
  return ResponseStream(
596
685
  span=span,
597
686
  response=response,
598
687
  start_time=start_time,
599
- request_kwargs=kwargs,
688
+ request_kwargs=non_sentinel_kwargs,
600
689
  tracer=tracer,
601
690
  )
602
691
  except Exception as e:
603
- response_id = kwargs.get("response_id")
692
+ response_id = non_sentinel_kwargs.get("response_id")
604
693
  existing_data = {}
605
694
  if response_id and response_id in responses:
606
695
  existing_data = responses[response_id].model_dump()
@@ -609,42 +698,49 @@ async def async_responses_get_or_create_wrapper(
609
698
  start_time=existing_data.get("start_time", start_time),
610
699
  response_id=response_id or "",
611
700
  input=process_input(
612
- kwargs.get("input", existing_data.get("input", []))
701
+ non_sentinel_kwargs.get("input", existing_data.get("input", []))
613
702
  ),
614
- instructions=kwargs.get(
703
+ instructions=non_sentinel_kwargs.get(
615
704
  "instructions", existing_data.get("instructions", "")
616
705
  ),
617
- tools=get_tools_from_kwargs(kwargs) or existing_data.get("tools", []),
706
+ tools=get_tools_from_kwargs(non_sentinel_kwargs) or existing_data.get("tools", []),
618
707
  output_blocks=existing_data.get("output_blocks", {}),
619
708
  usage=existing_data.get("usage"),
620
- output_text=kwargs.get("output_text", existing_data.get("output_text")),
621
- request_model=kwargs.get("model", existing_data.get("request_model")),
709
+ output_text=non_sentinel_kwargs.get("output_text", existing_data.get("output_text")),
710
+ request_model=non_sentinel_kwargs.get("model", existing_data.get("request_model")),
622
711
  response_model=existing_data.get("response_model"),
623
712
  # Reasoning attributes
624
713
  request_reasoning_summary=(
625
- kwargs.get("reasoning", {}).get(
714
+ non_sentinel_kwargs.get("reasoning", {}).get(
626
715
  "summary", existing_data.get("request_reasoning_summary")
627
716
  )
628
717
  ),
629
718
  request_reasoning_effort=(
630
- kwargs.get("reasoning", {}).get(
719
+ non_sentinel_kwargs.get("reasoning", {}).get(
631
720
  "effort", existing_data.get("request_reasoning_effort")
632
721
  )
633
722
  ),
634
- response_reasoning_effort=kwargs.get("reasoning", {}).get("effort"),
635
- request_service_tier=kwargs.get("service_tier"),
723
+ response_reasoning_effort=non_sentinel_kwargs.get("reasoning", {}).get("effort"),
724
+ request_service_tier=non_sentinel_kwargs.get("service_tier"),
636
725
  response_service_tier=existing_data.get("response_service_tier"),
726
+ # Capture trace context to maintain continuity
727
+ trace_context=existing_data.get("trace_context", context_api.get_current()),
637
728
  )
638
729
  except Exception:
639
730
  traced_data = None
640
731
 
732
+ # Restore the original trace context to maintain trace continuity
733
+ ctx = (traced_data.trace_context if traced_data and traced_data.trace_context
734
+ else context_api.get_current())
641
735
  span = tracer.start_span(
642
736
  SPAN_NAME,
643
737
  kind=SpanKind.CLIENT,
644
738
  start_time=(
645
739
  start_time if traced_data is None else int(traced_data.start_time)
646
740
  ),
741
+ context=ctx,
647
742
  )
743
+ _set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
648
744
  span.set_attribute(ERROR_TYPE, e.__class__.__name__)
649
745
  span.record_exception(e)
650
746
  span.set_status(StatusCode.ERROR, str(e))
@@ -660,7 +756,7 @@ async def async_responses_get_or_create_wrapper(
660
756
  else:
661
757
  existing_data = existing_data.model_dump()
662
758
 
663
- request_tools = get_tools_from_kwargs(kwargs)
759
+ request_tools = get_tools_from_kwargs(non_sentinel_kwargs)
664
760
 
665
761
  merged_tools = existing_data.get("tools", []) + request_tools
666
762
 
@@ -677,40 +773,46 @@ async def async_responses_get_or_create_wrapper(
677
773
  traced_data = TracedData(
678
774
  start_time=existing_data.get("start_time", start_time),
679
775
  response_id=parsed_response.id,
680
- input=process_input(existing_data.get("input", kwargs.get("input"))),
681
- instructions=existing_data.get("instructions", kwargs.get("instructions")),
776
+ input=process_input(existing_data.get("input", non_sentinel_kwargs.get("input"))),
777
+ instructions=existing_data.get("instructions", non_sentinel_kwargs.get("instructions")),
682
778
  tools=merged_tools if merged_tools else None,
683
779
  output_blocks={block.id: block for block in parsed_response.output}
684
780
  | existing_data.get("output_blocks", {}),
685
781
  usage=existing_data.get("usage", parsed_response.usage),
686
782
  output_text=existing_data.get("output_text", parsed_response_output_text),
687
- request_model=existing_data.get("request_model", kwargs.get("model")),
783
+ request_model=existing_data.get("request_model", non_sentinel_kwargs.get("model")),
688
784
  response_model=existing_data.get("response_model", parsed_response.model),
689
785
  # Reasoning attributes
690
786
  request_reasoning_summary=(
691
- kwargs.get("reasoning", {}).get(
787
+ non_sentinel_kwargs.get("reasoning", {}).get(
692
788
  "summary", existing_data.get("request_reasoning_summary")
693
789
  )
694
790
  ),
695
791
  request_reasoning_effort=(
696
- kwargs.get("reasoning", {}).get(
792
+ non_sentinel_kwargs.get("reasoning", {}).get(
697
793
  "effort", existing_data.get("request_reasoning_effort")
698
794
  )
699
795
  ),
700
- response_reasoning_effort=kwargs.get("reasoning", {}).get("effort"),
701
- request_service_tier=existing_data.get("request_service_tier", kwargs.get("service_tier")),
796
+ response_reasoning_effort=non_sentinel_kwargs.get("reasoning", {}).get("effort"),
797
+ request_service_tier=existing_data.get("request_service_tier", non_sentinel_kwargs.get("service_tier")),
702
798
  response_service_tier=existing_data.get("response_service_tier", parsed_response.service_tier),
799
+ # Capture trace context to maintain continuity across async operations
800
+ trace_context=existing_data.get("trace_context", context_api.get_current()),
703
801
  )
704
802
  responses[parsed_response.id] = traced_data
705
803
  except Exception:
706
804
  return response
707
805
 
708
806
  if parsed_response.status == "completed":
807
+ # Restore the original trace context to maintain trace continuity
808
+ ctx = traced_data.trace_context if traced_data.trace_context else context_api.get_current()
709
809
  span = tracer.start_span(
710
810
  SPAN_NAME,
711
811
  kind=SpanKind.CLIENT,
712
812
  start_time=int(traced_data.start_time),
813
+ context=ctx,
713
814
  )
815
+ _set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
714
816
  set_data_attributes(traced_data, span)
715
817
  span.end()
716
818
 
@@ -723,18 +825,24 @@ def responses_cancel_wrapper(tracer: Tracer, wrapped, instance, args, kwargs):
723
825
  if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
724
826
  return wrapped(*args, **kwargs)
725
827
 
828
+ non_sentinel_kwargs = _sanitize_sentinel_values(kwargs)
829
+
726
830
  response = wrapped(*args, **kwargs)
727
831
  if isinstance(response, Stream):
728
832
  return response
729
833
  parsed_response = parse_response(response)
730
834
  existing_data = responses.pop(parsed_response.id, None)
731
835
  if existing_data is not None:
836
+ # Restore the original trace context to maintain trace continuity
837
+ ctx = existing_data.trace_context if existing_data.trace_context else context_api.get_current()
732
838
  span = tracer.start_span(
733
839
  SPAN_NAME,
734
840
  kind=SpanKind.CLIENT,
735
841
  start_time=existing_data.start_time,
736
842
  record_exception=True,
843
+ context=ctx,
737
844
  )
845
+ _set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
738
846
  span.record_exception(Exception("Response cancelled"))
739
847
  set_data_attributes(existing_data, span)
740
848
  span.end()
@@ -749,18 +857,24 @@ async def async_responses_cancel_wrapper(
749
857
  if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
750
858
  return await wrapped(*args, **kwargs)
751
859
 
860
+ non_sentinel_kwargs = _sanitize_sentinel_values(kwargs)
861
+
752
862
  response = await wrapped(*args, **kwargs)
753
863
  if isinstance(response, (Stream, AsyncStream)):
754
864
  return response
755
865
  parsed_response = parse_response(response)
756
866
  existing_data = responses.pop(parsed_response.id, None)
757
867
  if existing_data is not None:
868
+ # Restore the original trace context to maintain trace continuity
869
+ ctx = existing_data.trace_context if existing_data.trace_context else context_api.get_current()
758
870
  span = tracer.start_span(
759
871
  SPAN_NAME,
760
872
  kind=SpanKind.CLIENT,
761
873
  start_time=existing_data.start_time,
762
874
  record_exception=True,
875
+ context=ctx,
763
876
  )
877
+ _set_request_attributes(span, prepare_kwargs_for_shared_attributes(non_sentinel_kwargs), instance)
764
878
  span.record_exception(Exception("Response cancelled"))
765
879
  set_data_attributes(existing_data, span)
766
880
  span.end()
@@ -788,7 +902,8 @@ class ResponseStream(ObjectProxy):
788
902
  super().__init__(response)
789
903
  self._span = span
790
904
  self._start_time = start_time
791
- self._request_kwargs = request_kwargs or {}
905
+ # Filter sentinel values (defensive, in case called directly without prior filtering)
906
+ self._request_kwargs = _sanitize_sentinel_values(request_kwargs or {})
792
907
  self._tracer = tracer
793
908
  self._traced_data = traced_data or TracedData(
794
909
  start_time=start_time,
@@ -804,7 +919,9 @@ class ResponseStream(ObjectProxy):
804
919
  request_reasoning_summary=self._request_kwargs.get("reasoning", {}).get(
805
920
  "summary"
806
921
  ),
807
- request_reasoning_effort=self._request_kwargs.get("reasoning", {}).get("effort"),
922
+ request_reasoning_effort=self._request_kwargs.get("reasoning", {}).get(
923
+ "effort"
924
+ ),
808
925
  response_reasoning_effort=None,
809
926
  request_service_tier=self._request_kwargs.get("service_tier"),
810
927
  response_service_tier=None,
@@ -0,0 +1 @@
1
+ __version__ = "0.49.7"
paid/client.py CHANGED
@@ -1,8 +1,13 @@
1
1
  # This file was auto-generated by Fern from our API Definition.
2
+ import os
2
3
  import typing
3
4
  import warnings
4
5
 
5
6
  import httpx
7
+ from dotenv import load_dotenv
8
+
9
+ # Load environment variables from .env file
10
+ load_dotenv()
6
11
  from .agents.client import AgentsClient, AsyncAgentsClient
7
12
  from .contacts.client import AsyncContactsClient, ContactsClient
8
13
  from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
@@ -60,11 +65,19 @@ class Paid:
60
65
  *,
61
66
  base_url: typing.Optional[str] = None,
62
67
  environment: PaidEnvironment = PaidEnvironment.PRODUCTION,
63
- token: typing.Union[str, typing.Callable[[], str]],
68
+ token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None,
64
69
  timeout: typing.Optional[float] = None,
65
70
  follow_redirects: typing.Optional[bool] = True,
66
71
  httpx_client: typing.Optional[httpx.Client] = None,
67
72
  ):
73
+ # If token is not provided, try to get it from environment variable
74
+ if token is None:
75
+ token = os.environ.get("PAID_API_KEY")
76
+ if token is None:
77
+ raise ValueError(
78
+ "API token must be provided either via the 'token' parameter or the 'PAID_API_KEY' environment variable"
79
+ )
80
+
68
81
  _defaulted_timeout = (
69
82
  timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read
70
83
  )
@@ -338,11 +351,19 @@ class AsyncPaid:
338
351
  *,
339
352
  base_url: typing.Optional[str] = None,
340
353
  environment: PaidEnvironment = PaidEnvironment.PRODUCTION,
341
- token: typing.Union[str, typing.Callable[[], str]],
354
+ token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None,
342
355
  timeout: typing.Optional[float] = None,
343
356
  follow_redirects: typing.Optional[bool] = True,
344
357
  httpx_client: typing.Optional[httpx.AsyncClient] = None,
345
358
  ):
359
+ # If token is not provided, try to get it from environment variable
360
+ if token is None:
361
+ token = os.environ.get("PAID_API_KEY")
362
+ if token is None:
363
+ raise ValueError(
364
+ "API token must be provided either via the 'token' parameter or the 'PAID_API_KEY' environment variable"
365
+ )
366
+
346
367
  _defaulted_timeout = (
347
368
  timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read
348
369
  )
paid/tracing/__init__.py CHANGED
@@ -7,13 +7,14 @@ from .distributed_tracing import (
7
7
  unset_tracing_token,
8
8
  )
9
9
  from .signal import signal
10
- from .tracing import initialize_tracing
10
+ from .tracing import get_paid_tracer_provider, initialize_tracing
11
11
 
12
12
  __all__ = [
13
13
  "generate_tracing_token",
14
14
  "paid_autoinstrument",
15
15
  "paid_tracing",
16
16
  "initialize_tracing",
17
+ "get_paid_tracer_provider",
17
18
  "set_tracing_token",
18
19
  "unset_tracing_token",
19
20
  "signal",
@@ -22,8 +22,7 @@ except ImportError:
22
22
  ANTHROPIC_AVAILABLE = False
23
23
 
24
24
  try:
25
- # from opentelemetry.instrumentation.openai import OpenAIInstrumentor
26
- from paid._vendor.opentelemetry.instrumentation.openai import OpenAIInstrumentor # remove once openai instrumentor is upstream
25
+ from opentelemetry.instrumentation.openai import OpenAIInstrumentor
27
26
 
28
27
  OPENAI_AVAILABLE = True
29
28
  except ImportError:
paid/tracing/tracing.py CHANGED
@@ -56,6 +56,22 @@ def set_token(token: str) -> None:
56
56
  # Initialized at module load with defaults, never None (uses no-op provider if not initialized or API key isn't available)
57
57
  paid_tracer_provider: Union[TracerProvider, NoOpTracerProvider] = NoOpTracerProvider()
58
58
 
59
+ def get_paid_tracer_provider() -> Optional[TracerProvider]:
60
+ """Export the tracer provider to the user.
61
+ Initialize tracing if not already. Never return NoOpTracerProvider.
62
+
63
+ Returns:
64
+ The tracer provider instance.
65
+ """
66
+ global paid_tracer_provider
67
+
68
+ if get_token() is None:
69
+ initialize_tracing()
70
+
71
+ if not isinstance(paid_tracer_provider, TracerProvider):
72
+ return None
73
+
74
+ return paid_tracer_provider
59
75
 
60
76
  class PaidSpanProcessor(SpanProcessor):
61
77
  """