payi 0.1.0a110__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.
Files changed (83) hide show
  1. payi/__init__.py +3 -1
  2. payi/_base_client.py +12 -12
  3. payi/_client.py +8 -8
  4. payi/_compat.py +48 -48
  5. payi/_models.py +87 -59
  6. payi/_qs.py +7 -7
  7. payi/_streaming.py +4 -6
  8. payi/_types.py +53 -12
  9. payi/_utils/__init__.py +9 -2
  10. payi/_utils/_compat.py +45 -0
  11. payi/_utils/_datetime_parse.py +136 -0
  12. payi/_utils/_sync.py +3 -31
  13. payi/_utils/_transform.py +13 -3
  14. payi/_utils/_typing.py +6 -1
  15. payi/_utils/_utils.py +5 -6
  16. payi/_version.py +1 -1
  17. payi/lib/AnthropicInstrumentor.py +83 -57
  18. payi/lib/BedrockInstrumentor.py +292 -57
  19. payi/lib/GoogleGenAiInstrumentor.py +18 -31
  20. payi/lib/OpenAIInstrumentor.py +56 -72
  21. payi/lib/ProviderRequest.py +216 -0
  22. payi/lib/StreamWrappers.py +379 -0
  23. payi/lib/VertexInstrumentor.py +18 -37
  24. payi/lib/VertexRequest.py +16 -2
  25. payi/lib/data/cohere_embed_english_v3.json +30706 -0
  26. payi/lib/helpers.py +53 -1
  27. payi/lib/instrument.py +404 -668
  28. payi/resources/categories/__init__.py +0 -14
  29. payi/resources/categories/categories.py +25 -53
  30. payi/resources/categories/resources.py +27 -23
  31. payi/resources/ingest.py +126 -132
  32. payi/resources/limits/__init__.py +14 -14
  33. payi/resources/limits/limits.py +58 -58
  34. payi/resources/limits/properties.py +171 -0
  35. payi/resources/requests/request_id/properties.py +8 -8
  36. payi/resources/requests/request_id/result.py +3 -3
  37. payi/resources/requests/response_id/properties.py +8 -8
  38. payi/resources/requests/response_id/result.py +3 -3
  39. payi/resources/use_cases/definitions/definitions.py +27 -27
  40. payi/resources/use_cases/definitions/kpis.py +23 -23
  41. payi/resources/use_cases/definitions/limit_config.py +14 -14
  42. payi/resources/use_cases/definitions/version.py +3 -3
  43. payi/resources/use_cases/kpis.py +15 -15
  44. payi/resources/use_cases/properties.py +6 -6
  45. payi/resources/use_cases/use_cases.py +7 -7
  46. payi/types/__init__.py +2 -0
  47. payi/types/bulk_ingest_response.py +3 -20
  48. payi/types/categories/__init__.py +0 -1
  49. payi/types/categories/resource_list_params.py +5 -1
  50. payi/types/category_list_resources_params.py +5 -1
  51. payi/types/category_resource_response.py +31 -1
  52. payi/types/ingest_event_param.py +7 -6
  53. payi/types/ingest_units_params.py +5 -4
  54. payi/types/limit_create_params.py +3 -3
  55. payi/types/limit_list_response.py +1 -3
  56. payi/types/limit_response.py +1 -3
  57. payi/types/limits/__init__.py +2 -9
  58. payi/types/limits/{tag_remove_params.py → property_update_params.py} +4 -5
  59. payi/types/limits/{tag_delete_response.py → property_update_response.py} +3 -3
  60. payi/types/requests/request_id/property_update_params.py +2 -2
  61. payi/types/requests/response_id/property_update_params.py +2 -2
  62. payi/types/shared/__init__.py +2 -0
  63. payi/types/shared/api_error.py +18 -0
  64. payi/types/shared/pay_i_common_models_budget_management_create_limit_base.py +3 -3
  65. payi/types/shared/properties_request.py +11 -0
  66. payi/types/shared/xproxy_result.py +2 -0
  67. payi/types/shared_params/pay_i_common_models_budget_management_create_limit_base.py +3 -3
  68. payi/types/use_cases/definitions/limit_config_create_params.py +3 -3
  69. payi/types/use_cases/property_update_params.py +2 -2
  70. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/METADATA +6 -6
  71. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/RECORD +73 -75
  72. payi/resources/categories/fixed_cost_resources.py +0 -196
  73. payi/resources/limits/tags.py +0 -507
  74. payi/types/categories/fixed_cost_resource_create_params.py +0 -21
  75. payi/types/limits/limit_tags.py +0 -16
  76. payi/types/limits/tag_create_params.py +0 -13
  77. payi/types/limits/tag_create_response.py +0 -10
  78. payi/types/limits/tag_list_response.py +0 -10
  79. payi/types/limits/tag_remove_response.py +0 -10
  80. payi/types/limits/tag_update_params.py +0 -13
  81. payi/types/limits/tag_update_response.py +0 -10
  82. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/WHEEL +0 -0
  83. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/licenses/LICENSE +0 -0
