payi 0.1.0a40__py3-none-any.whl → 0.1.0a41__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/_constants.py CHANGED
@@ -6,7 +6,7 @@ RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response"
6
6
  OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to"
7
7
 
8
8
  # default timeout is 1 minute
9
- DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0)
9
+ DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0)
10
10
  DEFAULT_MAX_RETRIES = 2
11
11
  DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20)
12
12
 
payi/_models.py CHANGED
@@ -172,7 +172,7 @@ class BaseModel(pydantic.BaseModel):
172
172
  @override
173
173
  def __str__(self) -> str:
174
174
  # mypy complains about an invalid self arg
175
- return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc]
175
+ return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc]
176
176
 
177
177
  # Override the 'construct' method in a way that supports recursive parsing without validation.
178
178
  # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
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.40" # x-release-please-version
4
+ __version__ = "0.1.0-alpha.41" # x-release-please-version
@@ -7,7 +7,7 @@ from wrapt import wrap_function_wrapper # type: ignore
7
7
  from payi.types import IngestUnitsParams
8
8
  from payi.types.ingest_units_params import Units
9
9
 
10
- from .instrument import PayiInstrumentor
10
+ from .instrument import IsStreaming, PayiInstrumentor
11
11
 
12
12
 
13
13
  class AnthropicIntrumentor:
@@ -48,14 +48,15 @@ def chat_wrapper(
48
48
  kwargs: Any,
49
49
  ) -> Any:
50
50
  return instrumentor.chat_wrapper(
51
- "system.anthropic",
52
- process_chunk,
53
- process_request,
54
- process_synchronous_response,
55
- wrapped,
56
- instance,
57
- args,
58
- kwargs,
51
+ category="system.anthropic",
52
+ process_chunk=process_chunk,
53
+ process_request=process_request,
54
+ process_synchronous_response=process_synchronous_response,
55
+ is_streaming=IsStreaming.kwargs,
56
+ wrapped=wrapped,
57
+ instance=instance,
58
+ args=args,
59
+ kwargs=kwargs,
59
60
  )
60
61
 
61
62
 
@@ -81,10 +82,10 @@ def process_chunk(chunk: Any, ingest: IngestUnitsParams) -> None:
81
82
  ingest["units"]["text"]["output"] = usage.output_tokens
82
83
 
83
84
 
84
- def process_synchronous_response(response: Any, ingest: IngestUnitsParams, log_prompt_and_response: bool) -> None:
85
+ def process_synchronous_response(response: Any, ingest: IngestUnitsParams, log_prompt_and_response: bool, *args: Any, **kwargs: 'dict[str, Any]') -> Any: # noqa: ARG001
85
86
  usage = response.usage
86
87
  input = usage.input_tokens
87
- ouptut = usage.output_tokens
88
+ output = usage.output_tokens
88
89
  units: dict[str, Units] = ingest["units"]
89
90
 
90
91
  if hasattr(usage, "cache_creation_input_tokens") and usage.cache_creation_input_tokens > 0:
