mirascope 2.0.0a1__py3-none-any.whl → 2.0.0a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. mirascope/__init__.py +2 -2
  2. mirascope/api/__init__.py +6 -0
  3. mirascope/api/_generated/README.md +207 -0
  4. mirascope/api/_generated/__init__.py +85 -0
  5. mirascope/api/_generated/client.py +155 -0
  6. mirascope/api/_generated/core/__init__.py +52 -0
  7. mirascope/api/_generated/core/api_error.py +23 -0
  8. mirascope/api/_generated/core/client_wrapper.py +58 -0
  9. mirascope/api/_generated/core/datetime_utils.py +30 -0
  10. mirascope/api/_generated/core/file.py +70 -0
  11. mirascope/api/_generated/core/force_multipart.py +16 -0
  12. mirascope/api/_generated/core/http_client.py +619 -0
  13. mirascope/api/_generated/core/http_response.py +55 -0
  14. mirascope/api/_generated/core/jsonable_encoder.py +102 -0
  15. mirascope/api/_generated/core/pydantic_utilities.py +310 -0
  16. mirascope/api/_generated/core/query_encoder.py +60 -0
  17. mirascope/api/_generated/core/remove_none_from_dict.py +11 -0
  18. mirascope/api/_generated/core/request_options.py +35 -0
  19. mirascope/api/_generated/core/serialization.py +282 -0
  20. mirascope/api/_generated/docs/__init__.py +4 -0
  21. mirascope/api/_generated/docs/client.py +95 -0
  22. mirascope/api/_generated/docs/raw_client.py +132 -0
  23. mirascope/api/_generated/environment.py +9 -0
  24. mirascope/api/_generated/errors/__init__.py +7 -0
  25. mirascope/api/_generated/errors/bad_request_error.py +15 -0
  26. mirascope/api/_generated/health/__init__.py +7 -0
  27. mirascope/api/_generated/health/client.py +96 -0
  28. mirascope/api/_generated/health/raw_client.py +129 -0
  29. mirascope/api/_generated/health/types/__init__.py +8 -0
  30. mirascope/api/_generated/health/types/health_check_response.py +24 -0
  31. mirascope/api/_generated/health/types/health_check_response_status.py +5 -0
  32. mirascope/api/_generated/reference.md +167 -0
  33. mirascope/api/_generated/traces/__init__.py +55 -0
  34. mirascope/api/_generated/traces/client.py +162 -0
  35. mirascope/api/_generated/traces/raw_client.py +168 -0
  36. mirascope/api/_generated/traces/types/__init__.py +95 -0
  37. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item.py +36 -0
  38. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource.py +31 -0
  39. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item.py +25 -0
  40. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value.py +54 -0
  41. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_array_value.py +23 -0
  42. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value.py +28 -0
  43. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value_values_item.py +24 -0
  44. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item.py +35 -0
  45. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope.py +35 -0
  46. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item.py +27 -0
  47. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value.py +54 -0
  48. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_array_value.py +23 -0
  49. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value.py +28 -0
  50. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value_values_item.py +24 -0
  51. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item.py +60 -0
  52. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item.py +29 -0
  53. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value.py +54 -0
  54. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_array_value.py +23 -0
  55. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value.py +28 -0
  56. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value_values_item.py +24 -0
  57. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_status.py +24 -0
  58. mirascope/api/_generated/traces/types/traces_create_response.py +27 -0
  59. mirascope/api/_generated/traces/types/traces_create_response_partial_success.py +28 -0
  60. mirascope/api/_generated/types/__init__.py +21 -0
  61. mirascope/api/_generated/types/http_api_decode_error.py +31 -0
  62. mirascope/api/_generated/types/http_api_decode_error_tag.py +5 -0
  63. mirascope/api/_generated/types/issue.py +44 -0
  64. mirascope/api/_generated/types/issue_tag.py +17 -0
  65. mirascope/api/_generated/types/property_key.py +7 -0
  66. mirascope/api/_generated/types/property_key_tag.py +29 -0
  67. mirascope/api/_generated/types/property_key_tag_tag.py +5 -0
  68. mirascope/api/client.py +255 -0
  69. mirascope/api/settings.py +81 -0
  70. mirascope/llm/__init__.py +41 -11
  71. mirascope/llm/calls/calls.py +81 -57
  72. mirascope/llm/calls/decorator.py +121 -115
  73. mirascope/llm/content/__init__.py +3 -2
  74. mirascope/llm/context/_utils.py +19 -6
  75. mirascope/llm/exceptions.py +30 -16
  76. mirascope/llm/formatting/_utils.py +9 -5
  77. mirascope/llm/formatting/format.py +2 -2
  78. mirascope/llm/formatting/from_call_args.py +2 -2
  79. mirascope/llm/messages/message.py +13 -5
  80. mirascope/llm/models/__init__.py +2 -2
  81. mirascope/llm/models/models.py +189 -81
  82. mirascope/llm/prompts/__init__.py +13 -12
  83. mirascope/llm/prompts/_utils.py +27 -24
  84. mirascope/llm/prompts/decorator.py +133 -204
  85. mirascope/llm/prompts/prompts.py +424 -0
  86. mirascope/llm/prompts/protocols.py +25 -59
  87. mirascope/llm/providers/__init__.py +38 -0
  88. mirascope/llm/{clients → providers}/_missing_import_stubs.py +8 -6
  89. mirascope/llm/providers/anthropic/__init__.py +24 -0
  90. mirascope/llm/{clients → providers}/anthropic/_utils/decode.py +5 -4
  91. mirascope/llm/{clients → providers}/anthropic/_utils/encode.py +31 -10
  92. mirascope/llm/providers/anthropic/model_id.py +40 -0
  93. mirascope/llm/{clients/anthropic/clients.py → providers/anthropic/provider.py} +33 -418
  94. mirascope/llm/{clients → providers}/base/__init__.py +3 -3
  95. mirascope/llm/{clients → providers}/base/_utils.py +10 -7
  96. mirascope/llm/{clients/base/client.py → providers/base/base_provider.py} +255 -126
  97. mirascope/llm/providers/google/__init__.py +21 -0
  98. mirascope/llm/{clients → providers}/google/_utils/decode.py +6 -4
  99. mirascope/llm/{clients → providers}/google/_utils/encode.py +30 -24
  100. mirascope/llm/providers/google/model_id.py +28 -0
  101. mirascope/llm/providers/google/provider.py +438 -0
  102. mirascope/llm/providers/load_provider.py +48 -0
  103. mirascope/llm/providers/mlx/__init__.py +24 -0
  104. mirascope/llm/providers/mlx/_utils.py +107 -0
  105. mirascope/llm/providers/mlx/encoding/__init__.py +8 -0
  106. mirascope/llm/providers/mlx/encoding/base.py +69 -0
  107. mirascope/llm/providers/mlx/encoding/transformers.py +131 -0
  108. mirascope/llm/providers/mlx/mlx.py +237 -0
  109. mirascope/llm/providers/mlx/model_id.py +17 -0
  110. mirascope/llm/providers/mlx/provider.py +411 -0
  111. mirascope/llm/providers/model_id.py +16 -0
  112. mirascope/llm/providers/openai/__init__.py +6 -0
  113. mirascope/llm/providers/openai/completions/__init__.py +20 -0
  114. mirascope/llm/{clients/openai/responses → providers/openai/completions}/_utils/__init__.py +2 -0
  115. mirascope/llm/{clients → providers}/openai/completions/_utils/decode.py +5 -3
  116. mirascope/llm/{clients → providers}/openai/completions/_utils/encode.py +33 -23
  117. mirascope/llm/providers/openai/completions/provider.py +456 -0
  118. mirascope/llm/providers/openai/model_id.py +31 -0
  119. mirascope/llm/providers/openai/model_info.py +246 -0
  120. mirascope/llm/providers/openai/provider.py +386 -0
  121. mirascope/llm/providers/openai/responses/__init__.py +21 -0
  122. mirascope/llm/{clients → providers}/openai/responses/_utils/decode.py +5 -3
  123. mirascope/llm/{clients → providers}/openai/responses/_utils/encode.py +28 -17
  124. mirascope/llm/providers/openai/responses/provider.py +470 -0
  125. mirascope/llm/{clients → providers}/openai/shared/_utils.py +7 -3
  126. mirascope/llm/providers/provider_id.py +13 -0
  127. mirascope/llm/providers/provider_registry.py +167 -0
  128. mirascope/llm/responses/base_response.py +10 -5
  129. mirascope/llm/responses/base_stream_response.py +10 -5
  130. mirascope/llm/responses/response.py +24 -13
  131. mirascope/llm/responses/root_response.py +7 -12
  132. mirascope/llm/responses/stream_response.py +35 -23
  133. mirascope/llm/tools/__init__.py +9 -2
  134. mirascope/llm/tools/_utils.py +12 -3
  135. mirascope/llm/tools/decorator.py +10 -10
  136. mirascope/llm/tools/protocols.py +4 -4
  137. mirascope/llm/tools/tool_schema.py +44 -9
  138. mirascope/llm/tools/tools.py +12 -11
  139. mirascope/ops/__init__.py +156 -0
  140. mirascope/ops/_internal/__init__.py +5 -0
  141. mirascope/ops/_internal/closure.py +1118 -0
  142. mirascope/ops/_internal/configuration.py +126 -0
  143. mirascope/ops/_internal/context.py +76 -0
  144. mirascope/ops/_internal/exporters/__init__.py +26 -0
  145. mirascope/ops/_internal/exporters/exporters.py +342 -0
  146. mirascope/ops/_internal/exporters/processors.py +104 -0
  147. mirascope/ops/_internal/exporters/types.py +165 -0
  148. mirascope/ops/_internal/exporters/utils.py +29 -0
  149. mirascope/ops/_internal/instrumentation/__init__.py +8 -0
  150. mirascope/ops/_internal/instrumentation/llm/__init__.py +8 -0
  151. mirascope/ops/_internal/instrumentation/llm/encode.py +238 -0
  152. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/__init__.py +38 -0
  153. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_input_messages.py +31 -0
  154. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_output_messages.py +38 -0
  155. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_system_instructions.py +18 -0
  156. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/shared.py +100 -0
  157. mirascope/ops/_internal/instrumentation/llm/llm.py +1288 -0
  158. mirascope/ops/_internal/propagation.py +198 -0
  159. mirascope/ops/_internal/protocols.py +51 -0
  160. mirascope/ops/_internal/session.py +139 -0
  161. mirascope/ops/_internal/spans.py +232 -0
  162. mirascope/ops/_internal/traced_calls.py +371 -0
  163. mirascope/ops/_internal/traced_functions.py +394 -0
  164. mirascope/ops/_internal/tracing.py +276 -0
  165. mirascope/ops/_internal/types.py +13 -0
  166. mirascope/ops/_internal/utils.py +75 -0
  167. mirascope/ops/_internal/versioned_calls.py +512 -0
  168. mirascope/ops/_internal/versioned_functions.py +346 -0
  169. mirascope/ops/_internal/versioning.py +303 -0
  170. mirascope/ops/exceptions.py +21 -0
  171. {mirascope-2.0.0a1.dist-info → mirascope-2.0.0a3.dist-info}/METADATA +77 -1
  172. mirascope-2.0.0a3.dist-info/RECORD +206 -0
  173. {mirascope-2.0.0a1.dist-info → mirascope-2.0.0a3.dist-info}/WHEEL +1 -1
  174. mirascope/graphs/__init__.py +0 -22
  175. mirascope/graphs/finite_state_machine.py +0 -625
  176. mirascope/llm/agents/__init__.py +0 -15
  177. mirascope/llm/agents/agent.py +0 -97
  178. mirascope/llm/agents/agent_template.py +0 -45
  179. mirascope/llm/agents/decorator.py +0 -176
  180. mirascope/llm/calls/base_call.py +0 -33
  181. mirascope/llm/clients/__init__.py +0 -34
  182. mirascope/llm/clients/anthropic/__init__.py +0 -25
  183. mirascope/llm/clients/anthropic/model_ids.py +0 -8
  184. mirascope/llm/clients/google/__init__.py +0 -20
  185. mirascope/llm/clients/google/clients.py +0 -853
  186. mirascope/llm/clients/google/model_ids.py +0 -15
  187. mirascope/llm/clients/openai/__init__.py +0 -25
  188. mirascope/llm/clients/openai/completions/__init__.py +0 -28
  189. mirascope/llm/clients/openai/completions/_utils/model_features.py +0 -81
  190. mirascope/llm/clients/openai/completions/clients.py +0 -833
  191. mirascope/llm/clients/openai/completions/model_ids.py +0 -8
  192. mirascope/llm/clients/openai/responses/__init__.py +0 -26
  193. mirascope/llm/clients/openai/responses/_utils/model_features.py +0 -87
  194. mirascope/llm/clients/openai/responses/clients.py +0 -832
  195. mirascope/llm/clients/openai/responses/model_ids.py +0 -8
  196. mirascope/llm/clients/providers.py +0 -175
  197. mirascope-2.0.0a1.dist-info/RECORD +0 -102
  198. /mirascope/llm/{clients → providers}/anthropic/_utils/__init__.py +0 -0
  199. /mirascope/llm/{clients → providers}/base/kwargs.py +0 -0
  200. /mirascope/llm/{clients → providers}/base/params.py +0 -0
  201. /mirascope/llm/{clients → providers}/google/_utils/__init__.py +0 -0
  202. /mirascope/llm/{clients → providers}/google/message.py +0 -0
  203. /mirascope/llm/{clients/openai/completions → providers/openai/responses}/_utils/__init__.py +0 -0
  204. /mirascope/llm/{clients → providers}/openai/shared/__init__.py +0 -0
  205. {mirascope-2.0.0a1.dist-info → mirascope-2.0.0a3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1288 @@
1
+ """OpenTelemetry GenAI instrumentation for `mirascope.llm`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import weakref
7
+ from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence
8
+ from contextlib import AbstractContextManager, contextmanager
9
+ from dataclasses import dataclass
10
+ from functools import wraps
11
+ from types import TracebackType
12
+ from typing import TYPE_CHECKING, Protocol, TypeAlias, overload, runtime_checkable
13
+ from typing_extensions import TypeIs
14
+
15
+ from opentelemetry.semconv._incubating.attributes import (
16
+ gen_ai_attributes as GenAIAttributes,
17
+ )
18
+ from opentelemetry.semconv.attributes import (
19
+ error_attributes as ErrorAttributes,
20
+ )
21
+
22
+ from .....llm.context import Context, DepsT
23
+ from .....llm.formatting import Format, FormattableT
24
+ from .....llm.formatting._utils import create_tool_schema
25
+ from .....llm.messages import Message
26
+ from .....llm.models import Model
27
+ from .....llm.providers import Params, ProviderId
28
+ from .....llm.providers.model_id import ModelId
29
+ from .....llm.responses import (
30
+ AsyncContextResponse,
31
+ AsyncContextStreamResponse,
32
+ AsyncResponse,
33
+ ContextResponse,
34
+ ContextStreamResponse,
35
+ Response,
36
+ StreamResponse,
37
+ StreamResponseChunk,
38
+ )
39
+ from .....llm.responses.root_response import RootResponse
40
+ from .....llm.tools import (
41
+ AnyToolFn,
42
+ AnyToolSchema,
43
+ AsyncContextTool,
44
+ AsyncContextToolkit,
45
+ AsyncTool,
46
+ AsyncToolkit,
47
+ ContextTool,
48
+ ContextToolkit,
49
+ Tool,
50
+ Toolkit,
51
+ )
52
+ from .....llm.tools.tool_schema import ToolSchema
53
+ from .....llm.tools.toolkit import BaseToolkit, ToolkitT
54
+ from .....llm.types import Jsonable
55
+ from ...configuration import (
56
+ get_tracer,
57
+ )
58
+ from ...utils import json_dumps
59
+ from .encode import (
60
+ map_finish_reason,
61
+ snapshot_from_root_response,
62
+ split_request_messages,
63
+ )
64
+
65
+ # TODO: refactor alongside all other import error handling to provide nice error messages
66
+ try:
67
+ from opentelemetry import trace as otel_trace
68
+ from opentelemetry.trace import SpanKind, Status, StatusCode
69
+ except ImportError: # pragma: no cover
70
+ if not TYPE_CHECKING:
71
+ otel_trace = None
72
+ SpanKind = None
73
+ StatusCode = None
74
+ Status = None
75
+
76
+ if TYPE_CHECKING:
77
+ from opentelemetry import trace as otel_trace
78
+ from opentelemetry.trace import (
79
+ Span,
80
+ SpanKind,
81
+ Status,
82
+ StatusCode,
83
+ Tracer,
84
+ )
85
+ from opentelemetry.util.types import AttributeValue
86
+
87
+ from . import gen_ai_types
88
+ else:
89
+ AttributeValue = None
90
+ Span = None
91
+ Tracer = None
92
+
93
+
94
+ ToolsParam: TypeAlias = (
95
+ Sequence[ToolSchema[AnyToolFn]] | BaseToolkit[AnyToolSchema] | None
96
+ )
97
+ FormatParam: TypeAlias = Format[FormattableT] | None
98
+ ParamsDict: TypeAlias = Mapping[str, str | int | float | bool | Sequence[str] | None]
99
+ SpanAttributes: TypeAlias = Mapping[str, AttributeValue]
100
+ AttributeSetter: TypeAlias = Callable[[str, AttributeValue], None]
101
+ ParamsValue = str | int | float | bool | Sequence[str] | None
102
+
103
+
104
+ @dataclass(slots=True)
105
+ class SpanContext:
106
+ """Container for a GenAI span and its associated dropped parameters."""
107
+
108
+ span: Span | None
109
+ """The active span, if any."""
110
+
111
+ dropped_params: dict[str, Jsonable]
112
+ """Parameters that could not be recorded as span attributes."""
113
+
114
+
115
+ @runtime_checkable
116
+ class Identifiable(Protocol):
117
+ """Protocol for objects with an optional ID attribute."""
118
+
119
+ id: str | None
120
+ """Optional ID attribute."""
121
+
122
+
123
+ _PARAM_ATTRIBUTE_MAP: Mapping[str, str] = {
124
+ "temperature": GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE,
125
+ "max_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
126
+ "max_output_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
127
+ "max_completion_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
128
+ "top_p": GenAIAttributes.GEN_AI_REQUEST_TOP_P,
129
+ "top_k": GenAIAttributes.GEN_AI_REQUEST_TOP_K,
130
+ "frequency_penalty": GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY,
131
+ "presence_penalty": GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY,
132
+ "seed": GenAIAttributes.GEN_AI_REQUEST_SEED,
133
+ "stop_sequences": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES,
134
+ "stop": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES,
135
+ "n": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT,
136
+ "choice_count": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT,
137
+ }
138
+
139
+
140
+ def _record_exception(span: Span, exc: Exception) -> None:
141
+ """Record exception details on span following OpenTelemetry semantic conventions."""
142
+ span.record_exception(exc)
143
+ span.set_attribute(ErrorAttributes.ERROR_TYPE, exc.__class__.__name__)
144
+ error_message = str(exc)
145
+ if error_message:
146
+ span.set_attribute("error.message", error_message)
147
+ span.set_status(Status(StatusCode.ERROR, error_message))
148
+
149
+
150
+ def _infer_output_type(format_obj: FormatParam) -> str:
151
+ """Infer the GenAI output type from the format parameter."""
152
+ if format_obj is None:
153
+ return GenAIAttributes.GenAiOutputTypeValues.TEXT.value
154
+ return GenAIAttributes.GenAiOutputTypeValues.JSON.value
155
+
156
+
157
+ def _apply_param_attributes(
158
+ attrs: dict[str, AttributeValue], params: ParamsDict
159
+ ) -> None:
160
+ """Apply model parameters as span attributes."""
161
+ if not params:
162
+ return
163
+
164
+ for key, attr_key in _PARAM_ATTRIBUTE_MAP.items():
165
+ if key not in params:
166
+ continue
167
+ value = params[key]
168
+ if value is None:
169
+ continue
170
+ if key in {"stop", "stop_sequences"} and isinstance(value, str):
171
+ value = [value]
172
+ attrs[attr_key] = value
173
+
174
+
175
+ def _set_json_attribute(
176
+ setter: AttributeSetter,
177
+ *,
178
+ key: str,
179
+ payload: (
180
+ gen_ai_types.SystemInstructions
181
+ | gen_ai_types.InputMessages
182
+ | gen_ai_types.OutputMessages
183
+ ),
184
+ ) -> None:
185
+ """Assign a JSON-serialized attribute to a span."""
186
+ if not payload:
187
+ return
188
+ setter(key, json_dumps(payload))
189
+
190
+
191
+ def _assign_request_message_attributes(
192
+ setter: AttributeSetter,
193
+ *,
194
+ messages: Sequence[Message],
195
+ ) -> None:
196
+ """Assign request message attributes to a span."""
197
+ system_payload, input_payload = split_request_messages(messages)
198
+ _set_json_attribute(
199
+ setter,
200
+ key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
201
+ payload=system_payload,
202
+ )
203
+ _set_json_attribute(
204
+ setter,
205
+ key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
206
+ payload=input_payload,
207
+ )
208
+
209
+
210
+ def _collect_tool_schemas(
211
+ tools: Sequence[ToolSchema[AnyToolFn]] | BaseToolkit[AnyToolSchema],
212
+ ) -> list[ToolSchema[AnyToolFn]]:
213
+ """Collect ToolSchema instances from a tools parameter."""
214
+ iterable = list(tools.tools) if isinstance(tools, BaseToolkit) else list(tools)
215
+ schemas: list[ToolSchema[AnyToolFn]] = []
216
+ for tool in iterable:
217
+ if isinstance(tool, ToolSchema):
218
+ schemas.append(tool)
219
+ return schemas
220
+
221
+
222
+ def _serialize_tool_definitions(
223
+ tools: ToolsParam,
224
+ format: FormatParam = None,
225
+ ) -> str | None:
226
+ """Serialize tool definitions to JSON for span attributes."""
227
+ if tools is None:
228
+ tool_schemas: list[ToolSchema[AnyToolFn]] = []
229
+ else:
230
+ tool_schemas = _collect_tool_schemas(tools)
231
+
232
+ if isinstance(format, Format) and format.mode == "tool":
233
+ tool_schemas.append(create_tool_schema(format))
234
+
235
+ if not tool_schemas:
236
+ return None
237
+ definitions: list[dict[str, str | int | bool | dict[str, str | int | bool]]] = []
238
+ for tool in tool_schemas:
239
+ definitions.append(
240
+ {
241
+ "name": tool.name,
242
+ "description": tool.description,
243
+ "strict": tool.strict,
244
+ "parameters": tool.parameters.model_dump(by_alias=True, mode="json"),
245
+ }
246
+ )
247
+ return json_dumps(definitions)
248
+
249
+
250
+ def _build_request_attributes(
251
+ *,
252
+ operation: str,
253
+ provider: ProviderId,
254
+ model_id: ModelId,
255
+ messages: Sequence[Message],
256
+ tools: ToolsParam,
257
+ format: FormatParam,
258
+ params: ParamsDict,
259
+ ) -> dict[str, AttributeValue]:
260
+ """Build GenAI request attributes for a span."""
261
+ attrs: dict[str, AttributeValue] = {
262
+ GenAIAttributes.GEN_AI_OPERATION_NAME: operation,
263
+ GenAIAttributes.GEN_AI_PROVIDER_NAME: provider,
264
+ GenAIAttributes.GEN_AI_REQUEST_MODEL: model_id,
265
+ GenAIAttributes.GEN_AI_OUTPUT_TYPE: _infer_output_type(format),
266
+ }
267
+ _apply_param_attributes(attrs, params)
268
+
269
+ _assign_request_message_attributes(
270
+ attrs.__setitem__,
271
+ messages=messages,
272
+ )
273
+
274
+ tool_payload = _serialize_tool_definitions(tools, format=format)
275
+ if tool_payload:
276
+ # The incubating semconv module does not yet expose a constant for this key.
277
+ attrs["gen_ai.tool.definitions"] = tool_payload
278
+
279
+ return attrs
280
+
281
+
282
+ def _extract_response_id(
283
+ raw: dict[str, str | int] | str | Identifiable | None,
284
+ ) -> str | None:
285
+ """Extract response ID from raw response data."""
286
+ if isinstance(raw, dict):
287
+ for key in ("id", "response_id", "responseId"):
288
+ value = raw.get(key)
289
+ if isinstance(value, str):
290
+ return value
291
+ elif isinstance(raw, Identifiable):
292
+ return raw.id
293
+ return None
294
+
295
+
296
+ def _attach_response(
297
+ span: Span,
298
+ response: RootResponse[ToolkitT, FormattableT | None],
299
+ *,
300
+ request_messages: Sequence[Message],
301
+ ) -> None:
302
+ """Attach response attributes to a GenAI span."""
303
+ span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.model_id)
304
+ span.set_attribute(
305
+ GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS,
306
+ [map_finish_reason(response.finish_reason)],
307
+ )
308
+ response_id = _extract_response_id(getattr(response, "raw", None))
309
+ if response_id:
310
+ span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_ID, response_id)
311
+
312
+ snapshot = snapshot_from_root_response(
313
+ response,
314
+ request_messages=request_messages,
315
+ )
316
+ _set_json_attribute(
317
+ span.set_attribute,
318
+ key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
319
+ payload=snapshot.system_instructions,
320
+ )
321
+ _set_json_attribute(
322
+ span.set_attribute,
323
+ key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
324
+ payload=snapshot.inputs,
325
+ )
326
+ _set_json_attribute(
327
+ span.set_attribute,
328
+ key=GenAIAttributes.GEN_AI_OUTPUT_MESSAGES,
329
+ payload=snapshot.outputs,
330
+ )
331
+ # TODO: Emit gen_ai.usage metrics once Response exposes provider-agnostic usage fields.
332
+
333
+
334
+ _ORIGINAL_MODEL_CALL = Model.call
335
+ _MODEL_CALL_WRAPPED = False
336
+ _ORIGINAL_MODEL_CALL_ASYNC = Model.call_async
337
+ _MODEL_CALL_ASYNC_WRAPPED = False
338
+ _ORIGINAL_MODEL_CONTEXT_CALL = Model.context_call
339
+ _MODEL_CONTEXT_CALL_WRAPPED = False
340
+ _ORIGINAL_MODEL_CONTEXT_CALL_ASYNC = Model.context_call_async
341
+ _MODEL_CONTEXT_CALL_ASYNC_WRAPPED = False
342
+ _ORIGINAL_MODEL_STREAM = Model.stream
343
+ _MODEL_STREAM_WRAPPED = False
344
+ _ORIGINAL_MODEL_CONTEXT_STREAM = Model.context_stream
345
+ _MODEL_CONTEXT_STREAM_WRAPPED = False
346
+ _ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC = Model.context_stream_async
347
+ _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED = False
348
+
349
+
350
+ def _is_supported_param_value(value: object) -> TypeIs[ParamsValue]:
351
+ """Returns True if the value can be exported as an OTEL attribute."""
352
+ if isinstance(value, str | int | float | bool) or value is None:
353
+ return True
354
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes):
355
+ return all(isinstance(item, str) for item in value)
356
+ return False
357
+
358
+
359
+ def _normalize_dropped_value(value: object) -> Jsonable:
360
+ """Returns a JSON-safe representation for unsupported param values."""
361
+ if isinstance(value, str | int | float | bool) or value is None:
362
+ return value
363
+ if isinstance(value, Mapping):
364
+ normalized: dict[str, Jsonable] = {}
365
+ for key, item in value.items():
366
+ normalized[str(key)] = _normalize_dropped_value(item)
367
+ return normalized
368
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
369
+ return [_normalize_dropped_value(item) for item in value]
370
+ try:
371
+ return str(value)
372
+ except Exception: # pragma: no cover
373
+ return f"<{type(value).__name__}>"
374
+
375
+
376
+ def _params_as_mapping(params: Params) -> tuple[ParamsDict, dict[str, Jsonable]]:
377
+ """Returns supported params and a mapping of dropped params."""
378
+ filtered: dict[str, ParamsValue] = {}
379
+ dropped: dict[str, Jsonable] = {}
380
+ for key, value in params.items():
381
+ if _is_supported_param_value(value):
382
+ filtered[key] = value
383
+ else:
384
+ dropped[key] = _normalize_dropped_value(value)
385
+ return filtered, dropped
386
+
387
+
388
+ def _record_dropped_params(
389
+ span: Span,
390
+ dropped_params: Mapping[str, Jsonable],
391
+ ) -> None:
392
+ """Emit an event with JSON-encoded params that cannot become attributes.
393
+
394
+ See https://opentelemetry.io/docs/specs/otel/common/ for the attribute type limits,
395
+ https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/ for the GenAI
396
+ guidance on recording richer payloads via events, and
397
+ https://opentelemetry.io/blog/2025/complex-attribute-types/ for the recommendation
398
+ to serialize unsupported complex types instead of dropping them outright.
399
+ """
400
+ if not dropped_params:
401
+ return None
402
+ payload = json_dumps(dropped_params)
403
+ span.add_event(
404
+ "gen_ai.request.params.untracked",
405
+ attributes={
406
+ "gen_ai.untracked_params.count": len(dropped_params),
407
+ "gen_ai.untracked_params.keys": list(dropped_params.keys()),
408
+ "gen_ai.untracked_params.json": payload,
409
+ },
410
+ )
411
+ return None
412
+
413
+
414
+ @contextmanager
415
+ def _start_model_span(
416
+ model: Model,
417
+ *,
418
+ messages: Sequence[Message],
419
+ tools: ToolsParam,
420
+ format: FormatParam,
421
+ activate: bool = True,
422
+ ) -> Iterator[SpanContext]:
423
+ """Context manager that yields a SpanContext for a model call."""
424
+ params, dropped_params = _params_as_mapping(model.params)
425
+ tracer = get_tracer()
426
+
427
+ if tracer is None or otel_trace is None:
428
+ yield SpanContext(None, dropped_params)
429
+ return
430
+
431
+ operation = GenAIAttributes.GenAiOperationNameValues.CHAT.value
432
+ attrs = _build_request_attributes(
433
+ operation=operation,
434
+ provider=model.provider_id,
435
+ model_id=model.model_id,
436
+ messages=messages,
437
+ tools=tools,
438
+ format=format,
439
+ params=params,
440
+ )
441
+ span_name = f"{operation} {model.model_id}"
442
+
443
+ if activate:
444
+ with tracer.start_as_current_span(
445
+ name=span_name,
446
+ kind=SpanKind.CLIENT,
447
+ ) as active_span:
448
+ for key, value in attrs.items():
449
+ active_span.set_attribute(key, value)
450
+ try:
451
+ yield SpanContext(active_span, dropped_params)
452
+ except Exception as exc:
453
+ _record_exception(active_span, exc)
454
+ raise
455
+ return
456
+
457
+ span = tracer.start_span(
458
+ name=span_name,
459
+ kind=SpanKind.CLIENT,
460
+ )
461
+ for key, value in attrs.items():
462
+ span.set_attribute(key, value)
463
+ try:
464
+ yield SpanContext(span, dropped_params)
465
+ except Exception as exc:
466
+ _record_exception(span, exc)
467
+ raise
468
+ finally:
469
+ span.end()
470
+
471
+
472
+ @overload
473
+ def _instrumented_model_call(
474
+ self: Model,
475
+ *,
476
+ messages: Sequence[Message],
477
+ tools: Sequence[Tool] | Toolkit | None = None,
478
+ format: None = None,
479
+ ) -> Response: ...
480
+
481
+
482
+ @overload
483
+ def _instrumented_model_call(
484
+ self: Model,
485
+ *,
486
+ messages: Sequence[Message],
487
+ tools: Sequence[Tool] | Toolkit | None = None,
488
+ format: type[FormattableT] | Format[FormattableT],
489
+ ) -> Response[FormattableT]: ...
490
+
491
+
492
+ @overload
493
+ def _instrumented_model_call(
494
+ self: Model,
495
+ *,
496
+ messages: Sequence[Message],
497
+ tools: Sequence[Tool] | Toolkit | None = None,
498
+ format: type[FormattableT] | Format[FormattableT] | None = None,
499
+ ) -> Response | Response[FormattableT]: ...
500
+
501
+
502
+ @wraps(_ORIGINAL_MODEL_CALL)
503
+ def _instrumented_model_call(
504
+ self: Model,
505
+ *,
506
+ messages: Sequence[Message],
507
+ tools: Sequence[Tool] | Toolkit | None = None,
508
+ format: FormatParam = None,
509
+ ) -> Response | Response[FormattableT]:
510
+ """Returns a GenAI-instrumented result of `Model.call`."""
511
+ with _start_model_span(
512
+ self,
513
+ messages=messages,
514
+ tools=tools,
515
+ format=format,
516
+ ) as span_ctx:
517
+ response = _ORIGINAL_MODEL_CALL(
518
+ self,
519
+ messages=messages,
520
+ tools=tools,
521
+ format=format,
522
+ )
523
+ if span_ctx.span is not None:
524
+ _attach_response(
525
+ span_ctx.span,
526
+ response,
527
+ request_messages=messages,
528
+ )
529
+ _record_dropped_params(span_ctx.span, span_ctx.dropped_params)
530
+ return response
531
+
532
+
533
+ def _wrap_model_call() -> None:
534
+ """Returns None. Replaces `Model.call` with the instrumented wrapper."""
535
+ global _MODEL_CALL_WRAPPED
536
+ if _MODEL_CALL_WRAPPED:
537
+ return
538
+ Model.call = _instrumented_model_call
539
+ _MODEL_CALL_WRAPPED = True
540
+
541
+
542
+ def _unwrap_model_call() -> None:
543
+ """Returns None. Restores the original `Model.call` implementation."""
544
+ global _MODEL_CALL_WRAPPED
545
+ if not _MODEL_CALL_WRAPPED:
546
+ return
547
+ Model.call = _ORIGINAL_MODEL_CALL
548
+ _MODEL_CALL_WRAPPED = False
549
+
550
+
551
+ @overload
552
+ async def _instrumented_model_call_async(
553
+ self: Model,
554
+ *,
555
+ messages: Sequence[Message],
556
+ tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
557
+ format: None = None,
558
+ ) -> AsyncResponse: ...
559
+
560
+
561
+ @overload
562
+ async def _instrumented_model_call_async(
563
+ self: Model,
564
+ *,
565
+ messages: Sequence[Message],
566
+ tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
567
+ format: type[FormattableT] | Format[FormattableT],
568
+ ) -> AsyncResponse[FormattableT]: ...
569
+
570
+
571
+ @overload
572
+ async def _instrumented_model_call_async(
573
+ self: Model,
574
+ *,
575
+ messages: Sequence[Message],
576
+ tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
577
+ format: type[FormattableT] | Format[FormattableT] | None = None,
578
+ ) -> AsyncResponse | AsyncResponse[FormattableT]: ...
579
+
580
+
581
+ @wraps(_ORIGINAL_MODEL_CALL_ASYNC)
582
+ async def _instrumented_model_call_async(
583
+ self: Model,
584
+ *,
585
+ messages: Sequence[Message],
586
+ tools: Sequence[AsyncTool] | AsyncToolkit | None = None,
587
+ format: FormatParam = None,
588
+ ) -> AsyncResponse | AsyncResponse[FormattableT]:
589
+ """Returns a GenAI-instrumented result of `Model.call_async`."""
590
+ with _start_model_span(
591
+ self,
592
+ messages=messages,
593
+ tools=tools,
594
+ format=format,
595
+ activate=True,
596
+ ) as span_ctx:
597
+ response = await _ORIGINAL_MODEL_CALL_ASYNC(
598
+ self,
599
+ messages=messages,
600
+ tools=tools,
601
+ format=format,
602
+ )
603
+ if span_ctx.span is not None:
604
+ _attach_response(
605
+ span_ctx.span,
606
+ response,
607
+ request_messages=messages,
608
+ )
609
+ _record_dropped_params(span_ctx.span, span_ctx.dropped_params)
610
+ return response
611
+
612
+
613
+ def _wrap_model_call_async() -> None:
614
+ """Returns None. Replaces `Model.call_async` with the instrumented wrapper."""
615
+ global _MODEL_CALL_ASYNC_WRAPPED
616
+ if _MODEL_CALL_ASYNC_WRAPPED:
617
+ return
618
+ Model.call_async = _instrumented_model_call_async
619
+ _MODEL_CALL_ASYNC_WRAPPED = True
620
+
621
+
622
+ def _unwrap_model_call_async() -> None:
623
+ """Returns None. Restores the original `Model.call_async` implementation."""
624
+ global _MODEL_CALL_ASYNC_WRAPPED
625
+ if not _MODEL_CALL_ASYNC_WRAPPED:
626
+ return
627
+ Model.call_async = _ORIGINAL_MODEL_CALL_ASYNC
628
+ _MODEL_CALL_ASYNC_WRAPPED = False
629
+
630
+
631
+ @overload
632
+ def _instrumented_model_context_call(
633
+ self: Model,
634
+ *,
635
+ ctx: Context[DepsT],
636
+ messages: Sequence[Message],
637
+ tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
638
+ format: None = None,
639
+ ) -> ContextResponse[DepsT, None]: ...
640
+
641
+
642
+ @overload
643
+ def _instrumented_model_context_call(
644
+ self: Model,
645
+ *,
646
+ ctx: Context[DepsT],
647
+ messages: Sequence[Message],
648
+ tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
649
+ format: type[FormattableT] | Format[FormattableT],
650
+ ) -> ContextResponse[DepsT, FormattableT]: ...
651
+
652
+
653
+ @overload
654
+ def _instrumented_model_context_call(
655
+ self: Model,
656
+ *,
657
+ ctx: Context[DepsT],
658
+ messages: Sequence[Message],
659
+ tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
660
+ format: type[FormattableT] | Format[FormattableT] | None = None,
661
+ ) -> ContextResponse[DepsT, None] | ContextResponse[DepsT, FormattableT]: ...
662
+
663
+
664
+ @wraps(_ORIGINAL_MODEL_CONTEXT_CALL)
665
+ def _instrumented_model_context_call(
666
+ self: Model,
667
+ *,
668
+ ctx: Context[DepsT],
669
+ messages: Sequence[Message],
670
+ tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
671
+ format: FormatParam = None,
672
+ ) -> ContextResponse[DepsT, None] | ContextResponse[DepsT, FormattableT]:
673
+ """Returns a GenAI-instrumented result of `Model.context_call`."""
674
+ with _start_model_span(
675
+ self,
676
+ messages=messages,
677
+ tools=tools,
678
+ format=format,
679
+ activate=True,
680
+ ) as span_ctx:
681
+ response = _ORIGINAL_MODEL_CONTEXT_CALL(
682
+ self,
683
+ ctx=ctx,
684
+ messages=messages,
685
+ tools=tools,
686
+ format=format,
687
+ )
688
+ if span_ctx.span is not None:
689
+ _attach_response(
690
+ span_ctx.span,
691
+ response,
692
+ request_messages=messages,
693
+ )
694
+ _record_dropped_params(span_ctx.span, span_ctx.dropped_params)
695
+ return response
696
+
697
+
698
+ def _wrap_model_context_call() -> None:
699
+ """Returns None. Replaces `Model.context_call` with the instrumented wrapper."""
700
+ global _MODEL_CONTEXT_CALL_WRAPPED
701
+ if _MODEL_CONTEXT_CALL_WRAPPED:
702
+ return
703
+ Model.context_call = _instrumented_model_context_call
704
+ _MODEL_CONTEXT_CALL_WRAPPED = True
705
+
706
+
707
+ def _unwrap_model_context_call() -> None:
708
+ """Returns None. Restores the original `Model.context_call` implementation."""
709
+ global _MODEL_CONTEXT_CALL_WRAPPED
710
+ if not _MODEL_CONTEXT_CALL_WRAPPED:
711
+ return
712
+ Model.context_call = _ORIGINAL_MODEL_CONTEXT_CALL
713
+ _MODEL_CONTEXT_CALL_WRAPPED = False
714
+
715
+
716
+ @overload
717
+ async def _instrumented_model_context_call_async(
718
+ self: Model,
719
+ *,
720
+ ctx: Context[DepsT],
721
+ messages: Sequence[Message],
722
+ tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
723
+ | AsyncContextToolkit[DepsT]
724
+ | None = None,
725
+ format: None = None,
726
+ ) -> AsyncContextResponse[DepsT, None]: ...
727
+
728
+
729
+ @overload
730
+ async def _instrumented_model_context_call_async(
731
+ self: Model,
732
+ *,
733
+ ctx: Context[DepsT],
734
+ messages: Sequence[Message],
735
+ tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
736
+ | AsyncContextToolkit[DepsT]
737
+ | None = None,
738
+ format: type[FormattableT] | Format[FormattableT],
739
+ ) -> AsyncContextResponse[DepsT, FormattableT]: ...
740
+
741
+
742
+ @overload
743
+ async def _instrumented_model_context_call_async(
744
+ self: Model,
745
+ *,
746
+ ctx: Context[DepsT],
747
+ messages: Sequence[Message],
748
+ tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
749
+ | AsyncContextToolkit[DepsT]
750
+ | None = None,
751
+ format: type[FormattableT] | Format[FormattableT] | None = None,
752
+ ) -> AsyncContextResponse[DepsT, None] | AsyncContextResponse[DepsT, FormattableT]: ...
753
+
754
+
755
+ @wraps(_ORIGINAL_MODEL_CONTEXT_CALL_ASYNC)
756
+ async def _instrumented_model_context_call_async(
757
+ self: Model,
758
+ *,
759
+ ctx: Context[DepsT],
760
+ messages: Sequence[Message],
761
+ tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
762
+ | AsyncContextToolkit[DepsT]
763
+ | None = None,
764
+ format: FormatParam = None,
765
+ ) -> AsyncContextResponse[DepsT, None] | AsyncContextResponse[DepsT, FormattableT]:
766
+ """Returns a GenAI-instrumented result of `Model.context_call_async`."""
767
+ with _start_model_span(
768
+ self,
769
+ messages=messages,
770
+ tools=tools,
771
+ format=format,
772
+ activate=True,
773
+ ) as span_ctx:
774
+ response = await _ORIGINAL_MODEL_CONTEXT_CALL_ASYNC(
775
+ self,
776
+ ctx=ctx,
777
+ messages=messages,
778
+ tools=tools,
779
+ format=format,
780
+ )
781
+ if span_ctx.span is not None:
782
+ _attach_response(
783
+ span_ctx.span,
784
+ response,
785
+ request_messages=messages,
786
+ )
787
+ _record_dropped_params(span_ctx.span, span_ctx.dropped_params)
788
+ return response
789
+
790
+
791
+ def _wrap_model_context_call_async() -> None:
792
+ """Returns None. Replaces `Model.context_call_async` with the instrumented wrapper."""
793
+ global _MODEL_CONTEXT_CALL_ASYNC_WRAPPED
794
+ if _MODEL_CONTEXT_CALL_ASYNC_WRAPPED:
795
+ return
796
+ Model.context_call_async = _instrumented_model_context_call_async
797
+ _MODEL_CONTEXT_CALL_ASYNC_WRAPPED = True
798
+
799
+
800
+ def _unwrap_model_context_call_async() -> None:
801
+ """Returns None. Restores the original `Model.context_call_async` implementation."""
802
+ global _MODEL_CONTEXT_CALL_ASYNC_WRAPPED
803
+ if not _MODEL_CONTEXT_CALL_ASYNC_WRAPPED:
804
+ return
805
+ Model.context_call_async = _ORIGINAL_MODEL_CONTEXT_CALL_ASYNC
806
+ _MODEL_CONTEXT_CALL_ASYNC_WRAPPED = False
807
+
808
+
809
+ @overload
810
+ def _instrumented_model_stream(
811
+ self: Model,
812
+ *,
813
+ messages: Sequence[Message],
814
+ tools: Sequence[Tool] | Toolkit | None = None,
815
+ format: None = None,
816
+ ) -> StreamResponse: ...
817
+
818
+
819
+ @overload
820
+ def _instrumented_model_stream(
821
+ self: Model,
822
+ *,
823
+ messages: Sequence[Message],
824
+ tools: Sequence[Tool] | Toolkit | None = None,
825
+ format: type[FormattableT] | Format[FormattableT],
826
+ ) -> StreamResponse[FormattableT]: ...
827
+
828
+
829
+ @overload
830
+ def _instrumented_model_stream(
831
+ self: Model,
832
+ *,
833
+ messages: Sequence[Message],
834
+ tools: Sequence[Tool] | Toolkit | None = None,
835
+ format: type[FormattableT] | Format[FormattableT] | None = None,
836
+ ) -> StreamResponse | StreamResponse[FormattableT]: ...
837
+
838
+
839
+ @wraps(_ORIGINAL_MODEL_STREAM)
840
+ def _instrumented_model_stream(
841
+ self: Model,
842
+ *,
843
+ messages: Sequence[Message],
844
+ tools: Sequence[Tool] | Toolkit | None = None,
845
+ format: FormatParam = None,
846
+ ) -> StreamResponse | StreamResponse[FormattableT]:
847
+ """Returns a GenAI-instrumented result of `Model.stream`."""
848
+ span_cm = _start_model_span(
849
+ self,
850
+ messages=messages,
851
+ tools=tools,
852
+ format=format,
853
+ activate=False,
854
+ )
855
+ span_ctx = span_cm.__enter__()
856
+ if span_ctx.span is None:
857
+ response = _ORIGINAL_MODEL_STREAM(
858
+ self,
859
+ messages=messages,
860
+ tools=tools,
861
+ format=format,
862
+ )
863
+ span_cm.__exit__(None, None, None)
864
+ return response
865
+
866
+ try:
867
+ with otel_trace.use_span(span_ctx.span, end_on_exit=False):
868
+ response = _ORIGINAL_MODEL_STREAM(
869
+ self,
870
+ messages=messages,
871
+ tools=tools,
872
+ format=format,
873
+ )
874
+ except Exception as exc:
875
+ span_cm.__exit__(type(exc), exc, exc.__traceback__)
876
+ raise
877
+
878
+ _record_dropped_params(span_ctx.span, span_ctx.dropped_params)
879
+
880
+ try:
881
+ _attach_stream_span_handlers(
882
+ response=response,
883
+ span_cm=span_cm,
884
+ span=span_ctx.span,
885
+ request_messages=messages,
886
+ )
887
+ except Exception as exc: # pragma: no cover
888
+ span_cm.__exit__(type(exc), exc, exc.__traceback__)
889
+ raise
890
+
891
+ return response
892
+
893
+
894
+ def _attach_stream_span_handlers(
895
+ *,
896
+ response: ContextStreamResponse[DepsT, FormattableT | None]
897
+ | StreamResponse[FormattableT | None],
898
+ span_cm: AbstractContextManager[SpanContext],
899
+ span: Span,
900
+ request_messages: Sequence[Message],
901
+ ) -> None:
902
+ """Returns None. Closes the span when streaming completes."""
903
+ chunk_iterator: Iterator[StreamResponseChunk] = response._chunk_iterator
904
+
905
+ response_ref = weakref.ref(response)
906
+ closed = False
907
+
908
+ def _close_span(
909
+ exc_type: type[BaseException] | None,
910
+ exc: BaseException | None,
911
+ tb: TracebackType | None,
912
+ ) -> None:
913
+ nonlocal closed
914
+ if closed:
915
+ return
916
+ closed = True
917
+ response_obj = response_ref()
918
+ if response_obj is not None:
919
+ _attach_response(
920
+ span,
921
+ response_obj,
922
+ request_messages=request_messages,
923
+ )
924
+ span_cm.__exit__(exc_type, exc, tb)
925
+
926
+ def _wrapped_iterator() -> Iterator[StreamResponseChunk]:
927
+ with otel_trace.use_span(span, end_on_exit=False):
928
+ try:
929
+ yield from chunk_iterator
930
+ except Exception as exc: # noqa: BLE001
931
+ _close_span(type(exc), exc, exc.__traceback__)
932
+ raise
933
+ else:
934
+ _close_span(None, None, None)
935
+ finally:
936
+ _close_span(None, None, None)
937
+
938
+ response._chunk_iterator = _wrapped_iterator()
939
+
940
+
941
+ def _wrap_model_stream() -> None:
942
+ """Returns None. Replaces `Model.stream` with the instrumented wrapper."""
943
+ global _MODEL_STREAM_WRAPPED
944
+ if _MODEL_STREAM_WRAPPED:
945
+ return
946
+ Model.stream = _instrumented_model_stream
947
+ _MODEL_STREAM_WRAPPED = True
948
+
949
+
950
+ def _unwrap_model_stream() -> None:
951
+ """Returns None. Restores the original `Model.stream` implementation."""
952
+ global _MODEL_STREAM_WRAPPED
953
+ if not _MODEL_STREAM_WRAPPED:
954
+ return
955
+ Model.stream = _ORIGINAL_MODEL_STREAM
956
+ _MODEL_STREAM_WRAPPED = False
957
+
958
+
959
+ @overload
960
+ def _instrumented_model_context_stream(
961
+ self: Model,
962
+ *,
963
+ ctx: Context[DepsT],
964
+ messages: Sequence[Message],
965
+ tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
966
+ format: None = None,
967
+ ) -> ContextStreamResponse[DepsT, None]: ...
968
+
969
+
970
+ @overload
971
+ def _instrumented_model_context_stream(
972
+ self: Model,
973
+ *,
974
+ ctx: Context[DepsT],
975
+ messages: Sequence[Message],
976
+ tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
977
+ format: type[FormattableT] | Format[FormattableT],
978
+ ) -> ContextStreamResponse[DepsT, FormattableT]: ...
979
+
980
+
981
+ @overload
982
+ def _instrumented_model_context_stream(
983
+ self: Model,
984
+ *,
985
+ ctx: Context[DepsT],
986
+ messages: Sequence[Message],
987
+ tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
988
+ format: type[FormattableT] | Format[FormattableT] | None = None,
989
+ ) -> (
990
+ ContextStreamResponse[DepsT, None] | ContextStreamResponse[DepsT, FormattableT]
991
+ ): ...
992
+
993
+
994
+ @wraps(_ORIGINAL_MODEL_CONTEXT_STREAM)
995
+ def _instrumented_model_context_stream(
996
+ self: Model,
997
+ *,
998
+ ctx: Context[DepsT],
999
+ messages: Sequence[Message],
1000
+ tools: Sequence[Tool | ContextTool[DepsT]] | ContextToolkit[DepsT] | None = None,
1001
+ format: FormatParam = None,
1002
+ ) -> ContextStreamResponse[DepsT, None] | ContextStreamResponse[DepsT, FormattableT]:
1003
+ """Returns a GenAI-instrumented result of `Model.context_stream`."""
1004
+ span_cm = _start_model_span(
1005
+ self,
1006
+ messages=messages,
1007
+ tools=tools,
1008
+ format=format,
1009
+ activate=False,
1010
+ )
1011
+ span_ctx = span_cm.__enter__()
1012
+ if span_ctx.span is None:
1013
+ response = _ORIGINAL_MODEL_CONTEXT_STREAM(
1014
+ self,
1015
+ ctx=ctx,
1016
+ messages=messages,
1017
+ tools=tools,
1018
+ format=format,
1019
+ )
1020
+ span_cm.__exit__(None, None, None)
1021
+ return response
1022
+
1023
+ try:
1024
+ with otel_trace.use_span(span_ctx.span, end_on_exit=False):
1025
+ response = _ORIGINAL_MODEL_CONTEXT_STREAM(
1026
+ self,
1027
+ ctx=ctx,
1028
+ messages=messages,
1029
+ tools=tools,
1030
+ format=format,
1031
+ )
1032
+ except Exception as exc:
1033
+ span_cm.__exit__(type(exc), exc, exc.__traceback__)
1034
+ raise
1035
+
1036
+ _record_dropped_params(span_ctx.span, span_ctx.dropped_params)
1037
+
1038
+ try:
1039
+ _attach_stream_span_handlers(
1040
+ response=response,
1041
+ span_cm=span_cm,
1042
+ span=span_ctx.span,
1043
+ request_messages=messages,
1044
+ )
1045
+ except Exception as exc: # pragma: no cover
1046
+ span_cm.__exit__(type(exc), exc, exc.__traceback__)
1047
+ raise
1048
+
1049
+ return response
1050
+
1051
+
1052
+ def _wrap_model_context_stream() -> None:
1053
+ """Returns None. Replaces `Model.context_stream` with the instrumented wrapper."""
1054
+ global _MODEL_CONTEXT_STREAM_WRAPPED
1055
+ if _MODEL_CONTEXT_STREAM_WRAPPED:
1056
+ return
1057
+ Model.context_stream = _instrumented_model_context_stream
1058
+ _MODEL_CONTEXT_STREAM_WRAPPED = True
1059
+
1060
+
1061
+ def _unwrap_model_context_stream() -> None:
1062
+ """Returns None. Restores the original `Model.context_stream` implementation."""
1063
+ global _MODEL_CONTEXT_STREAM_WRAPPED
1064
+ if not _MODEL_CONTEXT_STREAM_WRAPPED:
1065
+ return
1066
+ Model.context_stream = _ORIGINAL_MODEL_CONTEXT_STREAM
1067
+ _MODEL_CONTEXT_STREAM_WRAPPED = False
1068
+
1069
+
1070
+ def _attach_async_stream_span_handlers(
1071
+ *,
1072
+ response: AsyncContextStreamResponse[DepsT, FormattableT | None],
1073
+ span_cm: AbstractContextManager[SpanContext],
1074
+ span: Span,
1075
+ request_messages: Sequence[Message],
1076
+ ) -> None:
1077
+ """Returns None. Closes the span when async streaming completes."""
1078
+ chunk_iterator: AsyncIterator[StreamResponseChunk] = response._chunk_iterator
1079
+
1080
+ response_ref = weakref.ref(response)
1081
+ closed = False
1082
+
1083
+ def _close_span(
1084
+ exc_type: type[BaseException] | None,
1085
+ exc: BaseException | None,
1086
+ tb: TracebackType | None,
1087
+ ) -> None:
1088
+ nonlocal closed
1089
+ if closed:
1090
+ return
1091
+ closed = True
1092
+ response_obj = response_ref()
1093
+ if response_obj is not None:
1094
+ _attach_response(
1095
+ span,
1096
+ response_obj,
1097
+ request_messages=request_messages,
1098
+ )
1099
+ span_cm.__exit__(exc_type, exc, tb)
1100
+
1101
+ async def _wrapped_iterator() -> AsyncIterator[StreamResponseChunk]:
1102
+ try:
1103
+ async for chunk in chunk_iterator:
1104
+ yield chunk
1105
+ except Exception as exc: # noqa: BLE001
1106
+ _close_span(type(exc), exc, exc.__traceback__)
1107
+ raise
1108
+ else:
1109
+ _close_span(None, None, None)
1110
+ finally:
1111
+ _close_span(None, None, None)
1112
+
1113
+ response._chunk_iterator = _wrapped_iterator()
1114
+
1115
+
1116
+ @overload
1117
+ async def _instrumented_model_context_stream_async(
1118
+ self: Model,
1119
+ *,
1120
+ ctx: Context[DepsT],
1121
+ messages: Sequence[Message],
1122
+ tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
1123
+ | AsyncContextToolkit[DepsT]
1124
+ | None = None,
1125
+ format: None = None,
1126
+ ) -> AsyncContextStreamResponse[DepsT, None]: ...
1127
+
1128
+
1129
+ @overload
1130
+ async def _instrumented_model_context_stream_async(
1131
+ self: Model,
1132
+ *,
1133
+ ctx: Context[DepsT],
1134
+ messages: Sequence[Message],
1135
+ tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
1136
+ | AsyncContextToolkit[DepsT]
1137
+ | None = None,
1138
+ format: type[FormattableT] | Format[FormattableT],
1139
+ ) -> AsyncContextStreamResponse[DepsT, FormattableT]: ...
1140
+
1141
+
1142
+ @overload
1143
+ async def _instrumented_model_context_stream_async(
1144
+ self: Model,
1145
+ *,
1146
+ ctx: Context[DepsT],
1147
+ messages: Sequence[Message],
1148
+ tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
1149
+ | AsyncContextToolkit[DepsT]
1150
+ | None = None,
1151
+ format: type[FormattableT] | Format[FormattableT] | None = None,
1152
+ ) -> (
1153
+ AsyncContextStreamResponse[DepsT, None]
1154
+ | AsyncContextStreamResponse[DepsT, FormattableT]
1155
+ ): ...
1156
+
1157
+
1158
+ @wraps(_ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC)
1159
+ async def _instrumented_model_context_stream_async(
1160
+ self: Model,
1161
+ *,
1162
+ ctx: Context[DepsT],
1163
+ messages: Sequence[Message],
1164
+ tools: Sequence[AsyncTool | AsyncContextTool[DepsT]]
1165
+ | AsyncContextToolkit[DepsT]
1166
+ | None = None,
1167
+ format: FormatParam = None,
1168
+ ) -> (
1169
+ AsyncContextStreamResponse[DepsT, None]
1170
+ | AsyncContextStreamResponse[DepsT, FormattableT]
1171
+ ):
1172
+ """Returns a GenAI-instrumented result of `Model.context_stream_async`."""
1173
+ span_cm = _start_model_span(
1174
+ self,
1175
+ messages=messages,
1176
+ tools=tools,
1177
+ format=format,
1178
+ activate=False,
1179
+ )
1180
+ span_ctx = span_cm.__enter__()
1181
+ if span_ctx.span is None:
1182
+ response = await _ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC(
1183
+ self,
1184
+ ctx=ctx,
1185
+ messages=messages,
1186
+ tools=tools,
1187
+ format=format,
1188
+ )
1189
+ span_cm.__exit__(None, None, None)
1190
+ return response
1191
+
1192
+ try:
1193
+ with otel_trace.use_span(span_ctx.span, end_on_exit=False):
1194
+ response = await _ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC(
1195
+ self,
1196
+ ctx=ctx,
1197
+ messages=messages,
1198
+ tools=tools,
1199
+ format=format,
1200
+ )
1201
+ except Exception as exc:
1202
+ span_cm.__exit__(type(exc), exc, exc.__traceback__)
1203
+ raise
1204
+
1205
+ _record_dropped_params(span_ctx.span, span_ctx.dropped_params)
1206
+
1207
+ try:
1208
+ _attach_async_stream_span_handlers(
1209
+ response=response,
1210
+ span_cm=span_cm,
1211
+ span=span_ctx.span,
1212
+ request_messages=messages,
1213
+ )
1214
+ except Exception as exc: # pragma: no cover
1215
+ span_cm.__exit__(type(exc), exc, exc.__traceback__)
1216
+ raise
1217
+
1218
+ return response
1219
+
1220
+
1221
+ def _wrap_model_context_stream_async() -> None:
1222
+ """Returns None. Replaces `Model.context_stream_async` with the instrumented wrapper."""
1223
+ global _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED
1224
+ if _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED:
1225
+ return
1226
+ Model.context_stream_async = _instrumented_model_context_stream_async
1227
+ _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED = True
1228
+
1229
+
1230
+ def _unwrap_model_context_stream_async() -> None:
1231
+ """Returns None. Restores the original `Model.context_stream_async` implementation."""
1232
+ global _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED
1233
+ if not _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED:
1234
+ return
1235
+ Model.context_stream_async = _ORIGINAL_MODEL_CONTEXT_STREAM_ASYNC
1236
+ _MODEL_CONTEXT_STREAM_ASYNC_WRAPPED = False
1237
+
1238
+
1239
+ def instrument_llm() -> None:
1240
+ """Enable GenAI 1.38 span emission for future `llm.Model` calls and streams.
1241
+
1242
+ Uses the tracer provider configured via `ops.configure()`. If no provider
1243
+ was configured, uses the global OpenTelemetry tracer provider.
1244
+
1245
+ Example:
1246
+
1247
+ Enable instrumentation with a custom provider:
1248
+ ```python
1249
+ from mirascope import ops
1250
+ from opentelemetry.sdk.trace import TracerProvider
1251
+
1252
+ provider = TracerProvider()
1253
+ ops.configure(tracer_provider=provider)
1254
+ ops.instrument_llm()
1255
+ ```
1256
+ """
1257
+ if otel_trace is None: # pragma: no cover
1258
+ raise ImportError(
1259
+ "OpenTelemetry is not installed. Run `pip install mirascope[otel]` "
1260
+ "and ensure `opentelemetry-api` is available before calling "
1261
+ "`instrument_llm`."
1262
+ )
1263
+
1264
+ os.environ.setdefault("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_latest_experimental")
1265
+
1266
+ if get_tracer() is None: # pragma: no cover
1267
+ raise RuntimeError(
1268
+ "You must call `configure()` before calling `instrument_llm()`."
1269
+ )
1270
+
1271
+ _wrap_model_call()
1272
+ _wrap_model_call_async()
1273
+ _wrap_model_context_call()
1274
+ _wrap_model_context_call_async()
1275
+ _wrap_model_stream()
1276
+ _wrap_model_context_stream()
1277
+ _wrap_model_context_stream_async()
1278
+
1279
+
1280
+ def uninstrument_llm() -> None:
1281
+ """Disable previously configured instrumentation."""
1282
+ _unwrap_model_call()
1283
+ _unwrap_model_call_async()
1284
+ _unwrap_model_context_call()
1285
+ _unwrap_model_context_call_async()
1286
+ _unwrap_model_stream()
1287
+ _unwrap_model_context_stream()
1288
+ _unwrap_model_context_stream_async()