payi 0.1.0a110__py3-none-any.whl → 0.1.0a137__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. payi/__init__.py +3 -1
  2. payi/_base_client.py +12 -12
  3. payi/_client.py +8 -8
  4. payi/_compat.py +48 -48
  5. payi/_models.py +87 -59
  6. payi/_qs.py +7 -7
  7. payi/_streaming.py +4 -6
  8. payi/_types.py +53 -12
  9. payi/_utils/__init__.py +9 -2
  10. payi/_utils/_compat.py +45 -0
  11. payi/_utils/_datetime_parse.py +136 -0
  12. payi/_utils/_sync.py +3 -31
  13. payi/_utils/_transform.py +13 -3
  14. payi/_utils/_typing.py +6 -1
  15. payi/_utils/_utils.py +5 -6
  16. payi/_version.py +1 -1
  17. payi/lib/AnthropicInstrumentor.py +83 -57
  18. payi/lib/BedrockInstrumentor.py +292 -57
  19. payi/lib/GoogleGenAiInstrumentor.py +18 -31
  20. payi/lib/OpenAIInstrumentor.py +56 -72
  21. payi/lib/ProviderRequest.py +216 -0
  22. payi/lib/StreamWrappers.py +379 -0
  23. payi/lib/VertexInstrumentor.py +18 -37
  24. payi/lib/VertexRequest.py +16 -2
  25. payi/lib/data/cohere_embed_english_v3.json +30706 -0
  26. payi/lib/helpers.py +53 -1
  27. payi/lib/instrument.py +404 -668
  28. payi/resources/categories/__init__.py +0 -14
  29. payi/resources/categories/categories.py +25 -53
  30. payi/resources/categories/resources.py +27 -23
  31. payi/resources/ingest.py +126 -132
  32. payi/resources/limits/__init__.py +14 -14
  33. payi/resources/limits/limits.py +58 -58
  34. payi/resources/limits/properties.py +171 -0
  35. payi/resources/requests/request_id/properties.py +8 -8
  36. payi/resources/requests/request_id/result.py +3 -3
  37. payi/resources/requests/response_id/properties.py +8 -8
  38. payi/resources/requests/response_id/result.py +3 -3
  39. payi/resources/use_cases/definitions/definitions.py +27 -27
  40. payi/resources/use_cases/definitions/kpis.py +23 -23
  41. payi/resources/use_cases/definitions/limit_config.py +14 -14
  42. payi/resources/use_cases/definitions/version.py +3 -3
  43. payi/resources/use_cases/kpis.py +15 -15
  44. payi/resources/use_cases/properties.py +6 -6
  45. payi/resources/use_cases/use_cases.py +7 -7
  46. payi/types/__init__.py +2 -0
  47. payi/types/bulk_ingest_response.py +3 -20
  48. payi/types/categories/__init__.py +0 -1
  49. payi/types/categories/resource_list_params.py +5 -1
  50. payi/types/category_list_resources_params.py +5 -1
  51. payi/types/category_resource_response.py +31 -1
  52. payi/types/ingest_event_param.py +7 -6
  53. payi/types/ingest_units_params.py +5 -4
  54. payi/types/limit_create_params.py +3 -3
  55. payi/types/limit_list_response.py +1 -3
  56. payi/types/limit_response.py +1 -3
  57. payi/types/limits/__init__.py +2 -9
  58. payi/types/limits/{tag_remove_params.py → property_update_params.py} +4 -5
  59. payi/types/limits/{tag_delete_response.py → property_update_response.py} +3 -3
  60. payi/types/requests/request_id/property_update_params.py +2 -2
  61. payi/types/requests/response_id/property_update_params.py +2 -2
  62. payi/types/shared/__init__.py +2 -0
  63. payi/types/shared/api_error.py +18 -0
  64. payi/types/shared/pay_i_common_models_budget_management_create_limit_base.py +3 -3
  65. payi/types/shared/properties_request.py +11 -0
  66. payi/types/shared/xproxy_result.py +2 -0
  67. payi/types/shared_params/pay_i_common_models_budget_management_create_limit_base.py +3 -3
  68. payi/types/use_cases/definitions/limit_config_create_params.py +3 -3
  69. payi/types/use_cases/property_update_params.py +2 -2
  70. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/METADATA +6 -6
  71. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/RECORD +73 -75
  72. payi/resources/categories/fixed_cost_resources.py +0 -196
  73. payi/resources/limits/tags.py +0 -507
  74. payi/types/categories/fixed_cost_resource_create_params.py +0 -21
  75. payi/types/limits/limit_tags.py +0 -16
  76. payi/types/limits/tag_create_params.py +0 -13
  77. payi/types/limits/tag_create_response.py +0 -10
  78. payi/types/limits/tag_list_response.py +0 -10
  79. payi/types/limits/tag_remove_response.py +0 -10
  80. payi/types/limits/tag_update_params.py +0 -13
  81. payi/types/limits/tag_update_response.py +0 -10
  82. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/WHEEL +0 -0
  83. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,136 @@
