payi 0.1.0a76__py3-none-any.whl → 0.1.0a78__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.

Potentially problematic release.


This version of payi might be problematic. Click here for more details.

payi/_base_client.py CHANGED
@@ -960,6 +960,9 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
960
960
  if self.custom_auth is not None:
961
961
  kwargs["auth"] = self.custom_auth
962
962
 
963
+ if options.follow_redirects is not None:
964
+ kwargs["follow_redirects"] = options.follow_redirects
965
+
963
966
  log.debug("Sending HTTP Request: %s %s", request.method, request.url)
964
967
 
965
968
  response = None
@@ -1460,6 +1463,9 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1460
1463
  if self.custom_auth is not None:
1461
1464
  kwargs["auth"] = self.custom_auth
1462
1465
 
1466
+ if options.follow_redirects is not None:
1467
+ kwargs["follow_redirects"] = options.follow_redirects
1468
+
1463
1469
  log.debug("Sending HTTP Request: %s %s", request.method, request.url)
1464
1470
 
1465
1471
  response = None
payi/_models.py CHANGED
@@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
737
737
  idempotency_key: str
738
738
  json_data: Body
739
739
  extra_json: AnyMapping
740
+ follow_redirects: bool
740
741
 
741
742
 
742
743
  @final
@@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel):
750
751
  files: Union[HttpxRequestFiles, None] = None
751
752
  idempotency_key: Union[str, None] = None
752
753
  post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
754
+ follow_redirects: Union[bool, None] = None
753
755
 
754
756
  # It should be noted that we cannot use `json` here as that would override
755
757
  # a BaseModel method in an incompatible fashion.
payi/_types.py CHANGED
@@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False):
100
100
  params: Query
101
101
  extra_json: AnyMapping
102
102
  idempotency_key: str
103
+ follow_redirects: bool
103
104
 
104
105
 
105
106
  # Sentinel class used until PEP 0661 is accepted
@@ -215,3 +216,4 @@ class _GenericAlias(Protocol):
215
216
 
216
217
  class HttpxSendArgs(TypedDict, total=False):
217
218
  auth: httpx.Auth
219
+ follow_redirects: bool
payi/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "payi"
4
- __version__ = "0.1.0-alpha.76" # x-release-please-version
4
+ __version__ = "0.1.0-alpha.78" # x-release-please-version
@@ -8,7 +8,7 @@ from wrapt import wrap_function_wrapper # type: ignore
8
8
  from payi.lib.helpers import PayiCategories
9
9
  from payi.types.ingest_units_params import Units
10
10
 
11
- from .instrument import _IsStreaming, _ProviderRequest, _PayiInstrumentor
11
+ from .instrument import _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
12
12
 
13
13
 
14
14
  class AnthropicInstrumentor:
@@ -32,7 +32,7 @@ class AnthropicInstrumentor:
32
32
  wrap_function_wrapper(
33
33
  "anthropic.resources.messages",
34
34
  "Messages.stream",
35
- messages_wrapper(instrumentor),
35
+ stream_messages_wrapper(instrumentor),
36
36
  )
37
37
 