@@ -97,11 +98,13 @@ def process_synchronous_response(response: Any, ingest: IngestUnitsParams, log_p
97
98
 
98
99
  input = PayiInstrumentor.update_for_vision(input, units)
99
100
 
100
- units["text"] = Units(input=input, output=ouptut)
101
+ units["text"] = Units(input=input, output=output)
101
102
 
102
103
  if log_prompt_and_response:
103
104
  ingest["provider_response_json"] = response.to_json()
104
105
 
106
+ return None
107
+
105
108
  def has_image_and_get_texts(encoding: tiktoken.Encoding, content: Union[str, 'list[Any]']) -> 'tuple[bool, int]':
106
109
  if isinstance(content, str):
107
110
  return False, 0
@@ -0,0 +1,288 @@
1
+ import json
2
+ import logging
3
+ from typing import Any
4
+ from functools import wraps
5
+
6
+ from wrapt import ObjectProxy, wrap_function_wrapper # type: ignore
7
+
8
+ from payi.types.ingest_units_params import Units, IngestUnitsParams
9
+ from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
10
+
11
+ from .instrument import IsStreaming, PayiInstrumentor
12
+
13
+
14
+ class BedrockInstrumentor:
15
+ @staticmethod
16
+ def instrument(instrumentor: PayiInstrumentor) -> None:
17
+ try:
18
+ import boto3 # type: ignore # noqa: F401 I001
19
+
20
+ # wrap_function_wrapper(
21
+ # "anthropic.resources.completions",
22
+ # "Completions.create",
23
+ # chat_wrapper(instrumentor),
24
+ # )
25
+
26
+ wrap_function_wrapper(
27
+ "botocore.client",
28
+ "ClientCreator.create_client",
29
+ create_client_wrapper(instrumentor),
30
+ )
31
+
32
+ wrap_function_wrapper(
33
+ "botocore.session",
34
+ "Session.create_client",
35
+ create_client_wrapper(instrumentor),
36
+ )
37
+
38
+ except Exception as e:
39
+ logging.debug(f"Error instrumenting bedrock: {e}")
40
+ return
41
+
42
+ @PayiInstrumentor.payi_wrapper
43
+ def create_client_wrapper(instrumentor: PayiInstrumentor, wrapped: Any, instance: Any, args: Any, kwargs: Any) -> Any: # noqa: ARG001
44
+ if kwargs.get("service_name") != "bedrock-runtime":
45
+ return wrapped(*args, **kwargs)
46
+
47
+ try:
48
+ client: Any = wrapped(*args, **kwargs)
49
+ client.invoke_model = wrap_invoke(instrumentor, client.invoke_model)
50
+ client.invoke_model_with_response_stream = wrap_invoke_stream(instrumentor, client.invoke_model_with_response_stream)
51
+ client.converse = wrap_converse(instrumentor, client.converse)
52
+ client.converse_stream = wrap_converse_stream(instrumentor, client.converse_stream)
53
+
54
+ return client
55
+ except Exception as e:
56
+ logging.debug(f"Error instrumenting bedrock client: {e}")
57
+
58
+ return wrapped(*args, **kwargs)
59
+
60
+ class InvokeResponseWrapper(ObjectProxy): # type: ignore
61
+ def __init__(
62
+ self,
63
+ response: Any,
64
+ instrumentor: PayiInstrumentor,
65
+ ingest: IngestUnitsParams,
66
+ log_prompt_and_response: bool
67
+ ) -> None:
68
+
69
+ super().__init__(response) # type: ignore
70
+ self._response = response
71
+ self._instrumentor = instrumentor
72
+ self._ingest = ingest
73
+ self._log_prompt_and_response = log_prompt_and_response
74
+
75
+ def read(self, amt: Any =None): # type: ignore
76
+ # data is array of bytes
77
+ data: Any = self.__wrapped__.read(amt) # type: ignore
78
+ response = json.loads(data)
79
+
80
+ resource = self._ingest["resource"]
81
+ if not resource:
82
+ return
83
+
84
+ input: int = 0
85
+ output: int = 0
86
+ units: dict[str, Units] = self._ingest["units"]
87
+
88
+ if resource.startswith("meta.llama3"):
89
+ input = response['prompt_token_count']
90
+ output = response['generation_token_count']
91
+ elif resource.startswith("anthropic."):
92
+ usage = response['usage']
93
+ input = usage['input_tokens']
94
+ output = usage['output_tokens']
95
+ units["text"] = Units(input=input, output=output)
96
+
97
+ if self._log_prompt_and_response:
98
+ self._ingest["provider_response_json"] = data.decode('utf-8')
99
+
100
+ self._instrumentor._ingest_units(self._ingest)
101
+
102
+ return data
103
+
104
+ def wrap_invoke(instrumentor: PayiInstrumentor, wrapped: Any) -> Any:
105
+ @wraps(wrapped)
106
+ def invoke_wrapper(*args: Any, **kwargs: 'dict[str, Any]') -> Any:
107
+ modelId:str = kwargs.get("modelId", "") # type: ignore
108
+
109
+ if modelId.startswith("meta.llama3") or modelId.startswith("anthropic."):
110
+ return instrumentor.chat_wrapper(
111
+ category="system.aws.bedrock",
112
+ process_chunk=None,
113
+ process_request=process_invoke_request,
114
+ process_synchronous_response=process_synchronous_invoke_response,
115
+ is_streaming=IsStreaming.false,
116
+ wrapped=wrapped,
117
+ instance=None,
118
+ args=args,
119
+ kwargs=kwargs,
120
+ )
121
+ return wrapped(*args, **kwargs)
122
+
123
+ return invoke_wrapper
124
+
125
+ def wrap_invoke_stream(instrumentor: PayiInstrumentor, wrapped: Any) -> Any:
126
+ @wraps(wrapped)
127
+ def invoke_wrapper(*args: Any, **kwargs: Any) -> Any:
128
+ modelId:str = kwargs.get("modelId", "") # type: ignore
129
+
130
+ if modelId.startswith("meta.llama3") or modelId.startswith("anthropic."):
131
+ return instrumentor.chat_wrapper(
132
+ category="system.aws.bedrock",
133
+ process_chunk=process_invoke_streaming_anthropic_chunk if modelId.startswith("anthropic.") else process_invoke_streaming_llama_chunk,
134
+ process_request=process_invoke_request,
135
+ process_synchronous_response=None,
136
+ is_streaming=IsStreaming.true,
137
+ wrapped=wrapped,
138
+ instance=None,
139
+ args=args,
140
+ kwargs=kwargs,
141
+ )
142
+ return wrapped(*args, **kwargs)
143
+
144
+ return invoke_wrapper
145
+
146
+ def wrap_converse(instrumentor: PayiInstrumentor, wrapped: Any) -> Any:
147
+ @wraps(wrapped)
148
+ def invoke_wrapper(*args: Any, **kwargs: 'dict[str, Any]') -> Any:
149
+ modelId:str = kwargs.get("modelId", "") # type: ignore
150
+
151
+ if modelId.startswith("meta.llama3") or modelId.startswith("anthropic."):
152
+ return instrumentor.chat_wrapper(
153
+ category="system.aws.bedrock",
154
+ process_chunk=None,
155
+ process_request=process_converse_request,
156
+ process_synchronous_response=process_synchronous_converse_response,
157
+ is_streaming=IsStreaming.false,
158
+ wrapped=wrapped,
159
+ instance=None,
160
+ args=args,
161
+ kwargs=kwargs,
162
+ )
163
+ return wrapped(*args, **kwargs)
164
+
165
+ return invoke_wrapper
166
+
167
+ def wrap_converse_stream(instrumentor: PayiInstrumentor, wrapped: Any) -> Any:
168
+ @wraps(wrapped)
169
+ def invoke_wrapper(*args: Any, **kwargs: Any) -> Any:
170
+ modelId:str = kwargs.get("modelId", "") # type: ignore
171
+
172
+ if modelId.startswith("meta.llama3") or modelId.startswith("anthropic."):
173
+ return instrumentor.chat_wrapper(
174
+ category="system.aws.bedrock",
175
+ process_chunk=process_converse_streaming_chunk,
176
+ process_request=process_converse_request,
177
+ process_synchronous_response=None,
178
+ is_streaming=IsStreaming.true,
179
+ wrapped=wrapped,
180
+ instance=None,
181
+ args=args,
182
+ kwargs=kwargs,
183
+ )
184
+ return wrapped(*args, **kwargs)
185
+
186
+ return invoke_wrapper
187
+
188
+ def process_invoke_streaming_anthropic_chunk(chunk: str, ingest: IngestUnitsParams) -> None:
189
+ chunk_dict = json.loads(chunk)
190
+ type = chunk_dict.get("type", "")
191
+
192
+ if type == "message_start":
193
+ usage = chunk_dict['message']['usage']
194
+ units = ingest["units"]
195
+
196
+ input = PayiInstrumentor.update_for_vision(usage['input_tokens'], units)
197
+
198
+ units["text"] = Units(input=input, output=0)
199
+
200
+ text_cache_write: int = usage.get("cache_creation_input_tokens", 0)
201
+ if text_cache_write > 0:
202
+ units["text_cache_write"] = Units(input=text_cache_write, output=0)
203
+
204
+ text_cache_read: int = usage.get("cache_read_input_tokens", 0)
205
+ if text_cache_read > 0:
206
+ units["text_cache_read"] = Units(input=text_cache_read, output=0)
207
+
208
+ elif type == "message_delta":
209
+ usage = chunk_dict['usage']
210
+ ingest["units"]["text"]["output"] = usage['output_tokens']
211
+
212
+ def process_invoke_streaming_llama_chunk(chunk: str, ingest: IngestUnitsParams) -> None:
213
+ chunk_dict = json.loads(chunk)
214
+ metrics = chunk_dict.get("amazon-bedrock-invocationMetrics", {})
215
+ if metrics:
216
+ input = metrics.get("inputTokenCount", 0)
217
+ output = metrics.get("outputTokenCount", 0)
218
+ ingest["units"]["text"] = Units(input=input, output=output)
219
+
220
+ def process_synchronous_invoke_response(
221
+ response: Any,
222
+ ingest: IngestUnitsParams,
223
+ log_prompt_and_response: bool,
224
+ instrumentor: PayiInstrumentor,
225
+ **kargs: Any) -> Any: # noqa: ARG001
226
+
227
+ metadata = response.get("ResponseMetadata", {})
228
+
229
+ # request_id = metadata.get("RequestId", "")
230
+ # if request_id:
231
+ # ingest["provider_request_id"] = request_id
232
+
233
+ response_headers = metadata.get("HTTPHeaders", {}).copy()
234
+ if response_headers:
235
+ ingest["provider_response_headers"] = [PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v) for k, v in response_headers.items()]
236
+
237
+ response["body"] = InvokeResponseWrapper(
238
+ response=response["body"],
239
+ instrumentor=instrumentor,
240
+ ingest=ingest,
241
+ log_prompt_and_response=log_prompt_and_response)
242
+
243
+ return response
244
+
245
+ def process_invoke_request(ingest: IngestUnitsParams, kwargs: Any) -> None: # noqa: ARG001
246
+ return
247
+
248
+ def process_converse_streaming_chunk(chunk: 'dict[str, Any]', ingest: IngestUnitsParams) -> None:
249
+ metadata = chunk.get("metadata", {})
250
+
251
+ if metadata:
252
+ usage = metadata['usage']
253
+ input = usage["inputTokens"]
254
+ output = usage["outputTokens"]
255
+ ingest["units"]["text"] = Units(input=input, output=output)
256
+
257
+ def process_synchronous_converse_response(
258
+ response: 'dict[str, Any]',
259
+ ingest: IngestUnitsParams,
260
+ log_prompt_and_response: bool,
261
+ **kargs: Any) -> Any: # noqa: ARG001
262
+
263
+ usage = response["usage"]
264
+ input = usage["inputTokens"]
265
+ output = usage["outputTokens"]
266
+
267
+ units: dict[str, Units] = ingest["units"]
268
+ units["text"] = Units(input=input, output=output)
269
+
270
+ metadata = response.get("ResponseMetadata", {})
271
+
272
+ # request_id = metadata.get("RequestId", "")
273
+ # if request_id:
274
+ # ingest["provider_request_id"] = request_id
275
+
276
+ response_headers = metadata.get("HTTPHeaders", {})
277
+ if response_headers:
278
+ ingest["provider_response_headers"] = [PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v) for k, v in response_headers.items()]
279
+
280
+ if log_prompt_and_response:
281
+ response_without_metadata = response.copy()
282
+ response_without_metadata.pop("ResponseMetadata", None)
283
+ ingest["provider_response_json"] = json.dumps(response_without_metadata)
284
+
285
+ return None
286
+
287
+ def process_converse_request(ingest: IngestUnitsParams, kwargs: Any) -> None: # noqa: ARG001
288
+ return
payi/lib/Instruments.py CHANGED
@@ -5,3 +5,4 @@ class Instruments(Enum):
5
5
  ALL = "all"