1
+ """
2
+ This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py
3
+ without the Pydantic v1 specific errors.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from typing import Dict, Union, Optional
10
+ from datetime import date, datetime, timezone, timedelta
11
+
12
+ from .._types import StrBytesIntFloat
13
+
14
+ date_expr = r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
15
+ time_expr = (
16
+ r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
17
+ r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
18
+ r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
19
+ )
20
+
21
+ date_re = re.compile(f"{date_expr}$")
22
+ datetime_re = re.compile(f"{date_expr}[T ]{time_expr}")
23
+
24
+
25
+ EPOCH = datetime(1970, 1, 1)
26
+ # if greater than this, the number is in ms, if less than or equal it's in seconds
27
+ # (in seconds this is 11th October 2603, in ms it's 20th August 1970)
28
+ MS_WATERSHED = int(2e10)
29
+ # slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
30
+ MAX_NUMBER = int(3e20)
31
+
32
+
33
+ def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]:
34
+ if isinstance(value, (int, float)):
35
+ return value
36
+ try:
37
+ return float(value)
38
+ except ValueError:
39
+ return None
40
+ except TypeError:
41
+ raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None
42
+
43
+
44
+ def _from_unix_seconds(seconds: Union[int, float]) -> datetime:
45
+ if seconds > MAX_NUMBER:
46
+ return datetime.max
47
+ elif seconds < -MAX_NUMBER:
48
+ return datetime.min
49
+
50
+ while abs(seconds) > MS_WATERSHED:
51
+ seconds /= 1000
52
+ dt = EPOCH + timedelta(seconds=seconds)
53
+ return dt.replace(tzinfo=timezone.utc)
54
+
55
+
56
+ def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]:
57
+ if value == "Z":
58
+ return timezone.utc
59
+ elif value is not None:
60
+ offset_mins = int(value[-2:]) if len(value) > 3 else 0
61
+ offset = 60 * int(value[1:3]) + offset_mins
62
+ if value[0] == "-":
63
+ offset = -offset
64
+ return timezone(timedelta(minutes=offset))
65
+ else:
66
+ return None
67
+
68
+
69
+ def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
70
+ """
71
+ Parse a datetime/int/float/string and return a datetime.datetime.
72
+
73
+ This function supports time zone offsets. When the input contains one,
74
+ the output uses a timezone with a fixed offset from UTC.
75
+
76
+ Raise ValueError if the input is well formatted but not a valid datetime.
77
+ Raise ValueError if the input isn't well formatted.
78
+ """
79
+ if isinstance(value, datetime):
80
+ return value
81
+
82
+ number = _get_numeric(value, "datetime")
83
+ if number is not None:
84
+ return _from_unix_seconds(number)
85
+
86
+ if isinstance(value, bytes):
87
+ value = value.decode()
88
+
89
+ assert not isinstance(value, (float, int))
90
+
91
+ match = datetime_re.match(value)
92
+ if match is None:
93
+ raise ValueError("invalid datetime format")
94
+
95
+ kw = match.groupdict()
96
+ if kw["microsecond"]:
97
+ kw["microsecond"] = kw["microsecond"].ljust(6, "0")
98
+
99
+ tzinfo = _parse_timezone(kw.pop("tzinfo"))
100
+ kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None}
101
+ kw_["tzinfo"] = tzinfo
102
+
103
+ return datetime(**kw_) # type: ignore
104
+
105
+
106
+ def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
107
+ """
108
+ Parse a date/int/float/string and return a datetime.date.
109
+
110
+ Raise ValueError if the input is well formatted but not a valid date.
111
+ Raise ValueError if the input isn't well formatted.
112
+ """
113
+ if isinstance(value, date):
114
+ if isinstance(value, datetime):
115
+ return value.date()
116
+ else:
117
+ return value
118
+
119
+ number = _get_numeric(value, "date")
120
+ if number is not None:
121
+ return _from_unix_seconds(number).date()
122
+
123
+ if isinstance(value, bytes):
124
+ value = value.decode()
125
+
126
+ assert not isinstance(value, (float, int))
127
+ match = date_re.match(value)
128
+ if match is None:
129
+ raise ValueError("invalid date format")
130
+
131
+ kw = {k: int(v) for k, v in match.groupdict().items()}
132
+
133
+ try:
134
+ return date(**kw)
135
+ except ValueError:
136
+ raise ValueError("invalid date format") from None
payi/_utils/_sync.py CHANGED
@@ -1,10 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import sys
4
3
  import asyncio
5
4
  import functools
6
- import contextvars
7
- from typing import Any, TypeVar, Callable, Awaitable
5
+ from typing import TypeVar, Callable, Awaitable
8
6
  from typing_extensions import ParamSpec
9
7
 
10
8
  import anyio
@@ -15,34 +13,11 @@ T_Retval = TypeVar("T_Retval")
15
13
  T_ParamSpec = ParamSpec("T_ParamSpec")
16
14
 
17
15
 
18
- if sys.version_info >= (3, 9):
19
- _asyncio_to_thread = asyncio.to_thread
20
- else:
21
- # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
22
- # for Python 3.8 support
23
- async def _asyncio_to_thread(
24
- func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
25
- ) -> Any:
26
- """Asynchronously run function *func* in a separate thread.
27
-
28
- Any *args and **kwargs supplied for this function are directly passed
29
- to *func*. Also, the current :class:`contextvars.Context` is propagated,
30
- allowing context variables from the main thread to be accessed in the
31
- separate thread.
32
-
33
- Returns a coroutine that can be awaited to get the eventual result of *func*.
34
- """
35
- loop = asyncio.events.get_running_loop()
36
- ctx = contextvars.copy_context()
37
- func_call = functools.partial(ctx.run, func, *args, **kwargs)
38
- return await loop.run_in_executor(None, func_call)
39
-
40
-
41
16
  async def to_thread(
42
17
  func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
43
18
  ) -> T_Retval:
44
19
  if sniffio.current_async_library() == "asyncio":
45
- return await _asyncio_to_thread(func, *args, **kwargs)
20
+ return await asyncio.to_thread(func, *args, **kwargs)
46
21
 
47
22
  return await anyio.to_thread.run_sync(
48
23
  functools.partial(func, *args, **kwargs),
@@ -53,10 +28,7 @@ async def to_thread(
53
28
  def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
54
29
  """
