payi 0.1.0a107__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.
- payi/__init__.py +3 -1
- payi/_base_client.py +12 -12
- payi/_client.py +8 -8
- payi/_compat.py +48 -48
- payi/_models.py +87 -59
- payi/_qs.py +7 -7
- payi/_streaming.py +4 -6
- payi/_types.py +53 -12
- payi/_utils/__init__.py +9 -2
- payi/_utils/_compat.py +45 -0
- payi/_utils/_datetime_parse.py +136 -0
- payi/_utils/_sync.py +3 -31
- payi/_utils/_transform.py +13 -3
- payi/_utils/_typing.py +6 -1
- payi/_utils/_utils.py +5 -6
- payi/_version.py +1 -1
- payi/lib/AnthropicInstrumentor.py +83 -57
- payi/lib/BedrockInstrumentor.py +292 -57
- payi/lib/GoogleGenAiInstrumentor.py +18 -31
- payi/lib/OpenAIInstrumentor.py +56 -72
- payi/lib/ProviderRequest.py +216 -0
- payi/lib/StreamWrappers.py +379 -0
- payi/lib/VertexInstrumentor.py +18 -37
- payi/lib/VertexRequest.py +16 -2
- payi/lib/data/cohere_embed_english_v3.json +30706 -0
- payi/lib/helpers.py +62 -5
- payi/lib/instrument.py +433 -659
- payi/resources/categories/__init__.py +0 -14
- payi/resources/categories/categories.py +25 -53
- payi/resources/categories/resources.py +27 -23
- payi/resources/ingest.py +126 -132
- payi/resources/limits/__init__.py +14 -14
- payi/resources/limits/limits.py +58 -58
- payi/resources/limits/properties.py +171 -0
- payi/resources/requests/request_id/properties.py +8 -8
- payi/resources/requests/request_id/result.py +3 -3
- payi/resources/requests/response_id/properties.py +8 -8
- payi/resources/requests/response_id/result.py +3 -3
- payi/resources/use_cases/definitions/definitions.py +27 -27
- payi/resources/use_cases/definitions/kpis.py +23 -23
- payi/resources/use_cases/definitions/limit_config.py +14 -14
- payi/resources/use_cases/definitions/version.py +3 -3
- payi/resources/use_cases/kpis.py +15 -15
- payi/resources/use_cases/properties.py +6 -6
- payi/resources/use_cases/use_cases.py +7 -7
- payi/types/__init__.py +2 -0
- payi/types/bulk_ingest_response.py +3 -20
- payi/types/categories/__init__.py +0 -1
- payi/types/categories/resource_list_params.py +5 -1
- payi/types/category_list_resources_params.py +5 -1
- payi/types/category_resource_response.py +31 -1
- payi/types/ingest_event_param.py +7 -6
- payi/types/ingest_units_params.py +5 -4
- payi/types/limit_create_params.py +3 -3
- payi/types/limit_list_response.py +1 -3
- payi/types/limit_response.py +1 -3
- payi/types/limits/__init__.py +2 -9
- payi/types/limits/{tag_remove_params.py → property_update_params.py} +4 -5
- payi/types/limits/{tag_delete_response.py → property_update_response.py} +3 -3
- payi/types/requests/request_id/property_update_params.py +2 -2
- payi/types/requests/response_id/property_update_params.py +2 -2
- payi/types/shared/__init__.py +2 -0
- payi/types/shared/api_error.py +18 -0
- payi/types/shared/pay_i_common_models_budget_management_create_limit_base.py +3 -3
- payi/types/shared/properties_request.py +11 -0
- payi/types/shared/xproxy_result.py +2 -0
- payi/types/shared_params/pay_i_common_models_budget_management_create_limit_base.py +3 -3
- payi/types/use_cases/definitions/limit_config_create_params.py +3 -3
- payi/types/use_cases/property_update_params.py +2 -2
- {payi-0.1.0a107.dist-info → payi-0.1.0a137.dist-info}/METADATA +6 -6
- {payi-0.1.0a107.dist-info → payi-0.1.0a137.dist-info}/RECORD +73 -75
- payi/resources/categories/fixed_cost_resources.py +0 -196
- payi/resources/limits/tags.py +0 -507
- payi/types/categories/fixed_cost_resource_create_params.py +0 -21
- payi/types/limits/limit_tags.py +0 -16
- payi/types/limits/tag_create_params.py +0 -13
- payi/types/limits/tag_create_response.py +0 -10
- payi/types/limits/tag_list_response.py +0 -10
- payi/types/limits/tag_remove_response.py +0 -10
- payi/types/limits/tag_update_params.py +0 -13
- payi/types/limits/tag_update_response.py +0 -10
- {payi-0.1.0a107.dist-info → payi-0.1.0a137.dist-info}/WHEEL +0 -0
- {payi-0.1.0a107.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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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,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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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]',
|
|
176
|
-
self._ingest["resource"] =
|
|
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
|
|
224
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
309
|
+
usage = message.get('usage', {})
|
|
278
310
|
units = request._ingest["units"]
|
|
279
311
|
|
|
280
|
-
input =
|
|
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
|
-
|
|
289
|
-
|
|
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
|
|