6
6
  OPENAI = "openai"
7
7
  ANTHROPIC = "anthropic"
8
+ AWS_BEDROCK = "aws.bedrock"
@@ -9,7 +9,7 @@ from wrapt import wrap_function_wrapper # type: ignore
9
9
  from payi.types import IngestUnitsParams
10
10
  from payi.types.ingest_units_params import Units
11
11
 
12
- from .instrument import PayiInstrumentor
12
+ from .instrument import IsStreaming, PayiInstrumentor
13
13
 
14
14
 
15
15
  class OpenAiInstrumentor:
@@ -37,18 +37,19 @@ def chat_wrapper(
37
37
  kwargs: Any,
38
38
  ) -> Any:
39
39
  return instrumentor.chat_wrapper(
40
- "system.openai",
41
- process_chat_chunk,
42
- process_request,
43
- process_chat_synchronous_response,
44
- wrapped,
45
- instance,
46
- args,
47
- kwargs,
40
+ category="system.openai",
41
+ process_chunk=process_chat_chunk,
42
+ process_request=process_request,
43
+ process_synchronous_response=process_chat_synchronous_response,
44
+ is_streaming=IsStreaming.kwargs,
45
+ wrapped=wrapped,
46
+ instance=instance,
47
+ args=args,
48
+ kwargs=kwargs,
48
49
  )
