payi 0.1.0a107__py3-none-any.whl → 0.1.0a137__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.
- payi/__init__.py +3 -1
- payi/_base_client.py +12 -12
- payi/_client.py +8 -8
- payi/_compat.py +48 -48
- payi/_models.py +87 -59
- payi/_qs.py +7 -7
- payi/_streaming.py +4 -6
- payi/_types.py +53 -12
- payi/_utils/__init__.py +9 -2
- payi/_utils/_compat.py +45 -0
- payi/_utils/_datetime_parse.py +136 -0
- payi/_utils/_sync.py +3 -31
- payi/_utils/_transform.py +13 -3
- payi/_utils/_typing.py +6 -1
- payi/_utils/_utils.py +5 -6
- payi/_version.py +1 -1
- payi/lib/AnthropicInstrumentor.py +83 -57
- payi/lib/BedrockInstrumentor.py +292 -57
- payi/lib/GoogleGenAiInstrumentor.py +18 -31
- payi/lib/OpenAIInstrumentor.py +56 -72
- payi/lib/ProviderRequest.py +216 -0
- payi/lib/StreamWrappers.py +379 -0
- payi/lib/VertexInstrumentor.py +18 -37
- payi/lib/VertexRequest.py +16 -2
- payi/lib/data/cohere_embed_english_v3.json +30706 -0
- payi/lib/helpers.py +62 -5
- payi/lib/instrument.py +433 -659
- payi/resources/categories/__init__.py +0 -14
- payi/resources/categories/categories.py +25 -53
- payi/resources/categories/resources.py +27 -23
- payi/resources/ingest.py +126 -132
- payi/resources/limits/__init__.py +14 -14
- payi/resources/limits/limits.py +58 -58
- payi/resources/limits/properties.py +171 -0
- payi/resources/requests/request_id/properties.py +8 -8
- payi/resources/requests/request_id/result.py +3 -3
- payi/resources/requests/response_id/properties.py +8 -8
- payi/resources/requests/response_id/result.py +3 -3
- payi/resources/use_cases/definitions/definitions.py +27 -27
- payi/resources/use_cases/definitions/kpis.py +23 -23
- payi/resources/use_cases/definitions/limit_config.py +14 -14
- payi/resources/use_cases/definitions/version.py +3 -3
- payi/resources/use_cases/kpis.py +15 -15
- payi/resources/use_cases/properties.py +6 -6
- payi/resources/use_cases/use_cases.py +7 -7
- payi/types/__init__.py +2 -0
- payi/types/bulk_ingest_response.py +3 -20
- payi/types/categories/__init__.py +0 -1
- payi/types/categories/resource_list_params.py +5 -1
- payi/types/category_list_resources_params.py +5 -1
- payi/types/category_resource_response.py +31 -1
- payi/types/ingest_event_param.py +7 -6
- payi/types/ingest_units_params.py +5 -4
- payi/types/limit_create_params.py +3 -3
- payi/types/limit_list_response.py +1 -3
- payi/types/limit_response.py +1 -3
- payi/types/limits/__init__.py +2 -9
- payi/types/limits/{tag_remove_params.py → property_update_params.py} +4 -5
- payi/types/limits/{tag_delete_response.py → property_update_response.py} +3 -3
- payi/types/requests/request_id/property_update_params.py +2 -2
- payi/types/requests/response_id/property_update_params.py +2 -2
- payi/types/shared/__init__.py +2 -0
- payi/types/shared/api_error.py +18 -0
- payi/types/shared/pay_i_common_models_budget_management_create_limit_base.py +3 -3
- payi/types/shared/properties_request.py +11 -0
- payi/types/shared/xproxy_result.py +2 -0
- payi/types/shared_params/pay_i_common_models_budget_management_create_limit_base.py +3 -3
- payi/types/use_cases/definitions/limit_config_create_params.py +3 -3
- payi/types/use_cases/property_update_params.py +2 -2
- {payi-0.1.0a107.dist-info → payi-0.1.0a137.dist-info}/METADATA +6 -6
- {payi-0.1.0a107.dist-info → payi-0.1.0a137.dist-info}/RECORD +73 -75
- payi/resources/categories/fixed_cost_resources.py +0 -196
- payi/resources/limits/tags.py +0 -507
- payi/types/categories/fixed_cost_resource_create_params.py +0 -21
- payi/types/limits/limit_tags.py +0 -16
- payi/types/limits/tag_create_params.py +0 -13
- payi/types/limits/tag_create_response.py +0 -10
- payi/types/limits/tag_list_response.py +0 -10
- payi/types/limits/tag_remove_response.py +0 -10
- payi/types/limits/tag_update_params.py +0 -13
- payi/types/limits/tag_update_response.py +0 -10
- {payi-0.1.0a107.dist-info → payi-0.1.0a137.dist-info}/WHEEL +0 -0
- {payi-0.1.0a107.dist-info → payi-0.1.0a137.dist-info}/licenses/LICENSE +0 -0
payi/lib/instrument.py
CHANGED
|
@@ -1,150 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
4
|
+
import copy
|
|
2
5
|
import json
|
|
3
6
|
import time
|
|
4
7
|
import uuid
|
|
8
|
+
import atexit
|
|
5
9
|
import asyncio
|
|
6
10
|
import inspect
|
|
7
11
|
import logging
|
|
12
|
+
import threading
|
|
8
13
|
import traceback
|
|
9
|
-
from abc import abstractmethod
|
|
10
14
|
from enum import Enum
|
|
11
|
-
from typing import Any, Set, Union,
|
|
15
|
+
from typing import Any, Set, Union, Optional, Sequence, TypedDict, cast
|
|
12
16
|
from datetime import datetime, timezone
|
|
13
|
-
from dataclasses import dataclass
|
|
14
17
|
|
|
15
18
|
import nest_asyncio # type: ignore
|
|
16
|
-
from wrapt import
|
|
19
|
+
from wrapt import wrap_function_wrapper # type: ignore
|
|
17
20
|
|
|
18
21
|
from payi import Payi, AsyncPayi, APIStatusError, APIConnectionError, __version__ as _payi_version
|
|
19
22
|
from payi.types import IngestUnitsParams
|
|
20
|
-
from payi.lib.helpers import PayiHeaderNames
|
|
23
|
+
from payi.lib.helpers import PayiHeaderNames, _compact_json
|
|
21
24
|
from payi.types.shared import XproxyResult
|
|
22
25
|
from payi.types.ingest_response import IngestResponse
|
|
23
|
-
from payi.types.ingest_units_params import Units, ProviderResponseFunctionCall
|
|
24
26
|
from payi.types.shared.xproxy_error import XproxyError
|
|
25
27
|
from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
|
|
26
28
|
|
|
27
29
|
from .helpers import PayiCategories
|
|
28
30
|
from .Stopwatch import Stopwatch
|
|
31
|
+
from .StreamWrappers import _GeneratorWrapper, _StreamManagerWrapper, _StreamIteratorWrapper
|
|
32
|
+
from .ProviderRequest import PriceAs, _StreamingType, _ProviderRequest
|
|
29
33
|
|
|
30
34
|
global _g_logger
|
|
31
35
|
_g_logger: logging.Logger = logging.getLogger("payi.instrument")
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def __init__(
|
|
40
|
-
self,
|
|
41
|
-
instrumentor: '_PayiInstrumentor',
|
|
42
|
-
category: str,
|
|
43
|
-
streaming_type: '_StreamingType',
|
|
44
|
-
module_name: str,
|
|
45
|
-
module_version: str,
|
|
46
|
-
is_aws_client: Optional[bool] = None,
|
|
47
|
-
is_google_vertex_or_genai_client: Optional[bool] = None,
|
|
48
|
-
) -> None:
|
|
49
|
-
self._instrumentor: '_PayiInstrumentor' = instrumentor
|
|
50
|
-
self._module_name: str = module_name
|
|
51
|
-
self._module_version: str = module_version
|
|
52
|
-
self._estimated_prompt_tokens: Optional[int] = None
|
|
53
|
-
self._category: str = category
|
|
54
|
-
self._ingest: IngestUnitsParams = { "category": category, "units": {} } # type: ignore
|
|
55
|
-
self._streaming_type: '_StreamingType' = streaming_type
|
|
56
|
-
self._is_aws_client: Optional[bool] = is_aws_client
|
|
57
|
-
self._is_google_vertex_or_genai_client: Optional[bool] = is_google_vertex_or_genai_client
|
|
58
|
-
self._function_call_builder: Optional[dict[int, ProviderResponseFunctionCall]] = None
|
|
59
|
-
self._building_function_response: bool = False
|
|
60
|
-
self._function_calls: Optional[list[ProviderResponseFunctionCall]] = None
|
|
61
|
-
|
|
62
|
-
def process_chunk(self, _chunk: Any) -> _ChunkResult:
|
|
63
|
-
return _ChunkResult(send_chunk_to_caller=True)
|
|
64
|
-
|
|
65
|
-
def process_synchronous_response(self, response: Any, log_prompt_and_response: bool, kwargs: Any) -> Optional[object]: # noqa: ARG002
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
@abstractmethod
|
|
69
|
-
def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
|
|
70
|
-
...
|
|
71
|
-
|
|
72
|
-
def process_request_prompt(self, prompt: 'dict[str, Any]', args: Sequence[Any], kwargs: 'dict[str, Any]') -> None:
|
|
73
|
-
...
|
|
74
|
-
|
|
75
|
-
def process_initial_stream_response(self, response: Any) -> None:
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
def remove_inline_data(self, prompt: 'dict[str, Any]') -> bool:# noqa: ARG002
|
|
79
|
-
return False
|
|
80
|
-
|
|
81
|
-
@property
|
|
82
|
-
def is_aws_client(self) -> bool:
|
|
83
|
-
return self._is_aws_client if self._is_aws_client is not None else False
|
|
84
|
-
|
|
85
|
-
@property
|
|
86
|
-
def is_google_vertex_or_genai_client(self) -> bool:
|
|
87
|
-
return self._is_google_vertex_or_genai_client if self._is_google_vertex_or_genai_client is not None else False
|
|
88
|
-
|
|
89
|
-
def process_exception(self, exception: Exception, kwargs: Any, ) -> bool: # noqa: ARG002
|
|
90
|
-
self.exception_to_semantic_failure(exception)
|
|
91
|
-
return True
|
|
92
|
-
|
|
93
|
-
@property
|
|
94
|
-
def supports_extra_headers(self) -> bool:
|
|
95
|
-
return not self.is_aws_client and not self.is_google_vertex_or_genai_client
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def streaming_type(self) -> '_StreamingType':
|
|
99
|
-
return self._streaming_type
|
|
100
|
-
|
|
101
|
-
def exception_to_semantic_failure(self, e: Exception) -> None:
|
|
102
|
-
exception_str = f"{type(e).__name__}"
|
|
103
|
-
|
|
104
|
-
fields: list[str] = []
|
|
105
|
-
|
|
106
|
-
for attr in dir(e):
|
|
107
|
-
if not attr.startswith("__"):
|
|
108
|
-
try:
|
|
109
|
-
value = getattr(e, attr)
|
|
110
|
-
if value and not inspect.ismethod(value) and not inspect.isfunction(value) and not callable(value):
|
|
111
|
-
fields.append(f"{attr}={value}")
|
|
112
|
-
except Exception as _ex:
|
|
113
|
-
pass
|
|
114
|
-
|
|
115
|
-
existing_properties = self._ingest.get("properties", None)
|
|
116
|
-
if not existing_properties:
|
|
117
|
-
existing_properties = {}
|
|
118
|
-
|
|
119
|
-
existing_properties['system.failure'] = exception_str
|
|
120
|
-
if fields:
|
|
121
|
-
failure_description = ",".join(fields)
|
|
122
|
-
existing_properties["system.failure.description"] = failure_description[:128]
|
|
123
|
-
|
|
124
|
-
self._ingest["properties"] = existing_properties
|
|
37
|
+
class PayiInstrumentModelMapping(TypedDict, total=False):
|
|
38
|
+
model: str
|
|
39
|
+
price_as_category: Optional[str]
|
|
40
|
+
price_as_resource: Optional[str]
|
|
41
|
+
# "global", "datazone", "region", "region.<region_name>"
|
|
42
|
+
resource_scope: Optional[str]
|
|
125
43
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
44
|
+
class PayiInstrumentAwsBedrockConfig(TypedDict, total=False):
|
|
45
|
+
guardrail_trace: Optional[bool]
|
|
46
|
+
add_streaming_xproxy_result: Optional[bool]
|
|
47
|
+
model_mappings: Optional[Sequence[PayiInstrumentModelMapping]]
|
|
129
48
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
49
|
+
class PayiInstrumentAzureOpenAiConfig(TypedDict, total=False):
|
|
50
|
+
# map deployment name known model
|
|
51
|
+
model_mappings: Sequence[PayiInstrumentModelMapping]
|
|
133
52
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
else:
|
|
137
|
-
function = self._function_call_builder[index]
|
|
138
|
-
if name:
|
|
139
|
-
function["name"] = function["name"] + name
|
|
140
|
-
if arguments:
|
|
141
|
-
function["arguments"] = (function.get("arguments", "") or "") + arguments
|
|
142
|
-
|
|
143
|
-
def add_synchronous_function_call(self, name: str, arguments: Optional[str]) -> None:
|
|
144
|
-
if not self._function_calls:
|
|
145
|
-
self._function_calls = []
|
|
146
|
-
self._ingest["provider_response_function_calls"] = self._function_calls
|
|
147
|
-
self._function_calls.append(ProviderResponseFunctionCall(name=name, arguments=arguments))
|
|
53
|
+
class PayiInstrumentOfflineInstrumentationConfig(TypedDict, total=False):
|
|
54
|
+
file_name: str
|
|
148
55
|
|
|
149
56
|
class PayiInstrumentConfig(TypedDict, total=False):
|
|
150
57
|
proxy: bool
|
|
@@ -155,37 +62,55 @@ class PayiInstrumentConfig(TypedDict, total=False):
|
|
|
155
62
|
use_case_name: Optional[str]
|
|
156
63
|
use_case_id: Optional[str]
|
|
157
64
|
use_case_version: Optional[int]
|
|
158
|
-
use_case_properties: Optional["dict[str, str]"]
|
|
65
|
+
use_case_properties: Optional["dict[str, Optional[str]]"]
|
|
159
66
|
user_id: Optional[str]
|
|
67
|
+
account_name: Optional[str]
|
|
160
68
|
request_tags: Optional["list[str]"]
|
|
161
|
-
request_properties: Optional["dict[str, str]"]
|
|
69
|
+
request_properties: Optional["dict[str, Optional[str]]"]
|
|
70
|
+
aws_config: Optional[PayiInstrumentAwsBedrockConfig]
|
|
71
|
+
azure_openai_config: Optional[PayiInstrumentAzureOpenAiConfig]
|
|
72
|
+
offline_instrumentation: Optional[PayiInstrumentOfflineInstrumentationConfig]
|
|
162
73
|
|
|
163
74
|
class PayiContext(TypedDict, total=False):
|
|
164
75
|
use_case_name: Optional[str]
|
|
165
76
|
use_case_id: Optional[str]
|
|
166
77
|
use_case_version: Optional[int]
|
|
167
78
|
use_case_step: Optional[str]
|
|
168
|
-
use_case_properties: Optional["dict[str, str]"]
|
|
79
|
+
use_case_properties: Optional["dict[str, Optional[str]]"]
|
|
169
80
|
limit_ids: Optional['list[str]']
|
|
170
81
|
user_id: Optional[str]
|
|
82
|
+
account_name: Optional[str]
|
|
171
83
|
request_tags: Optional["list[str]"]
|
|
172
|
-
request_properties: Optional["dict[str, str]"]
|
|
84
|
+
request_properties: Optional["dict[str, Optional[str]]"]
|
|
173
85
|
price_as_category: Optional[str]
|
|
174
86
|
price_as_resource: Optional[str]
|
|
175
87
|
resource_scope: Optional[str]
|
|
176
88
|
last_result: Optional[Union[XproxyResult, XproxyError]]
|
|
177
89
|
|
|
90
|
+
class PayiInstanceDefaultContext(TypedDict, total=False):
|
|
91
|
+
use_case_name: Optional[str]
|
|
92
|
+
use_case_id: Optional[str]
|
|
93
|
+
use_case_version: Optional[int]
|
|
94
|
+
use_case_properties: Optional["dict[str, str]"]
|
|
95
|
+
limit_ids: Optional['list[str]']
|
|
96
|
+
user_id: Optional[str]
|
|
97
|
+
account_name: Optional[str]
|
|
98
|
+
request_properties: Optional["dict[str, str]"]
|
|
99
|
+
price_as_category: Optional[str]
|
|
100
|
+
price_as_resource: Optional[str]
|
|
101
|
+
resource_scope: Optional[str]
|
|
102
|
+
|
|
178
103
|
class _Context(TypedDict, total=False):
|
|
179
104
|
proxy: Optional[bool]
|
|
180
105
|
use_case_name: Optional[str]
|
|
181
106
|
use_case_id: Optional[str]
|
|
182
107
|
use_case_version: Optional[int]
|
|
183
108
|
use_case_step: Optional[str]
|
|
184
|
-
use_case_properties: Optional["dict[str, str]"]
|
|
109
|
+
use_case_properties: Optional["dict[str, Optional[str]]"]
|
|
185
110
|
limit_ids: Optional['list[str]']
|
|
186
111
|
user_id: Optional[str]
|
|
187
|
-
|
|
188
|
-
request_properties: Optional["dict[str, str]"]
|
|
112
|
+
account_name: Optional[str]
|
|
113
|
+
request_properties: Optional["dict[str, Optional[str]]"]
|
|
189
114
|
price_as_category: Optional[str]
|
|
190
115
|
price_as_resource: Optional[str]
|
|
191
116
|
resource_scope: Optional[str]
|
|
@@ -195,10 +120,14 @@ class _IsStreaming(Enum):
|
|
|
195
120
|
true = 1
|
|
196
121
|
kwargs = 2
|
|
197
122
|
|
|
198
|
-
class
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
123
|
+
class _ThreadLocalContextStorage(threading.local):
|
|
124
|
+
"""
|
|
125
|
+
Thread-local storage for context stacks. Each thread gets its own context stack.
|
|
126
|
+
|
|
127
|
+
Note: We don't use __init__ because threading.local's __init__ semantics are tricky.
|
|
128
|
+
Instead, we lazily initialize the context_stack attribute in the property accessor.
|
|
129
|
+
"""
|
|
130
|
+
context_stack: "list[_Context]"
|
|
202
131
|
|
|
203
132
|
class _InternalTrackContext:
|
|
204
133
|
def __init__(
|
|
@@ -230,9 +159,6 @@ class _PayiInstrumentor:
|
|
|
230
159
|
instruments: Union[Set[str], None] = None,
|
|
231
160
|
log_prompt_and_response: bool = True,
|
|
232
161
|
logger: Optional[logging.Logger] = None,
|
|
233
|
-
prompt_and_response_logger: Optional[
|
|
234
|
-
Callable[[str, "dict[str, str]"], None]
|
|
235
|
-
] = None, # (request id, dict of data to store) -> None
|
|
236
162
|
global_config: PayiInstrumentConfig = {},
|
|
237
163
|
caller_filename: str = ""
|
|
238
164
|
):
|
|
@@ -249,14 +175,16 @@ class _PayiInstrumentor:
|
|
|
249
175
|
if self._apayi:
|
|
250
176
|
_g_logger.debug(f"Pay-i instrumentor initialized with AsyncPayi instance: {self._apayi}")
|
|
251
177
|
|
|
252
|
-
|
|
178
|
+
# Thread-local storage for context stacks - each thread gets its own stack
|
|
179
|
+
self._thread_local_storage = _ThreadLocalContextStorage()
|
|
180
|
+
|
|
253
181
|
self._log_prompt_and_response: bool = log_prompt_and_response
|
|
254
|
-
self._prompt_and_response_logger: Optional[Callable[[str, dict[str, str]], None]] = prompt_and_response_logger
|
|
255
182
|
|
|
256
183
|
self._blocked_limits: set[str] = set()
|
|
257
184
|
self._exceeded_limits: set[str] = set()
|
|
258
185
|
|
|
259
|
-
|
|
186
|
+
# by not setting to time.time() the first connection error is always logged
|
|
187
|
+
self._api_connection_error_last_log_time: float = 0
|
|
260
188
|
self._api_connection_error_count: int = 0
|
|
261
189
|
self._api_connection_error_window: int = global_config.get("connection_error_logging_window", 60)
|
|
262
190
|
if self._api_connection_error_window < 0:
|
|
@@ -269,21 +197,43 @@ class _PayiInstrumentor:
|
|
|
269
197
|
|
|
270
198
|
self._last_result: Optional[Union[XproxyResult, XproxyError]] = None
|
|
271
199
|
|
|
200
|
+
self._offline_instrumentation = global_config.pop("offline_instrumentation", None)
|
|
201
|
+
self._offline_ingest_packets: list[IngestUnitsParams] = []
|
|
202
|
+
self._offline_instrumentation_file_name: Optional[str] = None
|
|
203
|
+
|
|
204
|
+
if self._offline_instrumentation is not None:
|
|
205
|
+
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
|
206
|
+
self._offline_instrumentation_file_name = self._offline_instrumentation.get("file_name", f"payi_instrumentation_{timestamp}.json")
|
|
207
|
+
|
|
208
|
+
# Register exit handler to write packets when process exits
|
|
209
|
+
atexit.register(lambda: self._write_offline_ingest_packets())
|
|
210
|
+
|
|
272
211
|
global_instrumentation = global_config.pop("global_instrumentation", True)
|
|
273
212
|
|
|
213
|
+
# configure first, then instrument
|
|
214
|
+
aws_config = global_config.get("aws_config", None)
|
|
215
|
+
if aws_config:
|
|
216
|
+
from .BedrockInstrumentor import BedrockInstrumentor
|
|
217
|
+
BedrockInstrumentor.configure(aws_config=aws_config)
|
|
218
|
+
|
|
219
|
+
azure_openai_config = global_config.get("azure_openai_config", None)
|
|
220
|
+
if azure_openai_config:
|
|
221
|
+
from .OpenAIInstrumentor import OpenAiInstrumentor
|
|
222
|
+
OpenAiInstrumentor.configure(azure_openai_config=azure_openai_config)
|
|
223
|
+
|
|
274
224
|
if instruments is None or "*" in instruments:
|
|
275
225
|
self._instrument_all()
|
|
276
226
|
else:
|
|
277
|
-
self._instrument_specific(instruments)
|
|
227
|
+
self._instrument_specific(instruments=instruments)
|
|
228
|
+
|
|
229
|
+
self._instrument_futures()
|
|
278
230
|
|
|
279
231
|
if global_instrumentation:
|
|
280
232
|
if "proxy" not in global_config:
|
|
281
233
|
global_config["proxy"] = self._proxy_default
|
|
282
234
|
|
|
283
235
|
# Use default clients if not provided for global ingest instrumentation
|
|
284
|
-
|
|
285
|
-
self._payi = Payi()
|
|
286
|
-
self._apayi = AsyncPayi()
|
|
236
|
+
self._ensure_payi_clients()
|
|
287
237
|
|
|
288
238
|
if "use_case_name" not in global_config and caller_filename:
|
|
289
239
|
description = f"Default use case for {caller_filename}.py"
|
|
@@ -292,23 +242,53 @@ class _PayiInstrumentor:
|
|
|
292
242
|
self._payi.use_cases.definitions.create(name=caller_filename, description=description)
|
|
293
243
|
elif self._apayi:
|
|
294
244
|
self._call_async_use_case_definition_create(use_case_name=caller_filename, use_case_description=description)
|
|
245
|
+
else:
|
|
246
|
+
# in the case of _local_instrumentation is not None
|
|
247
|
+
pass
|
|
295
248
|
global_config["use_case_name"] = caller_filename
|
|
296
249
|
except Exception as e:
|
|
297
250
|
self._logger.error(f"Error creating default use case definition based on file name {caller_filename}: {e}")
|
|
298
251
|
|
|
299
252
|
self.__enter__()
|
|
300
253
|
|
|
301
|
-
# _init_current_context will update the
|
|
254
|
+
# _init_current_context will update the current context stack location
|
|
302
255
|
context: _Context = {}
|
|
256
|
+
|
|
303
257
|
# Copy allowed keys from global_config into context
|
|
304
258
|
# Dynamically use keys from _Context TypedDict
|
|
305
259
|
context_keys = list(_Context.__annotations__.keys()) if hasattr(_Context, '__annotations__') else []
|
|
306
260
|
for key in context_keys:
|
|
307
261
|
if key in global_config:
|
|
308
|
-
context[key] = global_config[key] # type: ignore
|
|
262
|
+
context[key] = global_config[key] # type: ignore[literal-required]
|
|
263
|
+
|
|
264
|
+
self._init_current_context(**context)
|
|
265
|
+
|
|
266
|
+
def _ensure_payi_clients(self) -> None:
|
|
267
|
+
if self._offline_instrumentation is not None:
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
if not self._payi and not self._apayi:
|
|
271
|
+
self._payi = Payi()
|
|
272
|
+
self._apayi = AsyncPayi()
|
|
273
|
+
|
|
274
|
+
def _instrument_futures(self) -> None:
|
|
275
|
+
"""Install hooks for all common concurrent execution patterns."""
|
|
276
|
+
def _thread_wrapper(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
|
|
277
|
+
return self._thread_submit_wrapper(wrapped, instance, args, kwargs)
|
|
278
|
+
|
|
279
|
+
async def _task_wrapper(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any:
|
|
280
|
+
return await self._create_task_wrapper(wrapped, instance, args, kwargs)
|
|
309
281
|
|
|
310
|
-
|
|
282
|
+
try:
|
|
283
|
+
wrap_function_wrapper("concurrent.futures", "ThreadPoolExecutor.submit", _thread_wrapper)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
self._logger.debug(f"Error wrapping ThreadPoolExecutor.submit: {e}")
|
|
311
286
|
|
|
287
|
+
try:
|
|
288
|
+
wrap_function_wrapper("asyncio", "create_task", _task_wrapper)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self._logger.debug(f"Error wrapping asyncio.create_task: {e}")
|
|
291
|
+
|
|
312
292
|
def _instrument_all(self) -> None:
|
|
313
293
|
self._instrument_openai()
|
|
314
294
|
self._instrument_anthropic()
|
|
@@ -372,6 +352,93 @@ class _PayiInstrumentor:
|
|
|
372
352
|
except Exception as e:
|
|
373
353
|
self._logger.error(f"Error instrumenting Google GenAi: {e}")
|
|
374
354
|
|
|
355
|
+
def _thread_submit_wrapper(
|
|
356
|
+
self,
|
|
357
|
+
wrapped: Any,
|
|
358
|
+
_instance: Any,
|
|
359
|
+
args: Any,
|
|
360
|
+
kwargs: Any,
|
|
361
|
+
) -> Any:
|
|
362
|
+
if len(args) > 0:
|
|
363
|
+
fn = args[0]
|
|
364
|
+
fn_args = args[1:]
|
|
365
|
+
captured_context = copy.deepcopy(self._context_safe)
|
|
366
|
+
|
|
367
|
+
def context_wrapper(*inner_args: Any, **inner_kwargs: Any) -> Any:
|
|
368
|
+
with self:
|
|
369
|
+
# self._context_stack[-1].update(captured_context)
|
|
370
|
+
self._init_current_context(**captured_context)
|
|
371
|
+
return fn(*inner_args, **inner_kwargs)
|
|
372
|
+
|
|
373
|
+
return wrapped(context_wrapper, *fn_args, **kwargs)
|
|
374
|
+
return wrapped(*args, **kwargs)
|
|
375
|
+
|
|
376
|
+
async def _create_task_wrapper(
|
|
377
|
+
self,
|
|
378
|
+
wrapped: Any,
|
|
379
|
+
_instance: Any,
|
|
380
|
+
args: Any,
|
|
381
|
+
kwargs: Any,
|
|
382
|
+
) -> Any:
|
|
383
|
+
if len(args) > 0:
|
|
384
|
+
coro = args[0]
|
|
385
|
+
captured_context = copy.deepcopy(self._context_safe)
|
|
386
|
+
|
|
387
|
+
async def context_wrapper() -> Any:
|
|
388
|
+
with self:
|
|
389
|
+
# self._context_stack[-1].update(captured_context)
|
|
390
|
+
self._init_current_context(**captured_context)
|
|
391
|
+
return await coro
|
|
392
|
+
|
|
393
|
+
return wrapped(context_wrapper(), *args[1:], **kwargs)
|
|
394
|
+
return wrapped(*args, **kwargs)
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def _model_mapping_to_context_dict(model_mappings: Sequence[PayiInstrumentModelMapping]) -> 'dict[str, _Context]':
|
|
398
|
+
context: dict[str, _Context] = {}
|
|
399
|
+
for mapping in model_mappings:
|
|
400
|
+
model = mapping.get("model", "")
|
|
401
|
+
if not model:
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
price_as_category = mapping.get("price_as_category", None)
|
|
405
|
+
price_as_resource = mapping.get("price_as_resource", None)
|
|
406
|
+
resource_scope = mapping.get("resource_scope", None)
|
|
407
|
+
|
|
408
|
+
if not price_as_category and not price_as_resource:
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
context[model] = _Context(
|
|
412
|
+
price_as_category=price_as_category,
|
|
413
|
+
price_as_resource=price_as_resource,
|
|
414
|
+
resource_scope=resource_scope,
|
|
415
|
+
)
|
|
416
|
+
return context
|
|
417
|
+
|
|
418
|
+
def _write_offline_ingest_packets(self) -> None:
|
|
419
|
+
if not self._offline_instrumentation_file_name or not self._offline_ingest_packets:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
# Convert datetime objects to ISO strings for JSON serialization
|
|
424
|
+
serializable_packets: list[IngestUnitsParams] = []
|
|
425
|
+
for packet in self._offline_ingest_packets:
|
|
426
|
+
serializable_packet = packet.copy()
|
|
427
|
+
|
|
428
|
+
# Convert datetime fields to ISO format strings
|
|
429
|
+
if 'event_timestamp' in serializable_packet and isinstance(serializable_packet['event_timestamp'], datetime):
|
|
430
|
+
serializable_packet['event_timestamp'] = serializable_packet['event_timestamp'].isoformat()
|
|
431
|
+
|
|
432
|
+
serializable_packets.append(serializable_packet)
|
|
433
|
+
|
|
434
|
+
with open(self._offline_instrumentation_file_name, 'w', encoding='utf-8') as f:
|
|
435
|
+
json.dump(serializable_packets, f)
|
|
436
|
+
|
|
437
|
+
self._logger.debug(f"Written {len(self._offline_ingest_packets)} ingest packets to {self._offline_instrumentation_file_name}")
|
|
438
|
+
|
|
439
|
+
except Exception as e:
|
|
440
|
+
self._logger.error(f"Error writing offline ingest packets to {self._offline_instrumentation_file_name}: {e}")
|
|
441
|
+
|
|
375
442
|
@staticmethod
|
|
376
443
|
def _create_logged_ingest_units(
|
|
377
444
|
ingest_units: IngestUnitsParams,
|
|
@@ -388,10 +455,10 @@ class _PayiInstrumentor:
|
|
|
388
455
|
|
|
389
456
|
return log_ingest_units
|
|
390
457
|
|
|
391
|
-
def
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
458
|
+
def _after_invoke_update_request(
|
|
459
|
+
self,
|
|
460
|
+
request: _ProviderRequest,
|
|
461
|
+
extra_headers: 'dict[str, str]') -> None:
|
|
395
462
|
ingest_units = request._ingest
|
|
396
463
|
|
|
397
464
|
if request._module_version:
|
|
@@ -401,9 +468,14 @@ class _PayiInstrumentor:
|
|
|
401
468
|
# convert the function call builder to a list of function calls
|
|
402
469
|
ingest_units["provider_response_function_calls"] = list(request._function_call_builder.values())
|
|
403
470
|
|
|
471
|
+
if "provider_response_id" not in ingest_units or not ingest_units["provider_response_id"]:
|
|
472
|
+
ingest_units["provider_response_id"] = f"payi_{uuid.uuid4()}"
|
|
473
|
+
|
|
404
474
|
if 'resource' not in ingest_units or ingest_units['resource'] == '':
|
|
405
475
|
ingest_units['resource'] = "system.unknown_model"
|
|
406
476
|
|
|
477
|
+
request.merge_internal_request_properties()
|
|
478
|
+
|
|
407
479
|
request_json = ingest_units.get('provider_request_json', "")
|
|
408
480
|
if request_json and self._instrument_inline_data is False:
|
|
409
481
|
try:
|
|
@@ -411,7 +483,7 @@ class _PayiInstrumentor:
|
|
|
411
483
|
if request.remove_inline_data(prompt_dict):
|
|
412
484
|
self._logger.debug(f"Removed inline data from provider_request_json")
|
|
413
485
|
# store the modified dict back as JSON string
|
|
414
|
-
ingest_units['provider_request_json'] =
|
|
486
|
+
ingest_units['provider_request_json'] = _compact_json(prompt_dict)
|
|
415
487
|
|
|
416
488
|
except Exception as e:
|
|
417
489
|
self._logger.error(f"Error serializing provider_request_json: {e}")
|
|
@@ -421,19 +493,6 @@ class _PayiInstrumentor:
|
|
|
421
493
|
if not units or all(unit.get("input", 0) == 0 and unit.get("output", 0) == 0 for unit in units.values()):
|
|
422
494
|
self._logger.info('ingesting with no token counts')
|
|
423
495
|
|
|
424
|
-
if self._log_prompt_and_response and self._prompt_and_response_logger:
|
|
425
|
-
response_json = ingest_units.pop("provider_response_json", None)
|
|
426
|
-
request_json = ingest_units.pop("provider_request_json", None)
|
|
427
|
-
stack_trace = ingest_units.get("properties", {}).pop("system.stack_trace", None) # type: ignore
|
|
428
|
-
|
|
429
|
-
if response_json is not None:
|
|
430
|
-
# response_json is a list of strings, convert a single json string
|
|
431
|
-
log_data["provider_response_json"] = json.dumps(response_json)
|
|
432
|
-
if request_json is not None:
|
|
433
|
-
log_data["provider_request_json"] = request_json
|
|
434
|
-
if stack_trace is not None:
|
|
435
|
-
log_data["stack_trace"] = stack_trace
|
|
436
|
-
|
|
437
496
|
def _process_ingest_units_response(self, ingest_response: IngestResponse) -> None:
|
|
438
497
|
if ingest_response.xproxy_result.limits:
|
|
439
498
|
for limit_id, state in ingest_response.xproxy_result.limits.items():
|
|
@@ -476,11 +535,8 @@ class _PayiInstrumentor:
|
|
|
476
535
|
|
|
477
536
|
self._logger.debug(f"_aingest_units")
|
|
478
537
|
|
|
479
|
-
# return early if there are no units to ingest and on a successul ingest request
|
|
480
|
-
log_data: 'dict[str,str]' = {}
|
|
481
538
|
extra_headers: 'dict[str, str]' = {}
|
|
482
|
-
|
|
483
|
-
self._process_ingest_units(request, log_data=log_data, extra_headers=extra_headers)
|
|
539
|
+
self._after_invoke_update_request(request, extra_headers=extra_headers)
|
|
484
540
|
|
|
485
541
|
try:
|
|
486
542
|
if self._logger.isEnabledFor(logging.DEBUG):
|
|
@@ -490,6 +546,18 @@ class _PayiInstrumentor:
|
|
|
490
546
|
ingest_response = await self._apayi.ingest.units(**ingest_units, extra_headers=extra_headers)
|
|
491
547
|
elif self._payi:
|
|
492
548
|
ingest_response = self._payi.ingest.units(**ingest_units, extra_headers=extra_headers)
|
|
549
|
+
elif self._offline_instrumentation is not None:
|
|
550
|
+
self._offline_ingest_packets.append(ingest_units.copy())
|
|
551
|
+
|
|
552
|
+
# simulate a successful ingest for local instrumentation
|
|
553
|
+
now=datetime.now(timezone.utc)
|
|
554
|
+
ingest_response = IngestResponse(
|
|
555
|
+
event_timestamp=now,
|
|
556
|
+
ingest_timestamp=now,
|
|
557
|
+
request_id="local_instrumentation",
|
|
558
|
+
xproxy_result=XproxyResult(request_id="local_instrumentation"))
|
|
559
|
+
pass
|
|
560
|
+
|
|
493
561
|
else:
|
|
494
562
|
self._logger.error("No payi instance to ingest units")
|
|
495
563
|
return XproxyError(code="configuration_error", message="No Payi or AsyncPayi instance configured for ingesting units")
|
|
@@ -499,10 +567,6 @@ class _PayiInstrumentor:
|
|
|
499
567
|
if ingest_response:
|
|
500
568
|
self._process_ingest_units_response(ingest_response)
|
|
501
569
|
|
|
502
|
-
if ingest_response and self._log_prompt_and_response and self._prompt_and_response_logger:
|
|
503
|
-
request_id = ingest_response.xproxy_result.request_id
|
|
504
|
-
self._prompt_and_response_logger(request_id, log_data) # type: ignore
|
|
505
|
-
|
|
506
570
|
return ingest_response.xproxy_result
|
|
507
571
|
|
|
508
572
|
except APIConnectionError as api_ex:
|
|
@@ -561,7 +625,7 @@ class _PayiInstrumentor:
|
|
|
561
625
|
# Try to get the response body as JSON
|
|
562
626
|
body = e.body
|
|
563
627
|
if body is None:
|
|
564
|
-
self._logger.
|
|
628
|
+
self._logger.warning(f"Pay-i ingest exception {e}, status {e.status_code} has no body")
|
|
565
629
|
return XproxyError(code="unknown_error", message=str(e))
|
|
566
630
|
|
|
567
631
|
# If body is bytes, decode to string
|
|
@@ -575,8 +639,9 @@ class _PayiInstrumentor:
|
|
|
575
639
|
if not body_dict:
|
|
576
640
|
try:
|
|
577
641
|
body_dict = json.loads(body) # type: ignore
|
|
578
|
-
except Exception
|
|
579
|
-
|
|
642
|
+
except Exception:
|
|
643
|
+
body_type = type(body).__name__ # type: ignore
|
|
644
|
+
self._logger.warning(f"Pay-i ingest exception {e}, status {e.status_code} cannot parse response JSON body for body type {body_type}")
|
|
580
645
|
return XproxyError(code="invalid_json", message=str(e))
|
|
581
646
|
|
|
582
647
|
xproxy_error = body_dict.get("xproxy_error", {})
|
|
@@ -585,7 +650,7 @@ class _PayiInstrumentor:
|
|
|
585
650
|
return XproxyError(code=code, message=message)
|
|
586
651
|
|
|
587
652
|
except Exception as ex:
|
|
588
|
-
self._logger.
|
|
653
|
+
self._logger.warning(f"Pay-i ingest exception {e}, status {e.status_code} processing handled exception {ex}")
|
|
589
654
|
return XproxyError(code="exception", message=str(ex))
|
|
590
655
|
|
|
591
656
|
def _ingest_units_worker(self, request: _ProviderRequest) -> Optional[Union[XproxyResult, XproxyError]]:
|
|
@@ -594,10 +659,8 @@ class _PayiInstrumentor:
|
|
|
594
659
|
|
|
595
660
|
self._logger.debug(f"_ingest_units")
|
|
596
661
|
|
|
597
|
-
# return early if there are no units to ingest and on a successul ingest request
|
|
598
|
-
log_data: 'dict[str,str]' = {}
|
|
599
662
|
extra_headers: 'dict[str, str]' = {}
|
|
600
|
-
self.
|
|
663
|
+
self._after_invoke_update_request(request, extra_headers=extra_headers)
|
|
601
664
|
|
|
602
665
|
try:
|
|
603
666
|
if self._payi:
|
|
@@ -609,16 +672,18 @@ class _PayiInstrumentor:
|
|
|
609
672
|
|
|
610
673
|
self._process_ingest_units_response(ingest_response)
|
|
611
674
|
|
|
612
|
-
if self._log_prompt_and_response and self._prompt_and_response_logger:
|
|
613
|
-
request_id = ingest_response.xproxy_result.request_id
|
|
614
|
-
self._prompt_and_response_logger(request_id, log_data) # type: ignore
|
|
615
|
-
|
|
616
675
|
return ingest_response.xproxy_result
|
|
617
676
|
elif self._apayi:
|
|
618
677
|
# task runs async. aingest_units will invoke the callback and post process
|
|
619
678
|
sync_response = self._call_aingest_sync(request)
|
|
620
679
|
self._logger.debug(f"_ingest_units: apayi success ({sync_response})")
|
|
621
680
|
return sync_response
|
|
681
|
+
elif self._offline_instrumentation is not None:
|
|
682
|
+
self._offline_ingest_packets.append(ingest_units.copy())
|
|
683
|
+
|
|
684
|
+
# simulate a successful ingest for local instrumentation
|
|
685
|
+
return XproxyResult(request_id="local_instrumentation")
|
|
686
|
+
|
|
622
687
|
else:
|
|
623
688
|
self._logger.error("No payi instance to ingest units")
|
|
624
689
|
return XproxyError(code="configuration_error", message="No Payi or AsyncPayi instance configured for ingesting units")
|
|
@@ -636,6 +701,21 @@ class _PayiInstrumentor:
|
|
|
636
701
|
def _ingest_units(self, request: _ProviderRequest) -> Optional[Union[XproxyResult, XproxyError]]:
|
|
637
702
|
return self.set_xproxy_result(self._ingest_units_worker(request))
|
|
638
703
|
|
|
704
|
+
@property
|
|
705
|
+
def _context_stack(self) -> "list[_Context]":
|
|
706
|
+
"""
|
|
707
|
+
Get the thread-local context stack. On first access per thread,
|
|
708
|
+
initializes with the current state of the main thread's context stack.
|
|
709
|
+
"""
|
|
710
|
+
# Lazy-initialize the context_stack for this thread if it doesn't exist
|
|
711
|
+
if not hasattr(self._thread_local_storage, 'context_stack'):
|
|
712
|
+
self._thread_local_storage.context_stack = []
|
|
713
|
+
|
|
714
|
+
stack = self._thread_local_storage.context_stack
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
return stack
|
|
718
|
+
|
|
639
719
|
def _setup_call_func(
|
|
640
720
|
self
|
|
641
721
|
) -> _Context:
|
|
@@ -646,6 +726,31 @@ class _PayiInstrumentor:
|
|
|
646
726
|
|
|
647
727
|
return {}
|
|
648
728
|
|
|
729
|
+
@staticmethod
|
|
730
|
+
def _valid_str_or_none(value: Optional[str], default: Optional[str] = None) -> Optional[str]:
|
|
731
|
+
if value is None:
|
|
732
|
+
return default
|
|
733
|
+
elif len(value) == 0:
|
|
734
|
+
# an empty string explicitly blocks the default value
|
|
735
|
+
return None
|
|
736
|
+
else:
|
|
737
|
+
return value
|
|
738
|
+
|
|
739
|
+
@staticmethod
|
|
740
|
+
def _valid_properties_or_none(value: Optional["dict[str, Optional[str]]"], default: Optional["dict[str, Optional[str]]"] = None) -> Optional["dict[str, Optional[str]]"]:
|
|
741
|
+
if value is None:
|
|
742
|
+
return default.copy() if default else None
|
|
743
|
+
elif len(value) == 0:
|
|
744
|
+
# an empty dictionary explicitly blocks the default value
|
|
745
|
+
return None
|
|
746
|
+
elif default:
|
|
747
|
+
# merge dictionaries, child overrides parent keys
|
|
748
|
+
merged = default.copy()
|
|
749
|
+
merged.update(value)
|
|
750
|
+
return merged
|
|
751
|
+
else:
|
|
752
|
+
return value.copy()
|
|
753
|
+
|
|
649
754
|
def _init_current_context(
|
|
650
755
|
self,
|
|
651
756
|
proxy: Optional[bool] = None,
|
|
@@ -655,16 +760,16 @@ class _PayiInstrumentor:
|
|
|
655
760
|
use_case_version: Optional[int]= None,
|
|
656
761
|
use_case_step: Optional[str]= None,
|
|
657
762
|
user_id: Optional[str]= None,
|
|
658
|
-
|
|
659
|
-
request_properties: Optional["dict[str, str]"] = None,
|
|
660
|
-
use_case_properties: Optional["dict[str, str]"] = None,
|
|
763
|
+
account_name: Optional[str]= None,
|
|
764
|
+
request_properties: Optional["dict[str, Optional[str]]"] = None,
|
|
765
|
+
use_case_properties: Optional["dict[str, Optional[str]]"] = None,
|
|
661
766
|
price_as_category: Optional[str] = None,
|
|
662
767
|
price_as_resource: Optional[str] = None,
|
|
663
768
|
resource_scope: Optional[str] = None,
|
|
664
769
|
) -> None:
|
|
665
770
|
|
|
666
771
|
# there will always be a current context
|
|
667
|
-
context: _Context = self.
|
|
772
|
+
context: _Context = self._context # type: ignore
|
|
668
773
|
parent_context: _Context = self._context_stack[-2] if len(self._context_stack) > 1 else {}
|
|
669
774
|
|
|
670
775
|
parent_proxy = parent_context.get("proxy", self._proxy_default)
|
|
@@ -705,26 +810,12 @@ class _PayiInstrumentor:
|
|
|
705
810
|
assign_use_case_values = True
|
|
706
811
|
|
|
707
812
|
if assign_use_case_values:
|
|
708
|
-
context["
|
|
709
|
-
context["
|
|
710
|
-
context["use_case_step"] = use_case_step
|
|
813
|
+
context["use_case_version"] = use_case_version if use_case_version is not None else parent_use_case_version
|
|
814
|
+
context["use_case_id"] = self._valid_str_or_none(use_case_id, parent_use_case_id)
|
|
815
|
+
context["use_case_step"] = self._valid_str_or_none(use_case_step, parent_use_case_step)
|
|
711
816
|
|
|
712
817
|
parent_use_case_properties = parent_context.get("use_case_properties", None)
|
|
713
|
-
|
|
714
|
-
if not use_case_properties:
|
|
715
|
-
# an empty dictionary explicitly blocks inheriting from the parent state
|
|
716
|
-
context["use_case_properties"] = None
|
|
717
|
-
else:
|
|
718
|
-
if parent_use_case_properties:
|
|
719
|
-
# merge dictionaries, child overrides parent keys
|
|
720
|
-
merged = parent_use_case_properties.copy()
|
|
721
|
-
merged.update(use_case_properties)
|
|
722
|
-
context["use_case_properties"] = merged
|
|
723
|
-
else:
|
|
724
|
-
context["use_case_properties"] = use_case_properties.copy()
|
|
725
|
-
elif parent_use_case_properties:
|
|
726
|
-
# use the parent use_case_properties if it exists
|
|
727
|
-
context["use_case_properties"] = parent_use_case_properties.copy()
|
|
818
|
+
context["use_case_properties"] = self._valid_properties_or_none(use_case_properties, parent_use_case_properties)
|
|
728
819
|
|
|
729
820
|
parent_limit_ids = parent_context.get("limit_ids", None)
|
|
730
821
|
if limit_ids is None:
|
|
@@ -738,46 +829,13 @@ class _PayiInstrumentor:
|
|
|
738
829
|
context["limit_ids"] = list(set(limit_ids) | set(parent_limit_ids)) if parent_limit_ids else limit_ids.copy()
|
|
739
830
|
|
|
740
831
|
parent_user_id = parent_context.get("user_id", None)
|
|
741
|
-
|
|
742
|
-
# use the parent user_id if it exists
|
|
743
|
-
context["user_id"] = parent_user_id
|
|
744
|
-
elif len(user_id) == 0:
|
|
745
|
-
# caller passing an empty string explicitly blocks inheriting from the parent state
|
|
746
|
-
context["user_id"] = None
|
|
747
|
-
else:
|
|
748
|
-
context["user_id"] = user_id
|
|
832
|
+
context["user_id"] = self._valid_str_or_none(user_id, parent_user_id)
|
|
749
833
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if len(request_tags) == 0:
|
|
753
|
-
# caller passing an empty list explicitly blocks inheriting from the parent state
|
|
754
|
-
context["request_tags"] = None
|
|
755
|
-
else:
|
|
756
|
-
if parent_request_tags:
|
|
757
|
-
# union of new and parent lists if the parent context contains request tags
|
|
758
|
-
context["request_tags"] = list(set(request_tags) | set(parent_request_tags))
|
|
759
|
-
else:
|
|
760
|
-
context["request_tags"] = request_tags.copy()
|
|
761
|
-
elif parent_request_tags:
|
|
762
|
-
# use the parent request_tags if it exists
|
|
763
|
-
context["request_tags"] = parent_request_tags.copy()
|
|
834
|
+
parent_account_name = parent_context.get("account_name", None)
|
|
835
|
+
context["account_name"] = self._valid_str_or_none(account_name, parent_account_name)
|
|
764
836
|
|
|
765
837
|
parent_request_properties = parent_context.get("request_properties", None)
|
|
766
|
-
|
|
767
|
-
if not request_properties:
|
|
768
|
-
# an empty dictionary explicitly blocks inheriting from the parent state
|
|
769
|
-
context["request_properties"] = None
|
|
770
|
-
else:
|
|
771
|
-
if parent_request_properties:
|
|
772
|
-
# merge dictionaries, child overrides parent keys
|
|
773
|
-
merged = parent_request_properties.copy()
|
|
774
|
-
merged.update(request_properties)
|
|
775
|
-
context["request_properties"] = merged
|
|
776
|
-
else:
|
|
777
|
-
context["request_properties"] = request_properties.copy()
|
|
778
|
-
elif parent_request_properties:
|
|
779
|
-
# use the parent request_properties if it exists
|
|
780
|
-
context["request_properties"] = parent_request_properties.copy()
|
|
838
|
+
context["request_properties"] = self._valid_properties_or_none(request_properties, parent_request_properties)
|
|
781
839
|
|
|
782
840
|
if price_as_category:
|
|
783
841
|
context["price_as_category"] = price_as_category
|
|
@@ -795,9 +853,9 @@ class _PayiInstrumentor:
|
|
|
795
853
|
use_case_id: Optional[str],
|
|
796
854
|
use_case_version: Optional[int],
|
|
797
855
|
user_id: Optional[str],
|
|
798
|
-
|
|
799
|
-
request_properties: Optional["dict[str, str]"] = None,
|
|
800
|
-
use_case_properties: Optional["dict[str, str]"] = None,
|
|
856
|
+
account_name: Optional[str],
|
|
857
|
+
request_properties: Optional["dict[str, Optional[str]]"] = None,
|
|
858
|
+
use_case_properties: Optional["dict[str, Optional[str]]"] = None,
|
|
801
859
|
*args: Any,
|
|
802
860
|
**kwargs: Any,
|
|
803
861
|
) -> Any:
|
|
@@ -809,7 +867,7 @@ class _PayiInstrumentor:
|
|
|
809
867
|
use_case_id=use_case_id,
|
|
810
868
|
use_case_version=use_case_version,
|
|
811
869
|
user_id=user_id,
|
|
812
|
-
|
|
870
|
+
account_name=account_name,
|
|
813
871
|
request_properties=request_properties,
|
|
814
872
|
use_case_properties=use_case_properties
|
|
815
873
|
)
|
|
@@ -824,9 +882,9 @@ class _PayiInstrumentor:
|
|
|
824
882
|
use_case_id: Optional[str],
|
|
825
883
|
use_case_version: Optional[int],
|
|
826
884
|
user_id: Optional[str],
|
|
827
|
-
|
|
828
|
-
request_properties: Optional["dict[str, str]"] = None,
|
|
829
|
-
use_case_properties: Optional["dict[str, str]"] = None,
|
|
885
|
+
account_name: Optional[str],
|
|
886
|
+
request_properties: Optional["dict[str, Optional[str]]"] = None,
|
|
887
|
+
use_case_properties: Optional["dict[str, Optional[str]]"] = None,
|
|
830
888
|
*args: Any,
|
|
831
889
|
**kwargs: Any,
|
|
832
890
|
) -> Any:
|
|
@@ -838,40 +896,52 @@ class _PayiInstrumentor:
|
|
|
838
896
|
use_case_id=use_case_id,
|
|
839
897
|
use_case_version=use_case_version,
|
|
840
898
|
user_id=user_id,
|
|
841
|
-
|
|
899
|
+
account_name=account_name,
|
|
842
900
|
request_properties=request_properties,
|
|
843
901
|
use_case_properties=use_case_properties)
|
|
844
902
|
return func(*args, **kwargs)
|
|
845
903
|
|
|
846
904
|
def __enter__(self) -> Any:
|
|
847
|
-
# Push a new context dictionary onto the stack
|
|
905
|
+
# Push a new context dictionary onto the thread-local stack
|
|
848
906
|
self._context_stack.append({})
|
|
849
907
|
return self
|
|
850
908
|
|
|
851
909
|
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
852
|
-
# Pop the current context off the stack
|
|
910
|
+
# Pop the current context off the thread-local stack
|
|
853
911
|
if self._context_stack:
|
|
854
912
|
self._context_stack.pop()
|
|
855
913
|
|
|
856
|
-
|
|
914
|
+
@property
|
|
915
|
+
def _context(self) -> Optional[_Context]:
|
|
857
916
|
# Return the current top of the stack
|
|
858
917
|
return self._context_stack[-1] if self._context_stack else None
|
|
859
918
|
|
|
860
|
-
|
|
919
|
+
@property
|
|
920
|
+
def _context_safe(self) -> _Context:
|
|
861
921
|
# Return the current top of the stack
|
|
862
|
-
return self.
|
|
922
|
+
return self._context or {}
|
|
923
|
+
|
|
924
|
+
def _extract_price_as(self, extra_headers: "dict[str, str]") -> PriceAs:
|
|
925
|
+
context = self._context_safe
|
|
863
926
|
|
|
864
|
-
|
|
927
|
+
return PriceAs(
|
|
928
|
+
category=extra_headers.pop(PayiHeaderNames.price_as_category, None) or context.get("price_as_category", None),
|
|
929
|
+
resource=extra_headers.pop(PayiHeaderNames.price_as_resource, None) or context.get("price_as_resource", None),
|
|
930
|
+
resource_scope=extra_headers.pop(PayiHeaderNames.resource_scope, None) or context.get("resource_scope", None),
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
def _before_invoke_update_request(
|
|
865
934
|
self,
|
|
866
935
|
request: _ProviderRequest,
|
|
867
|
-
context: _Context,
|
|
868
936
|
ingest_extra_headers: "dict[str, str]", # do not conflict with potential kwargs["extra_headers"]
|
|
869
937
|
args: Sequence[Any],
|
|
870
938
|
kwargs: 'dict[str, Any]',
|
|
871
939
|
) -> None:
|
|
872
940
|
|
|
941
|
+
# pop and ignore the request tags header since it is no longer processed
|
|
942
|
+
ingest_extra_headers.pop(PayiHeaderNames.request_tags, None)
|
|
943
|
+
|
|
873
944
|
limit_ids = ingest_extra_headers.pop(PayiHeaderNames.limit_ids, None)
|
|
874
|
-
request_tags = ingest_extra_headers.pop(PayiHeaderNames.request_tags, None)
|
|
875
945
|
|
|
876
946
|
use_case_name = ingest_extra_headers.pop(PayiHeaderNames.use_case_name, None)
|
|
877
947
|
use_case_id = ingest_extra_headers.pop(PayiHeaderNames.use_case_id, None)
|
|
@@ -879,11 +949,13 @@ class _PayiInstrumentor:
|
|
|
879
949
|
use_case_step = ingest_extra_headers.pop(PayiHeaderNames.use_case_step, None)
|
|
880
950
|
|
|
881
951
|
user_id = ingest_extra_headers.pop(PayiHeaderNames.user_id, None)
|
|
952
|
+
account_name = ingest_extra_headers.pop(PayiHeaderNames.account_name, None)
|
|
953
|
+
|
|
954
|
+
request_properties = ingest_extra_headers.pop(PayiHeaderNames.request_properties, "")
|
|
955
|
+
use_case_properties = ingest_extra_headers.pop(PayiHeaderNames.use_case_properties, "")
|
|
882
956
|
|
|
883
957
|
if limit_ids:
|
|
884
958
|
request._ingest["limit_ids"] = limit_ids.split(",")
|
|
885
|
-
if request_tags:
|
|
886
|
-
request._ingest["request_tags"] = request_tags.split(",")
|
|
887
959
|
if use_case_name:
|
|
888
960
|
request._ingest["use_case_name"] = use_case_name
|
|
889
961
|
if use_case_id:
|
|
@@ -894,14 +966,12 @@ class _PayiInstrumentor:
|
|
|
894
966
|
request._ingest["use_case_step"] = use_case_step
|
|
895
967
|
if user_id:
|
|
896
968
|
request._ingest["user_id"] = user_id
|
|
897
|
-
|
|
898
|
-
|
|
969
|
+
if account_name:
|
|
970
|
+
request._ingest["account_name"] = account_name
|
|
899
971
|
if request_properties:
|
|
900
|
-
request._ingest["properties"] = request_properties
|
|
901
|
-
|
|
902
|
-
use_case_properties = context.get("use_case_properties", None)
|
|
972
|
+
request._ingest["properties"] = json.loads(request_properties)
|
|
903
973
|
if use_case_properties:
|
|
904
|
-
request._ingest["use_case_properties"] = use_case_properties
|
|
974
|
+
request._ingest["use_case_properties"] = json.loads(use_case_properties)
|
|
905
975
|
|
|
906
976
|
if len(ingest_extra_headers) > 0:
|
|
907
977
|
request._ingest["provider_request_headers"] = [PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v) for k, v in ingest_extra_headers.items()]
|
|
@@ -925,10 +995,10 @@ class _PayiInstrumentor:
|
|
|
925
995
|
request.process_request_prompt(provider_prompt, args, kwargs)
|
|
926
996
|
|
|
927
997
|
if self._log_prompt_and_response:
|
|
928
|
-
request._ingest["provider_request_json"] =
|
|
998
|
+
request._ingest["provider_request_json"] = _compact_json(provider_prompt)
|
|
929
999
|
|
|
930
1000
|
request._ingest["event_timestamp"] = datetime.now(timezone.utc)
|
|
931
|
-
|
|
1001
|
+
|
|
932
1002
|
async def async_invoke_wrapper(
|
|
933
1003
|
self,
|
|
934
1004
|
request: _ProviderRequest,
|
|
@@ -940,7 +1010,7 @@ class _PayiInstrumentor:
|
|
|
940
1010
|
) -> Any:
|
|
941
1011
|
self._logger.debug(f"async_invoke_wrapper: instance {instance}, category {request._category}")
|
|
942
1012
|
|
|
943
|
-
context = self.
|
|
1013
|
+
context = self._context
|
|
944
1014
|
|
|
945
1015
|
# Bedrock client does not have an async method
|
|
946
1016
|
|
|
@@ -952,23 +1022,29 @@ class _PayiInstrumentor:
|
|
|
952
1022
|
|
|
953
1023
|
# after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
|
|
954
1024
|
extra_headers: Optional[dict[str, str]] = kwargs.get("extra_headers")
|
|
955
|
-
|
|
956
|
-
extra_headers = {}
|
|
1025
|
+
extra_headers = (extra_headers or {}).copy()
|
|
957
1026
|
self._update_extra_headers(context, extra_headers)
|
|
958
1027
|
|
|
959
1028
|
if context.get("proxy", self._proxy_default):
|
|
960
|
-
if
|
|
1029
|
+
if not request.supports_extra_headers:
|
|
1030
|
+
kwargs.pop("extra_headers", None)
|
|
1031
|
+
elif extra_headers:
|
|
1032
|
+
# Pass the copy to the wrapped function. Assumes anthropic and openai clients
|
|
961
1033
|
kwargs["extra_headers"] = extra_headers
|
|
962
1034
|
|
|
963
1035
|
self._logger.debug(f"async_invoke_wrapper: sending proxy request")
|
|
964
1036
|
|
|
965
1037
|
return await wrapped(*args, **kwargs)
|
|
1038
|
+
|
|
1039
|
+
request._price_as = self._extract_price_as(extra_headers)
|
|
1040
|
+
if not request.supports_extra_headers and "extra_headers" in kwargs:
|
|
1041
|
+
kwargs.pop("extra_headers", None)
|
|
966
1042
|
|
|
967
1043
|
current_frame = inspect.currentframe()
|
|
968
1044
|
# f_back excludes the current frame, strip() cleans up whitespace and newlines
|
|
969
1045
|
stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
|
|
970
1046
|
|
|
971
|
-
request._ingest['properties'] = { 'system.stack_trace':
|
|
1047
|
+
request._ingest['properties'] = { 'system.stack_trace': _compact_json(stack) }
|
|
972
1048
|
|
|
973
1049
|
if request.process_request(instance, extra_headers, args, kwargs) is False:
|
|
974
1050
|
self._logger.debug(f"async_invoke_wrapper: calling wrapped instance")
|
|
@@ -985,9 +1061,13 @@ class _PayiInstrumentor:
|
|
|
985
1061
|
stream = False
|
|
986
1062
|
|
|
987
1063
|
try:
|
|
988
|
-
self.
|
|
1064
|
+
self._before_invoke_update_request(request, extra_headers, args, kwargs)
|
|
989
1065
|
self._logger.debug(f"async_invoke_wrapper: calling wrapped instance (stream={stream})")
|
|
990
1066
|
|
|
1067
|
+
if "extra_headers" in kwargs:
|
|
1068
|
+
# replace the original extra_headers with the updated copy which has all of the Pay-i headers removed
|
|
1069
|
+
kwargs["extra_headers"] = extra_headers
|
|
1070
|
+
|
|
991
1071
|
sw.start()
|
|
992
1072
|
response = await wrapped(*args, **kwargs)
|
|
993
1073
|
|
|
@@ -1034,6 +1114,8 @@ class _PayiInstrumentor:
|
|
|
1034
1114
|
request._ingest["end_to_end_latency_ms"] = duration
|
|
1035
1115
|
request._ingest["http_status_code"] = 200
|
|
1036
1116
|
|
|
1117
|
+
request.add_instrumented_response_headers(response)
|
|
1118
|
+
|
|
1037
1119
|
return_result: Any = request.process_synchronous_response(
|
|
1038
1120
|
response=response,
|
|
1039
1121
|
log_prompt_and_response=self._log_prompt_and_response,
|
|
@@ -1043,7 +1125,8 @@ class _PayiInstrumentor:
|
|
|
1043
1125
|
self._logger.debug(f"async_invoke_wrapper: process sync response return")
|
|
1044
1126
|
return return_result
|
|
1045
1127
|
|
|
1046
|
-
await self._aingest_units(request)
|
|
1128
|
+
xproxy_result = await self._aingest_units(request)
|
|
1129
|
+
request.assign_xproxy_result(response, xproxy_result)
|
|
1047
1130
|
|
|
1048
1131
|
self._logger.debug(f"async_invoke_wrapper: finished")
|
|
1049
1132
|
return response
|
|
@@ -1059,7 +1142,7 @@ class _PayiInstrumentor:
|
|
|
1059
1142
|
) -> Any:
|
|
1060
1143
|
self._logger.debug(f"invoke_wrapper: instance {instance}, category {request._category}")
|
|
1061
1144
|
|
|
1062
|
-
context = self.
|
|
1145
|
+
context = self._context
|
|
1063
1146
|
|
|
1064
1147
|
if not context:
|
|
1065
1148
|
if not request.supports_extra_headers:
|
|
@@ -1072,26 +1155,29 @@ class _PayiInstrumentor:
|
|
|
1072
1155
|
|
|
1073
1156
|
# after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
|
|
1074
1157
|
extra_headers: Optional[dict[str, str]] = kwargs.get("extra_headers")
|
|
1075
|
-
|
|
1076
|
-
extra_headers = {}
|
|
1158
|
+
extra_headers = (extra_headers or {}).copy()
|
|
1077
1159
|
self._update_extra_headers(context, extra_headers)
|
|
1078
1160
|
|
|
1079
1161
|
if context.get("proxy", self._proxy_default):
|
|
1080
1162
|
if not request.supports_extra_headers:
|
|
1081
1163
|
kwargs.pop("extra_headers", None)
|
|
1082
|
-
elif
|
|
1083
|
-
#
|
|
1164
|
+
elif extra_headers:
|
|
1165
|
+
# Pass the copy to the wrapped function. Assumes anthropic and openai clients
|
|
1084
1166
|
kwargs["extra_headers"] = extra_headers
|
|
1085
1167
|
|
|
1086
1168
|
self._logger.debug(f"invoke_wrapper: sending proxy request")
|
|
1087
1169
|
|
|
1088
1170
|
return wrapped(*args, **kwargs)
|
|
1171
|
+
|
|
1172
|
+
request._price_as = self._extract_price_as(extra_headers)
|
|
1173
|
+
if not request.supports_extra_headers and "extra_headers" in kwargs:
|
|
1174
|
+
kwargs.pop("extra_headers", None)
|
|
1089
1175
|
|
|
1090
1176
|
current_frame = inspect.currentframe()
|
|
1091
1177
|
# f_back excludes the current frame, strip() cleans up whitespace and newlines
|
|
1092
1178
|
stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
|
|
1093
1179
|
|
|
1094
|
-
request._ingest['properties'] = { 'system.stack_trace':
|
|
1180
|
+
request._ingest['properties'] = { 'system.stack_trace': _compact_json(stack) }
|
|
1095
1181
|
|
|
1096
1182
|
if request.process_request(instance, extra_headers, args, kwargs) is False:
|
|
1097
1183
|
self._logger.debug(f"invoke_wrapper: calling wrapped instance")
|
|
@@ -1108,9 +1194,13 @@ class _PayiInstrumentor:
|
|
|
1108
1194
|
stream = False
|
|
1109
1195
|
|
|
1110
1196
|
try:
|
|
1111
|
-
self.
|
|
1197
|
+
self._before_invoke_update_request(request, extra_headers, args, kwargs)
|
|
1112
1198
|
self._logger.debug(f"invoke_wrapper: calling wrapped instance (stream={stream})")
|
|
1113
1199
|
|
|
1200
|
+
if "extra_headers" in kwargs:
|
|
1201
|
+
# replace the original extra_headers with the updated copy which has all of the Pay-i headers removed
|
|
1202
|
+
kwargs["extra_headers"] = extra_headers
|
|
1203
|
+
|
|
1114
1204
|
sw.start()
|
|
1115
1205
|
response = wrapped(*args, **kwargs)
|
|
1116
1206
|
|
|
@@ -1167,15 +1257,19 @@ class _PayiInstrumentor:
|
|
|
1167
1257
|
request._ingest["end_to_end_latency_ms"] = duration
|
|
1168
1258
|
request._ingest["http_status_code"] = 200
|
|
1169
1259
|
|
|
1260
|
+
request.add_instrumented_response_headers(response)
|
|
1261
|
+
|
|
1170
1262
|
return_result: Any = request.process_synchronous_response(
|
|
1171
1263
|
response=response,
|
|
1172
1264
|
log_prompt_and_response=self._log_prompt_and_response,
|
|
1173
1265
|
kwargs=kwargs)
|
|
1266
|
+
|
|
1174
1267
|
if return_result:
|
|
1175
1268
|
self._logger.debug(f"invoke_wrapper: process sync response return")
|
|
1176
1269
|
return return_result
|
|
1177
1270
|
|
|
1178
|
-
self._ingest_units(request)
|
|
1271
|
+
xproxy_result = self._ingest_units(request)
|
|
1272
|
+
request.assign_xproxy_result(response, xproxy_result)
|
|
1179
1273
|
|
|
1180
1274
|
self._logger.debug(f"invoke_wrapper: finished")
|
|
1181
1275
|
return response
|
|
@@ -1184,7 +1278,7 @@ class _PayiInstrumentor:
|
|
|
1184
1278
|
self
|
|
1185
1279
|
) -> 'dict[str, str]':
|
|
1186
1280
|
extra_headers: dict[str, str] = {}
|
|
1187
|
-
context = self.
|
|
1281
|
+
context = self._context
|
|
1188
1282
|
if context:
|
|
1189
1283
|
self._update_extra_headers(context, extra_headers)
|
|
1190
1284
|
|
|
@@ -1207,19 +1301,44 @@ class _PayiInstrumentor:
|
|
|
1207
1301
|
context_use_case_step: Optional[str] = context.get("use_case_step")
|
|
1208
1302
|
|
|
1209
1303
|
context_user_id: Optional[str] = context.get("user_id")
|
|
1210
|
-
|
|
1304
|
+
context_account_name: Optional[str] = context.get("account_name")
|
|
1211
1305
|
|
|
1212
1306
|
context_price_as_category: Optional[str] = context.get("price_as_category")
|
|
1213
1307
|
context_price_as_resource: Optional[str] = context.get("price_as_resource")
|
|
1214
1308
|
context_resource_scope: Optional[str] = context.get("resource_scope")
|
|
1215
1309
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1310
|
+
context_request_properties: Optional[dict[str, Optional[str]]] = context.get("request_properties")
|
|
1311
|
+
context_use_case_properties: Optional[dict[str, Optional[str]]] = context.get("use_case_properties")
|
|
1312
|
+
|
|
1313
|
+
if PayiHeaderNames.request_properties in extra_headers:
|
|
1314
|
+
headers_request_properties = extra_headers.get(PayiHeaderNames.request_properties, None)
|
|
1315
|
+
|
|
1316
|
+
if not headers_request_properties:
|
|
1317
|
+
# headers_request_properties is empty, remove it from extra_headers
|
|
1318
|
+
extra_headers.pop(PayiHeaderNames.request_properties, None)
|
|
1319
|
+
else:
|
|
1320
|
+
# leave the value in extra_headers
|
|
1321
|
+
...
|
|
1322
|
+
elif context_request_properties:
|
|
1323
|
+
extra_headers[PayiHeaderNames.request_properties] = _compact_json(context_request_properties)
|
|
1324
|
+
|
|
1325
|
+
if PayiHeaderNames.use_case_properties in extra_headers:
|
|
1326
|
+
headers_use_case_properties = extra_headers.get(PayiHeaderNames.use_case_properties, None)
|
|
1327
|
+
|
|
1328
|
+
if not headers_use_case_properties:
|
|
1329
|
+
# headers_use_case_properties is empty, remove it from extra_headers
|
|
1330
|
+
extra_headers.pop(PayiHeaderNames.use_case_properties, None)
|
|
1331
|
+
else:
|
|
1332
|
+
# leave the value in extra_headers
|
|
1333
|
+
...
|
|
1334
|
+
elif context_use_case_properties:
|
|
1335
|
+
extra_headers[PayiHeaderNames.use_case_properties] = _compact_json(context_use_case_properties)
|
|
1336
|
+
|
|
1218
1337
|
# If the caller specifies limit_ids in extra_headers, it takes precedence over the decorator
|
|
1219
1338
|
if PayiHeaderNames.limit_ids in extra_headers:
|
|
1220
1339
|
headers_limit_ids = extra_headers.get(PayiHeaderNames.limit_ids)
|
|
1221
1340
|
|
|
1222
|
-
if
|
|
1341
|
+
if not headers_limit_ids:
|
|
1223
1342
|
# headers_limit_ids is empty, remove it from extra_headers
|
|
1224
1343
|
extra_headers.pop(PayiHeaderNames.limit_ids, None)
|
|
1225
1344
|
else:
|
|
@@ -1230,7 +1349,7 @@ class _PayiInstrumentor:
|
|
|
1230
1349
|
|
|
1231
1350
|
if PayiHeaderNames.user_id in extra_headers:
|
|
1232
1351
|
headers_user_id = extra_headers.get(PayiHeaderNames.user_id, None)
|
|
1233
|
-
if
|
|
1352
|
+
if not headers_user_id:
|
|
1234
1353
|
# headers_user_id is empty, remove it from extra_headers
|
|
1235
1354
|
extra_headers.pop(PayiHeaderNames.user_id, None)
|
|
1236
1355
|
else:
|
|
@@ -1239,9 +1358,20 @@ class _PayiInstrumentor:
|
|
|
1239
1358
|
elif context_user_id:
|
|
1240
1359
|
extra_headers[PayiHeaderNames.user_id] = context_user_id
|
|
1241
1360
|
|
|
1361
|
+
if PayiHeaderNames.account_name in extra_headers:
|
|
1362
|
+
headers_account_name = extra_headers.get(PayiHeaderNames.account_name, None)
|
|
1363
|
+
if not headers_account_name:
|
|
1364
|
+
# headers_account_name is empty, remove it from extra_headers
|
|
1365
|
+
extra_headers.pop(PayiHeaderNames.account_name, None)
|
|
1366
|
+
else:
|
|
1367
|
+
# leave the value in extra_headers
|
|
1368
|
+
...
|
|
1369
|
+
elif context_account_name:
|
|
1370
|
+
extra_headers[PayiHeaderNames.account_name] = context_account_name
|
|
1371
|
+
|
|
1242
1372
|
if PayiHeaderNames.use_case_name in extra_headers:
|
|
1243
1373
|
headers_use_case_name = extra_headers.get(PayiHeaderNames.use_case_name, None)
|
|
1244
|
-
if
|
|
1374
|
+
if not headers_use_case_name:
|
|
1245
1375
|
# headers_use_case_name is empty, remove all use case related headers
|
|
1246
1376
|
extra_headers.pop(PayiHeaderNames.use_case_name, None)
|
|
1247
1377
|
extra_headers.pop(PayiHeaderNames.use_case_id, None)
|
|
@@ -1257,10 +1387,7 @@ class _PayiInstrumentor:
|
|
|
1257
1387
|
if context_use_case_version is not None:
|
|
1258
1388
|
extra_headers[PayiHeaderNames.use_case_version] = str(context_use_case_version)
|
|
1259
1389
|
if context_use_case_step is not None:
|
|
1260
|
-
extra_headers[PayiHeaderNames.use_case_step] =
|
|
1261
|
-
|
|
1262
|
-
if PayiHeaderNames.request_tags not in extra_headers and context_request_tags:
|
|
1263
|
-
extra_headers[PayiHeaderNames.request_tags] = ",".join(context_request_tags)
|
|
1390
|
+
extra_headers[PayiHeaderNames.use_case_step] = context_use_case_step
|
|
1264
1391
|
|
|
1265
1392
|
if PayiHeaderNames.price_as_category not in extra_headers and context_price_as_category:
|
|
1266
1393
|
extra_headers[PayiHeaderNames.price_as_category] = context_price_as_category
|
|
@@ -1271,16 +1398,6 @@ class _PayiInstrumentor:
|
|
|
1271
1398
|
if PayiHeaderNames.resource_scope not in extra_headers and context_resource_scope:
|
|
1272
1399
|
extra_headers[PayiHeaderNames.resource_scope] = context_resource_scope
|
|
1273
1400
|
|
|
1274
|
-
@staticmethod
|
|
1275
|
-
def update_for_vision(input: int, units: 'dict[str, Units]', estimated_prompt_tokens: Optional[int]) -> int:
|
|
1276
|
-
if estimated_prompt_tokens:
|
|
1277
|
-
vision = input - estimated_prompt_tokens
|
|
1278
|
-
if (vision > 0):
|
|
1279
|
-
units["vision"] = Units(input=vision, output=0)
|
|
1280
|
-
input = estimated_prompt_tokens
|
|
1281
|
-
|
|
1282
|
-
return input
|
|
1283
|
-
|
|
1284
1401
|
@staticmethod
|
|
1285
1402
|
def payi_wrapper(func: Any) -> Any:
|
|
1286
1403
|
def _payi_wrapper(o: Any) -> Any:
|
|
@@ -1313,351 +1430,6 @@ class _PayiInstrumentor:
|
|
|
1313
1430
|
|
|
1314
1431
|
return _payi_awrapper
|
|
1315
1432
|
|
|
1316
|
-
class _StreamIteratorWrapper(ObjectProxy): # type: ignore
|
|
1317
|
-
def __init__(
|
|
1318
|
-
self,
|
|
1319
|
-
response: Any,
|
|
1320
|
-
instance: Any,
|
|
1321
|
-
instrumentor: _PayiInstrumentor,
|
|
1322
|
-
stopwatch: Stopwatch,
|
|
1323
|
-
request: _ProviderRequest,
|
|
1324
|
-
) -> None:
|
|
1325
|
-
|
|
1326
|
-
instrumentor._logger.debug(f"StreamIteratorWrapper: instance {instance}, category {request._category}")
|
|
1327
|
-
|
|
1328
|
-
request.process_initial_stream_response(response)
|
|
1329
|
-
|
|
1330
|
-
bedrock_from_stream: bool = False
|
|
1331
|
-
if request.is_aws_client:
|
|
1332
|
-
stream = response.get("stream", None)
|
|
1333
|
-
|
|
1334
|
-
if stream:
|
|
1335
|
-
response = stream
|
|
1336
|
-
bedrock_from_stream = True
|
|
1337
|
-
else:
|
|
1338
|
-
response = response.get("body")
|
|
1339
|
-
bedrock_from_stream = False
|
|
1340
|
-
|
|
1341
|
-
super().__init__(response) # type: ignore
|
|
1342
|
-
|
|
1343
|
-
self._response = response
|
|
1344
|
-
self._instance = instance
|
|
1345
|
-
|
|
1346
|
-
self._instrumentor = instrumentor
|
|
1347
|
-
self._stopwatch: Stopwatch = stopwatch
|
|
1348
|
-
self._responses: list[str] = []
|
|
1349
|
-
|
|
1350
|
-
self._request: _ProviderRequest = request
|
|
1351
|
-
|
|
1352
|
-
self._first_token: bool = True
|
|
1353
|
-
self._bedrock_from_stream: bool = bedrock_from_stream
|
|
1354
|
-
self._ingested: bool = False
|
|
1355
|
-
self._iter_started: bool = False
|
|
1356
|
-
|
|
1357
|
-
def __enter__(self) -> Any:
|
|
1358
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __enter__")
|
|
1359
|
-
return self
|
|
1360
|
-
|
|
1361
|
-
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
1362
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __exit__")
|
|
1363
|
-
self.__wrapped__.__exit__(exc_type, exc_val, exc_tb) # type: ignore
|
|
1364
|
-
|
|
1365
|
-
async def __aenter__(self) -> Any:
|
|
1366
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __aenter__")
|
|
1367
|
-
return self
|
|
1368
|
-
|
|
1369
|
-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
1370
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __aexit__")
|
|
1371
|
-
await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb) # type: ignore
|
|
1372
|
-
|
|
1373
|
-
def __iter__(self) -> Any:
|
|
1374
|
-
self._iter_started = True
|
|
1375
|
-
if self._request.is_aws_client:
|
|
1376
|
-
# MUST reside in a separate function so that the yield statement (e.g. the generator) doesn't implicitly return its own iterator and overriding self
|
|
1377
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: bedrock __iter__")
|
|
1378
|
-
return self._iter_bedrock()
|
|
1379
|
-
|
|
1380
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __iter__")
|
|
1381
|
-
return self
|
|
1382
|
-
|
|
1383
|
-
def _iter_bedrock(self) -> Any:
|
|
1384
|
-
# botocore EventStream doesn't have a __next__ method so iterate over the wrapped object in place
|
|
1385
|
-
for event in self.__wrapped__: # type: ignore
|
|
1386
|
-
result: Optional[_ChunkResult] = None
|
|
1387
|
-
|
|
1388
|
-
if (self._bedrock_from_stream):
|
|
1389
|
-
result = self._evaluate_chunk(event)
|
|
1390
|
-
else:
|
|
1391
|
-
chunk = event.get('chunk') # type: ignore
|
|
1392
|
-
if chunk:
|
|
1393
|
-
decode = chunk.get('bytes').decode() # type: ignore
|
|
1394
|
-
result = self._evaluate_chunk(decode)
|
|
1395
|
-
|
|
1396
|
-
if result and result.ingest:
|
|
1397
|
-
self._stop_iteration()
|
|
1398
|
-
|
|
1399
|
-
yield event
|
|
1400
|
-
|
|
1401
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: bedrock iter finished")
|
|
1402
|
-
|
|
1403
|
-
self._stop_iteration()
|
|
1404
|
-
|
|
1405
|
-
def __aiter__(self) -> Any:
|
|
1406
|
-
self._iter_started = True
|
|
1407
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __aiter__")
|
|
1408
|
-
return self
|
|
1409
|
-
|
|
1410
|
-
def __next__(self) -> object:
|
|
1411
|
-
try:
|
|
1412
|
-
chunk: object = self.__wrapped__.__next__() # type: ignore
|
|
1413
|
-
|
|
1414
|
-
if self._ingested:
|
|
1415
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __next__ already ingested, not processing chunk {chunk}")
|
|
1416
|
-
return chunk # type: ignore
|
|
1417
|
-
|
|
1418
|
-
result = self._evaluate_chunk(chunk)
|
|
1419
|
-
|
|
1420
|
-
if result.ingest:
|
|
1421
|
-
self._stop_iteration()
|
|
1422
|
-
|
|
1423
|
-
if result.send_chunk_to_caller:
|
|
1424
|
-
return chunk # type: ignore
|
|
1425
|
-
else:
|
|
1426
|
-
return self.__next__()
|
|
1427
|
-
except Exception as e:
|
|
1428
|
-
if isinstance(e, StopIteration):
|
|
1429
|
-
self._stop_iteration()
|
|
1430
|
-
else:
|
|
1431
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __next__ exception {e}")
|
|
1432
|
-
raise e
|
|
1433
|
-
|
|
1434
|
-
async def __anext__(self) -> object:
|
|
1435
|
-
try:
|
|
1436
|
-
chunk: object = await self.__wrapped__.__anext__() # type: ignore
|
|
1437
|
-
|
|
1438
|
-
if self._ingested:
|
|
1439
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __next__ already ingested, not processing chunk {chunk}")
|
|
1440
|
-
return chunk # type: ignore
|
|
1441
|
-
|
|
1442
|
-
result = self._evaluate_chunk(chunk)
|
|
1443
|
-
|
|
1444
|
-
if result.ingest:
|
|
1445
|
-
await self._astop_iteration()
|
|
1446
|
-
|
|
1447
|
-
if result.send_chunk_to_caller:
|
|
1448
|
-
return chunk # type: ignore
|
|
1449
|
-
else:
|
|
1450
|
-
return await self.__anext__()
|
|
1451
|
-
|
|
1452
|
-
except Exception as e:
|
|
1453
|
-
if isinstance(e, StopAsyncIteration):
|
|
1454
|
-
await self._astop_iteration()
|
|
1455
|
-
else:
|
|
1456
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: __anext__ exception {e}")
|
|
1457
|
-
raise e
|
|
1458
|
-
|
|
1459
|
-
def _evaluate_chunk(self, chunk: Any) -> _ChunkResult:
|
|
1460
|
-
if self._first_token:
|
|
1461
|
-
self._request._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
|
|
1462
|
-
self._first_token = False
|
|
1463
|
-
|
|
1464
|
-
if self._instrumentor._log_prompt_and_response:
|
|
1465
|
-
self._responses.append(self.chunk_to_json(chunk))
|
|
1466
|
-
|
|
1467
|
-
return self._request.process_chunk(chunk)
|
|
1468
|
-
|
|
1469
|
-
def _process_stop_iteration(self) -> None:
|
|
1470
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: process stop iteration")
|
|
1471
|
-
|
|
1472
|
-
self._stopwatch.stop()
|
|
1473
|
-
self._request._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
|
|
1474
|
-
self._request._ingest["http_status_code"] = 200
|
|
1475
|
-
|
|
1476
|
-
if self._instrumentor._log_prompt_and_response:
|
|
1477
|
-
self._request._ingest["provider_response_json"] = self._responses
|
|
1478
|
-
|
|
1479
|
-
async def _astop_iteration(self) -> None:
|
|
1480
|
-
if self._ingested:
|
|
1481
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: astop iteration already ingested, skipping")
|
|
1482
|
-
return
|
|
1483
|
-
|
|
1484
|
-
self._process_stop_iteration()
|
|
1485
|
-
|
|
1486
|
-
await self._instrumentor._aingest_units(self._request)
|
|
1487
|
-
self._ingested = True
|
|
1488
|
-
|
|
1489
|
-
def _stop_iteration(self) -> None:
|
|
1490
|
-
if self._ingested:
|
|
1491
|
-
self._instrumentor._logger.debug(f"StreamIteratorWrapper: stop iteration already ingested, skipping")
|
|
1492
|
-
return
|
|
1493
|
-
|
|
1494
|
-
self._process_stop_iteration()
|
|
1495
|
-
self._instrumentor._ingest_units(self._request)
|
|
1496
|
-
self._ingested = True
|
|
1497
|
-
|
|
1498
|
-
@staticmethod
|
|
1499
|
-
def chunk_to_json(chunk: Any) -> str:
|
|
1500
|
-
if hasattr(chunk, "to_json"):
|
|
1501
|
-
return str(chunk.to_json())
|
|
1502
|
-
elif isinstance(chunk, bytes):
|
|
1503
|
-
return chunk.decode()
|
|
1504
|
-
elif isinstance(chunk, str):
|
|
1505
|
-
return chunk
|
|
1506
|
-
else:
|
|
1507
|
-
# assume dict
|
|
1508
|
-
return json.dumps(chunk)
|
|
1509
|
-
|
|
1510
|
-
class _StreamManagerWrapper(ObjectProxy): # type: ignore
|
|
1511
|
-
def __init__(
|
|
1512
|
-
self,
|
|
1513
|
-
stream_manager: Any, # type: ignore
|
|
1514
|
-
instance: Any,
|
|
1515
|
-
instrumentor: _PayiInstrumentor,
|
|
1516
|
-
stopwatch: Stopwatch,
|
|
1517
|
-
request: _ProviderRequest,
|
|
1518
|
-
) -> None:
|
|
1519
|
-
instrumentor._logger.debug(f"StreamManagerWrapper: instance {instance}, category {request._category}")
|
|
1520
|
-
|
|
1521
|
-
super().__init__(stream_manager) # type: ignore
|
|
1522
|
-
|
|
1523
|
-
self._stream_manager = stream_manager
|
|
1524
|
-
self._instance = instance
|
|
1525
|
-
self._instrumentor = instrumentor
|
|
1526
|
-
self._stopwatch: Stopwatch = stopwatch
|
|
1527
|
-
self._responses: list[str] = []
|
|
1528
|
-
self._request: _ProviderRequest = request
|
|
1529
|
-
self._first_token: bool = True
|
|
1530
|
-
|
|
1531
|
-
def __enter__(self) -> _StreamIteratorWrapper:
|
|
1532
|
-
self._instrumentor._logger.debug(f"_StreamManagerWrapper: __enter__")
|
|
1533
|
-
|
|
1534
|
-
return _StreamIteratorWrapper(
|
|
1535
|
-
response=self.__wrapped__.__enter__(), # type: ignore
|
|
1536
|
-
instance=self._instance,
|
|
1537
|
-
instrumentor=self._instrumentor,
|
|
1538
|
-
stopwatch=self._stopwatch,
|
|
1539
|
-
request=self._request,
|
|
1540
|
-
)
|
|
1541
|
-
|
|
1542
|
-
class _GeneratorWrapper: # type: ignore
|
|
1543
|
-
def __init__(
|
|
1544
|
-
self,
|
|
1545
|
-
generator: Any,
|
|
1546
|
-
instance: Any,
|
|
1547
|
-
instrumentor: _PayiInstrumentor,
|
|
1548
|
-
stopwatch: Stopwatch,
|
|
1549
|
-
request: _ProviderRequest,
|
|
1550
|
-
) -> None:
|
|
1551
|
-
instrumentor._logger.debug(f"GeneratorWrapper: instance {instance}, category {request._category}")
|
|
1552
|
-
|
|
1553
|
-
super().__init__() # type: ignore
|
|
1554
|
-
|
|
1555
|
-
self._generator = generator
|
|
1556
|
-
self._instance = instance
|
|
1557
|
-
self._instrumentor = instrumentor
|
|
1558
|
-
self._stopwatch: Stopwatch = stopwatch
|
|
1559
|
-
self._log_prompt_and_response: bool = instrumentor._log_prompt_and_response
|
|
1560
|
-
self._responses: list[str] = []
|
|
1561
|
-
self._request: _ProviderRequest = request
|
|
1562
|
-
self._first_token: bool = True
|
|
1563
|
-
self._ingested: bool = False
|
|
1564
|
-
self._iter_started: bool = False
|
|
1565
|
-
|
|
1566
|
-
def __iter__(self) -> Any:
|
|
1567
|
-
self._iter_started = True
|
|
1568
|
-
self._instrumentor._logger.debug(f"GeneratorWrapper: __iter__")
|
|
1569
|
-
return self
|
|
1570
|
-
|
|
1571
|
-
def __aiter__(self) -> Any:
|
|
1572
|
-
self._instrumentor._logger.debug(f"GeneratorWrapper: __aiter__")
|
|
1573
|
-
return self
|
|
1574
|
-
|
|
1575
|
-
def _process_chunk(self, chunk: Any) -> _ChunkResult:
|
|
1576
|
-
if self._first_token:
|
|
1577
|
-
self._request._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
|
|
1578
|
-
self._first_token = False
|
|
1579
|
-
|
|
1580
|
-
if self._log_prompt_and_response:
|
|
1581
|
-
dict = self._chunk_to_dict(chunk)
|
|
1582
|
-
self._responses.append(json.dumps(dict))
|
|
1583
|
-
|
|
1584
|
-
return self._request.process_chunk(chunk)
|
|
1585
|
-
|
|
1586
|
-
def __next__(self) -> Any:
|
|
1587
|
-
try:
|
|
1588
|
-
chunk = next(self._generator)
|
|
1589
|
-
result = self._process_chunk(chunk)
|
|
1590
|
-
|
|
1591
|
-
if result.ingest:
|
|
1592
|
-
self._stop_iteration()
|
|
1593
|
-
|
|
1594
|
-
# ignore result.send_chunk_to_caller:
|
|
1595
|
-
return chunk
|
|
1596
|
-
|
|
1597
|
-
except Exception as e:
|
|
1598
|
-
if isinstance(e, StopIteration):
|
|
1599
|
-
self._stop_iteration()
|
|
1600
|
-
else:
|
|
1601
|
-
self._instrumentor._logger.debug(f"GeneratorWrapper: __next__ exception {e}")
|
|
1602
|
-
raise e
|
|
1603
|
-
|
|
1604
|
-
async def __anext__(self) -> Any:
|
|
1605
|
-
try:
|
|
1606
|
-
chunk = await anext(self._generator) # type: ignore
|
|
1607
|
-
result = self._process_chunk(chunk)
|
|
1608
|
-
|
|
1609
|
-
if result.ingest:
|
|
1610
|
-
await self._astop_iteration()
|
|
1611
|
-
|
|
1612
|
-
# ignore result.send_chunk_to_caller:
|
|
1613
|
-
return chunk # type: ignore
|
|
1614
|
-
|
|
1615
|
-
except Exception as e:
|
|
1616
|
-
if isinstance(e, StopAsyncIteration):
|
|
1617
|
-
await self._astop_iteration()
|
|
1618
|
-
else:
|
|
1619
|
-
self._instrumentor._logger.debug(f"GeneratorWrapper: __anext__ exception {e}")
|
|
1620
|
-
raise e
|
|
1621
|
-
|
|
1622
|
-
@staticmethod
|
|
1623
|
-
def _chunk_to_dict(chunk: Any) -> 'dict[str, object]':
|
|
1624
|
-
if hasattr(chunk, "to_dict"):
|
|
1625
|
-
return chunk.to_dict() # type: ignore
|
|
1626
|
-
elif hasattr(chunk, "to_json_dict"):
|
|
1627
|
-
return chunk.to_json_dict() # type: ignore
|
|
1628
|
-
else:
|
|
1629
|
-
return {}
|
|
1630
|
-
|
|
1631
|
-
def _stop_iteration(self) -> None:
|
|
1632
|
-
if self._ingested:
|
|
1633
|
-
self._instrumentor._logger.debug(f"GeneratorWrapper: stop iteration already ingested, skipping")
|
|
1634
|
-
return
|
|
1635
|
-
|
|
1636
|
-
self._process_stop_iteration()
|
|
1637
|
-
|
|
1638
|
-
self._instrumentor._ingest_units(self._request)
|
|
1639
|
-
self._ingested = True
|
|
1640
|
-
|
|
1641
|
-
async def _astop_iteration(self) -> None:
|
|
1642
|
-
if self._ingested:
|
|
1643
|
-
self._instrumentor._logger.debug(f"GeneratorWrapper: astop iteration already ingested, skipping")
|
|
1644
|
-
return
|
|
1645
|
-
|
|
1646
|
-
self._process_stop_iteration()
|
|
1647
|
-
|
|
1648
|
-
await self._instrumentor._aingest_units(self._request)
|
|
1649
|
-
self._ingested = True
|
|
1650
|
-
|
|
1651
|
-
def _process_stop_iteration(self) -> None:
|
|
1652
|
-
self._instrumentor._logger.debug(f"GeneratorWrapper: stop iteration")
|
|
1653
|
-
|
|
1654
|
-
self._stopwatch.stop()
|
|
1655
|
-
self._request._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
|
|
1656
|
-
self._request._ingest["http_status_code"] = 200
|
|
1657
|
-
|
|
1658
|
-
if self._log_prompt_and_response:
|
|
1659
|
-
self._request._ingest["provider_response_json"] = self._responses
|
|
1660
|
-
|
|
1661
1433
|
global _instrumentor
|
|
1662
1434
|
_instrumentor: Optional[_PayiInstrumentor] = None
|
|
1663
1435
|
|
|
@@ -1666,7 +1438,6 @@ def payi_instrument(
|
|
|
1666
1438
|
payi: Optional[Union[Payi, AsyncPayi, 'list[Union[Payi, AsyncPayi]]']] = None,
|
|
1667
1439
|
instruments: Optional[Set[str]] = None,
|
|
1668
1440
|
log_prompt_and_response: bool = True,
|
|
1669
|
-
prompt_and_response_logger: Optional[Callable[[str, "dict[str, str]"], None]] = None,
|
|
1670
1441
|
config: Optional[PayiInstrumentConfig] = None,
|
|
1671
1442
|
logger: Optional[logging.Logger] = None,
|
|
1672
1443
|
) -> None:
|
|
@@ -1699,7 +1470,6 @@ def payi_instrument(
|
|
|
1699
1470
|
instruments=instruments,
|
|
1700
1471
|
log_prompt_and_response=log_prompt_and_response,
|
|
1701
1472
|
logger=logger,
|
|
1702
|
-
prompt_and_response_logger=prompt_and_response_logger,
|
|
1703
1473
|
global_config=config if config else PayiInstrumentConfig(),
|
|
1704
1474
|
caller_filename=caller_filename
|
|
1705
1475
|
)
|
|
@@ -1711,14 +1481,15 @@ def track(
|
|
|
1711
1481
|
use_case_id: Optional[str] = None,
|
|
1712
1482
|
use_case_version: Optional[int] = None,
|
|
1713
1483
|
user_id: Optional[str] = None,
|
|
1484
|
+
account_name: Optional[str] = None,
|
|
1714
1485
|
request_tags: Optional["list[str]"] = None,
|
|
1715
1486
|
request_properties: Optional["dict[str, str]"] = None,
|
|
1716
1487
|
use_case_properties: Optional["dict[str, str]"] = None,
|
|
1717
1488
|
proxy: Optional[bool] = None,
|
|
1718
1489
|
) -> Any:
|
|
1490
|
+
_ = request_tags
|
|
1719
1491
|
|
|
1720
1492
|
def _track(func: Any) -> Any:
|
|
1721
|
-
import asyncio
|
|
1722
1493
|
if asyncio.iscoroutinefunction(func):
|
|
1723
1494
|
async def awrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1724
1495
|
if not _instrumentor:
|
|
@@ -1735,9 +1506,9 @@ def track(
|
|
|
1735
1506
|
use_case_id,
|
|
1736
1507
|
use_case_version,
|
|
1737
1508
|
user_id,
|
|
1738
|
-
|
|
1739
|
-
request_properties,
|
|
1740
|
-
use_case_properties,
|
|
1509
|
+
account_name,
|
|
1510
|
+
cast(Optional['dict[str, Optional[str]]'], request_properties),
|
|
1511
|
+
cast(Optional['dict[str, Optional[str]]'], use_case_properties),
|
|
1741
1512
|
*args,
|
|
1742
1513
|
**kwargs,
|
|
1743
1514
|
)
|
|
@@ -1748,7 +1519,7 @@ def track(
|
|
|
1748
1519
|
_g_logger.debug(f"track: no instrumentor!")
|
|
1749
1520
|
return func(*args, **kwargs)
|
|
1750
1521
|
|
|
1751
|
-
_instrumentor._logger.debug(f"track: call sync function (proxy={proxy}, limit_ids={limit_ids}, use_case_name={use_case_name}, use_case_id={use_case_id}, use_case_version={use_case_version}, user_id={user_id})")
|
|
1522
|
+
_instrumentor._logger.debug(f"track: call sync function (proxy={proxy}, limit_ids={limit_ids}, use_case_name={use_case_name}, use_case_id={use_case_id}, use_case_version={use_case_version}, user_id={user_id}, account_name={account_name})")
|
|
1752
1523
|
|
|
1753
1524
|
return _instrumentor._call_func(
|
|
1754
1525
|
func,
|
|
@@ -1758,9 +1529,9 @@ def track(
|
|
|
1758
1529
|
use_case_id,
|
|
1759
1530
|
use_case_version,
|
|
1760
1531
|
user_id,
|
|
1761
|
-
|
|
1762
|
-
request_properties,
|
|
1763
|
-
use_case_properties,
|
|
1532
|
+
account_name,
|
|
1533
|
+
cast(Optional['dict[str, Optional[str]]'], request_properties),
|
|
1534
|
+
cast(Optional['dict[str, Optional[str]]'], use_case_properties),
|
|
1764
1535
|
*args,
|
|
1765
1536
|
**kwargs,
|
|
1766
1537
|
)
|
|
@@ -1775,6 +1546,7 @@ def track_context(
|
|
|
1775
1546
|
use_case_version: Optional[int] = None,
|
|
1776
1547
|
use_case_step: Optional[str] = None,
|
|
1777
1548
|
user_id: Optional[str] = None,
|
|
1549
|
+
account_name: Optional[str] = None,
|
|
1778
1550
|
request_tags: Optional["list[str]"] = None,
|
|
1779
1551
|
request_properties: Optional["dict[str, str]"] = None,
|
|
1780
1552
|
use_case_properties: Optional["dict[str, str]"] = None,
|
|
@@ -1796,14 +1568,16 @@ def track_context(
|
|
|
1796
1568
|
context["use_case_step"] = use_case_step
|
|
1797
1569
|
|
|
1798
1570
|
context["user_id"] = user_id
|
|
1799
|
-
context["
|
|
1571
|
+
context["account_name"] = account_name
|
|
1800
1572
|
|
|
1801
1573
|
context["price_as_category"] = price_as_category
|
|
1802
1574
|
context["price_as_resource"] = price_as_resource
|
|
1803
1575
|
context["resource_scope"] = resource_scope
|
|
1804
1576
|
|
|
1805
|
-
context["request_properties"] = request_properties
|
|
1806
|
-
context["use_case_properties"] = use_case_properties
|
|
1577
|
+
context["request_properties"] = cast(Optional['dict[str, Optional[str]]'], request_properties)
|
|
1578
|
+
context["use_case_properties"] = cast(Optional['dict[str, Optional[str]]'], use_case_properties)
|
|
1579
|
+
|
|
1580
|
+
_ = request_tags
|
|
1807
1581
|
|
|
1808
1582
|
return _InternalTrackContext(context)
|
|
1809
1583
|
|
|
@@ -1814,7 +1588,7 @@ def get_context() -> PayiContext:
|
|
|
1814
1588
|
"""
|
|
1815
1589
|
if not _instrumentor:
|
|
1816
1590
|
return PayiContext()
|
|
1817
|
-
internal_context = _instrumentor.
|
|
1591
|
+
internal_context = _instrumentor._context_safe
|
|
1818
1592
|
|
|
1819
1593
|
context_dict = {
|
|
1820
1594
|
key: value
|