@@ -1,22 +1,32 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
- from typing import Any, Union, Optional, Sequence
4
+ from typing import Any, Dict, Union, Optional, Sequence
3
5
  from typing_extensions import override
4
6
  from importlib.metadata import version
5
7
 
6
8
  import tiktoken # type: ignore
7
9
  from wrapt import wrap_function_wrapper # type: ignore
8
10
 
9
- from payi.lib.helpers import PayiCategories, PayiHeaderNames
11
+ from payi.lib.helpers import PayiCategories
10
12
  from payi.types.ingest_units_params import Units
11
13
 
12
- from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
14
+ from .instrument import (
15
+ PayiInstrumentAzureOpenAiConfig,
16
+ _Context,
17
+ _IsStreaming,
18
+ _PayiInstrumentor,
19
+ )
13
20
  from .version_helper import get_version_helper
21
+ from .ProviderRequest import _ChunkResult, _StreamingType, _ProviderRequest
14
22
 
15
23
 
16
24
  class OpenAiInstrumentor:
17
25
  _module_name: str = "openai"
18
26
  _module_version: str = ""
19
27
 
28
+ _azure_openai_deployments: Dict[str, _Context] = {}
29
+
20
30
  @staticmethod
21
31
  def is_azure(instance: Any) -> bool:
22
32
  from openai import AzureOpenAI, AsyncAzureOpenAI # type: ignore # noqa: I001
@@ -24,52 +34,31 @@ class OpenAiInstrumentor:
24
34
  return isinstance(instance._client, (AsyncAzureOpenAI, AzureOpenAI))
25
35
 
26
36
  @staticmethod
27
- def instrument(instrumentor: _PayiInstrumentor) -> None:
28
- try:
29
- OpenAiInstrumentor._module_version = get_version_helper(OpenAiInstrumentor._module_name)
30
-
31
- wrap_function_wrapper(
32
- "openai.resources.chat.completions",
33
- "Completions.create",
34
- chat_wrapper(instrumentor),
35
- )
36
-
37
- wrap_function_wrapper(
38
- "openai.resources.chat.completions",
39
- "AsyncCompletions.create",
40
- achat_wrapper(instrumentor),
41
- )
42
-
43
- wrap_function_wrapper(
44
- "openai.resources.embeddings",
45
- "Embeddings.create",
46
- embeddings_wrapper(instrumentor),
47
- )
48
-
49
- wrap_function_wrapper(
50
- "openai.resources.embeddings",
51
- "AsyncEmbeddings.create",
52
- aembeddings_wrapper(instrumentor),
53
- )
54
- except Exception as e:
55
- instrumentor._logger.debug(f"Error instrumenting openai: {e}")
56
-
57
- # responses separately as they are relatively new and the client may not be using the latest openai module
58
- try:
59
- wrap_function_wrapper(
60
- "openai.resources.responses",
61
- "Responses.create",
62
- responses_wrapper(instrumentor),
63
- )
37
+ def configure(azure_openai_config: Optional[PayiInstrumentAzureOpenAiConfig]) -> None:
38
+ if azure_openai_config:
39
+ model_mappings = azure_openai_config.get("model_mappings", [])
40
+ OpenAiInstrumentor._azure_openai_deployments = _PayiInstrumentor._model_mapping_to_context_dict(model_mappings)
64
41
 