49
50
 
50
51
 
51
- def process_chat_synchronous_response(response: str, ingest: IngestUnitsParams, log_prompt_and_response: bool) -> None:
52
+ def process_chat_synchronous_response(response: str, ingest: IngestUnitsParams, log_prompt_and_response: bool, **kwargs: Any) -> Any: # noqa: ARG001
52
53
  response_dict = model_to_dict(response)
53
54
 
54
55
  add_usage_units(response_dict["usage"], ingest["units"])
@@ -56,6 +57,7 @@ def process_chat_synchronous_response(response: str, ingest: IngestUnitsParams,
56
57
  if log_prompt_and_response:
57
58
  ingest["provider_response_json"] = [json.dumps(response_dict)]
58
59
 
60
+ return None
59
61
 
60
62
  def process_chat_chunk(chunk: Any, ingest: IngestUnitsParams) -> None:
61
63
  model = model_to_dict(chunk)
payi/lib/instrument.py CHANGED
@@ -4,6 +4,7 @@ import asyncio
4
4
  import inspect
5
5
  import logging
6
6
  import traceback
7
+ from enum import Enum
7
8
  from typing import Any, Set, Union, Callable, Optional
8
9
 
9
10
  from wrapt import ObjectProxy # type: ignore
@@ -11,11 +12,17 @@ from wrapt import ObjectProxy # type: ignore
11
12
  from payi import Payi, AsyncPayi
12
13
  from payi.types import IngestUnitsParams
13
14
  from payi.types.ingest_units_params import Units
15
+ from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
14
16
 
15
17
  from .Stopwatch import Stopwatch
16
18
  from .Instruments import Instruments
17
19
 
18
20
 
21
+ class IsStreaming(Enum):
22
+ false = 0
23
+ true = 1
24
+ kwargs = 2
25
+
19
26
  class PayiInstrumentor:
20
27
  estimated_prompt_tokens: str = "estimated_prompt_tokens"
21
28
 
@@ -44,12 +51,15 @@ class PayiInstrumentor:
44
51
  def _instrument_all(self) -> None:
45
52
  self._instrument_openai()
46
53
  self._instrument_anthropic()
54
+ self._instrument_aws_bedrock()
47
55
 
48
56
  def _instrument_specific(self, instruments: Set[Instruments]) -> None:
49
57
  if Instruments.OPENAI in instruments:
50
58
  self._instrument_openai()
51
59
  if Instruments.ANTHROPIC in instruments:
52
60
  self._instrument_anthropic()
61
+ if Instruments.AWS_BEDROCK in instruments:
62
+ self._instrument_aws_bedrock()
53
63
 
54
64
  def _instrument_openai(self) -> None:
55
65
  from .OpenAIInstrumentor import OpenAiInstrumentor
@@ -69,6 +79,15 @@ class PayiInstrumentor:
69
79
  except Exception as e:
70
80
  logging.error(f"Error instrumenting Anthropic: {e}")
71
81
 
82
+ def _instrument_aws_bedrock(self) -> None:
83
+ from .BedrockInstrumentor import BedrockInstrumentor
84
+
85
+ try:
86
+ BedrockInstrumentor.instrument(self)
87
+
88
+ except Exception as e:
89
+ logging.error(f"Error instrumenting AWS bedrock: {e}")
90
+
72
91
  def _ingest_units(self, ingest_units: IngestUnitsParams) -> None:
73
92
  # return early if there are no units to ingest and on a successul ingest request
74
93
  if int(ingest_units.get("http_status_code") or 0) < 400:
@@ -206,18 +225,25 @@ class PayiInstrumentor:
206
225
  def chat_wrapper(
207
226
  self,
208
227
  category: str,
209
- process_chunk: Callable[[Any, IngestUnitsParams], None],
228
+ process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]],
210
229
  process_request: Optional[Callable[[IngestUnitsParams, Any], None]],
