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.
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/__init__.py +6 -6
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/__init__.py +2 -2
- opentelemetry/instrumentation/openai/shared/audio_wrappers.py +247 -0
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/chat_wrappers.py +5 -5
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/completion_wrappers.py +5 -5
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/embeddings_wrappers.py +5 -5
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/event_emitter.py +2 -2
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/image_gen_wrappers.py +3 -3
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/utils.py +24 -1
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v0/__init__.py +6 -6
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/__init__.py +45 -9
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/assistant_wrappers.py +6 -6
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/event_handler_wrapper.py +4 -4
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/responses_wrappers.py +186 -69
- opentelemetry/instrumentation/openai/version.py +1 -0
- paid/client.py +23 -2
- paid/tracing/__init__.py +2 -1
- paid/tracing/autoinstrumentation.py +1 -2
- paid/tracing/tracing.py +16 -0
- {paid_python-0.3.6.dist-info → paid_python-0.4.1.dist-info}/METADATA +9 -1
- {paid_python-0.3.6.dist-info → paid_python-0.4.1.dist-info}/RECORD +26 -28
- paid/_vendor/__init__.py +0 -0
- paid/_vendor/opentelemetry/__init__.py +0 -0
- paid/_vendor/opentelemetry/instrumentation/__init__.py +0 -0
- paid/_vendor/opentelemetry/instrumentation/openai/version.py +0 -1
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/config.py +0 -0
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/event_models.py +0 -0
- {paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/shared/span_utils.py +0 -0
- {paid_python-0.3.6.dist-info → paid_python-0.4.1.dist-info}/LICENSE +0 -0
- {paid_python-0.3.6.dist-info → paid_python-0.4.1.dist-info}/WHEEL +0 -0
{paid/_vendor/opentelemetry → opentelemetry}/instrumentation/openai/v1/responses_wrappers.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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[
|
|
138
|
-
response_service_tier: Optional[
|
|
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
|
-
|
|
197
|
-
|
|
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=
|
|
522
|
+
request_kwargs=non_sentinel_kwargs,
|
|
454
523
|
tracer=tracer,
|
|
455
524
|
)
|
|
456
525
|
except Exception as e:
|
|
457
|
-
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
|
-
|
|
535
|
+
non_sentinel_kwargs.get("input", existing_data.get("input", []))
|
|
467
536
|
),
|
|
468
|
-
instructions=
|
|
537
|
+
instructions=non_sentinel_kwargs.get(
|
|
469
538
|
"instructions", existing_data.get("instructions")
|
|
470
539
|
),
|
|
471
|
-
tools=get_tools_from_kwargs(
|
|
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=
|
|
543
|
+
output_text=non_sentinel_kwargs.get(
|
|
475
544
|
"output_text", existing_data.get("output_text", "")
|
|
476
545
|
),
|
|
477
|
-
request_model=
|
|
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
|
-
|
|
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
|
-
|
|
557
|
+
non_sentinel_kwargs.get("reasoning", {}).get(
|
|
489
558
|
"effort", existing_data.get("request_reasoning_effort")
|
|
490
559
|
)
|
|
491
560
|
),
|
|
492
|
-
response_reasoning_effort=
|
|
493
|
-
request_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(
|
|
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",
|
|
538
|
-
instructions=existing_data.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",
|
|
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
|
-
|
|
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
|
-
|
|
629
|
+
non_sentinel_kwargs.get("reasoning", {}).get(
|
|
554
630
|
"effort", existing_data.get("request_reasoning_effort")
|
|
555
631
|
)
|
|
556
632
|
),
|
|
557
|
-
response_reasoning_effort=
|
|
558
|
-
request_service_tier=existing_data.get("request_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=
|
|
688
|
+
request_kwargs=non_sentinel_kwargs,
|
|
600
689
|
tracer=tracer,
|
|
601
690
|
)
|
|
602
691
|
except Exception as e:
|
|
603
|
-
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
|
-
|
|
701
|
+
non_sentinel_kwargs.get("input", existing_data.get("input", []))
|
|
613
702
|
),
|
|
614
|
-
instructions=
|
|
703
|
+
instructions=non_sentinel_kwargs.get(
|
|
615
704
|
"instructions", existing_data.get("instructions", "")
|
|
616
705
|
),
|
|
617
|
-
tools=get_tools_from_kwargs(
|
|
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=
|
|
621
|
-
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
|
-
|
|
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
|
-
|
|
719
|
+
non_sentinel_kwargs.get("reasoning", {}).get(
|
|
631
720
|
"effort", existing_data.get("request_reasoning_effort")
|
|
632
721
|
)
|
|
633
722
|
),
|
|
634
|
-
response_reasoning_effort=
|
|
635
|
-
request_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(
|
|
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",
|
|
681
|
-
instructions=existing_data.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",
|
|
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
|
-
|
|
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
|
-
|
|
792
|
+
non_sentinel_kwargs.get("reasoning", {}).get(
|
|
697
793
|
"effort", existing_data.get("request_reasoning_effort")
|
|
698
794
|
)
|
|
699
795
|
),
|
|
700
|
-
response_reasoning_effort=
|
|
701
|
-
request_service_tier=existing_data.get("request_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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
"""
|