55
30
  Take a blocking function and create an async one that receives the same
56
- positional and keyword arguments. For python version 3.9 and above, it uses
57
- asyncio.to_thread to run the function in a separate thread. For python version
58
- 3.8, it uses locally defined copy of the asyncio.to_thread function which was
59
- introduced in python 3.9.
31
+ positional and keyword arguments.
60
32
 
61
33
  Usage:
62
34
 
payi/_utils/_transform.py CHANGED
@@ -16,18 +16,20 @@ from ._utils import (
16
16
  lru_cache,
17
17
  is_mapping,
18
18
  is_iterable,
19
+ is_sequence,
19
20
  )
20
21
  from .._files import is_base64_file_input
22
+ from ._compat import get_origin, is_typeddict
21
23
  from ._typing import (
22
24
  is_list_type,
23
25
  is_union_type,
24
26
  extract_type_arg,
25
27
  is_iterable_type,
26
28
  is_required_type,
29
+ is_sequence_type,
27
30
  is_annotated_type,
28
31
  strip_annotated_type,
29
32
  )
30
- from .._compat import get_origin, model_dump, is_typeddict
31
33
 
32
34
  _T = TypeVar("_T")
33
35
 
@@ -167,6 +169,8 @@ def _transform_recursive(
167
169
 
168
170
  Defaults to the same value as the `annotation` argument.
169
171
  """
172
+ from .._compat import model_dump
173
+
170
174
  if inner_type is None:
171
175
  inner_type = annotation
172
176
 
@@ -184,6 +188,8 @@ def _transform_recursive(
184
188
  (is_list_type(stripped_type) and is_list(data))
185
189
  # Iterable[T]
186
190
  or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
191
+ # Sequence[T]
192
+ or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
187
193
  ):
188
194
  # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
189
195
  # intended as an iterable, so we don't transform it.
@@ -262,7 +268,7 @@ def _transform_typeddict(
262
268
  annotations = get_type_hints(expected_type, include_extras=True)
263
269
  for key, value in data.items():
264
270
  if not is_given(value):
265
- # we don't need to include `NotGiven` values here as they'll
271
+ # we don't need to include omitted values here as they'll
266
272
  # be stripped out before the request is sent anyway
267
273
  continue
268
274
 
@@ -329,6 +335,8 @@ async def _async_transform_recursive(
329
335
 
330
336
  Defaults to the same value as the `annotation` argument.
331
337
  """
338
+ from .._compat import model_dump
339
+
332
340
  if inner_type is None:
333
341
  inner_type = annotation
334
342
 
@@ -346,6 +354,8 @@ async def _async_transform_recursive(
346
354
  (is_list_type(stripped_type) and is_list(data))
347
355
  # Iterable[T]
348
356
  or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
357
+ # Sequence[T]
358
+ or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
349
359
  ):
350
360
  # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
351
361
  # intended as an iterable, so we don't transform it.
@@ -424,7 +434,7 @@ async def _async_transform_typeddict(
424
434
  annotations = get_type_hints(expected_type, include_extras=True)
425
435
  for key, value in data.items():
426
436
  if not is_given(value):
427
- # we don't need to include `NotGiven` values here as they'll
437
+ # we don't need to include omitted values here as they'll
428
438
  # be stripped out before the request is sent anyway
429
439
  continue
430
440
 
payi/_utils/_typing.py CHANGED
@@ -15,7 +15,7 @@ from typing_extensions import (
15
15
 
16
16
  from ._utils import lru_cache
17
17
  from .._types import InheritsGeneric
18
- from .._compat import is_union as _is_union
18
+ from ._compat import is_union as _is_union
19
19
 
20
20
 
21
21
  def is_annotated_type(typ: type) -> bool:
@@ -26,6 +26,11 @@ def is_list_type(typ: type) -> bool:
26
26
  return (get_origin(typ) or typ) == list
27
27
 
28
28
 
29
+ def is_sequence_type(typ: type) -> bool:
30
+ origin = get_origin(typ) or typ
31
+ return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence
32
+
33
+
29
34
  def is_iterable_type(typ: type) -> bool:
30
35
  """If the given type is `typing.Iterable[T]`"""
31
36
  origin = get_origin(typ) or typ
payi/_utils/_utils.py CHANGED
@@ -21,8 +21,7 @@ from typing_extensions import TypeGuard
21
21
 
22
22
  import sniffio
23
23
 
24
- from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike
25
- from .._compat import parse_date as parse_date, parse_datetime as parse_datetime
24
+ from .._types import Omit, NotGiven, FileTypes, HeadersLike
26
25
 
27
26
  _T = TypeVar("_T")
28
27
  _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -64,7 +63,7 @@ def _extract_items(
64
63
  try:
65
64
  key = path[index]
66
65
  except IndexError:
67
- if isinstance(obj, NotGiven):
66
+ if not is_given(obj):
68
67
  # no value was provided - we can safely ignore
69
68
  return []
70
69
 
@@ -127,14 +126,14 @@ def _extract_items(
127
126
  return []
128
127
 
129
128
 
130
- def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]:
131
- return not isinstance(obj, NotGiven)
129
+ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]:
130
+ return not isinstance(obj, NotGiven) and not isinstance(obj, Omit)
132
131
 
133
132
 
134
133
  # Type safe methods for narrowing types with TypeVars.
135
134
  # The default narrowing for isinstance(obj, dict) is dict[unknown, unknown],
136
135
  # however this cause Pyright to rightfully report errors. As we know we don't
137
- # care about the contained types we can safely use `object` in it's place.
136
+ # care about the contained types we can safely use `object` in its place.
138
137
  #
139
138
  # There are two separate functions defined, `is_*` and `is_*_t` for different use cases.
140
139
  # `is_*` is for when you're dealing with an unknown input
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.110" # x-release-please-version
4
+ __version__ = "0.1.0-alpha.137" # x-release-please-version
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  from typing import Any, Union, Optional, Sequence
3
5
  from typing_extensions import override
@@ -8,8 +10,9 @@ from wrapt import wrap_function_wrapper # type: ignore
8
10
  from payi.lib.helpers import PayiCategories
9
11
  from payi.types.ingest_units_params import Units
10
12
 
11
- from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
13
+ from .instrument import _IsStreaming, _PayiInstrumentor
12
14
  from .version_helper import get_version_helper
15
+ from .ProviderRequest import _ChunkResult, _StreamingType, _ProviderRequest
13
16
 
14
17
 
15
18
  class AnthropicInstrumentor:
@@ -30,37 +33,26 @@ class AnthropicInstrumentor:
30
33
 
31
34
  @staticmethod
32
35
  def instrument(instrumentor: _PayiInstrumentor) -> None:
33
- try:
34
- AnthropicInstrumentor._module_version = get_version_helper(AnthropicInstrumentor._module_name)
35
-
36
- wrap_function_wrapper(
37
- "anthropic.resources.messages",
38
- "Messages.create",
39
- messages_wrapper(instrumentor),
40
- )
41
-
42
- wrap_function_wrapper(
43
- "anthropic.resources.messages",
44
- "Messages.stream",
45
- stream_messages_wrapper(instrumentor),
46
- )
47
-
48
- wrap_function_wrapper(
49
- "anthropic.resources.messages",
50
- "AsyncMessages.create",
51
- amessages_wrapper(instrumentor),
52
- )
53
-
54
- wrap_function_wrapper(
55
- "anthropic.resources.messages",
56
- "AsyncMessages.stream",
57
- astream_messages_wrapper(instrumentor),
58
- )
59
-
60
- except Exception as e:
61
- instrumentor._logger.debug(f"Error instrumenting anthropic: {e}")
62
- return
63
-
36
+ AnthropicInstrumentor._module_version = get_version_helper(AnthropicInstrumentor._module_name)
37
+
38
+ wrappers = [
39
+ ("anthropic._base_client", "AsyncAPIClient._process_response", _ProviderRequest.aprocess_response_wrapper),
40
+ ("anthropic._base_client", "SyncAPIClient._process_response", _ProviderRequest.process_response_wrapper),
41
+ ("anthropic.resources.messages", "Messages.create", messages_wrapper(instrumentor)),
42
+ ("anthropic.resources.messages", "Messages.stream", stream_messages_wrapper(instrumentor)),
43
+ ("anthropic.resources.beta.messages", "Messages.create", messages_wrapper(instrumentor)),
44
+ ("anthropic.resources.beta.messages", "Messages.stream", stream_messages_wrapper(instrumentor)),
45
+ ("anthropic.resources.messages", "AsyncMessages.create", amessages_wrapper(instrumentor)),
46
+ ("anthropic.resources.messages", "AsyncMessages.stream", astream_messages_wrapper(instrumentor)),
47
+ ("anthropic.resources.beta.messages", "AsyncMessages.create", amessages_wrapper(instrumentor)),
48
+ ("anthropic.resources.beta.messages", "AsyncMessages.stream", astream_messages_wrapper(instrumentor)),
49
+ ]
50
+
51
+ for module, method, wrapper in wrappers:
52
+ try:
53
+ wrap_function_wrapper(module, method, wrapper)
54
+ except Exception as e:
55
+ instrumentor._logger.debug(f"Error wrapping {module}.{method}: {e}")
64
56
 
65
57
  @_PayiInstrumentor.payi_wrapper
66
58
  def messages_wrapper(
@@ -171,15 +163,26 @@ class _AnthropicProviderRequest(_ProviderRequest):
171
163
 
172
164
  return None
173
165
 
166
+ def _update_resource_name(self, model: str) -> str:
167
+ return ("anthropic." if self._is_vertex else "") + model
168
+
174
169
  @override
175
- def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
176
- self._ingest["resource"] = ("anthropic." if self._is_vertex else "") + kwargs.get("model", "")
170
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
171
+ self._ingest["resource"] = self._update_resource_name(kwargs.get("model", ""))
172
+
173
+ if self._price_as.resource_scope:
174
+ self._ingest["resource_scope"] = self._price_as.resource_scope
175
+
176
+ # override defaults
177
+ if self._price_as.category:
178
+ self._ingest["category"] = self._price_as.category
179
+ if self._price_as.resource:
180
+ self._ingest["resource"] = self._update_resource_name(self._price_as.resource)
177
181
 
178
182
  self._instrumentor._logger.debug(f"Processing anthropic request: model {self._ingest['resource']}, category {self._category}")
179
183
 
180
184
  messages = kwargs.get("messages")
181
185
  if messages:
182
-
183
186
  anthropic_has_image_and_get_texts(self, messages)
184
187
 
185
188
  return True
@@ -220,23 +223,52 @@ class _AnthropicProviderRequest(_ProviderRequest):
220
223
 
221
224
  return True
222
225
 
223
- def anthropic_process_synchronous_response(request: _ProviderRequest, response: 'dict[str, Any]', log_prompt_and_response: bool, assign_id: bool) -> Any:
224
- usage = response['usage']
225
- input = usage['input_tokens']
226
- output = usage['output_tokens']
226
+ def anthropic_process_compute_input_cost(request: _ProviderRequest, usage: 'dict[str, Any]') -> int:
227
+ input = usage.get('input_tokens', 0)
227
228
  units: dict[str, Units] = request._ingest["units"]
228
229
 
229
230
  cache_creation_input_tokens = usage.get("cache_creation_input_tokens", 0)
230
- if cache_creation_input_tokens > 0:
231
- units["text_cache_write"] = Units(input=cache_creation_input_tokens, output=0)
231
+ cache_read_input_tokens = usage.get("cache_read_input_tokens", 0)
232
+
233
+ total_input_tokens = input + cache_creation_input_tokens + cache_read_input_tokens
234
+
235
+ request._is_large_context = total_input_tokens >= 200000
236
+ large_context = "_large_context" if request._is_large_context else ""
237
+
238
+ cache_creation: dict[str, int] = usage.get("cache_creation", {})
239
+ ephemeral_5m_input_tokens: Optional[int] = None
240
+ ephemeral_1h_input_tokens: Optional[int] = None
241
+ textCacheWriteAdded = False
242
+
243
+ if cache_creation:
244
+ ephemeral_5m_input_tokens = cache_creation.get("ephemeral_5m_input_tokens", 0)
245
+ if ephemeral_5m_input_tokens > 0:
246
+ textCacheWriteAdded = True
247
+ units["text_cache_write"+large_context] = Units(input=ephemeral_5m_input_tokens, output=0)
248
+
249
+ ephemeral_1h_input_tokens = cache_creation.get("ephemeral_1h_input_tokens", 0)
250
+ if ephemeral_1h_input_tokens > 0:
251
+ textCacheWriteAdded = True
252
+ units["text_cache_write_1h"+large_context] = Units(input=ephemeral_1h_input_tokens, output=0)
253
+
254
+ if textCacheWriteAdded is False and cache_creation_input_tokens > 0:
255
+ units["text_cache_write"+large_context] = Units(input=cache_creation_input_tokens, output=0)
232
256
 
233
257
  cache_read_input_tokens = usage.get("cache_read_input_tokens", 0)
234
258
  if cache_read_input_tokens > 0:
235
- units["text_cache_read"] = Units(input=cache_read_input_tokens, output=0)
259
+ units["text_cache_read"+large_context] = Units(input=cache_read_input_tokens, output=0)
260
+
261
+ return request.update_for_vision(input)
236
262
 
237
- input = _PayiInstrumentor.update_for_vision(input, units, request._estimated_prompt_tokens)
263
+ def anthropic_process_synchronous_response(request: _ProviderRequest, response: 'dict[str, Any]', log_prompt_and_response: bool, assign_id: bool) -> Any:
264
+ usage = response.get('usage', {})
265
+ units: dict[str, Units] = request._ingest["units"]
238
266
 
239
- units["text"] = Units(input=input, output=output)
267
+ input_tokens = anthropic_process_compute_input_cost(request, usage)
268
+ output = usage.get('output_tokens', 0)
269
+
270
+ large_context = "_large_context" if request._is_large_context else ""
271
+ units["text"+large_context] = Units(input=input_tokens, output=output)
240
272
 
241
273
  content = response.get('content', [])
242
274
  if content:
@@ -274,34 +306,28 @@ def anthropic_process_chunk(request: _ProviderRequest, chunk: 'dict[str, Any]',
274
306
  if model and 'resource' in request._ingest:
275
307
  request._instrumentor._logger.debug(f"Anthropic streaming, reported model: {model}, instrumented model {request._ingest['resource']}")
276
308
 
277
- usage = message['usage']
309
+ usage = message.get('usage', {})
278
310
  units = request._ingest["units"]
279
311
 
280
- input = _PayiInstrumentor.update_for_vision(usage['input_tokens'], units, request._estimated_prompt_tokens)
281
-
282
- units["text"] = Units(input=input, output=0)
283
-
284
- text_cache_write: int = usage.get("cache_creation_input_tokens", 0)
285
- if text_cache_write > 0:
286
- units["text_cache_write"] = Units(input=text_cache_write, output=0)
312
+ input = anthropic_process_compute_input_cost(request, usage)
287
313
 
288
- text_cache_read: int = usage.get("cache_read_input_tokens", 0)
289
- if text_cache_read > 0:
290
- units["text_cache_read"] = Units(input=text_cache_read, output=0)
314
+ large_context = "_large_context" if request._is_large_context else ""
315
+ units["text"+large_context] = Units(input=input, output=0)
291
316
 
292
317
  request._instrumentor._logger.debug(f"Anthropic streaming captured {input} input tokens, ")
293
318
 
294
319
  elif type == "message_delta":
295
320
  usage = chunk.get('usage', {})
296
321
  ingest = True
322
+ large_context = "_large_context" if request._is_large_context else ""
297
323
 
298
324
  # Web search will return an updated input tokens value at the end of streaming
299
325
  input_tokens = usage.get('input_tokens', None)
300
326
  if input_tokens is not None:
301
327
  request._instrumentor._logger.debug(f"Anthropic streaming finished, updated input tokens: {input_tokens}")
302
- request._ingest["units"]["text"]["input"] = input_tokens
328
+ request._ingest["units"]["text"+large_context]["input"] = input_tokens
303
329
 
304
- request._ingest["units"]["text"]["output"] = usage.get('output_tokens', 0)
330
+ request._ingest["units"]["text"+large_context]["output"] = usage.get('output_tokens', 0)
305
331
 
306
332
  request._instrumentor._logger.debug(f"Anthropic streaming finished: output tokens {usage.get('output_tokens', 0)} ")
307
333