211
- process_synchronous_response: Optional[Callable[[Any, IngestUnitsParams, bool], None]],
230
+ process_synchronous_response: Any,
231
+ is_streaming: IsStreaming,
212
232
  wrapped: Any,
213
233
  instance: Any,
214
234
  args: Any,
215
- kwargs: 'dict[str, Any]',
235
+ kwargs: Any,
216
236
  ) -> Any:
217
237
  context = self.get_context()
218
238
 
239
+ is_bedrock:bool = category == "system.aws.bedrock"
240
+
219
241
  if not context:
220
- # should not happen
242
+ if is_bedrock:
243
+ # boto3 doesn't allow extra_headers
244
+ kwargs.pop("extra_headers", None)
245
+
246
+ # wrapped function invoked outside of decorator scope
221
247
  return wrapped(*args, **kwargs)
222
248
 
223
249
  # after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
@@ -230,11 +256,14 @@ class PayiInstrumentor:
230
256
 
231
257
  return wrapped(*args, **kwargs)
232
258
 
233
- ingest: IngestUnitsParams = {"category": category, "resource": kwargs.get("model"), "units": {}} # type: ignore
259
+ ingest: IngestUnitsParams = {"category": category, "units": {}} # type: ignore
260
+ if is_bedrock:
261
+ # boto3 doesn't allow extra_headers
262
+ kwargs.pop("extra_headers", None)
263
+ ingest["resource"] = kwargs.get("modelId", "")
264
+ else:
265
+ ingest["resource"] = kwargs.get("model", "")
234
266
 
