telemetry-dev-bedrock 0.1.0__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.
- telemetry_dev_bedrock/__init__.py +7 -0
- telemetry_dev_bedrock/_fields.py +102 -0
- telemetry_dev_bedrock/_instrument.py +231 -0
- telemetry_dev_bedrock/_invoke_model.py +241 -0
- telemetry_dev_bedrock/_messages.py +162 -0
- telemetry_dev_bedrock/_registry.py +409 -0
- telemetry_dev_bedrock/_streams.py +575 -0
- telemetry_dev_bedrock/py.typed +0 -0
- telemetry_dev_bedrock-0.1.0.dist-info/METADATA +165 -0
- telemetry_dev_bedrock-0.1.0.dist-info/RECORD +11 -0
- telemetry_dev_bedrock-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
PROVIDER = "amazon-bedrock"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def clean(fields: dict[str, Any]) -> dict[str, Any]:
|
|
10
|
+
return {key: value for key, value in fields.items() if value is not None}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def metadata_fields(response: dict[str, Any] | BaseException | None) -> dict[str, Any]:
|
|
14
|
+
metadata: dict[str, Any] | None = None
|
|
15
|
+
if isinstance(response, dict) and isinstance(response.get("ResponseMetadata"), dict):
|
|
16
|
+
metadata = response["ResponseMetadata"]
|
|
17
|
+
else:
|
|
18
|
+
error_response = getattr(response, "response", None)
|
|
19
|
+
if isinstance(error_response, dict) and isinstance(
|
|
20
|
+
error_response.get("ResponseMetadata"), dict
|
|
21
|
+
):
|
|
22
|
+
metadata = error_response["ResponseMetadata"]
|
|
23
|
+
if metadata is None:
|
|
24
|
+
return {}
|
|
25
|
+
retry_attempts = metadata.get("RetryAttempts")
|
|
26
|
+
attempts = retry_attempts + 1 if isinstance(retry_attempts, int) else None
|
|
27
|
+
total_retry_delay = metadata.get("TotalRetryDelay")
|
|
28
|
+
if total_retry_delay is None:
|
|
29
|
+
total_retry_delay = metadata.get("totalRetryDelay")
|
|
30
|
+
attributes = clean(
|
|
31
|
+
{
|
|
32
|
+
"aws.http.status_code": metadata.get("HTTPStatusCode"),
|
|
33
|
+
"aws.request.attempts": attempts if attempts and attempts > 1 else None,
|
|
34
|
+
"aws.request.total_retry_delay_ms": total_retry_delay,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
return clean(
|
|
38
|
+
{
|
|
39
|
+
"response_id": metadata.get("RequestId"),
|
|
40
|
+
"attributes": attributes or None,
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def error_fields(error: BaseException) -> dict[str, Any]:
|
|
46
|
+
fields = metadata_fields(error)
|
|
47
|
+
response = getattr(error, "response", None)
|
|
48
|
+
code = None
|
|
49
|
+
if isinstance(response, dict) and isinstance(response.get("Error"), dict):
|
|
50
|
+
code = response["Error"].get("Code")
|
|
51
|
+
fields["error"] = error
|
|
52
|
+
if code:
|
|
53
|
+
attrs = dict(fields.get("attributes") or {})
|
|
54
|
+
attrs["aws.error.code"] = code
|
|
55
|
+
fields["attributes"] = attrs
|
|
56
|
+
return fields
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def usage_from_converse(value: Any) -> dict[str, int] | None:
|
|
60
|
+
if not isinstance(value, dict):
|
|
61
|
+
return None
|
|
62
|
+
usage = clean(
|
|
63
|
+
{
|
|
64
|
+
"input_tokens": value.get("inputTokens"),
|
|
65
|
+
"output_tokens": value.get("outputTokens"),
|
|
66
|
+
"total_tokens": value.get("totalTokens"),
|
|
67
|
+
"cache_read_input_tokens": value.get("cacheReadInputTokens"),
|
|
68
|
+
"cache_creation_input_tokens": value.get("cacheWriteInputTokens"),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
return usage or None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def metadata_attr_value(value: Any) -> Any:
|
|
75
|
+
if value is None or isinstance(value, str):
|
|
76
|
+
return value
|
|
77
|
+
return json.dumps(value, default=repr, ensure_ascii=False)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def merge_fields(*parts: dict[str, Any]) -> dict[str, Any]:
|
|
81
|
+
merged: dict[str, Any] = {}
|
|
82
|
+
for part in parts:
|
|
83
|
+
metadata = part.get("metadata")
|
|
84
|
+
attributes = part.get("attributes")
|
|
85
|
+
usage = part.get("usage")
|
|
86
|
+
for key, value in part.items():
|
|
87
|
+
if key not in {"metadata", "attributes", "usage"} and value is not None:
|
|
88
|
+
merged[key] = value
|
|
89
|
+
if isinstance(metadata, dict):
|
|
90
|
+
merged["metadata"] = {
|
|
91
|
+
**merged.get("metadata", {}),
|
|
92
|
+
**{
|
|
93
|
+
key: normalized
|
|
94
|
+
for key, value in metadata.items()
|
|
95
|
+
if (normalized := metadata_attr_value(value)) is not None
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
if isinstance(attributes, dict):
|
|
99
|
+
merged["attributes"] = {**merged.get("attributes", {}), **attributes}
|
|
100
|
+
if isinstance(usage, dict):
|
|
101
|
+
merged["usage"] = {**merged.get("usage", {}), **usage}
|
|
102
|
+
return merged
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
import telemetry_dev
|
|
11
|
+
from botocore.client import BaseClient
|
|
12
|
+
from botocore.exceptions import ClientError
|
|
13
|
+
from botocore.response import StreamingBody
|
|
14
|
+
|
|
15
|
+
from ._fields import error_fields, merge_fields, metadata_fields
|
|
16
|
+
from ._invoke_model import parse_body
|
|
17
|
+
from ._registry import OperationSpec, lookup_operation
|
|
18
|
+
from ._streams import InstrumentedEventStream
|
|
19
|
+
|
|
20
|
+
_T = TypeVar("_T", bound=BaseClient)
|
|
21
|
+
_SUPPORTED_SERVICES = frozenset({"bedrock-runtime", "bedrock-agent-runtime"})
|
|
22
|
+
_WRAPPED_ATTR = "_telemetry_dev_bedrock_wrapped"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class _Options:
|
|
27
|
+
capture_agent_trace: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_original_api_call: Callable[..., Any] | None = None
|
|
31
|
+
_patched_api_call: Callable[..., Any] | None = None
|
|
32
|
+
_LOCK = threading.Lock()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def wrap_bedrock(client: _T, *, capture_agent_trace: bool = False) -> _T:
|
|
36
|
+
with _LOCK:
|
|
37
|
+
if getattr(client, _WRAPPED_ATTR, False):
|
|
38
|
+
return client
|
|
39
|
+
options = _Options(capture_agent_trace=capture_agent_trace)
|
|
40
|
+
mapping = getattr(client.meta, "method_to_api_mapping", {})
|
|
41
|
+
if not isinstance(mapping, dict):
|
|
42
|
+
return client
|
|
43
|
+
for method_name, operation_name in mapping.items():
|
|
44
|
+
if not isinstance(method_name, str) or not isinstance(operation_name, str):
|
|
45
|
+
continue
|
|
46
|
+
service_name = client.meta.service_model.service_name
|
|
47
|
+
if lookup_operation(service_name, operation_name) is None or not hasattr(
|
|
48
|
+
client, method_name
|
|
49
|
+
):
|
|
50
|
+
continue
|
|
51
|
+
original = getattr(client, method_name)
|
|
52
|
+
setattr(
|
|
53
|
+
client, method_name, _make_method_wrapper(client, operation_name, original, options)
|
|
54
|
+
)
|
|
55
|
+
setattr(client, _WRAPPED_ATTR, True)
|
|
56
|
+
return client
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def instrument_bedrock(*, capture_agent_trace: bool = False) -> None:
|
|
60
|
+
options = _Options(capture_agent_trace=capture_agent_trace)
|
|
61
|
+
with _LOCK:
|
|
62
|
+
global _original_api_call, _patched_api_call
|
|
63
|
+
if _original_api_call is not None:
|
|
64
|
+
return
|
|
65
|
+
original = BaseClient._make_api_call
|
|
66
|
+
_original_api_call = original
|
|
67
|
+
|
|
68
|
+
def patched(self: BaseClient, operation_name: str, api_params: dict[str, Any]) -> Any:
|
|
69
|
+
if getattr(self, _WRAPPED_ATTR, False):
|
|
70
|
+
return original(self, operation_name, api_params)
|
|
71
|
+
return _call_with_span(
|
|
72
|
+
lambda op_name, params: original(self, op_name, params),
|
|
73
|
+
self,
|
|
74
|
+
operation_name,
|
|
75
|
+
api_params,
|
|
76
|
+
options,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_patched_api_call = patched
|
|
80
|
+
BaseClient._make_api_call = patched # type: ignore[method-assign]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def uninstrument_bedrock() -> None:
|
|
84
|
+
with _LOCK:
|
|
85
|
+
global _original_api_call, _patched_api_call
|
|
86
|
+
if _original_api_call is None or _patched_api_call is None:
|
|
87
|
+
return
|
|
88
|
+
if BaseClient._make_api_call is not _patched_api_call:
|
|
89
|
+
return
|
|
90
|
+
BaseClient._make_api_call = _original_api_call # type: ignore[method-assign]
|
|
91
|
+
_original_api_call = None
|
|
92
|
+
_patched_api_call = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _make_method_wrapper(
|
|
96
|
+
client: BaseClient,
|
|
97
|
+
operation_name: str,
|
|
98
|
+
original: Callable[..., Any],
|
|
99
|
+
options: _Options,
|
|
100
|
+
) -> Callable[..., Any]:
|
|
101
|
+
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
102
|
+
if args:
|
|
103
|
+
return original(*args, **kwargs)
|
|
104
|
+
return _call_with_span(
|
|
105
|
+
lambda _op, params: original(**params), client, operation_name, kwargs, options
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return wrapped
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _call_with_span(
|
|
112
|
+
original: Callable[[str, dict[str, Any]], Any],
|
|
113
|
+
client: BaseClient,
|
|
114
|
+
operation_name: str,
|
|
115
|
+
api_params: dict[str, Any],
|
|
116
|
+
options: _Options,
|
|
117
|
+
) -> Any:
|
|
118
|
+
service_name = client.meta.service_model.service_name
|
|
119
|
+
if service_name not in _SUPPORTED_SERVICES:
|
|
120
|
+
return original(operation_name, api_params)
|
|
121
|
+
spec = lookup_operation(service_name, operation_name)
|
|
122
|
+
if spec is None:
|
|
123
|
+
return original(operation_name, api_params)
|
|
124
|
+
|
|
125
|
+
started_at = time.perf_counter()
|
|
126
|
+
name, start_fields = _safe_start_fields(spec, api_params)
|
|
127
|
+
handle = telemetry_dev.start_span(name, **start_fields)
|
|
128
|
+
end = _end_once(handle)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
response = original(operation_name, api_params)
|
|
132
|
+
except ClientError as exc:
|
|
133
|
+
end(**error_fields(exc))
|
|
134
|
+
raise
|
|
135
|
+
except BaseException as exc:
|
|
136
|
+
end(error=exc)
|
|
137
|
+
raise
|
|
138
|
+
|
|
139
|
+
if not isinstance(response, dict):
|
|
140
|
+
end()
|
|
141
|
+
return response
|
|
142
|
+
|
|
143
|
+
if operation_name == "InvokeModel" and isinstance(response.get("body"), StreamingBody):
|
|
144
|
+
return _handle_streaming_body(spec, api_params, response, end)
|
|
145
|
+
|
|
146
|
+
stream_key = spec.stream_key
|
|
147
|
+
if stream_key is not None and response.get(stream_key) is not None:
|
|
148
|
+
state_factory = spec.stream_state_factory
|
|
149
|
+
if state_factory is None:
|
|
150
|
+
end(**metadata_fields(response))
|
|
151
|
+
return response
|
|
152
|
+
state = state_factory(options.capture_agent_trace)
|
|
153
|
+
try:
|
|
154
|
+
base_fields = merge_fields(
|
|
155
|
+
metadata_fields(response), spec.response_fields(api_params, response)
|
|
156
|
+
)
|
|
157
|
+
except Exception:
|
|
158
|
+
base_fields = metadata_fields(response)
|
|
159
|
+
|
|
160
|
+
def finish(fields: dict[str, Any]) -> None:
|
|
161
|
+
if fields.keys() == {"time_to_first_chunk_ms"}:
|
|
162
|
+
try:
|
|
163
|
+
handle.update(**fields)
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
return
|
|
167
|
+
end(**merge_fields(base_fields, fields))
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
**response,
|
|
171
|
+
stream_key: InstrumentedEventStream(response[stream_key], state, finish, started_at),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
end(**merge_fields(metadata_fields(response), spec.response_fields(api_params, response)))
|
|
175
|
+
return response
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _handle_streaming_body(
|
|
179
|
+
spec: OperationSpec,
|
|
180
|
+
params: dict[str, Any],
|
|
181
|
+
response: dict[str, Any],
|
|
182
|
+
end: Callable[..., None],
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
body = cast(StreamingBody, response["body"])
|
|
185
|
+
try:
|
|
186
|
+
raw = body.read()
|
|
187
|
+
except Exception:
|
|
188
|
+
end(**metadata_fields(response))
|
|
189
|
+
return response
|
|
190
|
+
parsed = parse_body(raw, response.get("contentType"))
|
|
191
|
+
response_for_fields = {**response, "_telemetry_dev_parsed_body": parsed}
|
|
192
|
+
try:
|
|
193
|
+
fields = merge_fields(
|
|
194
|
+
metadata_fields(response), spec.response_fields(params, response_for_fields)
|
|
195
|
+
)
|
|
196
|
+
except Exception:
|
|
197
|
+
fields = metadata_fields(response)
|
|
198
|
+
end(**fields)
|
|
199
|
+
return {**response, "body": StreamingBody(io.BytesIO(raw), len(raw))}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _safe_start_fields(spec: OperationSpec, params: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
203
|
+
try:
|
|
204
|
+
name = spec.span_name(params)
|
|
205
|
+
except Exception:
|
|
206
|
+
name = "bedrock"
|
|
207
|
+
try:
|
|
208
|
+
fields = spec.request_fields(params)
|
|
209
|
+
except Exception:
|
|
210
|
+
fields = {}
|
|
211
|
+
try:
|
|
212
|
+
fields = {"type": spec.span_type(params), **fields}
|
|
213
|
+
except Exception:
|
|
214
|
+
fields = {"type": "span", **fields}
|
|
215
|
+
return name, fields
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _end_once(handle: telemetry_dev.SpanHandle) -> Callable[..., None]:
|
|
219
|
+
ended = False
|
|
220
|
+
|
|
221
|
+
def end(**fields: Any) -> None:
|
|
222
|
+
nonlocal ended
|
|
223
|
+
if ended:
|
|
224
|
+
return
|
|
225
|
+
ended = True
|
|
226
|
+
try:
|
|
227
|
+
handle.end(**fields)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
return end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._fields import clean
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_body(body: Any, content_type: Any = "application/json") -> Any:
|
|
10
|
+
if isinstance(content_type, str) and "json" not in content_type:
|
|
11
|
+
return None
|
|
12
|
+
if isinstance(body, bytes | bytearray):
|
|
13
|
+
raw = bytes(body).decode("utf-8")
|
|
14
|
+
elif isinstance(body, str):
|
|
15
|
+
raw = body
|
|
16
|
+
elif all(hasattr(body, name) for name in ("tell", "read", "seek")):
|
|
17
|
+
seekable = getattr(body, "seekable", None)
|
|
18
|
+
if callable(seekable):
|
|
19
|
+
try:
|
|
20
|
+
if not seekable():
|
|
21
|
+
return None
|
|
22
|
+
except Exception:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
position = body.tell()
|
|
26
|
+
body.seek(position)
|
|
27
|
+
value = body.read()
|
|
28
|
+
body.seek(position)
|
|
29
|
+
except Exception:
|
|
30
|
+
return None
|
|
31
|
+
if isinstance(value, bytes | bytearray):
|
|
32
|
+
raw = bytes(value).decode("utf-8")
|
|
33
|
+
elif isinstance(value, str):
|
|
34
|
+
raw = value
|
|
35
|
+
else:
|
|
36
|
+
return None
|
|
37
|
+
else:
|
|
38
|
+
return None
|
|
39
|
+
try:
|
|
40
|
+
return json.loads(raw)
|
|
41
|
+
except Exception:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_embedding_model(model_id: Any) -> bool:
|
|
46
|
+
model = model_id.lower() if isinstance(model_id, str) else ""
|
|
47
|
+
return "titan-embed" in model or "cohere.embed" in model or "-embedding" in model
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def request_fields(params: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
model = params.get("modelId") if isinstance(params.get("modelId"), str) else None
|
|
52
|
+
body = parse_body(params.get("body"), params.get("contentType"))
|
|
53
|
+
fields = {"provider": "amazon-bedrock", "model": model, "input": body}
|
|
54
|
+
if is_embedding_model(model):
|
|
55
|
+
fields["output_type"] = "embedding"
|
|
56
|
+
fields.update(sampling_fields(model, body))
|
|
57
|
+
return clean(fields)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sampling_fields(model: str | None, body: Any) -> dict[str, Any]:
|
|
61
|
+
if not isinstance(body, dict):
|
|
62
|
+
return {}
|
|
63
|
+
lower = model.lower() if isinstance(model, str) else ""
|
|
64
|
+
if "amazon.titan" in lower:
|
|
65
|
+
cfg = (
|
|
66
|
+
body.get("textGenerationConfig")
|
|
67
|
+
if isinstance(body.get("textGenerationConfig"), dict)
|
|
68
|
+
else {}
|
|
69
|
+
)
|
|
70
|
+
return clean(
|
|
71
|
+
{
|
|
72
|
+
"temperature": cfg.get("temperature"),
|
|
73
|
+
"top_p": cfg.get("topP"),
|
|
74
|
+
"max_tokens": cfg.get("maxTokenCount"),
|
|
75
|
+
"stop_sequences": cfg.get("stopSequences"),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
if "amazon.nova" in lower:
|
|
79
|
+
cfg = body.get("inferenceConfig") if isinstance(body.get("inferenceConfig"), dict) else {}
|
|
80
|
+
return clean(
|
|
81
|
+
{
|
|
82
|
+
"temperature": cfg.get("temperature"),
|
|
83
|
+
"top_p": cfg.get("topP") or cfg.get("top_p"),
|
|
84
|
+
"top_k": cfg.get("topK"),
|
|
85
|
+
"max_tokens": cfg.get("maxTokens") or cfg.get("max_new_tokens"),
|
|
86
|
+
"stop_sequences": cfg.get("stopSequences"),
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
if "anthropic.claude" in lower:
|
|
90
|
+
return clean(
|
|
91
|
+
{
|
|
92
|
+
"temperature": body.get("temperature"),
|
|
93
|
+
"top_p": body.get("top_p"),
|
|
94
|
+
"top_k": body.get("top_k"),
|
|
95
|
+
"max_tokens": body.get("max_tokens"),
|
|
96
|
+
"stop_sequences": body.get("stop_sequences"),
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
if "meta.llama" in lower:
|
|
100
|
+
return clean(
|
|
101
|
+
{
|
|
102
|
+
"temperature": body.get("temperature"),
|
|
103
|
+
"top_p": body.get("top_p"),
|
|
104
|
+
"max_tokens": body.get("max_gen_len"),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return clean(
|
|
108
|
+
{
|
|
109
|
+
"temperature": body.get("temperature"),
|
|
110
|
+
"top_p": body.get("top_p") or body.get("topP"),
|
|
111
|
+
"max_tokens": body.get("max_tokens") or body.get("maxTokens"),
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def response_fields(
|
|
117
|
+
model: str | None, body: Any, headers: dict[str, Any] | None = None
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
if not isinstance(body, dict):
|
|
120
|
+
return header_usage_fields(headers)
|
|
121
|
+
lower = model.lower() if isinstance(model, str) else ""
|
|
122
|
+
if is_embedding_model(model):
|
|
123
|
+
return _with_header_usage(
|
|
124
|
+
{
|
|
125
|
+
"output": body,
|
|
126
|
+
"output_type": "embedding",
|
|
127
|
+
"usage": clean({"input_tokens": body.get("inputTextTokenCount")}) or None,
|
|
128
|
+
},
|
|
129
|
+
headers,
|
|
130
|
+
)
|
|
131
|
+
if "amazon.titan" in lower:
|
|
132
|
+
first = body.get("results", [{}])[0] if isinstance(body.get("results"), list) else {}
|
|
133
|
+
first = first if isinstance(first, dict) else {}
|
|
134
|
+
return clean(
|
|
135
|
+
{
|
|
136
|
+
"output": body,
|
|
137
|
+
"finish_reason": first.get("completionReason"),
|
|
138
|
+
"usage": clean(
|
|
139
|
+
{
|
|
140
|
+
"input_tokens": body.get("inputTextTokenCount"),
|
|
141
|
+
"output_tokens": first.get("tokenCount"),
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
or None,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
if "amazon.nova" in lower:
|
|
148
|
+
usage = body.get("usage") if isinstance(body.get("usage"), dict) else {}
|
|
149
|
+
return clean(
|
|
150
|
+
{
|
|
151
|
+
"output": body,
|
|
152
|
+
"finish_reason": body.get("stopReason"),
|
|
153
|
+
"usage": clean(
|
|
154
|
+
{
|
|
155
|
+
"input_tokens": usage.get("inputTokens"),
|
|
156
|
+
"output_tokens": usage.get("outputTokens"),
|
|
157
|
+
"total_tokens": usage.get("totalTokens"),
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
or None,
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
if "anthropic.claude" in lower:
|
|
164
|
+
usage = body.get("usage") if isinstance(body.get("usage"), dict) else {}
|
|
165
|
+
parsed_usage = clean(
|
|
166
|
+
{
|
|
167
|
+
"input_tokens": usage.get("input_tokens"),
|
|
168
|
+
"output_tokens": usage.get("output_tokens"),
|
|
169
|
+
"cache_read_input_tokens": usage.get("cache_read_input_tokens"),
|
|
170
|
+
"cache_creation_input_tokens": usage.get("cache_creation_input_tokens"),
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
return clean(
|
|
174
|
+
{
|
|
175
|
+
"output": body,
|
|
176
|
+
"finish_reason": body.get("stop_reason"),
|
|
177
|
+
"usage": parsed_usage or header_usage_fields(headers).get("usage"),
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
if "meta.llama" in lower:
|
|
181
|
+
return clean(
|
|
182
|
+
{
|
|
183
|
+
"output": body,
|
|
184
|
+
"finish_reason": body.get("stop_reason"),
|
|
185
|
+
"usage": clean(
|
|
186
|
+
{
|
|
187
|
+
"input_tokens": body.get("prompt_token_count"),
|
|
188
|
+
"output_tokens": body.get("generation_token_count"),
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
or None,
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
if "mistral" in lower:
|
|
195
|
+
first = body.get("outputs", [{}])[0] if isinstance(body.get("outputs"), list) else {}
|
|
196
|
+
first = first if isinstance(first, dict) else {}
|
|
197
|
+
return _with_header_usage(
|
|
198
|
+
{"output": body, "finish_reason": first.get("stop_reason")}, headers
|
|
199
|
+
)
|
|
200
|
+
if "cohere.command-r" in lower:
|
|
201
|
+
return _with_header_usage(
|
|
202
|
+
{"output": body, "finish_reason": body.get("finish_reason")}, headers
|
|
203
|
+
)
|
|
204
|
+
if "cohere.command" in lower:
|
|
205
|
+
first = (
|
|
206
|
+
body.get("generations", [{}])[0] if isinstance(body.get("generations"), list) else {}
|
|
207
|
+
)
|
|
208
|
+
first = first if isinstance(first, dict) else {}
|
|
209
|
+
return _with_header_usage(
|
|
210
|
+
{"output": body, "finish_reason": first.get("finish_reason")}, headers
|
|
211
|
+
)
|
|
212
|
+
return _with_header_usage({"output": body}, headers)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _with_header_usage(fields: dict[str, Any], headers: dict[str, Any] | None) -> dict[str, Any]:
|
|
216
|
+
cleaned = clean(fields)
|
|
217
|
+
if cleaned.get("usage"):
|
|
218
|
+
return cleaned
|
|
219
|
+
return clean({**cleaned, **header_usage_fields(headers)})
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def header_usage_fields(headers: dict[str, Any] | None) -> dict[str, Any]:
|
|
223
|
+
if not headers:
|
|
224
|
+
return {}
|
|
225
|
+
input_tokens = _int_header(headers, "x-amzn-bedrock-input-token-count")
|
|
226
|
+
output_tokens = _int_header(headers, "x-amzn-bedrock-output-token-count")
|
|
227
|
+
return clean(
|
|
228
|
+
{
|
|
229
|
+
"usage": clean({"input_tokens": input_tokens, "output_tokens": output_tokens}) or None,
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _int_header(headers: dict[str, Any], key: str) -> int | None:
|
|
235
|
+
value = headers.get(key)
|
|
236
|
+
if value is None:
|
|
237
|
+
return None
|
|
238
|
+
try:
|
|
239
|
+
return int(value)
|
|
240
|
+
except (TypeError, ValueError):
|
|
241
|
+
return None
|