telemetry-dev-litellm 0.2.1__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.1 → telemetry_dev_litellm-0.2.3}/PKG-INFO +3 -3
- {telemetry_dev_litellm-0.2.1 → telemetry_dev_litellm-0.2.3}/README.md +2 -2
- {telemetry_dev_litellm-0.2.1 → telemetry_dev_litellm-0.2.3}/pyproject.toml +1 -1
- {telemetry_dev_litellm-0.2.1 → telemetry_dev_litellm-0.2.3}/pyproject.toml.orig +1 -1
- {telemetry_dev_litellm-0.2.1 → telemetry_dev_litellm-0.2.3}/src/telemetry_dev_litellm/__init__.py +386 -103
- {telemetry_dev_litellm-0.2.1 → telemetry_dev_litellm-0.2.3}/LICENSE +0 -0
- {telemetry_dev_litellm-0.2.1 → 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.1 → 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:
|
|
@@ -455,6 +505,12 @@ def _output_type(response_format: Any) -> str | None:
|
|
|
455
505
|
return kind
|
|
456
506
|
|
|
457
507
|
|
|
508
|
+
def _prompt_attributes(messages: object) -> dict[str, Any]:
|
|
509
|
+
# Older cores have no prompt_attributes_for and record a prompt only inside with_prompt.
|
|
510
|
+
read = getattr(telemetry_dev, "prompt_attributes_for", None)
|
|
511
|
+
return cast("dict[str, Any]", read(messages)) if callable(read) else {}
|
|
512
|
+
|
|
513
|
+
|
|
458
514
|
def _completion_request(
|
|
459
515
|
args: tuple[Any, ...], kwargs: Mapping[str, Any]
|
|
460
516
|
) -> tuple[str, dict[str, Any]]:
|
|
@@ -478,6 +534,7 @@ def _completion_request(
|
|
|
478
534
|
"presence_penalty": _number(kwargs.get("presence_penalty")),
|
|
479
535
|
"output_type": _output_type(kwargs.get("response_format")),
|
|
480
536
|
"metadata": _request_metadata(kwargs),
|
|
537
|
+
"attributes": _prompt_attributes(_arg(args, kwargs, "messages", 1)) or None,
|
|
481
538
|
},
|
|
482
539
|
)
|
|
483
540
|
|
|
@@ -545,7 +602,10 @@ def _responses_request(
|
|
|
545
602
|
input_budget: telemetry_dev.CaptureBudget | None = None
|
|
546
603
|
if capture_input:
|
|
547
604
|
input_value, input_budget = _bounded_responses_native(_arg(args, kwargs, "input", 0))
|
|
548
|
-
attributes: dict[str, Any] = {
|
|
605
|
+
attributes: dict[str, Any] = {
|
|
606
|
+
"gen_ai.operation.name": "chat",
|
|
607
|
+
**_prompt_attributes(_arg(args, kwargs, "input", 0)),
|
|
608
|
+
}
|
|
549
609
|
if input_budget is not None and input_budget.truncated:
|
|
550
610
|
attributes["telemetry.dev.capture.truncated"] = True
|
|
551
611
|
return (
|
|
@@ -786,13 +846,6 @@ def _end_once(handle: telemetry_dev.SpanHandle) -> Callable[..., None]:
|
|
|
786
846
|
return end
|
|
787
847
|
|
|
788
848
|
|
|
789
|
-
def _safe_response_fields(mapper: ResponseMapper, response: Any) -> dict[str, Any]:
|
|
790
|
-
try:
|
|
791
|
-
return mapper(response)
|
|
792
|
-
except Exception:
|
|
793
|
-
return {}
|
|
794
|
-
|
|
795
|
-
|
|
796
849
|
def _safe_start_span(
|
|
797
850
|
op: str, mapper: RequestMapper, args: tuple[Any, ...], kwargs: Mapping[str, Any]
|
|
798
851
|
) -> tuple[telemetry_dev.SpanHandle, Callable[..., None], float, str | None]:
|
|
@@ -864,6 +917,8 @@ class _InstrumentedStream:
|
|
|
864
917
|
self._finish_reasons: dict[int, str] = {}
|
|
865
918
|
self._choice_indices: set[int] = set()
|
|
866
919
|
self._finish_reasons_truncated = False
|
|
920
|
+
self._reported = False
|
|
921
|
+
self._output_recorded = False
|
|
867
922
|
|
|
868
923
|
def __getattr__(self, name: str) -> Any:
|
|
869
924
|
return getattr(self._inner, name)
|
|
@@ -886,7 +941,7 @@ class _InstrumentedStream:
|
|
|
886
941
|
except BaseException:
|
|
887
942
|
pass
|
|
888
943
|
raise
|
|
889
|
-
self.
|
|
944
|
+
self._record_guarded(chunk, received_at)
|
|
890
945
|
return chunk
|
|
891
946
|
|
|
892
947
|
def __aiter__(self) -> AsyncIterator[Any]:
|
|
@@ -907,7 +962,7 @@ class _InstrumentedStream:
|
|
|
907
962
|
except BaseException:
|
|
908
963
|
pass
|
|
909
964
|
raise
|
|
910
|
-
self.
|
|
965
|
+
self._record_guarded(chunk, received_at)
|
|
911
966
|
return chunk
|
|
912
967
|
|
|
913
968
|
def __enter__(self) -> _InstrumentedStream:
|
|
@@ -940,7 +995,85 @@ class _InstrumentedStream:
|
|
|
940
995
|
return bool(await _maybe_await(exit_method(exc_type, exc, tb)))
|
|
941
996
|
return False
|
|
942
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
|
+
|
|
943
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
|
|
944
1077
|
response_id = _string(_field(chunk, "id"))
|
|
945
1078
|
if self._response_id is None and response_id is not None:
|
|
946
1079
|
self._response_id = response_id
|
|
@@ -950,32 +1083,7 @@ class _InstrumentedStream:
|
|
|
950
1083
|
usage = _usage_from(_field(chunk, "usage"))
|
|
951
1084
|
if usage is not None and (self._usage is None or _reports_tokens(usage)):
|
|
952
1085
|
self._usage = usage
|
|
953
|
-
has_output = False
|
|
954
1086
|
for fallback_index, choice in enumerate(_sequence_items(_field(chunk, "choices"))):
|
|
955
|
-
delta = _field(choice, "delta")
|
|
956
|
-
has_output = (
|
|
957
|
-
has_output
|
|
958
|
-
or any(
|
|
959
|
-
isinstance(value, str) and bool(value)
|
|
960
|
-
for value in (
|
|
961
|
-
_field(delta, "content"),
|
|
962
|
-
_field(delta, "reasoning_content"),
|
|
963
|
-
_field(delta, "refusal"),
|
|
964
|
-
_field(_field(delta, "audio"), "data"),
|
|
965
|
-
)
|
|
966
|
-
)
|
|
967
|
-
or (
|
|
968
|
-
isinstance(_field(_field(delta, "function_call"), "arguments"), str)
|
|
969
|
-
and bool(_field(_field(delta, "function_call"), "arguments"))
|
|
970
|
-
)
|
|
971
|
-
or any(
|
|
972
|
-
isinstance(arguments, str) and bool(arguments)
|
|
973
|
-
for arguments in (
|
|
974
|
-
_field(_field(tool, "function"), "arguments")
|
|
975
|
-
for tool in _sequence_items(_field(delta, "tool_calls"))
|
|
976
|
-
)
|
|
977
|
-
)
|
|
978
|
-
)
|
|
979
1087
|
choice_index = _number(_field(choice, "index"))
|
|
980
1088
|
key = (
|
|
981
1089
|
int(choice_index)
|
|
@@ -994,20 +1102,6 @@ class _InstrumentedStream:
|
|
|
994
1102
|
finish_reason = _string(_field(choice, "finish_reason"))
|
|
995
1103
|
if finish_reason is not None:
|
|
996
1104
|
self._finish_reasons[key] = finish_reason[:_FINISH_REASON_MAX_LENGTH]
|
|
997
|
-
if has_output:
|
|
998
|
-
self._saw_output = True
|
|
999
|
-
record_output_chunk = getattr(self._handle, "record_output_chunk", None)
|
|
1000
|
-
if callable(record_output_chunk):
|
|
1001
|
-
record_output_chunk(received_at * 1000)
|
|
1002
|
-
if not self._saw_first:
|
|
1003
|
-
self._saw_first = True
|
|
1004
|
-
self._handle.update(
|
|
1005
|
-
time_to_first_chunk_ms=(time.perf_counter() - self._started_at) * 1000,
|
|
1006
|
-
response_id=response_id,
|
|
1007
|
-
response_model=response_model,
|
|
1008
|
-
)
|
|
1009
|
-
if self._budget.accept(chunk):
|
|
1010
|
-
self._chunks.append(chunk)
|
|
1011
1105
|
|
|
1012
1106
|
def _finish(self, error: BaseException | None = None) -> None:
|
|
1013
1107
|
if self._finished:
|
|
@@ -1018,10 +1112,12 @@ class _InstrumentedStream:
|
|
|
1018
1112
|
try:
|
|
1019
1113
|
stream_chunk_builder = cast(Callable[..., Any], cast(Any, litellm).stream_chunk_builder)
|
|
1020
1114
|
rebuilt = stream_chunk_builder(self._chunks, messages=self._messages)
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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)
|
|
1025
1121
|
rebuilt_usage = fields.get("usage")
|
|
1026
1122
|
if self._usage is not None and (
|
|
1027
1123
|
rebuilt_usage is None
|
|
@@ -1069,7 +1165,10 @@ class _InstrumentedStream:
|
|
|
1069
1165
|
fields["error"] = error
|
|
1070
1166
|
if self._request_provider is None and fields.get("provider") is None:
|
|
1071
1167
|
fields["provider"] = _provider_from_error(error)
|
|
1072
|
-
|
|
1168
|
+
try:
|
|
1169
|
+
self._end(**fields)
|
|
1170
|
+
finally:
|
|
1171
|
+
_release_budget(self._budget)
|
|
1073
1172
|
|
|
1074
1173
|
def close(self) -> None:
|
|
1075
1174
|
self._finish()
|
|
@@ -1092,6 +1191,77 @@ class _InstrumentedStream:
|
|
|
1092
1191
|
await _maybe_await(close())
|
|
1093
1192
|
|
|
1094
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
|
+
|
|
1095
1265
|
class _InstrumentedResponsesStream:
|
|
1096
1266
|
def __init__(
|
|
1097
1267
|
self,
|
|
@@ -1122,6 +1292,8 @@ class _InstrumentedResponsesStream:
|
|
|
1122
1292
|
self._budget = _stream_capture_budget()
|
|
1123
1293
|
self._charges: dict[object, tuple[int, int]] = {}
|
|
1124
1294
|
self._annotation_charge_keys: dict[object, dict[int, object]] = {}
|
|
1295
|
+
self._reported = False
|
|
1296
|
+
self._record_failed = False
|
|
1125
1297
|
|
|
1126
1298
|
def __getattr__(self, name: str) -> Any:
|
|
1127
1299
|
return getattr(self._inner, name)
|
|
@@ -1143,7 +1315,7 @@ class _InstrumentedResponsesStream:
|
|
|
1143
1315
|
except BaseException:
|
|
1144
1316
|
pass
|
|
1145
1317
|
raise
|
|
1146
|
-
self.
|
|
1318
|
+
self._record_guarded(event, received_at)
|
|
1147
1319
|
return event
|
|
1148
1320
|
|
|
1149
1321
|
def __aiter__(self) -> AsyncIterator[Any]:
|
|
@@ -1163,7 +1335,7 @@ class _InstrumentedResponsesStream:
|
|
|
1163
1335
|
except BaseException:
|
|
1164
1336
|
pass
|
|
1165
1337
|
raise
|
|
1166
|
-
self.
|
|
1338
|
+
self._record_guarded(event, received_at)
|
|
1167
1339
|
return event
|
|
1168
1340
|
|
|
1169
1341
|
def __enter__(self) -> _InstrumentedResponsesStream:
|
|
@@ -1258,10 +1430,12 @@ class _InstrumentedResponsesStream:
|
|
|
1258
1430
|
def _materialize_partial(self, value: dict[str, Any]) -> dict[str, Any]:
|
|
1259
1431
|
if "_field" not in value:
|
|
1260
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.
|
|
1261
1435
|
field = cast(str, value["_field"])
|
|
1262
|
-
fragments = cast(
|
|
1436
|
+
fragments = cast(_DeltaBuffer | StringIO, value["_fragments"])
|
|
1263
1437
|
return {key: item for key, item in value.items() if key not in {"_field", "_fragments"}} | {
|
|
1264
|
-
field:
|
|
1438
|
+
field: fragments.getvalue()
|
|
1265
1439
|
}
|
|
1266
1440
|
|
|
1267
1441
|
def _set_completed_item(self, key: object, value: Any) -> None:
|
|
@@ -1306,7 +1480,7 @@ class _InstrumentedResponsesStream:
|
|
|
1306
1480
|
budget.bytes_used = max(0, self._budget.bytes_used - removed_bytes)
|
|
1307
1481
|
budget.items_used = max(0, self._budget.items_used - removed_items)
|
|
1308
1482
|
before = (budget.bytes_used, budget.items_used)
|
|
1309
|
-
if not budget.accept(value):
|
|
1483
|
+
if not budget.accept(value) or not _adopt_budget(self._budget, budget):
|
|
1310
1484
|
return False
|
|
1311
1485
|
for key in keys:
|
|
1312
1486
|
self._charges.pop(key, None)
|
|
@@ -1314,7 +1488,6 @@ class _InstrumentedResponsesStream:
|
|
|
1314
1488
|
budget.bytes_used - before[0],
|
|
1315
1489
|
budget.items_used - before[1],
|
|
1316
1490
|
)
|
|
1317
|
-
self._budget = budget
|
|
1318
1491
|
self._budget.truncated = bool(self._truncated_items) or self._truncation_overflow
|
|
1319
1492
|
return True
|
|
1320
1493
|
|
|
@@ -1337,7 +1510,7 @@ class _InstrumentedResponsesStream:
|
|
|
1337
1510
|
budget.items_used = max(0, self._budget.items_used - removed_items)
|
|
1338
1511
|
before = (budget.bytes_used, budget.items_used)
|
|
1339
1512
|
converted, budget = _bounded_responses_native(value, budget=budget)
|
|
1340
|
-
if converted is None or budget.truncated:
|
|
1513
|
+
if converted is None or budget.truncated or not _adopt_budget(self._budget, budget):
|
|
1341
1514
|
return False, None
|
|
1342
1515
|
for key in keys:
|
|
1343
1516
|
self._charges.pop(key, None)
|
|
@@ -1345,7 +1518,6 @@ class _InstrumentedResponsesStream:
|
|
|
1345
1518
|
budget.bytes_used - before[0],
|
|
1346
1519
|
budget.items_used - before[1],
|
|
1347
1520
|
)
|
|
1348
|
-
self._budget = budget
|
|
1349
1521
|
self._budget.truncated = bool(self._truncated_items) or self._truncation_overflow
|
|
1350
1522
|
return True, converted
|
|
1351
1523
|
|
|
@@ -1356,12 +1528,14 @@ class _InstrumentedResponsesStream:
|
|
|
1356
1528
|
self._budget.truncated = True
|
|
1357
1529
|
return False
|
|
1358
1530
|
if storage_key not in self._charges:
|
|
1359
|
-
candidate = self._materialize_partial({**target, "_fragments":
|
|
1531
|
+
candidate = self._materialize_partial({**target, "_fragments": StringIO(delta)})
|
|
1360
1532
|
return self._replace_charge([], candidate, storage_key)
|
|
1361
1533
|
if self._budget.items_used >= self._budget.max_items:
|
|
1362
1534
|
self._budget.truncated = True
|
|
1363
1535
|
return False
|
|
1364
|
-
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
|
|
1365
1539
|
self._budget.items_used += 1
|
|
1366
1540
|
charged_bytes, charged_items = self._charges[storage_key]
|
|
1367
1541
|
self._charges[storage_key] = (charged_bytes + delta_bytes, charged_items + 1)
|
|
@@ -1389,16 +1563,16 @@ class _InstrumentedResponsesStream:
|
|
|
1389
1563
|
keys = self._truncated_item_keys[owner]
|
|
1390
1564
|
if key in keys:
|
|
1391
1565
|
return
|
|
1392
|
-
if self._truncated_key_count <
|
|
1566
|
+
if self._truncated_key_count < _STREAM_CAPTURE_MAX_ITEMS:
|
|
1393
1567
|
keys.add(key)
|
|
1394
1568
|
self._truncated_key_count += 1
|
|
1395
1569
|
else:
|
|
1396
1570
|
self._truncation_overflow = True
|
|
1397
1571
|
return
|
|
1398
|
-
if self._retained_item_count <
|
|
1572
|
+
if self._retained_item_count < _STREAM_CAPTURE_MAX_ITEMS:
|
|
1399
1573
|
self._truncated_items.add(owner)
|
|
1400
1574
|
self._retained_item_count += 1
|
|
1401
|
-
if self._truncated_key_count <
|
|
1575
|
+
if self._truncated_key_count < _STREAM_CAPTURE_MAX_ITEMS:
|
|
1402
1576
|
self._truncated_item_keys[owner] = {key}
|
|
1403
1577
|
self._truncated_key_count += 1
|
|
1404
1578
|
else:
|
|
@@ -1423,16 +1597,53 @@ class _InstrumentedResponsesStream:
|
|
|
1423
1597
|
if not keys and not self._truncation_overflow:
|
|
1424
1598
|
self._resolve_truncated(item_key)
|
|
1425
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
|
+
|
|
1426
1633
|
def _record(self, event: Any, received_at: float) -> None:
|
|
1427
1634
|
event_type = _string(_field(event, "type"))
|
|
1428
1635
|
response = _field(event, "response")
|
|
1429
1636
|
if not self._saw_first:
|
|
1430
1637
|
self._saw_first = True
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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)
|
|
1436
1647
|
if event_type in {
|
|
1437
1648
|
"response.output_item.added",
|
|
1438
1649
|
"response.content_part.added",
|
|
@@ -1484,7 +1695,7 @@ class _InstrumentedResponsesStream:
|
|
|
1484
1695
|
**(target or {}),
|
|
1485
1696
|
"type": "output_text" if field == "text" else "refusal",
|
|
1486
1697
|
"_field": field,
|
|
1487
|
-
"_fragments":
|
|
1698
|
+
"_fragments": _DeltaBuffer(),
|
|
1488
1699
|
}
|
|
1489
1700
|
elif event_type in {
|
|
1490
1701
|
"response.reasoning_summary_text.delta",
|
|
@@ -1513,7 +1724,7 @@ class _InstrumentedResponsesStream:
|
|
|
1513
1724
|
**(target or {}),
|
|
1514
1725
|
"type": kind,
|
|
1515
1726
|
"_field": field,
|
|
1516
|
-
"_fragments":
|
|
1727
|
+
"_fragments": _DeltaBuffer(),
|
|
1517
1728
|
}
|
|
1518
1729
|
elif event_type in {
|
|
1519
1730
|
"response.function_call_arguments.delta",
|
|
@@ -1535,7 +1746,7 @@ class _InstrumentedResponsesStream:
|
|
|
1535
1746
|
**(seeded if isinstance(seeded, dict) else {}),
|
|
1536
1747
|
"type": event_kind[0],
|
|
1537
1748
|
"_field": event_kind[1],
|
|
1538
|
-
"_fragments":
|
|
1749
|
+
"_fragments": _DeltaBuffer(),
|
|
1539
1750
|
}
|
|
1540
1751
|
item_id = _string(_field(event, "item_id"))
|
|
1541
1752
|
if item_id is not None:
|
|
@@ -1545,12 +1756,13 @@ class _InstrumentedResponsesStream:
|
|
|
1545
1756
|
if value is not None:
|
|
1546
1757
|
target[field] = value
|
|
1547
1758
|
if target is not None:
|
|
1548
|
-
|
|
1759
|
+
# Every target above is created with a _DeltaBuffer under "_fragments".
|
|
1760
|
+
fragments = cast(_DeltaBuffer, target["_fragments"])
|
|
1549
1761
|
accepted = not self._delta_rejected_before(storage_key) and self._append_delta(
|
|
1550
1762
|
storage_key, target, delta
|
|
1551
1763
|
)
|
|
1552
1764
|
if accepted:
|
|
1553
|
-
fragments.
|
|
1765
|
+
fragments.write(delta)
|
|
1554
1766
|
self._remember_item_index(event, item_key)
|
|
1555
1767
|
if event_type in {
|
|
1556
1768
|
"response.function_call_arguments.delta",
|
|
@@ -1714,38 +1926,83 @@ class _InstrumentedResponsesStream:
|
|
|
1714
1926
|
if self._finished:
|
|
1715
1927
|
return
|
|
1716
1928
|
self._finished = True
|
|
1717
|
-
fields =
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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
|
|
1722
1944
|
if response is not None and self._capture_output:
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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)
|
|
1735
1979
|
self._budget.truncated = True
|
|
1980
|
+
self._report(exc)
|
|
1736
1981
|
if "output" not in fields:
|
|
1737
|
-
|
|
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)
|
|
1738
1990
|
if partial_output:
|
|
1739
1991
|
fields["output"] = partial_output
|
|
1740
1992
|
if self._budget.truncated or response is None or error is not None:
|
|
1741
1993
|
_mark_capture_incomplete(fields, self._mask_output_when_incomplete)
|
|
1994
|
+
elif fields_lost:
|
|
1995
|
+
_mark_capture_incomplete(fields, False)
|
|
1742
1996
|
if self._request_provider is not None:
|
|
1743
1997
|
fields.pop("provider", None)
|
|
1744
1998
|
if error is not None:
|
|
1745
1999
|
fields["error"] = error
|
|
1746
2000
|
if self._request_provider is None:
|
|
1747
2001
|
fields["provider"] = _provider_from_error(error)
|
|
1748
|
-
|
|
2002
|
+
try:
|
|
2003
|
+
self._end(**fields)
|
|
2004
|
+
finally:
|
|
2005
|
+
_release_budget(self._budget)
|
|
1749
2006
|
|
|
1750
2007
|
def close(self) -> None:
|
|
1751
2008
|
self._finish()
|
|
@@ -1849,7 +2106,20 @@ def _wrap_sync(
|
|
|
1849
2106
|
elif op == "rerank":
|
|
1850
2107
|
active_mapper = _rerank_response_without_output
|
|
1851
2108
|
try:
|
|
1852
|
-
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
|
|
1853
2123
|
except BaseException as exc:
|
|
1854
2124
|
end(
|
|
1855
2125
|
error=exc,
|
|
@@ -1909,7 +2179,20 @@ def _wrap_async(
|
|
|
1909
2179
|
elif op == "rerank":
|
|
1910
2180
|
active_mapper = _rerank_response_without_output
|
|
1911
2181
|
try:
|
|
1912
|
-
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
|
|
1913
2196
|
except BaseException as exc:
|
|
1914
2197
|
end(
|
|
1915
2198
|
error=exc,
|
|
File without changes
|
{telemetry_dev_litellm-0.2.1 → telemetry_dev_litellm-0.2.3}/src/telemetry_dev_litellm/py.typed
RENAMED
|
File without changes
|