235
- # blocked_limit = next((limit for limit in (context.get('limit_ids') or []) if limit in self._blocked_limits), None)
236
- # if blocked_limit:
237
- # raise Exception(f"Limit {blocked_limit} is blocked")
238
267
  current_frame = inspect.currentframe()
239
268
  # f_back excludes the current frame, strip() cleans up whitespace and newlines
240
269
  stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
@@ -245,7 +274,14 @@ class PayiInstrumentor:
245
274
  process_request(ingest, kwargs)
246
275
 
247
276
  sw = Stopwatch()
248
- stream = kwargs.get("stream", False)
277
+ stream: bool = False
278
+
279
+ if is_streaming == IsStreaming.kwargs:
280
+ stream = kwargs.get("stream", False)
281
+ elif is_streaming == IsStreaming.true:
282
+ stream = True
283
+ else:
284
+ stream = False
249
285
 
250
286
  try:
251
287
  limit_ids = extra_headers.pop("xProxy-Limit-IDs", None)
@@ -266,7 +302,7 @@ class PayiInstrumentor:
266
302
  ingest["user_id"] = user_id
267
303
 
268
304
  if len(extra_headers) > 0:
269
- ingest["provider_request_headers"] = {k: [v] for k, v in extra_headers.items()} # type: ignore
305
+ ingest["provider_request_headers"] = [PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v) for k, v in extra_headers.items()]
270
306
 
271
307
  provider_prompt = {}
272
308
  for k, v in kwargs.items():
@@ -292,7 +328,7 @@ class PayiInstrumentor:
292
328
  raise e
293
329
 
294
330
  if stream:
295
- return ChatStreamWrapper(
331
+ stream_result = ChatStreamWrapper(
296
332
  response=response,
297
333
  instance=instance,
298
334
  instrumentor=self,
@@ -300,15 +336,31 @@ class PayiInstrumentor:
300
336
  ingest=ingest,
301
337
  stopwatch=sw,
302
338
  process_chunk=process_chunk,
339
+ is_bedrock=is_bedrock,
303
340
  )
304
341
 
342
+ if is_bedrock:
343
+ if "body" in response:
344
+ response["body"] = stream_result
345
+ else:
346
+ response["stream"] = stream_result
347
+ return response
348
+
349
+ return stream_result
350
+
305
351
  sw.stop()
306
352
  duration = sw.elapsed_ms_int()
307
353
  ingest["end_to_end_latency_ms"] = duration
308
354
  ingest["http_status_code"] = 200
309
355
 
310
356
  if process_synchronous_response:
311
- process_synchronous_response(response, ingest, self._log_prompt_and_response)
357
+ return_result: Any = process_synchronous_response(
358
+ response=response,
359
+ ingest=ingest,
360
+ log_prompt_and_response=self._log_prompt_and_response,
361
+ instrumentor=self)
362
+ if return_result:
363
+ return return_result
312
364
 
313
365
  self._ingest_units(ingest)
314
366
 
@@ -387,7 +439,6 @@ class PayiInstrumentor:
387
439
 
388
440
  return _payi_wrapper
389
441
 
390
-
391
442
  class ChatStreamWrapper(ObjectProxy): # type: ignore
392
443
  def __init__(
393
444
  self,
@@ -398,7 +449,19 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
398
449
  stopwatch: Stopwatch,
399
450
  process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]] = None,
400
451
  log_prompt_and_response: bool = True,
452
+ is_bedrock: bool = False,
401
453
  ) -> None:
454
+
455
+ bedrock_from_stream: bool = False
456
+ if is_bedrock:
457
+ stream = response.get("stream", None)
458
+ if stream:
459
+ response = stream
460
+ bedrock_from_stream = True
461
+ else:
462
+ response = response.get("body")
463
+ bedrock_from_stream = False
464
+
402
465
  super().__init__(response) # type: ignore
403
466
 
404
467
  self._response = response
@@ -413,6 +476,8 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
413
476
  self._process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]] = process_chunk
414
477
 
415
478
  self._first_token: bool = True
479
+ self._is_bedrock: bool = is_bedrock
480
+ self._bedrock_from_stream: bool = bedrock_from_stream
416
481
 
417
482
  def __enter__(self) -> Any:
418
483
  return self
@@ -426,9 +491,26 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
426
491
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
427
492
  await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb) # type: ignore
428
493
 
