payi 0.1.0a82__py3-none-any.whl → 0.1.0a83__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/_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.82" # x-release-please-version
4
+ __version__ = "0.1.0-alpha.83" # x-release-please-version
@@ -7,7 +7,7 @@ from wrapt import wrap_function_wrapper # type: ignore
7
7
  from payi.lib.helpers import PayiCategories
8
8
  from payi.types.ingest_units_params import Units
9
9
 
10
- from .instrument import _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
10
+ from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
11
11
 
12
12
 
13
13
  class AnthropicInstrumentor:
@@ -133,7 +133,8 @@ class _AnthropicProviderRequest(_ProviderRequest):
133
133
  )
134
134
 
135
135
  @override
136
- def process_chunk(self, chunk: Any) -> bool:
136
+ def process_chunk(self, chunk: Any) -> _ChunkResult:
137
+ ingest = False
137
138
  if chunk.type == "message_start":
138
139
  self._ingest["provider_response_id"] = chunk.message.id
139
140
 
@@ -154,9 +155,15 @@ class _AnthropicProviderRequest(_ProviderRequest):
154
155
 
155
156
  elif chunk.type == "message_delta":
156
157
  usage = chunk.usage
158
+ ingest = True
159
+
160
+ # Web search will return an updated input tokens value at the end of streaming
161
+ if usage.input_tokens > 0:
162
+ self._ingest["units"]["text"]["input"] = usage.input_tokens
163
+
157
164
  self._ingest["units"]["text"]["output"] = usage.output_tokens
158
165
 
159
- return True
166
+ return _ChunkResult(send_chunk_to_caller=True, ingest=ingest)
160
167
 
161
168
  @override
162
169
  def process_synchronous_response(self, response: Any, log_prompt_and_response: bool, kwargs: Any) -> Any:
@@ -10,7 +10,7 @@ from payi.lib.helpers import PayiCategories, PayiHeaderNames, payi_aws_bedrock_u
10
10
  from payi.types.ingest_units_params import Units, IngestUnitsParams
11
11
  from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
12
12
 
13
- from .instrument import _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
13
+ from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
14
14
 
15
15
  _supported_model_prefixes = ["meta.llama3", "anthropic.", "amazon.nova-pro", "amazon.nova-lite", "amazon.nova-micro"]
16
16
 
@@ -22,8 +22,6 @@ class BedrockInstrumentor:
22
22
  BedrockInstrumentor._instrumentor = instrumentor
23
23
 
24
24
  try:
25
- import boto3 # type: ignore # noqa: F401 I001
26
-
27
25
  wrap_function_wrapper(
28
26
  "botocore.client",
29
27
  "ClientCreator.create_client",
@@ -43,7 +41,7 @@ class BedrockInstrumentor:
43
41
  @_PayiInstrumentor.payi_wrapper
44
42
  def create_client_wrapper(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ARG001
45
43
  if kwargs.get("service_name") != "bedrock-runtime":
46
- instrumentor._logger.debug(f"skipping client wrapper creation for {kwargs.get('service_name', '')} service")
44
+ # instrumentor._logger.debug(f"skipping client wrapper creation for {kwargs.get('service_name', '')} service")
47
45
  return wrapped(*args, **kwargs)
48
46
 
49
47
  try:
@@ -272,13 +270,14 @@ class _BedrockInvokeStreamingProviderRequest(_BedrockProviderRequest):
272
270
  self._is_anthropic: bool = model_id.startswith("anthropic.")
273
271
 
274
272
  @override
275
- def process_chunk(self, chunk: Any) -> bool:
273
+ def process_chunk(self, chunk: Any) -> _ChunkResult:
276
274
  if self._is_anthropic:
277
275
  return self.process_invoke_streaming_anthropic_chunk(chunk)
278
276
  else:
279
277
  return self.process_invoke_streaming_llama_chunk(chunk)
280
278
 
281
- def process_invoke_streaming_anthropic_chunk(self, chunk: str) -> bool:
279
+ def process_invoke_streaming_anthropic_chunk(self, chunk: str) -> _ChunkResult:
280
+ ingest = False
282
281
  chunk_dict = json.loads(chunk)
283
282
  type = chunk_dict.get("type", "")
284
283
 
@@ -301,18 +300,21 @@ class _BedrockInvokeStreamingProviderRequest(_BedrockProviderRequest):
301
300
  elif type == "message_delta":
302
301
  usage = chunk_dict['usage']
303
302
  self._ingest["units"]["text"]["output"] = usage['output_tokens']
303
+ ingest = True
304
304
 
305
- return True
305
+ return _ChunkResult(send_chunk_to_caller=True, ingest=ingest)
306
306
 
307
- def process_invoke_streaming_llama_chunk(self, chunk: str) -> bool:
307
+ def process_invoke_streaming_llama_chunk(self, chunk: str) -> _ChunkResult:
308
+ ingest = False
308
309
  chunk_dict = json.loads(chunk)
309
310
  metrics = chunk_dict.get("amazon-bedrock-invocationMetrics", {})
310
311
  if metrics:
311
312
  input = metrics.get("inputTokenCount", 0)
312
313
  output = metrics.get("outputTokenCount", 0)
313
314
  self._ingest["units"]["text"] = Units(input=input, output=output)
315
+ ingest = True
314
316
 
315
- return True
317
+ return _ChunkResult(send_chunk_to_caller=True, ingest=ingest)
316
318
 
317
319
  class _BedrockInvokeSynchronousProviderRequest(_BedrockProviderRequest):
318
320
  @override
@@ -374,7 +376,8 @@ class _BedrockConverseSynchronousProviderRequest(_BedrockProviderRequest):
374
376
 
375
377
  class _BedrockConverseStreamingProviderRequest(_BedrockProviderRequest):
376
378
  @override
377
- def process_chunk(self, chunk: 'dict[str, Any]') -> bool:
379
+ def process_chunk(self, chunk: 'dict[str, Any]') -> _ChunkResult:
380
+ ingest = False
378
381
  metadata = chunk.get("metadata", {})
379
382
 
380
383
  if metadata:
@@ -383,4 +386,6 @@ class _BedrockConverseStreamingProviderRequest(_BedrockProviderRequest):
383
386
  output = usage["outputTokens"]
384
387
  self._ingest["units"]["text"] = Units(input=input, output=output)
385
388
 
386
- return True
389
+ ingest = True
390
+
391
+ return _ChunkResult(send_chunk_to_caller=True, ingest=ingest)
@@ -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, _StreamingType, _ProviderRequest, _PayiInstrumentor
11
+ from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
12
12
 
13
13
 
14
14
  class GoogleGenAiInstrumentor:
@@ -248,7 +248,8 @@ class _GoogleGenAiRequest(_ProviderRequest):
248
248
  prompt["tool_config"] = tool_config
249
249
 
250
250
  @override
251
- def process_chunk(self, chunk: Any) -> bool:
251
+ def process_chunk(self, chunk: Any) -> _ChunkResult:
252
+ ingest = False
252
253
  response_dict: dict[str, Any] = chunk.to_json_dict()
253
254
  if "provider_response_id" not in self._ingest:
254
255
  id = response_dict.get("response_id", None)
@@ -267,8 +268,9 @@ class _GoogleGenAiRequest(_ProviderRequest):
267
268
  usage = response_dict.get("usage_metadata", {})
268
269
  if usage and "prompt_token_count" in usage and "candidates_token_count" in usage:
269
270
  self._compute_usage(response_dict, streaming_candidates_characters=self._candiates_character_count)
271
+ ingest = True
270
272
 
271
- return True
273
+ return _ChunkResult(send_chunk_to_caller=True, ingest=ingest)
272
274
 
273
275
  @staticmethod
274
276
  def _is_character_billing_model(model: str) -> bool:
@@ -9,7 +9,7 @@ from wrapt import wrap_function_wrapper # type: ignore
9
9
  from payi.lib.helpers import PayiCategories, PayiHeaderNames
10
10
  from payi.types.ingest_units_params import Units
11
11
 
12
- from .instrument import _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
12
+ from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
13
13
 
14
14
 
15
15
  class OpenAiInstrumentor:
@@ -22,8 +22,6 @@ class OpenAiInstrumentor:
22
22
  @staticmethod
23
23
  def instrument(instrumentor: _PayiInstrumentor) -> None:
24
24
  try:
25
- from openai import OpenAI # type: ignore # noqa: F401 I001
26
-
27
25
  wrap_function_wrapper(
28
26
  "openai.resources.chat.completions",
29
27
  "Completions.create",
@@ -47,7 +45,11 @@ class OpenAiInstrumentor:
47
45
  "AsyncEmbeddings.create",
48
46
  aembeddings_wrapper(instrumentor),
49
47
  )
48
+ except Exception as e:
49
+ instrumentor._logger.debug(f"Error instrumenting openai: {e}")
50
50
 
51
+ # responses separately as they are relatively new and the client may not be using the latest openai module
52
+ try:
51
53
  wrap_function_wrapper(
52
54
  "openai.resources.responses",
53
55
  "Responses.create",
@@ -62,8 +64,6 @@ class OpenAiInstrumentor:
62
64
 
63
65
  except Exception as e:
64
66
  instrumentor._logger.debug(f"Error instrumenting openai: {e}")
65
- return
66
-
67
67
 
68
68
  @_PayiInstrumentor.payi_wrapper
69
69
  def embeddings_wrapper(
@@ -338,7 +338,8 @@ class _OpenAiChatProviderRequest(_OpenAiProviderRequest):
338
338
  self._include_usage_added = False
339
339
 
340
340
  @override
341
- def process_chunk(self, chunk: Any) -> bool:
341
+ def process_chunk(self, chunk: Any) -> _ChunkResult:
342
+ ingest = False
342
343
  model = model_to_dict(chunk)
343
344
 
344
345
  if "provider_response_id" not in self._ingest:
@@ -356,8 +357,9 @@ class _OpenAiChatProviderRequest(_OpenAiProviderRequest):
356
357
  # packet which contains the usage to the client as they are not expecting the data
357
358
  if self._include_usage_added:
358
359
  send_chunk_to_client = False
360
+ ingest = True
359
361
 
360
- return send_chunk_to_client
362
+ return _ChunkResult(send_chunk_to_caller=send_chunk_to_client, ingest=ingest)
361
363
 
362
364
  @override
363
365
  def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
@@ -420,7 +422,8 @@ class _OpenAiResponsesProviderRequest(_OpenAiProviderRequest):
420
422
  input_tokens_details_key=_OpenAiProviderRequest.responses_input_tokens_details_key)
421
423
 
422
424
  @override
423
- def process_chunk(self, chunk: Any) -> bool:
425
+ def process_chunk(self, chunk: Any) -> _ChunkResult:
426
+ ingest = False
424
427
  model = model_to_dict(chunk)
425
428
  response: dict[str, Any] = model.get("response", {})
426
429
 
@@ -432,8 +435,9 @@ class _OpenAiResponsesProviderRequest(_OpenAiProviderRequest):
432
435
  usage = response.get("usage")
433
436
  if usage:
434
437
  self.add_usage_units(usage)
438
+ ingest = True
435
439
 
436
- return True
440
+ return _ChunkResult(send_chunk_to_caller=True, ingest=ingest)
437
441
 
438
442
  @override
439
443
  def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
@@ -8,33 +8,37 @@ 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, _StreamingType, _ProviderRequest, _PayiInstrumentor
11
+ from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
12
12
 
13
13
 
14
14
  class VertexInstrumentor:
15
15
  @staticmethod
16
16
  def instrument(instrumentor: _PayiInstrumentor) -> None:
17
17
  try:
18
- import vertexai # type: ignore # noqa: F401 I001
19
-
20
18
  wrap_function_wrapper(
21
19
  "vertexai.generative_models",
22
20
  "GenerativeModel.generate_content",
23
21
  generate_wrapper(instrumentor),
24
22
  )
25
23
 
26
- wrap_function_wrapper(
27
- "vertexai.preview.generative_models",
28
- "GenerativeModel.generate_content",
29
- generate_wrapper(instrumentor),
30
- )
31
-
32
24
  wrap_function_wrapper(
33
25
  "vertexai.generative_models",
34
26
  "GenerativeModel.generate_content_async",
35
27
  agenerate_wrapper(instrumentor),
36
28
  )
37
29
 
30
+ except Exception as e:
31
+ instrumentor._logger.debug(f"Error instrumenting vertex: {e}")
32
+ return
33
+
34
+ # separate instrumetning preview functionality from released in case it fails
35
+ try:
36
+ wrap_function_wrapper(
37
+ "vertexai.preview.generative_models",
38
+ "GenerativeModel.generate_content",
39
+ generate_wrapper(instrumentor),
40
+ )
41
+
38
42
  wrap_function_wrapper(
39
43
  "vertexai.preview.generative_models",
40
44
  "GenerativeModel.generate_content_async",
@@ -93,11 +97,19 @@ class _GoogleVertexRequest(_ProviderRequest):
93
97
  )
94
98
  self._prompt_character_count = 0
95
99
  self._candiates_character_count = 0
100
+ self._model_name: Optional[str] = None
96
101
 
97
102
  @override
98
103
  def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
99
104
  from vertexai.generative_models import Content, Image, Part # type: ignore # noqa: F401 I001
100
105
 
106
+ # Try to extra the model name as a backup if the response does not provide it (older vertexai versions do not)
107
+ if instance and hasattr(instance, "_model_name"):
108
+ model = instance._model_name
109
+ if model and isinstance(model, str):
110
+ # Extract the model name after the last slash
111
+ self._model_name = model.split('/')[-1]
112
+
101
113
  if not args:
102
114
  return True
103
115
 
@@ -191,17 +203,26 @@ class _GoogleVertexRequest(_ProviderRequest):
191
203
  # tool_config does not have to_dict or any other serializable object
192
204
  prompt["tool_config"] = str(tool_config) # type: ignore
193
205
 
206
+ def _get_model_name(self, response: 'dict[str, Any]') -> Optional[str]:
207
+ model: Optional[str] = response.get("model_version", None)
208
+ if model:
209
+ return model
210
+
211
+ return self._model_name
212
+
194
213
  @override
195
- def process_chunk(self, chunk: Any) -> bool:
214
+ def process_chunk(self, chunk: Any) -> _ChunkResult:
215
+ ingest = False
196
216
  response_dict: dict[str, Any] = chunk.to_dict()
197
217
  if "provider_response_id" not in self._ingest:
198
218
  id = response_dict.get("response_id", None)
199
219
  if id:
200
220
  self._ingest["provider_response_id"] = id
201
221
 
202
- model: str = response_dict.get("model_version", "")
203
-
204
- self._ingest["resource"] = "google." + model
222
+ if "resource" not in self._ingest:
223
+ model: Optional[str] = self._get_model_name(response_dict) # type: ignore[unreachable]
224
+ if model:
225
+ self._ingest["resource"] = "google." + model
205
226
 
206
227
  for candidate in response_dict.get("candidates", []):
207
228
  parts = candidate.get("content", {}).get("parts", [])
@@ -211,8 +232,9 @@ class _GoogleVertexRequest(_ProviderRequest):
211
232
  usage = response_dict.get("usage_metadata", {})
212
233
  if usage and "prompt_token_count" in usage and "candidates_token_count" in usage:
213
234
  self._compute_usage(response_dict, streaming_candidates_characters=self._candiates_character_count)
235
+ ingest = True
214
236
 
215
- return True
237
+ return _ChunkResult(send_chunk_to_caller=True, ingest=ingest)
216
238
 
217
239
  @staticmethod
218
240
  def _is_character_billing_model(model: str) -> bool:
@@ -230,7 +252,7 @@ class _GoogleVertexRequest(_ProviderRequest):
230
252
  if id:
231
253
  self._ingest["provider_response_id"] = id
232
254
 
233
- model: Optional[str] = response_dict.get("model_version", None)
255
+ model: Optional[str] = self._get_model_name(response_dict)
234
256
  if model:
235
257
  self._ingest["resource"] = "google." + model
236
258
 
@@ -256,7 +278,9 @@ class _GoogleVertexRequest(_ProviderRequest):
256
278
  prompt_tokens_details: list[dict[str, Any]] = usage.get("prompt_tokens_details", [])
257
279
  candidates_tokens_details: list[dict[str, Any]] = usage.get("candidates_tokens_details", [])
258
280
 
259
- model: str = response_dict.get("model_version", "")
281
+ model: Optional[str] = self._get_model_name(response_dict)
282
+ if not model:
283
+ model = ""
260
284
 
261
285
  # for character billing only
262
286
  large_context = "" if input < 128000 else "_large_context"
payi/lib/instrument.py CHANGED
@@ -10,6 +10,7 @@ from abc import abstractmethod
10
10
  from enum import Enum
11
11
  from typing import Any, Set, Union, Callable, Optional, Sequence, TypedDict
12
12
  from datetime import datetime, timezone
13
+ from dataclasses import dataclass
13
14
  from typing_extensions import deprecated
14
15
 
15
16
  import nest_asyncio # type: ignore
@@ -28,6 +29,11 @@ from .Stopwatch import Stopwatch
28
29
  global _g_logger
29
30
  _g_logger: logging.Logger = logging.getLogger("payi.instrument")
30
31
 
32
+ @dataclass
33
+ class _ChunkResult:
34
+ send_chunk_to_caller: bool
35
+ ingest: bool = False
36
+
31
37
  class _ProviderRequest:
32
38
  def __init__(self, instrumentor: '_PayiInstrumentor', category: str, streaming_type: '_StreamingType'):
33
39
  self._instrumentor: '_PayiInstrumentor' = instrumentor
@@ -36,8 +42,8 @@ class _ProviderRequest:
36
42
  self._ingest: IngestUnitsParams = { "category": category, "units": {} } # type: ignore
37
43
  self._streaming_type: '_StreamingType' = streaming_type
38
44
 
39
- def process_chunk(self, _chunk: Any) -> bool:
40
- return True
45
+ def process_chunk(self, _chunk: Any) -> _ChunkResult:
46
+ return _ChunkResult(send_chunk_to_caller=True)
41
47
 
42
48
  def process_synchronous_response(self, response: Any, log_prompt_and_response: bool, kwargs: Any) -> Optional[object]: # noqa: ARG002
43
49
  return None
@@ -275,8 +281,7 @@ class _PayiInstrumentor:
275
281
  if int(ingest_units.get("http_status_code") or 0) < 400:
276
282
  units = ingest_units.get("units", {})
277
283
  if not units or all(unit.get("input", 0) == 0 and unit.get("output", 0) == 0 for unit in units.values()):
278
- self._logger.error('No units to ingest!')
279
- return False
284
+ self._logger.info('ingesting with no token counts')
280
285
 
281
286
  if self._log_prompt_and_response and self._prompt_and_response_logger:
282
287
  response_json = ingest_units.pop("provider_response_json", None)
@@ -341,7 +346,7 @@ class _PayiInstrumentor:
341
346
 
342
347
  return ingest_response
343
348
  except Exception as e:
344
- self._logger.error(f"Error Pay-i ingesting request: {e}")
349
+ self._logger.error(f"Error Pay-i async ingesting: exception {e}, request {ingest_units}")
345
350
 
346
351
  return None
347
352
 
@@ -413,7 +418,7 @@ class _PayiInstrumentor:
413
418
  self._logger.error("No payi instance to ingest units")
414
419
 
415
420
  except Exception as e:
416
- self._logger.error(f"Error Pay-i ingesting request: {e}")
421
+ self._logger.error(f"Error Pay-i ingesting: exception {e}, request {ingest_units}")
417
422
 
418
423
  return None
419
424
 
@@ -1105,6 +1110,8 @@ class _StreamIteratorWrapper(ObjectProxy): # type: ignore
1105
1110
  self._first_token: bool = True
1106
1111
  self._is_bedrock: bool = request.is_bedrock()
1107
1112
  self._bedrock_from_stream: bool = bedrock_from_stream
1113
+ self._ingested: bool = False
1114
+ self._iter_started: bool = False
1108
1115
 
1109
1116
  def __enter__(self) -> Any:
1110
1117
  self._instrumentor._logger.debug(f"StreamIteratorWrapper: __enter__")
@@ -1123,6 +1130,7 @@ class _StreamIteratorWrapper(ObjectProxy): # type: ignore
1123
1130
  await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb) # type: ignore
1124
1131
 
1125
1132
  def __iter__(self) -> Any:
1133
+ self._iter_started = True
1126
1134
  if self._is_bedrock:
1127
1135
  # 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
1128
1136
  self._instrumentor._logger.debug(f"StreamIteratorWrapper: bedrock __iter__")
@@ -1134,13 +1142,19 @@ class _StreamIteratorWrapper(ObjectProxy): # type: ignore
1134
1142
  def _iter_bedrock(self) -> Any:
1135
1143
  # botocore EventStream doesn't have a __next__ method so iterate over the wrapped object in place
1136
1144
  for event in self.__wrapped__: # type: ignore
1145
+ result: Optional[_ChunkResult] = None
1146
+
1137
1147
  if (self._bedrock_from_stream):
1138
- self._evaluate_chunk(event)
1148
+ result = self._evaluate_chunk(event)
1139
1149
  else:
1140
1150
  chunk = event.get('chunk') # type: ignore
1141
1151
  if chunk:
1142
1152
  decode = chunk.get('bytes').decode() # type: ignore
1143
- self._evaluate_chunk(decode)
1153
+ result = self._evaluate_chunk(decode)
1154
+
1155
+ if result and result.ingest:
1156
+ self._stop_iteration()
1157
+
1144
1158
  yield event
1145
1159
 
1146
1160
  self._instrumentor._logger.debug(f"StreamIteratorWrapper: bedrock iter finished")
@@ -1148,40 +1162,60 @@ class _StreamIteratorWrapper(ObjectProxy): # type: ignore
1148
1162
  self._stop_iteration()
1149
1163
 
1150
1164
  def __aiter__(self) -> Any:
1165
+ self._iter_started = True
1151
1166
  self._instrumentor._logger.debug(f"StreamIteratorWrapper: __aiter__")
1152
1167
  return self
1153
1168
 
1154
1169
  def __next__(self) -> object:
1155
1170
  try:
1156
1171
  chunk: object = self.__wrapped__.__next__() # type: ignore
1172
+
1173
+ if self._ingested:
1174
+ self._instrumentor._logger.debug(f"StreamIteratorWrapper: __next__ already ingested, not processing chunk {chunk}")
1175
+ return chunk # type: ignore
1176
+
1177
+ result = self._evaluate_chunk(chunk)
1178
+
1179
+ if result.ingest:
1180
+ self._stop_iteration()
1181
+
1182
+ if result.send_chunk_to_caller:
1183
+ return chunk # type: ignore
1184
+ else:
1185
+ return self.__next__()
1157
1186
  except Exception as e:
1158
1187
  if isinstance(e, StopIteration):
1159
1188
  self._stop_iteration()
1160
1189
  else:
1161
1190
  self._instrumentor._logger.debug(f"StreamIteratorWrapper: __next__ exception {e}")
1162
1191
  raise e
1163
- else:
1164
- if self._evaluate_chunk(chunk) == False:
1165
- return self.__next__()
1166
-
1167
- return chunk # type: ignore
1168
1192
 
1169
1193
  async def __anext__(self) -> object:
1170
1194
  try:
1171
1195
  chunk: object = await self.__wrapped__.__anext__() # type: ignore
1196
+
1197
+ if self._ingested:
1198
+ self._instrumentor._logger.debug(f"StreamIteratorWrapper: __next__ already ingested, not processing chunk {chunk}")
1199
+ return chunk # type: ignore
1200
+
1201
+ result = self._evaluate_chunk(chunk)
1202
+
1203
+ if result.ingest:
1204
+ await self._astop_iteration()
1205
+
1206
+ if result.send_chunk_to_caller:
1207
+ return chunk # type: ignore
1208
+ else:
1209
+ return await self.__anext__()
1210
+
1172
1211
  except Exception as e:
1173
1212
  if isinstance(e, StopAsyncIteration):
1174
1213
  await self._astop_iteration()
1175
1214
  else:
1176
1215
  self._instrumentor._logger.debug(f"StreamIteratorWrapper: __anext__ exception {e}")
1177
1216
  raise e
1178
- else:
1179
- if self._evaluate_chunk(chunk) == False:
1180
- return await self.__anext__()
1181
1217
 
1182
- return chunk # type: ignore
1183
-
1184
- def _evaluate_chunk(self, chunk: Any) -> bool:
1218
+ def _evaluate_chunk(self, chunk: Any) -> _ChunkResult:
1185
1219
  if self._first_token:
1186
1220
  self._request._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
1187
1221
  self._first_token = False
@@ -1192,7 +1226,7 @@ class _StreamIteratorWrapper(ObjectProxy): # type: ignore
1192
1226
  return self._request.process_chunk(chunk)
1193
1227
 
1194
1228
  def _process_stop_iteration(self) -> None:
1195
- self._instrumentor._logger.debug(f"StreamIteratorWrapper: stop iteration")
1229
+ self._instrumentor._logger.debug(f"StreamIteratorWrapper: process stop iteration")
1196
1230
 
1197
1231
  self._stopwatch.stop()
1198
1232
  self._request._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
@@ -1202,12 +1236,23 @@ class _StreamIteratorWrapper(ObjectProxy): # type: ignore
1202
1236
  self._request._ingest["provider_response_json"] = self._responses
1203
1237
 
1204
1238
  async def _astop_iteration(self) -> None:
1239
+ if self._ingested:
1240
+ self._instrumentor._logger.debug(f"StreamIteratorWrapper: astop iteration already ingested, skipping")
1241
+ return
1242
+
1205
1243
  self._process_stop_iteration()
1244
+
1206
1245
  await self._instrumentor._aingest_units(self._request._ingest)
1246
+ self._ingested = True
1207
1247
 
1208
1248
  def _stop_iteration(self) -> None:
1249
+ if self._ingested:
1250
+ self._instrumentor._logger.debug(f"StreamIteratorWrapper: stop iteration already ingested, skipping")
1251
+ return
1252
+
1209
1253
  self._process_stop_iteration()
1210
1254
  self._instrumentor._ingest_units(self._request._ingest)
1255
+ self._ingested = True
1211
1256
 
1212
1257
  @staticmethod
1213
1258
  def chunk_to_json(chunk: Any) -> str:
@@ -1241,7 +1286,6 @@ class _StreamManagerWrapper(ObjectProxy): # type: ignore
1241
1286
  self._responses: list[str] = []
1242
1287
  self._request: _ProviderRequest = request
1243
1288
  self._first_token: bool = True
1244
- self._done: bool = False
1245
1289
 
1246
1290
  def __enter__(self) -> _StreamIteratorWrapper:
1247
1291
  self._instrumentor._logger.debug(f"_StreamManagerWrapper: __enter__")
@@ -1275,92 +1319,103 @@ class _GeneratorWrapper: # type: ignore
1275
1319
  self._responses: list[str] = []
1276
1320
  self._request: _ProviderRequest = request
1277
1321
  self._first_token: bool = True
1278
- self._done: bool = False
1279
-
1322
+ self._ingested: bool = False
1323
+ self._iter_started: bool = False
1324
+
1280
1325
  def __iter__(self) -> Any:
1326
+ self._iter_started = True
1281
1327
  self._instrumentor._logger.debug(f"GeneratorWrapper: __iter__")
1282
1328
  return self
1283
1329
 
1284
1330
  def __aiter__(self) -> Any:
1285
1331
  self._instrumentor._logger.debug(f"GeneratorWrapper: __aiter__")
1286
1332
  return self
1287
-
1288
- def __next__(self) -> Any:
1289
- if self._done:
1290
- raise StopIteration
1333
+
1334
+ def _process_chunk(self, chunk: Any) -> _ChunkResult:
1335
+ if self._first_token:
1336
+ self._request._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
1337
+ self._first_token = False
1291
1338
 
1339
+ if self._log_prompt_and_response:
1340
+ dict = self._chunk_to_dict(chunk)
1341
+ self._responses.append(json.dumps(dict))
1342
+
1343
+ return self._request.process_chunk(chunk)
1344
+
1345
+ def __next__(self) -> Any:
1292
1346
  try:
1293
1347
  chunk = next(self._generator)
1294
- return self._process_chunk(chunk)
1348
+ result = self._process_chunk(chunk)
1349
+
1350
+ if result.ingest:
1351
+ self._stop_iteration()
1352
+
1353
+ # ignore result.send_chunk_to_caller:
1354
+ return chunk
1295
1355
 
1296
1356
  except Exception as e:
1297
1357
  if isinstance(e, StopIteration):
1298
- self._process_stop_iteration()
1358
+ self._stop_iteration()
1299
1359
  else:
1300
1360
  self._instrumentor._logger.debug(f"GeneratorWrapper: __next__ exception {e}")
1301
1361
  raise e
1302
1362
 
1303
1363
  async def __anext__(self) -> Any:
1304
- if self._done:
1305
- raise StopAsyncIteration
1306
-
1307
1364
  try:
1308
1365
  chunk = await anext(self._generator) # type: ignore
1309
- return self._process_chunk(chunk)
1366
+ result = self._process_chunk(chunk)
1367
+
1368
+ if result.ingest:
1369
+ await self._astop_iteration()
1370
+
1371
+ # ignore result.send_chunk_to_caller:
1372
+ return chunk # type: ignore
1310
1373
 
1311
1374
  except Exception as e:
1312
1375
  if isinstance(e, StopAsyncIteration):
1313
- await self._process_async_stop_iteration()
1376
+ await self._astop_iteration()
1314
1377
  else:
1315
1378
  self._instrumentor._logger.debug(f"GeneratorWrapper: __anext__ exception {e}")
1316
1379
  raise e
1317
1380
 
1318
1381
  @staticmethod
1319
1382
  def _chunk_to_dict(chunk: Any) -> 'dict[str, object]':
1320
- if hasattr(chunk, "to_json"):
1321
- return chunk.to_json() # type: ignore
1383
+ if hasattr(chunk, "to_dict"):
1384
+ return chunk.to_dict() # type: ignore
1322
1385
  elif hasattr(chunk, "to_json_dict"):
1323
1386
  return chunk.to_json_dict() # type: ignore
1324
1387
  else:
1325
1388
  return {}
1326
1389
 
1327
- def _process_chunk(self, chunk: Any) -> Any:
1328
- if self._first_token:
1329
- self._request._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
1330
- self._first_token = False
1331
-
1332
- if self._log_prompt_and_response:
1333
- dict = self._chunk_to_dict(chunk)
1334
- self._responses.append(json.dumps(dict))
1335
-
1336
- self._request.process_chunk(chunk)
1337
- return chunk
1390
+ def _stop_iteration(self) -> None:
1391
+ if self._ingested:
1392
+ self._instrumentor._logger.debug(f"GeneratorWrapper: stop iteration already ingested, skipping")
1393
+ return
1338
1394
 
1339
- def _process_stop_iteration(self) -> None:
1340
- self._instrumentor._logger.debug(f"GeneratorWrapper: stop iteration")
1395
+ self._process_stop_iteration()
1341
1396
 
1342
- self._stopwatch.stop()
1343
- self._request._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
1344
- self._request._ingest["http_status_code"] = 200
1345
-
1346
- if self._log_prompt_and_response:
1347
- self._request._ingest["provider_response_json"] = self._responses
1348
-
1349
1397
  self._instrumentor._ingest_units(self._request._ingest)
1350
- self._done = True
1398
+ self._ingested = True
1351
1399
 
1352
- async def _process_async_stop_iteration(self) -> None:
1353
- self._instrumentor._logger.debug(f"GeneratorWrapper: async stop iteration")
1400
+ async def _astop_iteration(self) -> None:
1401
+ if self._ingested:
1402
+ self._instrumentor._logger.debug(f"GeneratorWrapper: astop iteration already ingested, skipping")
1403
+ return
1404
+
1405
+ self._process_stop_iteration()
1354
1406
 
1355
- self._stopwatch.stop()
1407
+ await self._instrumentor._aingest_units(self._request._ingest)
1408
+ self._ingested = True
1409
+
1410
+ def _process_stop_iteration(self) -> None:
1411
+ self._instrumentor._logger.debug(f"GeneratorWrapper: stop iteration")
1412
+
1413
+ self._stopwatch.stop()
1356
1414
  self._request._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
1357
1415
  self._request._ingest["http_status_code"] = 200
1358
1416
 
1359
1417
  if self._log_prompt_and_response:
1360
1418
  self._request._ingest["provider_response_json"] = self._responses
1361
-
1362
- await self._instrumentor._aingest_units(self._request._ingest)
1363
- self._done = True
1364
1419
 
1365
1420
  global _instrumentor
1366
1421
  _instrumentor: Optional[_PayiInstrumentor] = None
@@ -1630,4 +1685,4 @@ def proxy(
1630
1685
 
1631
1686
  return _proxy_wrapper
1632
1687
 
1633
- return _proxy
1688
+ return _proxy
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: payi
3
- Version: 0.1.0a82
3
+ Version: 0.1.0a83
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
@@ -11,7 +11,7 @@ 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
13
  payi/_types.py,sha256=7jE5MoQQFVoVxw5vVzvZ2Ao0kcjfNOGsBgyJfLBEnMo,6195
14
- payi/_version.py,sha256=KAmqXUJQtR0NFE11hO5_6a9OQxaHRxsEAfoNmO8-neY,165
14
+ payi/_version.py,sha256=xOYzE4HfPmVs-tN-N7yfhmNSvAgnTtRTnr0gUxo0tVg,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,14 +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=PNSrEsij-rRaRN0_rjqQI00NK_FahrrRN4gikpDiiTc,9186
29
- payi/lib/BedrockInstrumentor.py,sha256=qXkrYeYHba3gOnp_VnQ6sMAUVLx0RpeSJ8gMWz7-A2g,15136
30
- payi/lib/GoogleGenAiInstrumentor.py,sha256=WE_3tyrp96UDHXymY4ky28wtFTROF-v5mSzOP2XuGxw,14489
31
- payi/lib/OpenAIInstrumentor.py,sha256=hrEEzzeGtQmZfLxGsyMKhDtB0S7YPNfsjgMG5vx5jMA,18485
28
+ payi/lib/AnthropicInstrumentor.py,sha256=O48WQIK1WvjAz1lvIEYoqgnS-UmzKhb5becNpSjinbE,9515
29
+ payi/lib/BedrockInstrumentor.py,sha256=vtJoPsYJ8Re3ODe3onTPcq9nSblL7IBjnE811qhMkdU,15424
30
+ payi/lib/GoogleGenAiInstrumentor.py,sha256=ru2odN7aA66z_UGAHRpHsJ1dm5HbffQ0eFhcAHOqHR4,14610
31
+ payi/lib/OpenAIInstrumentor.py,sha256=FjIRlQk5t95ySspH0VXsBv7my_f-c6HluvReY2hRvmM,18852
32
32
  payi/lib/Stopwatch.py,sha256=7OJlxvr2Jyb6Zr1LYCYKczRB7rDVKkIR7gc4YoleNdE,764
33
- payi/lib/VertexInstrumentor.py,sha256=E0511pzzB5e3xY7xNSq_gn2BERnnWRPWkOx4tyqvQ3A,12779
33
+ payi/lib/VertexInstrumentor.py,sha256=lF1BcKKCQdHOMn563e1OdzeDfhjLhPyhZSyM3PycZjA,13881
34
34
  payi/lib/helpers.py,sha256=K1KAfWrpPT1UUGNxspLe1lHzQjP3XV5Pkh9IU4pKMok,4624
35
- payi/lib/instrument.py,sha256=w55jtWm6PoIJqAPEeRJXWc41QTuUxzgQB9BxIdFYMdU,64622
35
+ payi/lib/instrument.py,sha256=qgV6f-Z2Egl1WsjTDyxg-2Unx6iIizhV4DPi44qPEAY,66360
36
36
  payi/resources/__init__.py,sha256=1rtrPLWbNt8oJGOp6nwPumKLJ-ftez0B6qwLFyfcoP4,2972
37
37
  payi/resources/ingest.py,sha256=8HNHEyfgIyJNqCh0rOhO9msoc61-8IyifJ6AbxjCrDg,22612
38
38
  payi/resources/categories/__init__.py,sha256=w5gMiPdBSzJA_qfoVtFBElaoe8wGf_O63R7R1Spr6Gk,1093
@@ -142,7 +142,7 @@ payi/types/use_cases/definitions/kpi_retrieve_response.py,sha256=uQXliSvS3k-yDYw
142
142
  payi/types/use_cases/definitions/kpi_update_params.py,sha256=jbawdWAdMnsTWVH0qfQGb8W7_TXe3lq4zjSRu44d8p8,373
143
143
  payi/types/use_cases/definitions/kpi_update_response.py,sha256=zLyEoT0S8d7XHsnXZYT8tM7yDw0Aze0Mk-_Z6QeMtc8,459
144
144
  payi/types/use_cases/definitions/limit_config_create_params.py,sha256=pzQza_16N3z8cFNEKr6gPbFvuGFrwNuGxAYb--Kbo2M,449
145
- payi-0.1.0a82.dist-info/METADATA,sha256=mD2GWPPSx1-aeLthkLrXEp3Ng4-TBjMN6mDMIljfxkc,15180
146
- payi-0.1.0a82.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
147
- payi-0.1.0a82.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
148
- payi-0.1.0a82.dist-info/RECORD,,
145
+ payi-0.1.0a83.dist-info/METADATA,sha256=uY2n8H_NGmAxXXCm6a6yFC6HXxiuAl3I6RvYn5W2FcU,15180
146
+ payi-0.1.0a83.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
147
+ payi-0.1.0a83.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
148
+ payi-0.1.0a83.dist-info/RECORD,,