telemetry-dev-litellm 0.2.2__tar.gz → 0.2.3__tar.gz
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_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/PKG-INFO +3 -3
- {telemetry_dev_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/README.md +2 -2
- {telemetry_dev_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/pyproject.toml +1 -1
- {telemetry_dev_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/pyproject.toml.orig +1 -1
- {telemetry_dev_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/src/telemetry_dev_litellm/__init__.py +375 -102
- {telemetry_dev_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/LICENSE +0 -0
- {telemetry_dev_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/src/telemetry_dev_litellm/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telemetry-dev-litellm
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: LiteLLM integration for telemetry.dev Python SDK
|
|
5
5
|
Keywords: telemetry,opentelemetry,llm,genai,litellm,tracing
|
|
6
6
|
Author: telemetry.dev
|
|
@@ -146,7 +146,7 @@ Rerank spans emit `gen_ai.operation.name="rerank"`:
|
|
|
146
146
|
|
|
147
147
|
Responses spans emit `gen_ai.operation.name="chat"` and capture request input, instructions, sampling options, metadata, response output/status/id, usage, cost, and provider. Streaming Responses events are handled separately from chat chunks; terminal failure and error events mark the span as failed, and early close records the partial output collected so far.
|
|
148
148
|
|
|
149
|
-
Streamed chat chunks and Responses reconstruction are
|
|
149
|
+
Streamed chat chunks and Responses reconstruction are kept up to 16 MiB, and all streams in flight share the `max_stream_bytes` bound of the core (64 MiB by default). These bounds need a `telemetry-dev` core with `max_stream_bytes`; with an older core, each stream keeps up to 64 KiB and 1,024 items. Non-streamed Responses input and output and rerank input and results are retained up to 16 MiB each. The configured `max_attribute_length` does not shrink these retention bounds; the core applies it to the exported attribute after the mask runs. A chat stream that is cut at its bound, closed early, or fails, or whose response fails to rebuild or map when it ends or is closed (reported to `on_error`; the span then has no output), and a Responses stream that is cut at its bound, ends without a terminal response, or ends with an `error` event set `telemetry.dev.capture.truncated`. A chat chunk or Responses event whose output fails to record also sets the attribute, and no later chunk or event is kept: a chat stream keeps the chunks before it, and a Responses stream reads only its terminal event after it. A complete output in that terminal response still replaces what was recorded, and then the attribute is not set. An empty terminal output does not replace output already recorded from the deltas, or lost with an event. A failure in a chat chunk's ID, model, usage, or finish reasons, or in the time to first chunk and IDs of a stream's first event, loses only that metadata and does not set the attribute. A Responses stream whose terminal output fails to map keeps its response fields (status, error, usage) and sets the attribute, and one whose output fails to rebuild from its events when it ends or is closed also sets the attribute and has no output. With a mask configured, the incomplete reconstructed output is omitted because the mask cannot inspect the complete value. A Responses stream whose terminal response fields (ID, model, usage, cost, status, error) fail to map also sets the attribute; its output is mapped apart, so it is kept, also with a mask configured, and a failed response is still recorded as failed. Each of these stream failures is reported to `on_error`, once per stream. When the response of a successful non-streamed completion, embedding, Responses, or rerank call fails to map, the failure is reported to `on_error` and the span ends without output and with `telemetry.dev.capture.truncated`; it is marked failed only when it is a failed Responses response.
|
|
150
150
|
|
|
151
151
|
Aggregate usage keys are `input_tokens`, `output_tokens`, `total_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, and `reasoning_output_tokens`. The core contract also includes modality fields; Responses usage emits text, image, and audio input/output token details when LiteLLM exposes them.
|
|
152
152
|
|
|
@@ -154,7 +154,7 @@ Aggregate usage keys are `input_tokens`, `output_tokens`, `total_tokens`, `cache
|
|
|
154
154
|
|
|
155
155
|
On a LiteLLM or Router call made outside the LiteLLM proxy, each key of the call's `litellm_metadata` argument, or of `metadata` when `litellm_metadata` is absent or empty, is exported as `td.metadata.<key>`, except the keys listed below. That includes keys LiteLLM's Router adds to the argument, such as `model_group`, `deployment`, `model_info` and `api_base`. Rerank spans also add `result_count` and `search_units`.
|
|
156
156
|
|
|
157
|
-
Strings, numbers, booleans and non-empty sequences of only strings, only booleans or only numbers are exported as they are; other values, including empty sequences, are JSON-encoded.
|
|
157
|
+
Strings, numbers, booleans and non-empty sequences of only strings, only booleans or only numbers are exported as they are; other values, including empty sequences, are JSON-encoded. When `max_attribute_length` is set, each string, including each string in a sequence, and each JSON value is truncated to it; by default they are exported whole, or cut at 65,536 characters with `telemetry-dev` 0.3.2 or earlier. Metadata values are not passed to the `mask` callback, and metadata is exported whatever `capture_input` and `capture_output` are set to.
|
|
158
158
|
|
|
159
159
|
These top-level keys are always dropped:
|
|
160
160
|
|
|
@@ -122,7 +122,7 @@ Rerank spans emit `gen_ai.operation.name="rerank"`:
|
|
|
122
122
|
|
|
123
123
|
Responses spans emit `gen_ai.operation.name="chat"` and capture request input, instructions, sampling options, metadata, response output/status/id, usage, cost, and provider. Streaming Responses events are handled separately from chat chunks; terminal failure and error events mark the span as failed, and early close records the partial output collected so far.
|
|
124
124
|
|
|
125
|
-
Streamed chat chunks and Responses reconstruction are
|
|
125
|
+
Streamed chat chunks and Responses reconstruction are kept up to 16 MiB, and all streams in flight share the `max_stream_bytes` bound of the core (64 MiB by default). These bounds need a `telemetry-dev` core with `max_stream_bytes`; with an older core, each stream keeps up to 64 KiB and 1,024 items. Non-streamed Responses input and output and rerank input and results are retained up to 16 MiB each. The configured `max_attribute_length` does not shrink these retention bounds; the core applies it to the exported attribute after the mask runs. A chat stream that is cut at its bound, closed early, or fails, or whose response fails to rebuild or map when it ends or is closed (reported to `on_error`; the span then has no output), and a Responses stream that is cut at its bound, ends without a terminal response, or ends with an `error` event set `telemetry.dev.capture.truncated`. A chat chunk or Responses event whose output fails to record also sets the attribute, and no later chunk or event is kept: a chat stream keeps the chunks before it, and a Responses stream reads only its terminal event after it. A complete output in that terminal response still replaces what was recorded, and then the attribute is not set. An empty terminal output does not replace output already recorded from the deltas, or lost with an event. A failure in a chat chunk's ID, model, usage, or finish reasons, or in the time to first chunk and IDs of a stream's first event, loses only that metadata and does not set the attribute. A Responses stream whose terminal output fails to map keeps its response fields (status, error, usage) and sets the attribute, and one whose output fails to rebuild from its events when it ends or is closed also sets the attribute and has no output. With a mask configured, the incomplete reconstructed output is omitted because the mask cannot inspect the complete value. A Responses stream whose terminal response fields (ID, model, usage, cost, status, error) fail to map also sets the attribute; its output is mapped apart, so it is kept, also with a mask configured, and a failed response is still recorded as failed. Each of these stream failures is reported to `on_error`, once per stream. When the response of a successful non-streamed completion, embedding, Responses, or rerank call fails to map, the failure is reported to `on_error` and the span ends without output and with `telemetry.dev.capture.truncated`; it is marked failed only when it is a failed Responses response.
|
|
126
126
|
|
|
127
127
|
Aggregate usage keys are `input_tokens`, `output_tokens`, `total_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, and `reasoning_output_tokens`. The core contract also includes modality fields; Responses usage emits text, image, and audio input/output token details when LiteLLM exposes them.
|
|
128
128
|
|
|
@@ -130,7 +130,7 @@ Aggregate usage keys are `input_tokens`, `output_tokens`, `total_tokens`, `cache
|
|
|
130
130
|
|
|
131
131
|
On a LiteLLM or Router call made outside the LiteLLM proxy, each key of the call's `litellm_metadata` argument, or of `metadata` when `litellm_metadata` is absent or empty, is exported as `td.metadata.<key>`, except the keys listed below. That includes keys LiteLLM's Router adds to the argument, such as `model_group`, `deployment`, `model_info` and `api_base`. Rerank spans also add `result_count` and `search_units`.
|
|
132
132
|
|
|
133
|
-
Strings, numbers, booleans and non-empty sequences of only strings, only booleans or only numbers are exported as they are; other values, including empty sequences, are JSON-encoded.
|
|
133
|
+
Strings, numbers, booleans and non-empty sequences of only strings, only booleans or only numbers are exported as they are; other values, including empty sequences, are JSON-encoded. When `max_attribute_length` is set, each string, including each string in a sequence, and each JSON value is truncated to it; by default they are exported whole, or cut at 65,536 characters with `telemetry-dev` 0.3.2 or earlier. Metadata values are not passed to the `mask` callback, and metadata is exported whatever `capture_input` and `capture_output` are set to.
|
|
134
134
|
|
|
135
135
|
These top-level keys are always dropped:
|
|
136
136
|
|
{telemetry_dev_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/src/telemetry_dev_litellm/__init__.py
RENAMED
|
@@ -3,19 +3,21 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import inspect
|
|
5
5
|
import math
|
|
6
|
+
import os
|
|
6
7
|
import re
|
|
7
8
|
import threading
|
|
8
9
|
import time
|
|
9
10
|
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping, Sequence
|
|
10
11
|
from functools import wraps
|
|
11
12
|
from inspect import Parameter
|
|
13
|
+
from io import StringIO
|
|
12
14
|
from typing import Any, TypeVar, cast
|
|
13
15
|
|
|
14
16
|
import litellm
|
|
15
17
|
import telemetry_dev
|
|
16
18
|
from opentelemetry import trace
|
|
17
19
|
|
|
18
|
-
__version__ = "0.2.
|
|
20
|
+
__version__ = "0.2.3"
|
|
19
21
|
|
|
20
22
|
RequestMapper = Callable[[tuple[Any, ...], Mapping[str, Any]], tuple[str, dict[str, Any]]]
|
|
21
23
|
ResponseMapper = Callable[[Any], dict[str, Any]]
|
|
@@ -29,9 +31,25 @@ _DROPIN_WRAPPERS: dict[str, tuple[Any, Callable[..., Any]]] = {}
|
|
|
29
31
|
_PENDING_CLOSE_TASKS: set[asyncio.Future[Any]] = set()
|
|
30
32
|
_installed = False
|
|
31
33
|
_install_lock = threading.Lock()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _reset_install_lock_in_child() -> None:
|
|
37
|
+
"""A thread of the parent can hold the lock at a fork, and only this thread exists here."""
|
|
38
|
+
global _install_lock
|
|
39
|
+
_install_lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# A hook is enough: a fork that runs no hooks, as uWSGI's fork of its workers by default,
|
|
43
|
+
# starts from its master, which serves no requests. os.register_at_fork cannot remove a hook,
|
|
44
|
+
# and importlib.reload runs this module again with the same globals, so the hook of the first
|
|
45
|
+
# run stays. Windows has no fork.
|
|
46
|
+
if hasattr(os, "register_at_fork") and "_install_fork_hook" not in globals():
|
|
47
|
+
_install_fork_hook = True
|
|
48
|
+
os.register_at_fork(after_in_child=_reset_install_lock_in_child)
|
|
49
|
+
|
|
32
50
|
_OMIT = object()
|
|
33
|
-
#
|
|
34
|
-
|
|
51
|
+
# The core applies max_attribute_length after the mask. More choices, or more responses items
|
|
52
|
+
# and rejected parts tracked for truncation, than this are not kept.
|
|
35
53
|
_STREAM_CAPTURE_MAX_ITEMS = 1000
|
|
36
54
|
# Retained finish reasons stay outside the output budget so a terminal one is still exported,
|
|
37
55
|
# so each is capped instead.
|
|
@@ -109,7 +127,37 @@ def _native(value: Any) -> Any:
|
|
|
109
127
|
|
|
110
128
|
|
|
111
129
|
def _stream_capture_budget() -> telemetry_dev.CaptureBudget:
|
|
112
|
-
|
|
130
|
+
# Older cores have no for_stream and bound each budget at 64 KiB on its own.
|
|
131
|
+
for_stream = getattr(telemetry_dev.CaptureBudget, "for_stream", None)
|
|
132
|
+
budget = for_stream() if callable(for_stream) else telemetry_dev.CaptureBudget()
|
|
133
|
+
return cast(telemetry_dev.CaptureBudget, budget)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _release_budget(budget: telemetry_dev.CaptureBudget) -> None:
|
|
137
|
+
release = getattr(budget, "release", None)
|
|
138
|
+
if callable(release):
|
|
139
|
+
release()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _reserve_bytes(budget: telemetry_dev.CaptureBudget, total: int) -> bool:
|
|
143
|
+
# Older cores have no reserve_bytes; there the caller's check against max_bytes stands.
|
|
144
|
+
reserve = getattr(budget, "reserve_bytes", None)
|
|
145
|
+
if callable(reserve):
|
|
146
|
+
return bool(reserve(total))
|
|
147
|
+
budget.bytes_used = total
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _adopt_budget(
|
|
152
|
+
stream: telemetry_dev.CaptureBudget, measured: telemetry_dev.CaptureBudget
|
|
153
|
+
) -> bool:
|
|
154
|
+
"""Takes the counts of a budget that measured a replacement into the stream's own budget,
|
|
155
|
+
which holds the bytes in the stream budget. Returns False, and takes nothing, when another
|
|
156
|
+
stream took the room after the measure."""
|
|
157
|
+
if not _reserve_bytes(stream, measured.bytes_used):
|
|
158
|
+
return False
|
|
159
|
+
stream.items_used = measured.items_used
|
|
160
|
+
return True
|
|
113
161
|
|
|
114
162
|
|
|
115
163
|
def _capture_masked(handle: telemetry_dev.SpanHandle) -> bool:
|
|
@@ -146,7 +194,9 @@ def _bounded_responses_native(
|
|
|
146
194
|
):
|
|
147
195
|
active_budget.truncated = True
|
|
148
196
|
raise _CaptureLimit
|
|
149
|
-
active_budget.bytes_used
|
|
197
|
+
if not _reserve_bytes(active_budget, active_budget.bytes_used + byte_count):
|
|
198
|
+
active_budget.truncated = True
|
|
199
|
+
raise _CaptureLimit
|
|
150
200
|
active_budget.items_used += item_count
|
|
151
201
|
|
|
152
202
|
def convert(item: Any, item_type: str | None, depth: int) -> Any:
|
|
@@ -796,13 +846,6 @@ def _end_once(handle: telemetry_dev.SpanHandle) -> Callable[..., None]:
|
|
|
796
846
|
return end
|
|
797
847
|
|
|
798
848
|
|
|
799
|
-
def _safe_response_fields(mapper: ResponseMapper, response: Any) -> dict[str, Any]:
|
|
800
|
-
try:
|
|
801
|
-
return mapper(response)
|
|
802
|
-
except Exception:
|
|
803
|
-
return {}
|
|
804
|
-
|
|
805
|
-
|
|
806
849
|
def _safe_start_span(
|
|
807
850
|
op: str, mapper: RequestMapper, args: tuple[Any, ...], kwargs: Mapping[str, Any]
|
|
808
851
|
) -> tuple[telemetry_dev.SpanHandle, Callable[..., None], float, str | None]:
|
|
@@ -874,6 +917,8 @@ class _InstrumentedStream:
|
|
|
874
917
|
self._finish_reasons: dict[int, str] = {}
|
|
875
918
|
self._choice_indices: set[int] = set()
|
|
876
919
|
self._finish_reasons_truncated = False
|
|
920
|
+
self._reported = False
|
|
921
|
+
self._output_recorded = False
|
|
877
922
|
|
|
878
923
|
def __getattr__(self, name: str) -> Any:
|
|
879
924
|
return getattr(self._inner, name)
|
|
@@ -896,7 +941,7 @@ class _InstrumentedStream:
|
|
|
896
941
|
except BaseException:
|
|
897
942
|
pass
|
|
898
943
|
raise
|
|
899
|
-
self.
|
|
944
|
+
self._record_guarded(chunk, received_at)
|
|
900
945
|
return chunk
|
|
901
946
|
|
|
902
947
|
def __aiter__(self) -> AsyncIterator[Any]:
|
|
@@ -917,7 +962,7 @@ class _InstrumentedStream:
|
|
|
917
962
|
except BaseException:
|
|
918
963
|
pass
|
|
919
964
|
raise
|
|
920
|
-
self.
|
|
965
|
+
self._record_guarded(chunk, received_at)
|
|
921
966
|
return chunk
|
|
922
967
|
|
|
923
968
|
def __enter__(self) -> _InstrumentedStream:
|
|
@@ -950,7 +995,85 @@ class _InstrumentedStream:
|
|
|
950
995
|
return bool(await _maybe_await(exit_method(exc_type, exc, tb)))
|
|
951
996
|
return False
|
|
952
997
|
|
|
998
|
+
def _report(self, exc: Exception) -> None:
|
|
999
|
+
# A stream reports only its first failure; a broken mapper fails on every chunk.
|
|
1000
|
+
if self._reported:
|
|
1001
|
+
return
|
|
1002
|
+
self._reported = True
|
|
1003
|
+
try:
|
|
1004
|
+
self._handle.report_error(exc)
|
|
1005
|
+
except Exception:
|
|
1006
|
+
pass
|
|
1007
|
+
|
|
1008
|
+
def _record_guarded(self, chunk: Any, received_at: float) -> None:
|
|
1009
|
+
self._output_recorded = False
|
|
1010
|
+
try:
|
|
1011
|
+
self._record(chunk, received_at)
|
|
1012
|
+
except Exception as exc:
|
|
1013
|
+
if not self._output_recorded:
|
|
1014
|
+
# The chunk may be lost. The truncated budget keeps no later chunk, so the
|
|
1015
|
+
# output stays a prefix, flagged and withheld from a mask.
|
|
1016
|
+
self._budget.truncated = True
|
|
1017
|
+
self._report(exc)
|
|
1018
|
+
|
|
953
1019
|
def _record(self, chunk: Any, received_at: float) -> None:
|
|
1020
|
+
# Timings are recorded before the chunk is mapped, so a failure in its mapping neither
|
|
1021
|
+
# moves the first-chunk time to a later chunk nor drops the chunk from the chunk timings;
|
|
1022
|
+
# like a chunk past the bound, a lost chunk was still delivered. Each timing step loses
|
|
1023
|
+
# only its own value when it fails.
|
|
1024
|
+
if not self._saw_first:
|
|
1025
|
+
self._saw_first = True
|
|
1026
|
+
try:
|
|
1027
|
+
self._handle.update(time_to_first_chunk_ms=(received_at - self._started_at) * 1000)
|
|
1028
|
+
except Exception as exc:
|
|
1029
|
+
self._report(exc)
|
|
1030
|
+
try:
|
|
1031
|
+
self._handle.update(
|
|
1032
|
+
response_id=_string(_field(chunk, "id")),
|
|
1033
|
+
response_model=_string(_field(chunk, "model")),
|
|
1034
|
+
)
|
|
1035
|
+
except Exception as exc:
|
|
1036
|
+
self._report(exc)
|
|
1037
|
+
try:
|
|
1038
|
+
has_output = False
|
|
1039
|
+
for choice in _sequence_items(_field(chunk, "choices")):
|
|
1040
|
+
delta = _field(choice, "delta")
|
|
1041
|
+
has_output = (
|
|
1042
|
+
has_output
|
|
1043
|
+
or any(
|
|
1044
|
+
isinstance(value, str) and bool(value)
|
|
1045
|
+
for value in (
|
|
1046
|
+
_field(delta, "content"),
|
|
1047
|
+
_field(delta, "reasoning_content"),
|
|
1048
|
+
_field(delta, "refusal"),
|
|
1049
|
+
_field(_field(delta, "audio"), "data"),
|
|
1050
|
+
)
|
|
1051
|
+
)
|
|
1052
|
+
or (
|
|
1053
|
+
isinstance(_field(_field(delta, "function_call"), "arguments"), str)
|
|
1054
|
+
and bool(_field(_field(delta, "function_call"), "arguments"))
|
|
1055
|
+
)
|
|
1056
|
+
or any(
|
|
1057
|
+
isinstance(arguments, str) and bool(arguments)
|
|
1058
|
+
for arguments in (
|
|
1059
|
+
_field(_field(tool, "function"), "arguments")
|
|
1060
|
+
for tool in _sequence_items(_field(delta, "tool_calls"))
|
|
1061
|
+
)
|
|
1062
|
+
)
|
|
1063
|
+
)
|
|
1064
|
+
if has_output:
|
|
1065
|
+
self._saw_output = True
|
|
1066
|
+
record_output_chunk = getattr(self._handle, "record_output_chunk", None)
|
|
1067
|
+
if callable(record_output_chunk):
|
|
1068
|
+
record_output_chunk(received_at * 1000)
|
|
1069
|
+
except Exception as exc:
|
|
1070
|
+
self._report(exc)
|
|
1071
|
+
# The chunk is kept before its metadata is read, so a failure in the metadata (ID,
|
|
1072
|
+
# model, usage, finish reasons) loses only that metadata. A chunk object keeps its delta
|
|
1073
|
+
# fields where the budget does not walk, so the budget measures its JSON copy.
|
|
1074
|
+
if not self._budget.truncated and self._budget.accept(_native(chunk)):
|
|
1075
|
+
self._chunks.append(chunk)
|
|
1076
|
+
self._output_recorded = True
|
|
954
1077
|
response_id = _string(_field(chunk, "id"))
|
|
955
1078
|
if self._response_id is None and response_id is not None:
|
|
956
1079
|
self._response_id = response_id
|
|
@@ -960,32 +1083,7 @@ class _InstrumentedStream:
|
|
|
960
1083
|
usage = _usage_from(_field(chunk, "usage"))
|
|
961
1084
|
if usage is not None and (self._usage is None or _reports_tokens(usage)):
|
|
962
1085
|
self._usage = usage
|
|
963
|
-
has_output = False
|
|
964
1086
|
for fallback_index, choice in enumerate(_sequence_items(_field(chunk, "choices"))):
|
|
965
|
-
delta = _field(choice, "delta")
|
|
966
|
-
has_output = (
|
|
967
|
-
has_output
|
|
968
|
-
or any(
|
|
969
|
-
isinstance(value, str) and bool(value)
|
|
970
|
-
for value in (
|
|
971
|
-
_field(delta, "content"),
|
|
972
|
-
_field(delta, "reasoning_content"),
|
|
973
|
-
_field(delta, "refusal"),
|
|
974
|
-
_field(_field(delta, "audio"), "data"),
|
|
975
|
-
)
|
|
976
|
-
)
|
|
977
|
-
or (
|
|
978
|
-
isinstance(_field(_field(delta, "function_call"), "arguments"), str)
|
|
979
|
-
and bool(_field(_field(delta, "function_call"), "arguments"))
|
|
980
|
-
)
|
|
981
|
-
or any(
|
|
982
|
-
isinstance(arguments, str) and bool(arguments)
|
|
983
|
-
for arguments in (
|
|
984
|
-
_field(_field(tool, "function"), "arguments")
|
|
985
|
-
for tool in _sequence_items(_field(delta, "tool_calls"))
|
|
986
|
-
)
|
|
987
|
-
)
|
|
988
|
-
)
|
|
989
1087
|
choice_index = _number(_field(choice, "index"))
|
|
990
1088
|
key = (
|
|
991
1089
|
int(choice_index)
|
|
@@ -1004,20 +1102,6 @@ class _InstrumentedStream:
|
|
|
1004
1102
|
finish_reason = _string(_field(choice, "finish_reason"))
|
|
1005
1103
|
if finish_reason is not None:
|
|
1006
1104
|
self._finish_reasons[key] = finish_reason[:_FINISH_REASON_MAX_LENGTH]
|
|
1007
|
-
if has_output:
|
|
1008
|
-
self._saw_output = True
|
|
1009
|
-
record_output_chunk = getattr(self._handle, "record_output_chunk", None)
|
|
1010
|
-
if callable(record_output_chunk):
|
|
1011
|
-
record_output_chunk(received_at * 1000)
|
|
1012
|
-
if not self._saw_first:
|
|
1013
|
-
self._saw_first = True
|
|
1014
|
-
self._handle.update(
|
|
1015
|
-
time_to_first_chunk_ms=(time.perf_counter() - self._started_at) * 1000,
|
|
1016
|
-
response_id=response_id,
|
|
1017
|
-
response_model=response_model,
|
|
1018
|
-
)
|
|
1019
|
-
if self._budget.accept(chunk):
|
|
1020
|
-
self._chunks.append(chunk)
|
|
1021
1105
|
|
|
1022
1106
|
def _finish(self, error: BaseException | None = None) -> None:
|
|
1023
1107
|
if self._finished:
|
|
@@ -1028,10 +1112,12 @@ class _InstrumentedStream:
|
|
|
1028
1112
|
try:
|
|
1029
1113
|
stream_chunk_builder = cast(Callable[..., Any], cast(Any, litellm).stream_chunk_builder)
|
|
1030
1114
|
rebuilt = stream_chunk_builder(self._chunks, messages=self._messages)
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1115
|
+
if rebuilt is not None:
|
|
1116
|
+
fields = _completion_response(rebuilt)
|
|
1117
|
+
except Exception as exc:
|
|
1118
|
+
# The output is lost; the truncated budget flags it and keeps it from a mask.
|
|
1119
|
+
self._budget.truncated = True
|
|
1120
|
+
self._report(exc)
|
|
1035
1121
|
rebuilt_usage = fields.get("usage")
|
|
1036
1122
|
if self._usage is not None and (
|
|
1037
1123
|
rebuilt_usage is None
|
|
@@ -1079,7 +1165,10 @@ class _InstrumentedStream:
|
|
|
1079
1165
|
fields["error"] = error
|
|
1080
1166
|
if self._request_provider is None and fields.get("provider") is None:
|
|
1081
1167
|
fields["provider"] = _provider_from_error(error)
|
|
1082
|
-
|
|
1168
|
+
try:
|
|
1169
|
+
self._end(**fields)
|
|
1170
|
+
finally:
|
|
1171
|
+
_release_budget(self._budget)
|
|
1083
1172
|
|
|
1084
1173
|
def close(self) -> None:
|
|
1085
1174
|
self._finish()
|
|
@@ -1102,6 +1191,77 @@ class _InstrumentedStream:
|
|
|
1102
1191
|
await _maybe_await(close())
|
|
1103
1192
|
|
|
1104
1193
|
|
|
1194
|
+
# On Python 3.10 and 3.11, StringIO keeps up to 100,000 written strings in a list before it
|
|
1195
|
+
# joins them, about 60 bytes each that the stream bound does not charge: 15 bytes a character
|
|
1196
|
+
# for 4-character deltas. Deltas are joined 256 at a time, so that those waiting hold under
|
|
1197
|
+
# about 22 KB, onto a string that is written once it has 4,096 characters, so that what is
|
|
1198
|
+
# written costs about 1.5% more than its text. Python 3.12 dropped the list.
|
|
1199
|
+
_DELTA_BATCH = 256
|
|
1200
|
+
_WRITE_CHARS = 4096
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
class _DeltaBuffer:
|
|
1204
|
+
# A write takes several steps, unlike one StringIO call, so without the lock a read on
|
|
1205
|
+
# another thread, such as one that closes the stream, could copy a batch twice or drop it.
|
|
1206
|
+
# A fork copies the lock held if another thread was writing, and that thread does not run
|
|
1207
|
+
# in the child to release it. Every fork, also one that runs no at-fork hooks, changes the
|
|
1208
|
+
# process ID, so a child takes a new lock. The check is inline because it runs per delta.
|
|
1209
|
+
# A signal handler runs on the thread it interrupts, so one that reads the stream, such as
|
|
1210
|
+
# one that closes it, can read inside a write. So the lock is reentrant, and each step takes
|
|
1211
|
+
# the deltas and the tail out of the buffer before it joins or writes them, so that a read
|
|
1212
|
+
# inside the step does not write them as well; that read returns a prefix, which can leave
|
|
1213
|
+
# out the deltas being joined. A handler that writes inside a step is not supported. An
|
|
1214
|
+
# exception inside a step, such as KeyboardInterrupt from a handler, puts back the text the
|
|
1215
|
+
# step took unless the StringIO has it, so that a later read still has every delta.
|
|
1216
|
+
__slots__ = ("_buffer", "_lock", "_pending", "_pid", "_tail")
|
|
1217
|
+
|
|
1218
|
+
def __init__(self) -> None:
|
|
1219
|
+
self._buffer = StringIO()
|
|
1220
|
+
self._lock = threading.RLock()
|
|
1221
|
+
self._pid = os.getpid()
|
|
1222
|
+
self._pending: list[str] = []
|
|
1223
|
+
self._tail = ""
|
|
1224
|
+
|
|
1225
|
+
def write(self, text: str) -> None:
|
|
1226
|
+
pid = os.getpid()
|
|
1227
|
+
if self._pid != pid:
|
|
1228
|
+
self._lock = threading.RLock()
|
|
1229
|
+
self._pid = pid
|
|
1230
|
+
with self._lock:
|
|
1231
|
+
self._pending.append(text)
|
|
1232
|
+
if len(self._pending) >= _DELTA_BATCH:
|
|
1233
|
+
self._join(_WRITE_CHARS)
|
|
1234
|
+
|
|
1235
|
+
def getvalue(self) -> str:
|
|
1236
|
+
pid = os.getpid()
|
|
1237
|
+
if self._pid != pid:
|
|
1238
|
+
self._lock = threading.RLock()
|
|
1239
|
+
self._pid = pid
|
|
1240
|
+
with self._lock:
|
|
1241
|
+
self._join(1)
|
|
1242
|
+
return self._buffer.getvalue()
|
|
1243
|
+
|
|
1244
|
+
def _join(self, write_at: int) -> None:
|
|
1245
|
+
if not self._pending and len(self._tail) < write_at:
|
|
1246
|
+
return
|
|
1247
|
+
written = self._buffer.tell()
|
|
1248
|
+
pending: list[str] = []
|
|
1249
|
+
tail = ""
|
|
1250
|
+
try:
|
|
1251
|
+
pending, self._pending = self._pending, []
|
|
1252
|
+
tail, self._tail = self._tail, ""
|
|
1253
|
+
joined = "".join([tail, *pending])
|
|
1254
|
+
if len(joined) >= write_at:
|
|
1255
|
+
self._buffer.write(joined)
|
|
1256
|
+
else:
|
|
1257
|
+
self._tail = joined
|
|
1258
|
+
except BaseException:
|
|
1259
|
+
if self._buffer.tell() == written:
|
|
1260
|
+
self._pending[:0] = pending
|
|
1261
|
+
self._tail = tail + self._tail
|
|
1262
|
+
raise
|
|
1263
|
+
|
|
1264
|
+
|
|
1105
1265
|
class _InstrumentedResponsesStream:
|
|
1106
1266
|
def __init__(
|
|
1107
1267
|
self,
|
|
@@ -1132,6 +1292,8 @@ class _InstrumentedResponsesStream:
|
|
|
1132
1292
|
self._budget = _stream_capture_budget()
|
|
1133
1293
|
self._charges: dict[object, tuple[int, int]] = {}
|
|
1134
1294
|
self._annotation_charge_keys: dict[object, dict[int, object]] = {}
|
|
1295
|
+
self._reported = False
|
|
1296
|
+
self._record_failed = False
|
|
1135
1297
|
|
|
1136
1298
|
def __getattr__(self, name: str) -> Any:
|
|
1137
1299
|
return getattr(self._inner, name)
|
|
@@ -1153,7 +1315,7 @@ class _InstrumentedResponsesStream:
|
|
|
1153
1315
|
except BaseException:
|
|
1154
1316
|
pass
|
|
1155
1317
|
raise
|
|
1156
|
-
self.
|
|
1318
|
+
self._record_guarded(event, received_at)
|
|
1157
1319
|
return event
|
|
1158
1320
|
|
|
1159
1321
|
def __aiter__(self) -> AsyncIterator[Any]:
|
|
@@ -1173,7 +1335,7 @@ class _InstrumentedResponsesStream:
|
|
|
1173
1335
|
except BaseException:
|
|
1174
1336
|
pass
|
|
1175
1337
|
raise
|
|
1176
|
-
self.
|
|
1338
|
+
self._record_guarded(event, received_at)
|
|
1177
1339
|
return event
|
|
1178
1340
|
|
|
1179
1341
|
def __enter__(self) -> _InstrumentedResponsesStream:
|
|
@@ -1268,10 +1430,12 @@ class _InstrumentedResponsesStream:
|
|
|
1268
1430
|
def _materialize_partial(self, value: dict[str, Any]) -> dict[str, Any]:
|
|
1269
1431
|
if "_field" not in value:
|
|
1270
1432
|
return value
|
|
1433
|
+
# Each partial value with a "_field" has a _DeltaBuffer under "_fragments", or a StringIO
|
|
1434
|
+
# when _append_delta measures its first delta.
|
|
1271
1435
|
field = cast(str, value["_field"])
|
|
1272
|
-
fragments = cast(
|
|
1436
|
+
fragments = cast(_DeltaBuffer | StringIO, value["_fragments"])
|
|
1273
1437
|
return {key: item for key, item in value.items() if key not in {"_field", "_fragments"}} | {
|
|
1274
|
-
field:
|
|
1438
|
+
field: fragments.getvalue()
|
|
1275
1439
|
}
|
|
1276
1440
|
|
|
1277
1441
|
def _set_completed_item(self, key: object, value: Any) -> None:
|
|
@@ -1316,7 +1480,7 @@ class _InstrumentedResponsesStream:
|
|
|
1316
1480
|
budget.bytes_used = max(0, self._budget.bytes_used - removed_bytes)
|
|
1317
1481
|
budget.items_used = max(0, self._budget.items_used - removed_items)
|
|
1318
1482
|
before = (budget.bytes_used, budget.items_used)
|
|
1319
|
-
if not budget.accept(value):
|
|
1483
|
+
if not budget.accept(value) or not _adopt_budget(self._budget, budget):
|
|
1320
1484
|
return False
|
|
1321
1485
|
for key in keys:
|
|
1322
1486
|
self._charges.pop(key, None)
|
|
@@ -1324,7 +1488,6 @@ class _InstrumentedResponsesStream:
|
|
|
1324
1488
|
budget.bytes_used - before[0],
|
|
1325
1489
|
budget.items_used - before[1],
|
|
1326
1490
|
)
|
|
1327
|
-
self._budget = budget
|
|
1328
1491
|
self._budget.truncated = bool(self._truncated_items) or self._truncation_overflow
|
|
1329
1492
|
return True
|
|
1330
1493
|
|
|
@@ -1347,7 +1510,7 @@ class _InstrumentedResponsesStream:
|
|
|
1347
1510
|
budget.items_used = max(0, self._budget.items_used - removed_items)
|
|
1348
1511
|
before = (budget.bytes_used, budget.items_used)
|
|
1349
1512
|
converted, budget = _bounded_responses_native(value, budget=budget)
|
|
1350
|
-
if converted is None or budget.truncated:
|
|
1513
|
+
if converted is None or budget.truncated or not _adopt_budget(self._budget, budget):
|
|
1351
1514
|
return False, None
|
|
1352
1515
|
for key in keys:
|
|
1353
1516
|
self._charges.pop(key, None)
|
|
@@ -1355,7 +1518,6 @@ class _InstrumentedResponsesStream:
|
|
|
1355
1518
|
budget.bytes_used - before[0],
|
|
1356
1519
|
budget.items_used - before[1],
|
|
1357
1520
|
)
|
|
1358
|
-
self._budget = budget
|
|
1359
1521
|
self._budget.truncated = bool(self._truncated_items) or self._truncation_overflow
|
|
1360
1522
|
return True, converted
|
|
1361
1523
|
|
|
@@ -1366,12 +1528,14 @@ class _InstrumentedResponsesStream:
|
|
|
1366
1528
|
self._budget.truncated = True
|
|
1367
1529
|
return False
|
|
1368
1530
|
if storage_key not in self._charges:
|
|
1369
|
-
candidate = self._materialize_partial({**target, "_fragments":
|
|
1531
|
+
candidate = self._materialize_partial({**target, "_fragments": StringIO(delta)})
|
|
1370
1532
|
return self._replace_charge([], candidate, storage_key)
|
|
1371
1533
|
if self._budget.items_used >= self._budget.max_items:
|
|
1372
1534
|
self._budget.truncated = True
|
|
1373
1535
|
return False
|
|
1374
|
-
self._budget.bytes_used
|
|
1536
|
+
if not _reserve_bytes(self._budget, self._budget.bytes_used + delta_bytes):
|
|
1537
|
+
self._budget.truncated = True
|
|
1538
|
+
return False
|
|
1375
1539
|
self._budget.items_used += 1
|
|
1376
1540
|
charged_bytes, charged_items = self._charges[storage_key]
|
|
1377
1541
|
self._charges[storage_key] = (charged_bytes + delta_bytes, charged_items + 1)
|
|
@@ -1399,16 +1563,16 @@ class _InstrumentedResponsesStream:
|
|
|
1399
1563
|
keys = self._truncated_item_keys[owner]
|
|
1400
1564
|
if key in keys:
|
|
1401
1565
|
return
|
|
1402
|
-
if self._truncated_key_count <
|
|
1566
|
+
if self._truncated_key_count < _STREAM_CAPTURE_MAX_ITEMS:
|
|
1403
1567
|
keys.add(key)
|
|
1404
1568
|
self._truncated_key_count += 1
|
|
1405
1569
|
else:
|
|
1406
1570
|
self._truncation_overflow = True
|
|
1407
1571
|
return
|
|
1408
|
-
if self._retained_item_count <
|
|
1572
|
+
if self._retained_item_count < _STREAM_CAPTURE_MAX_ITEMS:
|
|
1409
1573
|
self._truncated_items.add(owner)
|
|
1410
1574
|
self._retained_item_count += 1
|
|
1411
|
-
if self._truncated_key_count <
|
|
1575
|
+
if self._truncated_key_count < _STREAM_CAPTURE_MAX_ITEMS:
|
|
1412
1576
|
self._truncated_item_keys[owner] = {key}
|
|
1413
1577
|
self._truncated_key_count += 1
|
|
1414
1578
|
else:
|
|
@@ -1433,16 +1597,53 @@ class _InstrumentedResponsesStream:
|
|
|
1433
1597
|
if not keys and not self._truncation_overflow:
|
|
1434
1598
|
self._resolve_truncated(item_key)
|
|
1435
1599
|
|
|
1600
|
+
def _report(self, exc: Exception) -> None:
|
|
1601
|
+
# A stream reports only its first failure; a broken mapper fails on every event.
|
|
1602
|
+
if self._reported:
|
|
1603
|
+
return
|
|
1604
|
+
self._reported = True
|
|
1605
|
+
try:
|
|
1606
|
+
self._handle.report_error(exc)
|
|
1607
|
+
except Exception:
|
|
1608
|
+
pass
|
|
1609
|
+
|
|
1610
|
+
def _record_guarded(self, event: Any, received_at: float) -> None:
|
|
1611
|
+
try:
|
|
1612
|
+
# After a lost event only the terminal events are read, so the output rebuilt from
|
|
1613
|
+
# deltas stays a prefix; a complete terminal snapshot still replaces it.
|
|
1614
|
+
if self._record_failed and _string(_field(event, "type")) not in {
|
|
1615
|
+
"response.completed",
|
|
1616
|
+
"response.incomplete",
|
|
1617
|
+
"response.failed",
|
|
1618
|
+
"error",
|
|
1619
|
+
}:
|
|
1620
|
+
return
|
|
1621
|
+
self._record(event, received_at)
|
|
1622
|
+
except Exception as exc:
|
|
1623
|
+
self._record_failed = True
|
|
1624
|
+
self._budget.truncated = True
|
|
1625
|
+
self._report(exc)
|
|
1626
|
+
if self._finished:
|
|
1627
|
+
# The end failed after it began, so the span ends here, flagged, with no output.
|
|
1628
|
+
try:
|
|
1629
|
+
self._end(attributes={_CAPTURE_TRUNCATED_ATTRIBUTE: True})
|
|
1630
|
+
finally:
|
|
1631
|
+
_release_budget(self._budget)
|
|
1632
|
+
|
|
1436
1633
|
def _record(self, event: Any, received_at: float) -> None:
|
|
1437
1634
|
event_type = _string(_field(event, "type"))
|
|
1438
1635
|
response = _field(event, "response")
|
|
1439
1636
|
if not self._saw_first:
|
|
1440
1637
|
self._saw_first = True
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1638
|
+
try:
|
|
1639
|
+
self._handle.update(
|
|
1640
|
+
time_to_first_chunk_ms=(received_at - self._started_at) * 1000,
|
|
1641
|
+
response_id=_string(_field(response, "id")),
|
|
1642
|
+
response_model=_string(_field(response, "model")),
|
|
1643
|
+
)
|
|
1644
|
+
except Exception as exc:
|
|
1645
|
+
# Only the first-event timing and IDs are lost; the event's output is recorded.
|
|
1646
|
+
self._report(exc)
|
|
1446
1647
|
if event_type in {
|
|
1447
1648
|
"response.output_item.added",
|
|
1448
1649
|
"response.content_part.added",
|
|
@@ -1494,7 +1695,7 @@ class _InstrumentedResponsesStream:
|
|
|
1494
1695
|
**(target or {}),
|
|
1495
1696
|
"type": "output_text" if field == "text" else "refusal",
|
|
1496
1697
|
"_field": field,
|
|
1497
|
-
"_fragments":
|
|
1698
|
+
"_fragments": _DeltaBuffer(),
|
|
1498
1699
|
}
|
|
1499
1700
|
elif event_type in {
|
|
1500
1701
|
"response.reasoning_summary_text.delta",
|
|
@@ -1523,7 +1724,7 @@ class _InstrumentedResponsesStream:
|
|
|
1523
1724
|
**(target or {}),
|
|
1524
1725
|
"type": kind,
|
|
1525
1726
|
"_field": field,
|
|
1526
|
-
"_fragments":
|
|
1727
|
+
"_fragments": _DeltaBuffer(),
|
|
1527
1728
|
}
|
|
1528
1729
|
elif event_type in {
|
|
1529
1730
|
"response.function_call_arguments.delta",
|
|
@@ -1545,7 +1746,7 @@ class _InstrumentedResponsesStream:
|
|
|
1545
1746
|
**(seeded if isinstance(seeded, dict) else {}),
|
|
1546
1747
|
"type": event_kind[0],
|
|
1547
1748
|
"_field": event_kind[1],
|
|
1548
|
-
"_fragments":
|
|
1749
|
+
"_fragments": _DeltaBuffer(),
|
|
1549
1750
|
}
|
|
1550
1751
|
item_id = _string(_field(event, "item_id"))
|
|
1551
1752
|
if item_id is not None:
|
|
@@ -1555,12 +1756,13 @@ class _InstrumentedResponsesStream:
|
|
|
1555
1756
|
if value is not None:
|
|
1556
1757
|
target[field] = value
|
|
1557
1758
|
if target is not None:
|
|
1558
|
-
|
|
1759
|
+
# Every target above is created with a _DeltaBuffer under "_fragments".
|
|
1760
|
+
fragments = cast(_DeltaBuffer, target["_fragments"])
|
|
1559
1761
|
accepted = not self._delta_rejected_before(storage_key) and self._append_delta(
|
|
1560
1762
|
storage_key, target, delta
|
|
1561
1763
|
)
|
|
1562
1764
|
if accepted:
|
|
1563
|
-
fragments.
|
|
1765
|
+
fragments.write(delta)
|
|
1564
1766
|
self._remember_item_index(event, item_key)
|
|
1565
1767
|
if event_type in {
|
|
1566
1768
|
"response.function_call_arguments.delta",
|
|
@@ -1724,38 +1926,83 @@ class _InstrumentedResponsesStream:
|
|
|
1724
1926
|
if self._finished:
|
|
1725
1927
|
return
|
|
1726
1928
|
self._finished = True
|
|
1727
|
-
fields =
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1929
|
+
fields: dict[str, Any] = {}
|
|
1930
|
+
fields_lost = False
|
|
1931
|
+
if response is not None:
|
|
1932
|
+
try:
|
|
1933
|
+
fields = _responses_response_without_output(response)
|
|
1934
|
+
except Exception as exc:
|
|
1935
|
+
# Only the response's own fields are lost: the output is mapped apart, so it is
|
|
1936
|
+
# flagged but not cut or withheld from a mask. A failed response is still
|
|
1937
|
+
# recorded as failed.
|
|
1938
|
+
fields_lost = True
|
|
1939
|
+
self._report(exc)
|
|
1940
|
+
try:
|
|
1941
|
+
fields = {"error": _response_error(response)}
|
|
1942
|
+
except Exception:
|
|
1943
|
+
pass
|
|
1732
1944
|
if response is not None and self._capture_output:
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1945
|
+
try:
|
|
1946
|
+
terminal_output, terminal_budget = _bounded_responses_native(
|
|
1947
|
+
_field(response, "output"),
|
|
1948
|
+
budget=telemetry_dev.CaptureBudget(
|
|
1949
|
+
self._budget.max_bytes, self._budget.max_items
|
|
1950
|
+
),
|
|
1951
|
+
)
|
|
1952
|
+
# An empty terminal output does not replace output recorded from the deltas, or
|
|
1953
|
+
# lost with an event, so it is read as no terminal output. A lost event leaves the
|
|
1954
|
+
# budget truncated until this replacement, so the truncated budget covers it.
|
|
1955
|
+
replaces_recorded = terminal_output != [] or not (
|
|
1956
|
+
self._charges or self._budget.truncated
|
|
1957
|
+
)
|
|
1958
|
+
if (
|
|
1959
|
+
terminal_output is not None
|
|
1960
|
+
and replaces_recorded
|
|
1961
|
+
and not terminal_budget.truncated
|
|
1962
|
+
and _adopt_budget(self._budget, terminal_budget)
|
|
1963
|
+
):
|
|
1964
|
+
fields["output"] = terminal_output
|
|
1965
|
+
self._budget.truncated = False
|
|
1966
|
+
self._retained_item_count -= len(self._truncated_items)
|
|
1967
|
+
self._truncated_items.clear()
|
|
1968
|
+
self._truncated_item_keys.clear()
|
|
1969
|
+
self._truncated_key_count = 0
|
|
1970
|
+
self._truncation_overflow = False
|
|
1971
|
+
elif replaces_recorded and (
|
|
1972
|
+
_field(response, "output") is not None or terminal_budget.truncated
|
|
1973
|
+
):
|
|
1974
|
+
self._budget.truncated = True
|
|
1975
|
+
except Exception as exc:
|
|
1976
|
+
# The terminal output is lost; the output rebuilt from the deltas, if any, is
|
|
1977
|
+
# flagged and withheld from a mask, and the mapped fields are kept.
|
|
1978
|
+
fields.pop("output", None)
|
|
1745
1979
|
self._budget.truncated = True
|
|
1980
|
+
self._report(exc)
|
|
1746
1981
|
if "output" not in fields:
|
|
1747
|
-
|
|
1982
|
+
try:
|
|
1983
|
+
partial_output = self._partial_output()
|
|
1984
|
+
except Exception as exc:
|
|
1985
|
+
# The output cannot be rebuilt whole, so none is exported, and the stream is
|
|
1986
|
+
# flagged.
|
|
1987
|
+
partial_output = None
|
|
1988
|
+
self._budget.truncated = True
|
|
1989
|
+
self._report(exc)
|
|
1748
1990
|
if partial_output:
|
|
1749
1991
|
fields["output"] = partial_output
|
|
1750
1992
|
if self._budget.truncated or response is None or error is not None:
|
|
1751
1993
|
_mark_capture_incomplete(fields, self._mask_output_when_incomplete)
|
|
1994
|
+
elif fields_lost:
|
|
1995
|
+
_mark_capture_incomplete(fields, False)
|
|
1752
1996
|
if self._request_provider is not None:
|
|
1753
1997
|
fields.pop("provider", None)
|
|
1754
1998
|
if error is not None:
|
|
1755
1999
|
fields["error"] = error
|
|
1756
2000
|
if self._request_provider is None:
|
|
1757
2001
|
fields["provider"] = _provider_from_error(error)
|
|
1758
|
-
|
|
2002
|
+
try:
|
|
2003
|
+
self._end(**fields)
|
|
2004
|
+
finally:
|
|
2005
|
+
_release_budget(self._budget)
|
|
1759
2006
|
|
|
1760
2007
|
def close(self) -> None:
|
|
1761
2008
|
self._finish()
|
|
@@ -1859,7 +2106,20 @@ def _wrap_sync(
|
|
|
1859
2106
|
elif op == "rerank":
|
|
1860
2107
|
active_mapper = _rerank_response_without_output
|
|
1861
2108
|
try:
|
|
1862
|
-
fields =
|
|
2109
|
+
fields = active_mapper(result)
|
|
2110
|
+
except Exception as exc:
|
|
2111
|
+
# The output and response fields are lost; the call is failed only if the response
|
|
2112
|
+
# says so.
|
|
2113
|
+
try:
|
|
2114
|
+
handle.report_error(exc)
|
|
2115
|
+
except Exception:
|
|
2116
|
+
pass
|
|
2117
|
+
fields: dict[str, Any] = {"attributes": {_CAPTURE_TRUNCATED_ATTRIBUTE: True}}
|
|
2118
|
+
if op == "responses":
|
|
2119
|
+
try:
|
|
2120
|
+
fields["error"] = _response_error(result)
|
|
2121
|
+
except Exception:
|
|
2122
|
+
pass
|
|
1863
2123
|
except BaseException as exc:
|
|
1864
2124
|
end(
|
|
1865
2125
|
error=exc,
|
|
@@ -1919,7 +2179,20 @@ def _wrap_async(
|
|
|
1919
2179
|
elif op == "rerank":
|
|
1920
2180
|
active_mapper = _rerank_response_without_output
|
|
1921
2181
|
try:
|
|
1922
|
-
fields =
|
|
2182
|
+
fields = active_mapper(result)
|
|
2183
|
+
except Exception as exc:
|
|
2184
|
+
# The output and response fields are lost; the call is failed only if the response
|
|
2185
|
+
# says so.
|
|
2186
|
+
try:
|
|
2187
|
+
handle.report_error(exc)
|
|
2188
|
+
except Exception:
|
|
2189
|
+
pass
|
|
2190
|
+
fields: dict[str, Any] = {"attributes": {_CAPTURE_TRUNCATED_ATTRIBUTE: True}}
|
|
2191
|
+
if op == "responses":
|
|
2192
|
+
try:
|
|
2193
|
+
fields["error"] = _response_error(result)
|
|
2194
|
+
except Exception:
|
|
2195
|
+
pass
|
|
1923
2196
|
except BaseException as exc:
|
|
1924
2197
|
end(
|
|
1925
2198
|
error=exc,
|
|
File without changes
|
{telemetry_dev_litellm-0.2.2 → telemetry_dev_litellm-0.2.3}/src/telemetry_dev_litellm/py.typed
RENAMED
|
File without changes
|