429
- def __iter__(self) -> Any:
494
+ def __iter__(self) -> Any:
495
+ if self._is_bedrock:
496
+ # MUST be reside in a separate function so that the yield statement doesn't implicitly return its own iterator and overriding self
497
+ return self._iter_bedrock()
430
498
  return self
431
499
 
500
+ def _iter_bedrock(self) -> Any:
501
+ # botocore EventStream doesn't have a __next__ method so iterate over the wrapped object in place
502
+ for event in self.__wrapped__: # type: ignore
503
+ if (self._bedrock_from_stream):
504
+ self._evaluate_chunk(event)
505
+ else:
506
+ chunk = event.get('chunk') # type: ignore
507
+ if chunk:
508
+ decode = chunk.get('bytes').decode() # type: ignore
509
+ self._evaluate_chunk(decode)
510
+ yield event
511
+
512
+ self._stop_iteration()
513
+
432
514
  def __aiter__(self) -> Any:
433
515
  return self
434
516
 
@@ -460,7 +542,7 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
460
542
  self._first_token = False
461
543
 
462
544
  if self._log_prompt_and_response:
463
- self._responses.append(chunk.to_json())
545
+ self._responses.append(self.chunk_to_json(chunk))
464
546
 
465
547
  if self._process_chunk:
466
548
  self._process_chunk(chunk, self._ingest)
@@ -475,11 +557,21 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
475
557
 
476
558
  self._instrumentor._ingest_units(self._ingest)
477
559
 
560
+ @staticmethod
561
+ def chunk_to_json(chunk: Any) -> str:
562
+ if hasattr(chunk, "to_json"):
563
+ return str(chunk.to_json())
564
+ elif isinstance(chunk, bytes):
565
+ return chunk.decode()
566
+ elif isinstance(chunk, str):
567
+ return chunk
568
+ else:
569
+ # assume dict
570
+ return json.dumps(chunk)
478
571
 
479
572
  global _instrumentor
480
573
  _instrumentor: PayiInstrumentor
481
574
 