65
- wrap_function_wrapper(
66
- "openai.resources.responses",
67
- "AsyncResponses.create",
68
- aresponses_wrapper(instrumentor),
69
- )
70
-
71
- except Exception as e:
72
- instrumentor._logger.debug(f"Error instrumenting openai: {e}")
42
+ @staticmethod
43
+ def instrument(instrumentor: _PayiInstrumentor) -> None:
44
+ OpenAiInstrumentor._module_version = get_version_helper(OpenAiInstrumentor._module_name)
45
+
46
+ wrappers = [
47
+ ("openai._base_client", "AsyncAPIClient._process_response", _ProviderRequest.aprocess_response_wrapper),
48
+ ("openai._base_client", "SyncAPIClient._process_response", _ProviderRequest.process_response_wrapper),
49
+ ("openai.resources.chat.completions", "Completions.create", chat_wrapper(instrumentor)),
50
+ ("openai.resources.chat.completions", "AsyncCompletions.create", achat_wrapper(instrumentor)),
51
+ ("openai.resources.embeddings", "Embeddings.create", embeddings_wrapper(instrumentor)),
52
+ ("openai.resources.embeddings", "AsyncEmbeddings.create", aembeddings_wrapper(instrumentor)),
53
+ ("openai.resources.responses", "Responses.create", responses_wrapper(instrumentor)),
54
+ ("openai.resources.responses", "AsyncResponses.create", aresponses_wrapper(instrumentor)),
55
+ ]
56
+
57
+ for module, method, wrapper in wrappers:
58
+ try:
59
+ wrap_function_wrapper(module, method, wrapper)
60
+ except Exception as e:
61
+ instrumentor._logger.debug(f"Error wrapping {module}.{method}: {e}")
73
62
 
74
63
  @_PayiInstrumentor.payi_wrapper