38
38
  wrap_function_wrapper(
@@ -44,7 +44,7 @@ class AnthropicInstrumentor:
44
44
  wrap_function_wrapper(
45
45
  "anthropic.resources.messages",
46
46
  "AsyncMessages.stream",
47
- amessages_wrapper(instrumentor),
47
+ astream_messages_wrapper(instrumentor),
48
48
  )
49
49
 
50
50
  except Exception as e:
@@ -61,7 +61,7 @@ def messages_wrapper(
61
61
  **kwargs: Any,
62
62
  ) -> Any:
63
63
  return instrumentor.invoke_wrapper(
64
- _AnthropicProviderRequest(instrumentor, instance),
64
+ _AnthropicProviderRequest(instrumentor=instrumentor, streaming_type=_StreamingType.iterator, instance=instance),
65
65
  _IsStreaming.kwargs,
66
66
  wrapped,
67
67
  instance,
@@ -69,6 +69,23 @@ def messages_wrapper(
69
69
  kwargs,
70
70
  )
71
71
 
72
+ @_PayiInstrumentor.payi_wrapper
73
+ def stream_messages_wrapper(
74
+ instrumentor: _PayiInstrumentor,
75
+ wrapped: Any,
76
+ instance: Any,
77
+ *args: Any,
78
+ **kwargs: Any,
79
+ ) -> Any:
80
+ return instrumentor.invoke_wrapper(
81
+ _AnthropicProviderRequest(instrumentor=instrumentor, streaming_type=_StreamingType.stream_manager, instance=instance),
82
+ _IsStreaming.true,
83
+ wrapped,
84
+ instance,
85
+ args,
86
+ kwargs,
87
+ )
88
+
72
89
  @_PayiInstrumentor.payi_awrapper
73
90
  async def amessages_wrapper(
74
91
  instrumentor: _PayiInstrumentor,
@@ -78,7 +95,7 @@ async def amessages_wrapper(
78
95
  **kwargs: Any,
79
96
  ) -> Any:
80
97
  return await instrumentor.async_invoke_wrapper(
81
- _AnthropicProviderRequest(instrumentor, instance),
98
+ _AnthropicProviderRequest(instrumentor=instrumentor, streaming_type=_StreamingType.iterator, instance=instance),
82
99
  _IsStreaming.kwargs,
83
100
  wrapped,
84
101
  instance,
@@ -86,12 +103,30 @@ async def amessages_wrapper(
86
103
  kwargs,
87
104
  )
88
105
 
106
+ @_PayiInstrumentor.payi_awrapper
107
+ async def astream_messages_wrapper(
108
+ instrumentor: _PayiInstrumentor,
109
+ wrapped: Any,
110
+ instance: Any,
111
+ *args: Any,
112
+ **kwargs: Any,
113
+ ) -> Any:
114
+ return await instrumentor.async_invoke_wrapper(
115
+ _AnthropicProviderRequest(instrumentor=instrumentor, streaming_type=_StreamingType.stream_manager, instance=instance),
116
+ _IsStreaming.true,
117
+ wrapped,
118
+ instance,
119
+ args,
120
+ kwargs,
121
+ )
122
+
89
123
  class _AnthropicProviderRequest(_ProviderRequest):
90
- def __init__(self, instrumentor: _PayiInstrumentor, instance: Any = None) -> None:
124
+ def __init__(self, instrumentor: _PayiInstrumentor, streaming_type: _StreamingType, instance: Any = None) -> None:
91
125
  self._vertex: bool = AnthropicInstrumentor.is_vertex(instance)
92
126
  super().__init__(
93
127
  instrumentor=instrumentor,
94
- category=PayiCategories.google_vertex if self._vertex else PayiCategories.anthropic
128
+ category=PayiCategories.google_vertex if self._vertex else PayiCategories.anthropic,
129
+ streaming_type=streaming_type,
95
130
  )
96
131
 
97
132
  @override
@@ -209,5 +244,4 @@ def has_image_and_get_texts(encoding: tiktoken.Encoding, content: Union[str, 'li
209
244
  token_count = sum(len(encoding.encode(item.get("text", ""))) for item in content if item.get("type") == "text")
210
245
  return has_image, token_count
211
246
 
212
- return False, 0
213
-
247
+ return False, 0
@@ -11,7 +11,7 @@ from payi.lib.helpers import PayiCategories, PayiHeaderNames, payi_aws_bedrock_u
11
11
  from payi.types.ingest_units_params import Units, IngestUnitsParams
12
12
  from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
13
13
 
14
- from .instrument import _IsStreaming, _ProviderRequest, _PayiInstrumentor
14
+ from .instrument import _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
15
15
 
16
16
  _supported_model_prefixes = ["meta.llama3", "anthropic.", "amazon.nova-pro", "amazon.nova-lite", "amazon.nova-micro"]
17
17
 
@@ -216,7 +216,11 @@ def wrap_converse_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
216
216
 
217
217
  class _BedrockProviderRequest(_ProviderRequest):
218
218
  def __init__(self, instrumentor: _PayiInstrumentor):
219
- super().__init__(instrumentor=instrumentor, category=PayiCategories.aws_bedrock)
219
+ super().__init__(
220
+ instrumentor=instrumentor,
221
+ category=PayiCategories.aws_bedrock,
222
+ streaming_type=_StreamingType.iterator,
223
+ )
220
224
 
221
225
  @override
222
226
  def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
@@ -0,0 +1,370 @@
1
+ import json
2
+ import math
3
+ import logging
4
+ from typing import Any, List, Union, Optional, Sequence
5
+ from typing_extensions import override
6
+
7
+ from wrapt import wrap_function_wrapper # type: ignore
8
+
9
+ from payi.lib.helpers import PayiCategories
10
+ from payi.types.ingest_units_params import Units
11
+
12
+ from .instrument import _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
13
+
14
+
15
+ class GoogleGenAiInstrumentor:
16
+ @staticmethod
17
+ def instrument(instrumentor: _PayiInstrumentor) -> None:
18
+ try:
19
+ wrap_function_wrapper(
20
+ "google.genai.models",
21
+ "Models.generate_content",
22
+ generate_wrapper(instrumentor),
23
+ )
24
+
25
+ wrap_function_wrapper(
26
+ "google.genai.models",
27
+ "Models.generate_content_stream",
28
+ generate_stream_wrapper(instrumentor),
29
+ )
30
+
31
+ wrap_function_wrapper(
32
+ "google.genai.models",
33
+ "AsyncModels.generate_content",
34
+ agenerate_wrapper(instrumentor),
35
+ )
36
+
37
+ wrap_function_wrapper(
38
+ "google.genai.models",
39
+ "AsyncModels.generate_content_stream",
40
+ agenerate_stream_wrapper(instrumentor),
41
+ )
42
+
43
+ except Exception as e:
44
+ logging.debug(f"Error instrumenting vertex: {e}")
45
+ return
46
+
47
+ @_PayiInstrumentor.payi_wrapper
48
+ def generate_wrapper(
49
+ instrumentor: _PayiInstrumentor,
50
+ wrapped: Any,
51
+ instance: Any,
52
+ *args: Any,
53
+ **kwargs: Any,
54
+ ) -> Any:
55
+ return instrumentor.invoke_wrapper(
56
+ _GoogleGenAiRequest(instrumentor),
57
+ _IsStreaming.false,
58
+ wrapped,
59
+ instance,
60
+ args,
61
+ kwargs,
62
+ )
63
+
64
+ @_PayiInstrumentor.payi_wrapper
65
+ def generate_stream_wrapper(
66
+ instrumentor: _PayiInstrumentor,
67
+ wrapped: Any,
68
+ instance: Any,
69
+ *args: Any,
70
+ **kwargs: Any,
71
+ ) -> Any:
72
+ return instrumentor.invoke_wrapper(
73
+ _GoogleGenAiRequest(instrumentor),
74
+ _IsStreaming.true,
75
+ wrapped,
76
+ instance,
77
+ args,
78
+ kwargs,
79
+ )
80
+
81
+ @_PayiInstrumentor.payi_awrapper
82
+ async def agenerate_wrapper(
83
+ instrumentor: _PayiInstrumentor,
84
+ wrapped: Any,
85
+ instance: Any,
86
+ *args: Any,
87
+ **kwargs: Any,
88
+ ) -> Any:
89
+ return await instrumentor.async_invoke_wrapper(
90
+ _GoogleGenAiRequest(instrumentor),
91
+ _IsStreaming.false,
92
+ wrapped,
93
+ instance,
94
+ args,
95
+ kwargs,
96
+ )
97
+
98
+ @_PayiInstrumentor.payi_wrapper
99
+ async def agenerate_stream_wrapper(
100
+ instrumentor: _PayiInstrumentor,
101
+ wrapped: Any,
102
+ instance: Any,
103
+ *args: Any,
104
+ **kwargs: Any,
105
+ ) -> Any:
106
+ return await instrumentor.async_invoke_wrapper(
107
+ _GoogleGenAiRequest(instrumentor),
108
+ _IsStreaming.true,
109
+ wrapped,
110
+ instance,
111
+ args,
112
+ kwargs,
113
+ )
114
+
115
+ def count_chars_skip_spaces(text: str) -> int:
116
+ return sum(1 for c in text if not c.isspace())
117
+
118
+ class _GoogleGenAiRequest(_ProviderRequest):
119
+ def __init__(self, instrumentor: _PayiInstrumentor):
120
+ super().__init__(
121
+ instrumentor=instrumentor,
122
+ category=PayiCategories.google_vertex,
123
+ streaming_type=_StreamingType.generator,
124
+ )
125
+ self._prompt_character_count = 0
126
+ self._candiates_character_count = 0
127
+
128
+ @override
129
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
130
+ from google.genai.types import Content, PIL_Image, Part # type: ignore # noqa: F401 I001
131
+
132
+ if not kwargs:
133
+ return True
134
+
135
+ model: str = kwargs.get("model", "")
136
+ self._ingest["resource"] = "google." + model
137
+
138
+ value: Union[ # type: ignore
139
+ Content,
140
+ str,
141
+ PIL_Image,
142
+ Part,
143
+ List[Union[str, PIL_Image, Part]],
144
+ ] = kwargs.get("contents", None) # type: ignore
145
+
146
+ items: List[Union[str, Image, Part]] = [] # type: ignore # noqa: F401 I001
147
+
148
+ if not value:
149
+ raise TypeError("value must not be empty")
150
+
151
+ if isinstance(value, Content):
152
+ items = value.parts # type: ignore
153
+ if isinstance(value, (str, PIL_Image, Part)):
154
+ items = [value] # type: ignore
155
+ if isinstance(value, list):
156
+ items = value # type: ignore
157
+
158
+ for item in items: # type: ignore
159
+ text = ""
160
+ if isinstance(item, Part):
161
+ d = item.to_json_dict() # type: ignore
162
+ if "text" in d:
163
+ text = d["text"] # type: ignore
164
+ elif isinstance(item, str):
165
+ text = item
166
+
167
+ if text != "":
168
+ self._prompt_character_count += count_chars_skip_spaces(text) # type: ignore
169
+
170
+ return True
171
+
172
+ @override
173
+ def process_request_prompt(self, prompt: 'dict[str, Any]', args: Sequence[Any], kwargs: 'dict[str, Any]') -> None:
174
+ from google.genai.types import Content, PIL_Image, Part, Tool, GenerateContentConfig, GenerateContentConfigDict, ToolConfig # type: ignore # noqa: F401 I001
175
+
176
+ key = "contents"
177
+
178
+ if not kwargs:
179
+ return
180
+
181
+ value: Union[ # type: ignore
182
+ Content,
183
+ str,
184
+ PIL_Image,
185
+ Part,
186
+ List[Union[str, PIL_Image, Part]],
187
+ ] = kwargs.get("contents", None) # type: ignore
188
+
189
+ items: List[Union[str, Image, Part]] = [] # type: ignore # noqa: F401 I001
190
+
191
+ if not value:
192
+ return
193
+
194
+ if isinstance(value, str):
195
+ prompt[key] = Content(parts=[Part.from_text(text=value)]).to_json_dict() # type: ignore
196
+ elif isinstance(value, (PIL_Image, Part)):
197
+ prompt[key] = Content(parts=[value]).to_json_dict() # type: ignore
198
+ elif isinstance(value, Content):
199
+ prompt[key] = value.to_json_dict() # type: ignore
200
+ elif isinstance(value, list):
201
+ items = value # type: ignore
202
+ parts = []
203
+
204
+ for item in items: # type: ignore
205
+ if isinstance(item, str):
206
+ parts.append(Part.from_text(text=item)) # type: ignore
207
+ elif isinstance(item, Part):
208
+ parts.append(item) # type: ignore
209
+ # elif isinstance(item, PIL_Image): TODO
210
+ # parts.append(Part.from_image(item)) # type: ignore
211
+
212
+ prompt[key] = Content(parts=parts).to_json_dict() # type: ignore
213
+
214
+ # tools: Optional[list[Tool]] = kwargs.get("tools", None) # type: ignore
215
+ # if tools:
216
+ # t: list[dict[Any, Any]] = []
217
+ # for tool in tools: # type: ignore
218
+ # if isinstance(tool, Tool):
219
+ # t.append(tool.text=()) # type: ignore
220
+ # if t:
221
+ # prompt["tools"] = t
222
+ config_kwarg = kwargs.get("config", None) # type: ignore
223
+ if config_kwarg is None:
224
+ return
225
+
226
+ config: GenerateContentConfigDict = {}
227
+ if isinstance(config_kwarg, GenerateContentConfig):
228
+ config = config_kwarg.to_json_dict() # type: ignore
229
+ else:
230
+ config = config_kwarg
231
+
232
+ tools = config.get("tools", None) # type: ignore
233
+ if isinstance(tools, list):
234
+ t: list[dict[str, object]] = []
235
+ for tool in tools: # type: ignore
236
+ if isinstance(tool, Tool):
237
+ t.append(tool.to_json_dict()) # type: ignore
238
+ if t:
239
+ prompt["tools"] = t
240
+
241
+ tool_config = config.get("tool_config", None) # type: ignore
242
+ if isinstance(tool_config, ToolConfig):
243
+ prompt["tool_config"] = tool_config.to_json_dict() # type: ignore
244
+ elif isinstance(tool_config, dict):
245
+ prompt["tool_config"] = tool_config
246
+
247
+ @override
248
+ def process_chunk(self, chunk: Any) -> bool:
249
+ response_dict: dict[str, Any] = chunk.to_json_dict()
250
+ if "provider_response_id" not in self._ingest:
251
+ id = response_dict.get("response_id", None)
252
+ if id:
253
+ self._ingest["provider_response_id"] = id
254
+
255
+ model: str = response_dict.get("model_version", "")
256
+
257
+ self._ingest["resource"] = "google." + model
258
+
259
+ for candidate in response_dict.get("candidates", []):
260
+ parts = candidate.get("content", {}).get("parts", [])
261
+ for part in parts:
262
+ self._candiates_character_count += count_chars_skip_spaces(part.get("text", ""))
263
+
264
+ usage = response_dict.get("usage_metadata", {})
265
+ if usage and "prompt_token_count" in usage and "candidates_token_count" in usage:
266
+ self._compute_usage(response_dict, streaming_candidates_characters=self._candiates_character_count)
267
+
268
+ return True
269
+
270
+ @staticmethod
271
+ def _is_character_billing_model(model: str) -> bool:
272
+ return model.startswith("gemini-1.")
273
+
274
+ @override
275
+ def process_synchronous_response(
276
+ self,
277
+ response: Any,
278
+ log_prompt_and_response: bool,
279
+ kwargs: Any) -> Any:
280
+ response_dict = response.to_json_dict()
281
+
282
+ self._ingest["provider_response_id"] = response_dict["response_id"]
283
+ self._ingest["resource"] = "google." + response_dict["model_version"]
284
+
285
+ self._compute_usage(response_dict)
286
+
287
+ if log_prompt_and_response:
288
+ self._ingest["provider_response_json"] = [json.dumps(response_dict)]
289
+
290
+ return None
291
+
292
+ def add_units(self, key: str, input: Optional[int] = None, output: Optional[int] = None) -> None:
293
+ if key not in self._ingest["units"]:
294
+ self._ingest["units"][key] = {}
295
+ if input is not None:
296
+ self._ingest["units"][key]["input"] = input
297
+ if output is not None:
298
+ self._ingest["units"][key]["output"] = output
299
+
300
+ def _compute_usage(self, response_dict: 'dict[str, Any]', streaming_candidates_characters: Optional[int] = None) -> None:
301
+ usage = response_dict.get("usage_metadata", {})
302
+ input = usage.get("prompt_token_count", 0)
303
+
304
+ prompt_tokens_details: list[dict[str, Any]] = usage.get("prompt_tokens_details")
305
+ candidates_tokens_details: list[dict[str, Any]] = usage.get("candidates_tokens_details")
306
+
307
+ model: str = response_dict.get("model_version", "")
308
+
309
+ if self._is_character_billing_model(model):
310
+ # gemini 1.0 and 1.5 units are reported in characters, per second, per image, etc...
311
+ large_context = "" if input < 128000 else "_large_context"
312
+
313
+ for details in prompt_tokens_details:
314
+ modality = details.get("modality", "")
315
+ if not modality:
316
+ continue
317
+
318
+ modality_token_count = details.get("token_count", 0)
319
+ if modality == "TEXT":
320
+ input = self._prompt_character_count
321
+ if input == 0:
322
+ # back up calc if nothing was calculated from the prompt
323
+ input = response_dict["usage_metadata"]["prompt_token_count"] * 4
324
+
325
+ output = 0
326
+ if streaming_candidates_characters is None:
327
+ for candidate in response_dict.get("candidates", []):
328
+ parts = candidate.get("content", {}).get("parts", [])
329
+ for part in parts:
330
+ output += count_chars_skip_spaces(part.get("text", ""))
331
+
332
+ if output == 0:
333
+ # back up calc if no parts
334
+ output = response_dict["usage_metadata"]["candidates_token_count"] * 4
335
+ else:
336
+ output = streaming_candidates_characters
337
+
338
+ self._ingest["units"]["text"+large_context] = Units(input=input, output=output)
339
+
340
+ elif modality == "IMAGE":
341
+ num_images = math.ceil(modality_token_count / 258)
342
+ self.add_units("vision"+large_context, input=num_images)
343
+
344
+ elif modality == "VIDEO":
345
+ video_seconds = math.ceil(modality_token_count / 285)
346
+ self.add_units("video"+large_context, input=video_seconds)
347
+
348
+ elif modality == "AUDIO":
349
+ audio_seconds = math.ceil(modality_token_count / 25)
350
+ self.add_units("audio"+large_context, input=audio_seconds)
351
+
352
+ elif model.startswith("gemini-2.0"):
353
+ for details in prompt_tokens_details:
354
+ modality = details.get("modality", "")
355
+ if not modality:
356
+ continue
357
+
358
+ modality_token_count = details.get("token_count", 0)
359
+ if modality == "IMAGE":
360
+ self.add_units("vision", input=modality_token_count)
361
+ elif modality in ("VIDEO", "AUDIO", "TEXT"):
362
+ self.add_units(modality.lower(), input=modality_token_count)
363
+ for details in candidates_tokens_details:
364
+ modality = details.get("modality", "")
365
+ if not modality:
366
+ continue
367
+
368
+ modality_token_count = details.get("token_count", 0)
369
+ if modality in ("VIDEO", "AUDIO", "TEXT", "IMAGE"):
370
+ self.add_units(modality.lower(), output=modality_token_count)
@@ -10,7 +10,7 @@ from wrapt import wrap_function_wrapper # type: ignore
10
10
  from payi.lib.helpers import PayiCategories, PayiHeaderNames
11
11
  from payi.types.ingest_units_params import Units
12
12
 
13
- from .instrument import _IsStreaming, _ProviderRequest, _PayiInstrumentor
13
+ from .instrument import _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
14
14
 
15
15
 
16
16
  class OpenAiInstrumentor:
@@ -178,7 +178,11 @@ class _OpenAiProviderRequest(_ProviderRequest):
178
178
  responses_input_tokens_details_key: str = "input_tokens_details"
179
179
 
180
180
  def __init__(self, instrumentor: _PayiInstrumentor, input_tokens_key: str, output_tokens_key: str, input_tokens_details_key: str) -> None:
181
- super().__init__(instrumentor=instrumentor, category=PayiCategories.openai)
181
+ super().__init__(
182
+ instrumentor=instrumentor,
183
+ category=PayiCategories.openai,
184
+ streaming_type=_StreamingType.iterator,
185
+ )
182
186
  self._input_tokens_key = input_tokens_key
183
187
  self._output_tokens_key = output_tokens_key
184
188
  self._input_tokens_details_key = input_tokens_details_key
@@ -9,7 +9,7 @@ from wrapt import wrap_function_wrapper # type: ignore
9
9
  from payi.lib.helpers import PayiCategories
10
10
  from payi.types.ingest_units_params import Units
11
11
 
12
- from .instrument import _IsStreaming, _ProviderRequest, _PayiInstrumentor
12
+ from .instrument import _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
13
13
 
14
14
 
15
15
  class VertexInstrumentor:
@@ -85,7 +85,11 @@ def count_chars_skip_spaces(text: str) -> int:
85
85
 
86
86
  class _GoogleVertexRequest(_ProviderRequest):
87
87
  def __init__(self, instrumentor: _PayiInstrumentor):
88
- super().__init__(instrumentor=instrumentor, category=PayiCategories.google_vertex)
88
+ super().__init__(
89
+ instrumentor=instrumentor,
90
+ category=PayiCategories.google_vertex,
91
+ streaming_type=_StreamingType.generator,
92
+ )
89
93
  self._prompt_character_count = 0
90
94
  self._candiates_character_count = 0
91
95
 
@@ -110,11 +114,10 @@ class _GoogleVertexRequest(_ProviderRequest):
110
114
  raise TypeError("value must not be empty")
111
115
 
112
116
  if isinstance(value, Content):
113
- return value.parts # type: ignore
117
+ items = value.parts # type: ignore
114
118
  if isinstance(value, (str, Image, Part)):
115
119
  items = [value] # type: ignore
116
-
117
- elif isinstance(value, list):
120
+ if isinstance(value, list):
118
121
  items = value # type: ignore
119
122
 
120
123
  for item in items: # type: ignore
payi/lib/instrument.py CHANGED
@@ -27,11 +27,12 @@ from .Stopwatch import Stopwatch
27
27
 
28
28
 
29
29
  class _ProviderRequest:
30
- def __init__(self, instrumentor: '_PayiInstrumentor', category: str):
30
+ def __init__(self, instrumentor: '_PayiInstrumentor', category: str, streaming_type: '_StreamingType'):
31
31
  self._instrumentor: '_PayiInstrumentor' = instrumentor
32
32
  self._estimated_prompt_tokens: Optional[int] = None
33
33
  self._category: str = category
34
34
  self._ingest: IngestUnitsParams = { "category": category, "units": {} } # type: ignore
35
+ self._streaming_type: '_StreamingType' = streaming_type
35
36
 
36
37
  def process_chunk(self, _chunk: Any) -> bool:
37
38
  return True
@@ -55,6 +56,10 @@ class _ProviderRequest:
55
56
  def process_exception(self, exception: Exception, kwargs: Any, ) -> bool: # noqa: ARG002
56
57
  self.exception_to_semantic_failure(exception)
57
58
  return True
59
+
60
+ @property
61
+ def streaming_type(self) -> '_StreamingType':
62
+ return self._streaming_type
58
63
 
59
64
  def exception_to_semantic_failure(self, e: Exception) -> None:
60
65
  exception_str = f"{type(e).__name__}"
@@ -111,6 +116,11 @@ class _IsStreaming(Enum):
111
116
  true = 1
112
117
  kwargs = 2
113
118
 
119
+ class _StreamingType(Enum):
120
+ generator = 0
121
+ iterator = 1
122
+ stream_manager = 2
123
+
114
124
  class _TrackContext:
115
125
  def __init__(
116
126
  self,
@@ -192,6 +202,7 @@ class _PayiInstrumentor:
192
202
  self._instrument_anthropic()
193
203
  self._instrument_aws_bedrock()
194
204
  self._instrument_google_vertex()
205
+ self._instrument_google_genai()
195
206
 
196
207
  def _instrument_specific(self, instruments: Set[str]) -> None:
197
208
  if PayiCategories.openai in instruments or PayiCategories.azure_openai in instruments:
@@ -202,6 +213,7 @@ class _PayiInstrumentor:
202
213
  self._instrument_aws_bedrock()
203
214
  if PayiCategories.google_vertex in instruments:
204
215
  self._instrument_google_vertex()
216
+ self._instrument_google_genai()
205
217
 
206
218
  def _instrument_openai(self) -> None:
207
219
  from .OpenAIInstrumentor import OpenAiInstrumentor
@@ -239,6 +251,14 @@ class _PayiInstrumentor:
239
251
  except Exception as e:
240
252
  logging.error(f"Error instrumenting Google Vertex: {e}")
241
253
 
254
+ def _instrument_google_genai(self) -> None:
255
+ from .GoogleGenAiInstrumentor import GoogleGenAiInstrumentor
256
+
257
+ try:
258
+ GoogleGenAiInstrumentor.instrument(self)
259
+
260
+ except Exception as e:
261
+ logging.error(f"Error instrumenting Google GenAi: {e}")
242
262
 
243
263
  def _process_ingest_units(self, ingest_units: IngestUnitsParams, log_data: 'dict[str, str]') -> bool:
244
264
  if int(ingest_units.get("http_status_code") or 0) < 400:
@@ -690,25 +710,30 @@ class _PayiInstrumentor:
690
710
  raise e
691
711
 
692
712
  if stream:
693
- if request.is_vertex():
713
+ if request.streaming_type == _StreamingType.generator:
694
714
  return _GeneratorWrapper(
695
715
  generator=response,
696
716
  instance=instance,
697
717
  instrumentor=self,
698
718
  stopwatch=sw,
699
719
  request=request,
700
- log_prompt_and_response=self._log_prompt_and_response)
701
-
702
- stream_result = ChatStreamWrapper(
703
- response=response,
704
- instance=instance,
705
- instrumentor=self,
706
- log_prompt_and_response=self._log_prompt_and_response,
707
- stopwatch=sw,
708
- request=request,
709
- )
710
-
711
- return stream_result
720
+ )
721
+ elif request.streaming_type == _StreamingType.stream_manager:
722
+ return _StreamManagerWrapper(
723
+ stream_manager=response,
724
+ instance=instance,
725
+ instrumentor=self,
726
+ stopwatch=sw,
727
+ request=request,
728
+ )
729
+ else:
730
+ return _StreamIteratorWrapper(
731
+ response=response,
732
+ instance=instance,
733
+ instrumentor=self,
734
+ stopwatch=sw,
735
+ request=request,
736
+ )
712
737
 
713
738
  sw.stop()
714
739
  duration = sw.elapsed_ms_int()
@@ -797,32 +822,40 @@ class _PayiInstrumentor:
797
822
  raise e
798
823
 
799
824
  if stream:
800
- if request.is_vertex():
825
+ if request.streaming_type == _StreamingType.generator:
801
826
  return _GeneratorWrapper(
802
827
  generator=response,
803
828
  instance=instance,
804
829
  instrumentor=self,
805
830
  stopwatch=sw,
806
831
  request=request,
807
- log_prompt_and_response=self._log_prompt_and_response)
808
-
809
- stream_result = ChatStreamWrapper(
810
- response=response,
811
- instance=instance,
812
- instrumentor=self,
813
- log_prompt_and_response=self._log_prompt_and_response,
814
- stopwatch=sw,
815
- request=request,
816
- )
832
+ )
833
+ elif request.streaming_type == _StreamingType.stream_manager:
834
+ return _StreamManagerWrapper(
835
+ stream_manager=response,
836
+ instance=instance,
837
+ instrumentor=self,
838
+ stopwatch=sw,
839
+ request=request,
840
+ )
841
+ else:
842
+ # request.streaming_type == _StreamingType.iterator
843
+ stream_result = _StreamIteratorWrapper(
844
+ response=response,
845
+ instance=instance,
846
+ instrumentor=self,
847
+ stopwatch=sw,
848
+ request=request,
849
+ )
817
850
 
818
- if request.is_bedrock():
819
- if "body" in response:
820
- response["body"] = stream_result
821
- else:
822
- response["stream"] = stream_result
823
- return response
824
-
825
- return stream_result
851
+ if request.is_bedrock():
852
+ if "body" in response:
853
+ response["body"] = stream_result
854
+ else:
855
+ response["stream"] = stream_result
856
+ return response
857
+
858
+ return stream_result
826
859
 
827
860
  sw.stop()
828
861
  duration = sw.elapsed_ms_int()
@@ -986,7 +1019,7 @@ class _PayiInstrumentor:
986
1019
 
987
1020
  return _payi_awrapper
988
1021
 
989
- class ChatStreamWrapper(ObjectProxy): # type: ignore
1022
+ class _StreamIteratorWrapper(ObjectProxy): # type: ignore
990
1023
  def __init__(
991
1024
  self,
992
1025
  response: Any,
@@ -994,7 +1027,6 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
994
1027
  instrumentor: _PayiInstrumentor,
995
1028
  stopwatch: Stopwatch,
996
1029
  request: _ProviderRequest,
997
- log_prompt_and_response: bool = True,
998
1030
  ) -> None:
999
1031
 
1000
1032
  bedrock_from_stream: bool = False
@@ -1016,7 +1048,6 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
1016
1048
 
1017
1049
  self._instrumentor = instrumentor
1018
1050
  self._stopwatch: Stopwatch = stopwatch
1019
- self._log_prompt_and_response: bool = log_prompt_and_response
1020
1051
  self._responses: list[str] = []
1021
1052
 
1022
1053
  self._request: _ProviderRequest = request
@@ -1091,7 +1122,7 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
1091
1122
  self._request._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
1092
1123
  self._first_token = False
1093
1124
 
1094
- if self._log_prompt_and_response:
1125
+ if self._instrumentor._log_prompt_and_response:
1095
1126
  self._responses.append(self.chunk_to_json(chunk))
1096
1127
 
1097
1128
  return self._request.process_chunk(chunk)
@@ -1101,7 +1132,7 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
1101
1132
  self._request._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
1102
1133
  self._request._ingest["http_status_code"] = 200
1103
1134
 
1104
- if self._log_prompt_and_response:
1135
+ if self._instrumentor._log_prompt_and_response:
1105
1136
  self._request._ingest["provider_response_json"] = self._responses
1106
1137
 
1107
1138
  async def _astop_iteration(self) -> None:
@@ -1124,6 +1155,35 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
1124
1155
  # assume dict
1125
1156
  return json.dumps(chunk)
1126
1157
 
1158
+ class _StreamManagerWrapper(ObjectProxy): # type: ignore
1159
+ def __init__(
1160
+ self,
1161
+ stream_manager: Any, # type: ignore
1162
+ instance: Any,
1163
+ instrumentor: _PayiInstrumentor,
1164
+ stopwatch: Stopwatch,
1165
+ request: _ProviderRequest,
1166
+ ) -> None:
1167
+ super().__init__(stream_manager) # type: ignore
1168
+
1169
+ self._stream_manager = stream_manager
1170
+ self._instance = instance
1171
+ self._instrumentor = instrumentor
1172
+ self._stopwatch: Stopwatch = stopwatch
1173
+ self._responses: list[str] = []
1174
+ self._request: _ProviderRequest = request
1175
+ self._first_token: bool = True
1176
+ self._done: bool = False
1177
+
1178
+ def __enter__(self) -> _StreamIteratorWrapper:
1179
+ return _StreamIteratorWrapper(
1180
+ response=self.__wrapped__.__enter__(), # type: ignore
1181
+ instance=self._instance,
1182
+ instrumentor=self._instrumentor,
1183
+ stopwatch=self._stopwatch,
1184
+ request=self._request,
1185
+ )
1186
+
1127
1187
  class _GeneratorWrapper: # type: ignore
1128
1188
  def __init__(
1129
1189
  self,
@@ -1132,7 +1192,6 @@ class _GeneratorWrapper: # type: ignore
1132
1192
  instrumentor: _PayiInstrumentor,
1133
1193
  stopwatch: Stopwatch,
1134
1194
  request: _ProviderRequest,
1135
- log_prompt_and_response: bool = True,
1136
1195
  ) -> None:
1137
1196
  super().__init__() # type: ignore
1138
1197
 
@@ -1140,7 +1199,7 @@ class _GeneratorWrapper: # type: ignore
1140
1199
  self._instance = instance
1141
1200
  self._instrumentor = instrumentor
1142
1201
  self._stopwatch: Stopwatch = stopwatch
1143
- self._log_prompt_and_response: bool = log_prompt_and_response
1202
+ self._log_prompt_and_response: bool = instrumentor._log_prompt_and_response
1144
1203
  self._responses: list[str] = []
1145
1204
  self._request: _ProviderRequest = request
1146
1205
  self._first_token: bool = True
@@ -1176,13 +1235,22 @@ class _GeneratorWrapper: # type: ignore
1176
1235
  await self._process_async_stop_iteration()
1177
1236
  raise stop_exception
1178
1237
 
1238
+ @staticmethod
1239
+ def _chunk_to_dict(chunk: Any) -> 'dict[str, object]':
1240
+ if hasattr(chunk, "to_json"):
1241
+ return chunk.to_json() # type: ignore
1242
+ elif hasattr(chunk, "to_json_dict"):
1243
+ return chunk.to_json_dict() # type: ignore
1244
+ else:
1245
+ return {}
1246
+
1179
1247
  def _process_chunk(self, chunk: Any) -> Any:
1180
1248
  if self._first_token:
1181
1249
  self._request._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
1182
1250
  self._first_token = False
1183
1251
 
1184
1252
  if self._log_prompt_and_response:
1185
- dict = chunk.to_dict() # type: ignore
1253
+ dict = self._chunk_to_dict(chunk)
1186
1254
  self._responses.append(json.dumps(dict))
1187
1255
 
1188
1256
  self._request.process_chunk(chunk)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: payi
3
- Version: 0.1.0a76
3
+ Version: 0.1.0a78
4
4
  Summary: The official Python library for the payi API
5
5
  Project-URL: Homepage, https://github.com/Pay-i/pay-i-python
6
6
  Project-URL: Repository, https://github.com/Pay-i/pay-i-python
@@ -188,12 +188,7 @@ client = Payi()
188
188
  use_case_definition = client.use_cases.definitions.create(
189
189
  description="x",
190
190
  name="x",
191
- limit_config={
192
- "max": 0,
193
- "limit_tags": ["tag1", "tag2"],
194
- "limit_type": "block",
195
- "threshold": 0,
196
- },
191
+ limit_config={"max": 0},
197
192
  )
198
193
  print(use_case_definition.limit_config)
199
194
  ```
@@ -1,17 +1,17 @@
1
1
  payi/__init__.py,sha256=4FRZqYbTvadWzWaSOtI2PmlFVjY4Z-jAi-T0DZAqR8c,2510
2
- payi/_base_client.py,sha256=pG4egpa2uhESXvjtszBekP-IdP12GY-vK5T54xnx8M8,64842
2
+ payi/_base_client.py,sha256=b8EliKyjMXslDv4v36gTGE_tIg3h9wi8eQmwAJ2xaBg,65090
3
3
  payi/_client.py,sha256=NoznzJFIQsFjEcPZWGJpHr94mOTMaQBuH-U_WGdjB10,17882
4
4
  payi/_compat.py,sha256=VWemUKbj6DDkQ-O4baSpHVLJafotzeXmCQGJugfVTIw,6580
5
5
  payi/_constants.py,sha256=S14PFzyN9-I31wiV7SmIlL5Ga0MLHxdvegInGdXH7tM,462
6
6
  payi/_exceptions.py,sha256=ItygKNrNXIVY0H6LsGVZvFuAHB3Vtm_VZXmWzCnpHy0,3216
7
7
  payi/_files.py,sha256=mf4dOgL4b0ryyZlbqLhggD3GVgDf6XxdGFAgce01ugE,3549
8
- payi/_models.py,sha256=mB2r2VWQq49jG-F0RIXDrBxPp3v-Eg12wMOtVTNxtv4,29057
8
+ payi/_models.py,sha256=G1vczEodX0vUySeVKbF-mbzlaObNL1oVAYH4c65agRk,29131
9
9
  payi/_qs.py,sha256=AOkSz4rHtK4YI3ZU_kzea-zpwBUgEY8WniGmTPyEimc,4846
10
10
  payi/_resource.py,sha256=j2jIkTr8OIC8sU6-05nxSaCyj4MaFlbZrwlyg4_xJos,1088
11
11
  payi/_response.py,sha256=rh9oJAvCKcPwQFm4iqH_iVrmK8bNx--YP_A2a4kN1OU,28776
12
12
  payi/_streaming.py,sha256=Z_wIyo206T6Jqh2rolFg2VXZgX24PahLmpURp0-NssU,10092
13
- payi/_types.py,sha256=2mbMK86K3W1aMTW7sOGQ-VND6-A2IuXKm8p4sYFztBU,6141
14
- payi/_version.py,sha256=4YxluuKLNaSBCqHaBtEGWEaho-Cg6sy67mKYht04Ys0,165
13
+ payi/_types.py,sha256=7jE5MoQQFVoVxw5vVzvZ2Ao0kcjfNOGsBgyJfLBEnMo,6195
14
+ payi/_version.py,sha256=562XNlpNWouu5fR8P9024ptW4UWs2S750PSKhFzqfdY,165
15
15
  payi/pagination.py,sha256=k2356QGPOUSjRF2vHpwLBdF6P-2vnQzFfRIJQAHGQ7A,1258
16
16
  payi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  payi/_utils/__init__.py,sha256=PNZ_QJuzZEgyYXqkO1HVhGkj5IU9bglVUcw7H-Knjzw,2062
@@ -25,13 +25,14 @@ payi/_utils/_transform.py,sha256=n7kskEWz6o__aoNvhFoGVyDoalNe6mJwp-g7BWkdj88,156
25
25
  payi/_utils/_typing.py,sha256=D0DbbNu8GnYQTSICnTSHDGsYXj8TcAKyhejb0XcnjtY,4602
26
26
  payi/_utils/_utils.py,sha256=ts4CiiuNpFiGB6YMdkQRh2SZvYvsl7mAF-JWHCcLDf4,12312
27
27
  payi/lib/.keep,sha256=wuNrz-5SXo3jJaJOJgz4vFHM41YH_g20F5cRQo0vLes,224
28
- payi/lib/AnthropicInstrumentor.py,sha256=pr4spNSyTM_VMtdWwoZPCV2o0ERGppoMhHo5rVHdmI8,7609
29
- payi/lib/BedrockInstrumentor.py,sha256=4IfOwq_MSGlvlSMxAocVu0G-Ctoir6nSL9KtpGfga8I,13969
30
- payi/lib/OpenAIInstrumentor.py,sha256=xmm2jxuVtuBZUNgORwgCZqiAGE_e6E28IwMEy9i7-nc,17361
28
+ payi/lib/AnthropicInstrumentor.py,sha256=q1Iq2NG06OHUwcwTgYjHa1_kwhbSxE3vrGk_fm5IGSM,8749
29
+ payi/lib/BedrockInstrumentor.py,sha256=cIjHlPQmVEAixabUoT3mV8h50lfzdc1u8iLj0gQNukE,14076
30
+ payi/lib/GoogleGenAiInstrumentor.py,sha256=y9FuEtuITaIac_N-WhmypAr-UF1lzRCc9YBRVwwwydQ,13411
31
+ payi/lib/OpenAIInstrumentor.py,sha256=iMOriAm-2zcRNjQDkEAG-NfZhBJUnkBDrNNbO920ajc,17468
31
32
  payi/lib/Stopwatch.py,sha256=7OJlxvr2Jyb6Zr1LYCYKczRB7rDVKkIR7gc4YoleNdE,764
32
- payi/lib/VertexInstrumentor.py,sha256=R0s3aEW7haj4aC5t1lMeA9siOgYvxlKQShLJ-B5iiNM,11642
33
+ payi/lib/VertexInstrumentor.py,sha256=Xmp5kRI8G0u2OieGmlNxIduwnqp8cAXO-fROpkdXbgs,11748
33
34
  payi/lib/helpers.py,sha256=K1KAfWrpPT1UUGNxspLe1lHzQjP3XV5Pkh9IU4pKMok,4624
34
- payi/lib/instrument.py,sha256=nUUEnyDJPNMgVvT1gRgSVX1T9D8IXvBG8UzUcmfM9ws,55743
35
+ payi/lib/instrument.py,sha256=UpZ6SGg3YI9lSxmwH5ziwe1xt_ca6FS35CcNJpG9ONM,58214
35
36
  payi/resources/__init__.py,sha256=1rtrPLWbNt8oJGOp6nwPumKLJ-ftez0B6qwLFyfcoP4,2972
36
37
  payi/resources/ingest.py,sha256=8HNHEyfgIyJNqCh0rOhO9msoc61-8IyifJ6AbxjCrDg,22612
37
38
  payi/resources/categories/__init__.py,sha256=w5gMiPdBSzJA_qfoVtFBElaoe8wGf_O63R7R1Spr6Gk,1093
@@ -141,7 +142,7 @@ payi/types/use_cases/definitions/kpi_retrieve_response.py,sha256=uQXliSvS3k-yDYw
141
142
  payi/types/use_cases/definitions/kpi_update_params.py,sha256=jbawdWAdMnsTWVH0qfQGb8W7_TXe3lq4zjSRu44d8p8,373
142
143
  payi/types/use_cases/definitions/kpi_update_response.py,sha256=zLyEoT0S8d7XHsnXZYT8tM7yDw0Aze0Mk-_Z6QeMtc8,459
143
144
  payi/types/use_cases/definitions/limit_config_create_params.py,sha256=pzQza_16N3z8cFNEKr6gPbFvuGFrwNuGxAYb--Kbo2M,449
144
- payi-0.1.0a76.dist-info/METADATA,sha256=eAuQd6niYj2-Tta60IPoh9WVIchQr7y_P7_BnAfMiig,15290
145
- payi-0.1.0a76.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
146
- payi-0.1.0a76.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
147
- payi-0.1.0a76.dist-info/RECORD,,
145
+ payi-0.1.0a78.dist-info/METADATA,sha256=kKY9frrRc7_WbcIUIphMDmRBHQVZgdSGH1eLkF26Pzk,15180
146
+ payi-0.1.0a78.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
147
+ payi-0.1.0a78.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
148
+ payi-0.1.0a78.dist-info/RECORD,,