482
-
483
575
  def payi_instrument(
484
576
  payi: Optional[Union[Payi, AsyncPayi]] = None,
485
577
  instruments: Optional[Set[Instruments]] = None,
@@ -520,7 +612,6 @@ def ingest(
520
612
 
521
613
  return _ingest
522
614
 
523
-
524
615
  def proxy(
525
616
  limit_ids: Optional["list[str]"] = None,
526
617
  request_tags: Optional["list[str]"] = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: payi
3
- Version: 0.1.0a40
3
+ Version: 0.1.0a41
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
@@ -2,16 +2,16 @@ payi/__init__.py,sha256=_eeZx9fx2Wp81adXh7qrpkmXCso7TiRSvIlLkQ0sQhY,2399
2
2
  payi/_base_client.py,sha256=CiHJoJuzSweHB7oidXWIlWdU0vrLCpPc9hMlj_S-anE,68128
3
3
  payi/_client.py,sha256=aUtMEmV02nTs3_pYYAR-OchCkofUHeXhhRs43tyDHLE,18760
4
4
  payi/_compat.py,sha256=VWemUKbj6DDkQ-O4baSpHVLJafotzeXmCQGJugfVTIw,6580
5
- payi/_constants.py,sha256=JE8kyZa2Q4NK_i4fO--8siEYTzeHnT0fYbOFDgDP4uk,464
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=B6f-C-F-PbDp3jRKCLksaAS9osC2g1xs7DpoZV1dlUE,28659
8
+ payi/_models.py,sha256=uZvPAaaeDwCYwvB-yq7nxZnZ70I2Na_KjSAqaPQWfh0,28659
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=CfrNS_3wbL8o9dRyRVfZQ5E1GUlA4CUIUEK8olmfGqE,28777
12
12
  payi/_streaming.py,sha256=Z_wIyo206T6Jqh2rolFg2VXZgX24PahLmpURp0-NssU,10092
13
13
  payi/_types.py,sha256=2mbMK86K3W1aMTW7sOGQ-VND6-A2IuXKm8p4sYFztBU,6141
14
- payi/_version.py,sha256=70Q3C2mH_Y5XHCDAYiBgNkVy3fyBDWf8eUqRwndh9Zc,165
14
+ payi/_version.py,sha256=4Z7oXmyN8CqDXoYFdr9Fz53f3-pCQaVF2JTZFXE0NCo,165
15
15
  payi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  payi/_utils/__init__.py,sha256=PNZ_QJuzZEgyYXqkO1HVhGkj5IU9bglVUcw7H-Knjzw,2062
17
17
  payi/_utils/_logs.py,sha256=fmnf5D9TOgkgZKfgYmSa3PiUc3SZgkchn6CzJUeo0SQ,768
@@ -23,12 +23,13 @@ payi/_utils/_transform.py,sha256=Dkkyr7OveGmOolepcvXmVJWE3kqim4b0nM0h7yWbgeY,134
23
23
  payi/_utils/_typing.py,sha256=nTJz0jcrQbEgxwy4TtAkNxuU0QHHlmc6mQtA6vIR8tg,4501
24
24
  payi/_utils/_utils.py,sha256=8UmbPOy_AAr2uUjjFui-VZSrVBHRj6bfNEKRp5YZP2A,12004
25
25
  payi/lib/.keep,sha256=wuNrz-5SXo3jJaJOJgz4vFHM41YH_g20F5cRQo0vLes,224
26
- payi/lib/AnthropicInstrumentor.py,sha256=XMLr8BIrBo7U3VPBnhyp9Wj-icZmu0MGVvvyDPmlF4c,4549
27
- payi/lib/Instruments.py,sha256=cyL2jxjpRluP9rN8Vn1nmVXq2NNLdZuFIsHMQWWqat4,115
28
- payi/lib/OpenAIInstrumentor.py,sha256=G3H8s1bNnq-LECAHjCkQDTG59pwnLPXKQKlQbhqgs4k,3998
26
+ payi/lib/AnthropicInstrumentor.py,sha256=uiobG6N6QOKyhyS_kSZPCrKCnsRnKSLPBPNn3sfcUYg,4771
27
+ payi/lib/BedrockInstrumentor.py,sha256=r5h01WJqx9PGszpwKWwVgPdCqUlWIbf6IiS01drN0qc,10684
28
+ payi/lib/Instruments.py,sha256=bapmVS9jbHtFknXCKDzsFFWvf5XLtzEpdlvI7iEWY-o,147
29
+ payi/lib/OpenAIInstrumentor.py,sha256=qYZJ-m723GCK3nGmI-JmRrNMpcDWmr4t4KYR1zWP-Ug,4195
29
30
  payi/lib/Stopwatch.py,sha256=vFyGVRvkppamP7W0IuZyypKLMIaqjhB7fcRG0dNyfnQ,757
30
31
  payi/lib/helpers.py,sha256=ZgkY8UE2YRc7ok2Pmxg_T9UMqKI8D8542JY3CP8RZCM,1597
31
- payi/lib/instrument.py,sha256=rLSwXRQZkTmMpWGpTzgZ42_4BOxJjGrTR3hwpF8XHro,20279
32
+ payi/lib/instrument.py,sha256=FH2bcmcQyFCd6GAc7tiikag9RwPfd5FR5QCCNMomt4E,23462
32
33
  payi/resources/__init__.py,sha256=isHGXSl9kOrZDduKrX3UenTwrdTpuKJVBjw6NYSBV20,3592
33
34
  payi/resources/billing_models.py,sha256=5w3RfGXtGlyq5vbTw6hQrx1UlzRBtlq8ArcFlf5e3TY,20152
34
35
  payi/resources/ingest.py,sha256=SvQspsYled4_ErOZKzVtazBIk0tUC1e34Lw8qw4SNEM,15484
@@ -109,7 +110,7 @@ payi/types/requests/request_result.py,sha256=phYQiqhwNaR9igP-Fhs34Y-__dlT7L4wq-r
109
110
  payi/types/shared/__init__.py,sha256=-xz5dxK5LBjLnsi2LpLq5btaGDFp-mSjJ0y2qKy0Yus,264
110
111
  payi/types/shared/evaluation_response.py,sha256=ejEToMA57PUu1SldEtJ5z9r4fAO3U0tvdjbsyIoVX1s,214
111
112
  payi/types/shared/pay_i_common_models_budget_management_cost_details_base.py,sha256=XmIzJXy4zAi-mfrDvEXiYjO3qF1EvugGUl-Gijj4TA4,268
112
- payi-0.1.0a40.dist-info/METADATA,sha256=qTLzlfTKFZ8cEWce30AegO7WzqqjejiYc6kJi3SqYN8,12625
113
- payi-0.1.0a40.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
114
- payi-0.1.0a40.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
115
- payi-0.1.0a40.dist-info/RECORD,,
113
+ payi-0.1.0a41.dist-info/METADATA,sha256=QTeHU7hACFAIb9eEk3z9jPIbSjQwiRcuCrNtzIkr3FU,12625
114
+ payi-0.1.0a41.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
115
+ payi-0.1.0a41.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
116
+ payi-0.1.0a41.dist-info/RECORD,,