75
64
  def embeddings_wrapper(
@@ -201,44 +190,39 @@ class _OpenAiProviderRequest(_ProviderRequest):
201
190
  self._input_tokens_details_key = input_tokens_details_key
202
191
 
203
192
  @override
204
- def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool: # type: ignore
205
- self._ingest["resource"] = kwargs.get("model", "")
193
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool: # type: ignore
194
+ model = kwargs.get("model", "")
206
195
 
207
196
  if not (instance and hasattr(instance, "_client")) or OpenAiInstrumentor.is_azure(instance) is False:
197
+ self._ingest["resource"] = model
208
198
  return True
209
199
 
210
- context = self._instrumentor.get_context_safe()
211
- price_as_category = extra_headers.get(PayiHeaderNames.price_as_category) or context.get("price_as_category")
212
- price_as_resource = extra_headers.get(PayiHeaderNames.price_as_resource) or context.get("price_as_resource")
213
- resource_scope = extra_headers.get(PayiHeaderNames.resource_scope) or context.get("resource_scope")
214
-
215
- if PayiHeaderNames.price_as_category in extra_headers:
216
- del extra_headers[PayiHeaderNames.price_as_category]
217
- if PayiHeaderNames.price_as_resource in extra_headers:
218
- del extra_headers[PayiHeaderNames.price_as_resource]
219
- if PayiHeaderNames.resource_scope in extra_headers:
220
- del extra_headers[PayiHeaderNames.resource_scope]
221
-
222
- if not price_as_resource and not price_as_category:
200
+ if not self._price_as.resource and not self._price_as.category and OpenAiInstrumentor._azure_openai_deployments:
201
+ deployment = OpenAiInstrumentor._azure_openai_deployments.get(model, {})
202
+ self._price_as.category = deployment.get("price_as_category", None)
203
+ self._price_as.resource = deployment.get("price_as_resource", None)
204
+ self._price_as.resource_scope = deployment.get("resource_scope", None)
205
+
206
+ if not self._price_as.resource and not self._price_as.category:
223
207
  self._instrumentor._logger.error("Azure OpenAI requires price as resource and/or category to be specified, not ingesting")
224
208
  return False
225
209
 
226
- if resource_scope:
227
- if not(resource_scope in ["global", "datazone"] or resource_scope.startswith("region")):
210
+ if self._price_as.resource_scope:
211
+ if not (self._price_as.resource_scope in ["global", "datazone"] or self._price_as.resource_scope.startswith("region")):
228
212
  self._instrumentor._logger.error("Azure OpenAI invalid resource scope, not ingesting")
229
213
  return False
230
214
 
231
- self._ingest["resource_scope"] = resource_scope
215
+ self._ingest["resource_scope"] = self._price_as.resource_scope
232
216
 
233
217
  self._category = PayiCategories.azure_openai
234
218
 
235
219
  self._ingest["category"] = self._category
236
220
 
237
- if price_as_category:
221
+ if self._price_as.category:
238
222
  # price as category overrides default
239
- self._ingest["category"] = price_as_category
240
- if price_as_resource:
241
- self._ingest["resource"] = price_as_resource
223
+ self._ingest["category"] = self._price_as.category
224
+ if self._price_as.resource:
225
+ self._ingest["resource"] = self._price_as.resource
242
226
 
243
227
  return True
244
228
 
@@ -304,7 +288,7 @@ class _OpenAiProviderRequest(_ProviderRequest):
304
288
  if input_cache != 0:
305
289
  units["text_cache_read"] = Units(input=input_cache, output=0)
306
290
 
307
- input = _PayiInstrumentor.update_for_vision(input - input_cache, units, self._estimated_prompt_tokens)
291
+ input = self.update_for_vision(input - input_cache)
308
292
 
309
293
  units["text"] = Units(input=input, output=output)
310
294
 
@@ -614,4 +598,4 @@ def model_to_dict(model: Any) -> Any:
614
598
  elif hasattr(model, "parse"): # Raw API response
615
599
  return model_to_dict(model.parse())
616
600
  else:
617
- return model
601
+ return model
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from abc import abstractmethod
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING, Any, Optional, Sequence
7
+ from dataclasses import dataclass
8
+
9
+ from payi.types import IngestUnitsParams
10
+ from payi.lib.helpers import PayiPropertyNames
11
+ from payi.types.ingest_units_params import ProviderResponseFunctionCall
12
+ from payi.types.shared.xproxy_error import XproxyError
13
+ from payi.types.shared.xproxy_result import XproxyResult
14
+ from payi.types.shared_params.ingest_units import IngestUnits
15
+ from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
16
+
17
+ from .helpers import _set_attr_safe
18
+
19
+ if TYPE_CHECKING:
20
+ from .instrument import _PayiInstrumentor
21
+
22
+ class _StreamingType(Enum):
23
+ generator = 0
24
+ iterator = 1
25
+ stream_manager = 2
26
+
27
+ @dataclass
28
+ class _ChunkResult:
29
+ send_chunk_to_caller: bool
30
+ ingest: bool = False
31
+
32
+ @dataclass
33
+ class PriceAs:
34
+ category: Optional[str]
35
+ resource: Optional[str]
36
+ resource_scope: Optional[str]
37
+
38
+ class _ProviderRequest:
39
+ excluded_headers = {
40
+ "transfer-encoding",
41
+ }
42
+
43
+ _instrumented_response_headers_attr = "_instrumented_response_headers"
44
+ _xproxy_result_attr = "xproxy_result"
45
+
46
+ def __init__(
47
+ self,
48
+ instrumentor: _PayiInstrumentor,
49
+ category: str,
50
+ streaming_type: _StreamingType,
51
+ module_name: str,
52
+ module_version: str,
53
+ is_aws_client: Optional[bool] = None,
54
+ is_google_vertex_or_genai_client: Optional[bool] = None,
55
+ ) -> None:
56
+ self._instrumentor: _PayiInstrumentor = instrumentor
57
+ self._module_name: str = module_name
58
+ self._module_version: str = module_version
59
+ self._estimated_prompt_tokens: Optional[int] = None
60
+ self._category: str = category
61
+ self._ingest: IngestUnitsParams = { "category": category, "units": {} } # type: ignore
62
+ self._streaming_type: '_StreamingType' = streaming_type
63
+ self._is_aws_client: Optional[bool] = is_aws_client
64
+ self._is_google_vertex_or_genai_client: Optional[bool] = is_google_vertex_or_genai_client
65
+ self._function_call_builder: Optional[dict[int, ProviderResponseFunctionCall]] = None
66
+ self._building_function_response: bool = False
67
+ self._function_calls: Optional[list[ProviderResponseFunctionCall]] = None
68
+ self._is_large_context: bool = False
69
+ self._internal_request_properties: dict[str, Optional[str]] = {}
70
+ self._price_as: PriceAs = PriceAs(category=None, resource=None, resource_scope=None)
71
+
72
+ def process_chunk(self, _chunk: Any) -> _ChunkResult:
73
+ return _ChunkResult(send_chunk_to_caller=True)
74
+
75
+ def process_synchronous_response(self, response: Any, log_prompt_and_response: bool, kwargs: Any) -> Optional[object]: # noqa: ARG002
76
+ return None
77
+
78
+ @abstractmethod
79
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
80
+ ...
81
+
82
+ def process_request_prompt(self, prompt: 'dict[str, Any]', args: Sequence[Any], kwargs: 'dict[str, Any]') -> None:
83
+ ...
84
+
85
+ def process_initial_stream_response(self, response: Any) -> None:
86
+ self.add_instrumented_response_headers(response)
87
+
88
+ def remove_inline_data(self, prompt: 'dict[str, Any]') -> bool:# noqa: ARG002
89
+ return False
90
+
91
+ @property
92
+ def is_aws_client(self) -> bool:
93
+ return self._is_aws_client if self._is_aws_client is not None else False
94
+
95
+ @property
96
+ def is_google_vertex_or_genai_client(self) -> bool:
97
+ return self._is_google_vertex_or_genai_client if self._is_google_vertex_or_genai_client is not None else False
98
+
99
+ def process_exception(self, exception: Exception, kwargs: Any, ) -> bool: # noqa: ARG002
100
+ self.exception_to_semantic_failure(exception)
101
+ return True
102
+
103
+ @property
104
+ def supports_extra_headers(self) -> bool:
105
+ return not self.is_aws_client and not self.is_google_vertex_or_genai_client
106
+
107
+ @property
108
+ def streaming_type(self) -> '_StreamingType':
109
+ return self._streaming_type
110
+
111
+ def add_internal_request_property(self, key: str, value: str) -> None:
112
+ self._internal_request_properties[key] = value
113
+
114
+ def exception_to_semantic_failure(self, e: Exception) -> None:
115
+ exception_str = f"{type(e).__name__}"
116
+
117
+ fields: list[str] = []
118
+
119
+ for attr in dir(e):
120
+ if not attr.startswith("__"):
121
+ try:
122
+ value = getattr(e, attr)
123
+ if value and not inspect.ismethod(value) and not inspect.isfunction(value) and not callable(value):
124
+ fields.append(f"{attr}={value}")
125
+ except Exception as _ex:
126
+ pass
127
+
128
+ self.add_internal_request_property(PayiPropertyNames.failure, exception_str)
129
+ if fields:
130
+ failure_description = ",".join(fields)
131
+ self.add_internal_request_property(PayiPropertyNames.failure_description, failure_description)
132
+
133
+ if "http_status_code" not in self._ingest:
134
+ # use a non existent http status code so when presented to the user, the origin is clear
135
+ self._ingest["http_status_code"] = 299
136
+
137
+ def add_streaming_function_call(self, index: int, name: Optional[str], arguments: Optional[str]) -> None:
138
+ if not self._function_call_builder:
139
+ self._function_call_builder = {}
140
+
141
+ if not index in self._function_call_builder:
142
+ self._function_call_builder[index] = ProviderResponseFunctionCall(name=name or "", arguments=arguments or "")
143
+ else:
144
+ function = self._function_call_builder[index]
145
+ if name:
146
+ function["name"] = function["name"] + name
147
+ if arguments:
148
+ function["arguments"] = (function.get("arguments", "") or "") + arguments
149
+
150
+ def add_synchronous_function_call(self, name: str, arguments: Optional[str]) -> None:
151
+ if not self._function_calls:
152
+ self._function_calls = []
153
+ self._ingest["provider_response_function_calls"] = self._function_calls
154
+ self._function_calls.append(ProviderResponseFunctionCall(name=name, arguments=arguments))
155
+
156
+ def add_instrumented_response_headers(self, response: Any) -> None:
157
+ response_headers = getattr(response, _ProviderRequest._instrumented_response_headers_attr, {})
158
+ if response_headers:
159
+ self.add_response_headers(response_headers)
160
+
161
+ def add_response_headers(self, response_headers: 'dict[str, Any]') -> None:
162
+ self._ingest["provider_response_headers"] = [
163
+ PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v)
164
+ for k, v in response_headers.items()
165
+ if (k_lower := k.lower()) not in _ProviderRequest.excluded_headers and not k_lower.startswith("content-")
166
+ ]
167
+
168
+ def merge_internal_request_properties(self) -> None:
169
+ if not self._internal_request_properties:
170
+ return
171
+
172
+ properties = self._ingest.get("properties") or {}
173
+ self._ingest["properties"] = properties
174
+ for key, value in self._internal_request_properties.items():
175
+ if key not in properties:
176
+ properties[key] = value
177
+
178
+ def update_for_vision(self, input: int) -> int:
179
+ if self._estimated_prompt_tokens:
180
+ vision = input - self._estimated_prompt_tokens
181
+ if (vision > 0):
182
+ key = "vision_large_context" if self._is_large_context else "vision"
183
+ self._ingest["units"][key] = IngestUnits(input=vision, output=0)
184
+ input = self._estimated_prompt_tokens
185
+
186
+ return input
187
+
188
+ @staticmethod
189
+ def assign_xproxy_result(o: Any, xproxy_result: XproxyResult | XproxyError| None) -> None:
190
+ if xproxy_result:
191
+ _set_attr_safe(o, _ProviderRequest._xproxy_result_attr, xproxy_result)
192
+
193
+ @staticmethod
194
+ def process_response_wrapper(wrapped: Any, _instance: Any, args: Any, kwargs: Any) -> Any:
195
+ httpResponse = kwargs.get("response", None)
196
+
197
+ r = wrapped(*args, **kwargs)
198
+
199
+ if httpResponse:
200
+ headers = getattr(httpResponse, "headers", None)
201
+ _set_attr_safe(r, _ProviderRequest._instrumented_response_headers_attr, dict(headers) if headers else {})
202
+
203
+ return r
204
+
205
+ @staticmethod
206
+ async def aprocess_response_wrapper(wrapped: Any, _instance: Any, args: Any, kwargs: Any) -> Any:
207
+ httpResponse = kwargs.get("response", None)
208
+
209
+ r = await wrapped(*args, **kwargs)
210
+
211
+ if httpResponse:
212
+ headers = getattr(httpResponse, "headers", None)
213
+ _set_attr_safe(r, _ProviderRequest._instrumented_response_headers_attr, dict(headers) if headers else {})
214
+
215
+ return r
216
+