mirascope 2.0.0a6__py3-none-any.whl → 2.0.1__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 (226) hide show
  1. mirascope/api/_generated/__init__.py +186 -5
  2. mirascope/api/_generated/annotations/client.py +38 -6
  3. mirascope/api/_generated/annotations/raw_client.py +366 -47
  4. mirascope/api/_generated/annotations/types/annotations_create_response.py +19 -6
  5. mirascope/api/_generated/annotations/types/annotations_get_response.py +19 -6
  6. mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item.py +22 -7
  7. mirascope/api/_generated/annotations/types/annotations_update_response.py +19 -6
  8. mirascope/api/_generated/api_keys/__init__.py +12 -2
  9. mirascope/api/_generated/api_keys/client.py +107 -6
  10. mirascope/api/_generated/api_keys/raw_client.py +486 -38
  11. mirascope/api/_generated/api_keys/types/__init__.py +7 -1
  12. mirascope/api/_generated/api_keys/types/api_keys_list_all_for_org_response_item.py +40 -0
  13. mirascope/api/_generated/client.py +36 -0
  14. mirascope/api/_generated/docs/raw_client.py +71 -9
  15. mirascope/api/_generated/environment.py +3 -3
  16. mirascope/api/_generated/environments/__init__.py +6 -0
  17. mirascope/api/_generated/environments/client.py +158 -9
  18. mirascope/api/_generated/environments/raw_client.py +620 -52
  19. mirascope/api/_generated/environments/types/__init__.py +10 -0
  20. mirascope/api/_generated/environments/types/environments_get_analytics_response.py +60 -0
  21. mirascope/api/_generated/environments/types/environments_get_analytics_response_top_functions_item.py +24 -0
  22. mirascope/api/_generated/{organizations/types/organizations_credits_response.py → environments/types/environments_get_analytics_response_top_models_item.py} +6 -3
  23. mirascope/api/_generated/errors/__init__.py +6 -0
  24. mirascope/api/_generated/errors/bad_request_error.py +5 -2
  25. mirascope/api/_generated/errors/conflict_error.py +5 -2
  26. mirascope/api/_generated/errors/payment_required_error.py +15 -0
  27. mirascope/api/_generated/errors/service_unavailable_error.py +14 -0
  28. mirascope/api/_generated/errors/too_many_requests_error.py +15 -0
  29. mirascope/api/_generated/functions/__init__.py +10 -0
  30. mirascope/api/_generated/functions/client.py +222 -8
  31. mirascope/api/_generated/functions/raw_client.py +975 -134
  32. mirascope/api/_generated/functions/types/__init__.py +28 -4
  33. mirascope/api/_generated/functions/types/functions_get_by_env_response.py +53 -0
  34. mirascope/api/_generated/functions/types/functions_get_by_env_response_dependencies_value.py +22 -0
  35. mirascope/api/_generated/functions/types/functions_list_by_env_response.py +25 -0
  36. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +56 -0
  37. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item_dependencies_value.py +22 -0
  38. mirascope/api/_generated/health/raw_client.py +74 -10
  39. mirascope/api/_generated/organization_invitations/__init__.py +33 -0
  40. mirascope/api/_generated/organization_invitations/client.py +546 -0
  41. mirascope/api/_generated/organization_invitations/raw_client.py +1519 -0
  42. mirascope/api/_generated/organization_invitations/types/__init__.py +53 -0
  43. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response.py +34 -0
  44. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response_role.py +7 -0
  45. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_request_role.py +7 -0
  46. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response.py +48 -0
  47. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_role.py +7 -0
  48. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_status.py +7 -0
  49. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response.py +48 -0
  50. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_role.py +7 -0
  51. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_status.py +7 -0
  52. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item.py +48 -0
  53. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_role.py +7 -0
  54. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_status.py +7 -0
  55. mirascope/api/_generated/organization_memberships/__init__.py +19 -0
  56. mirascope/api/_generated/organization_memberships/client.py +302 -0
  57. mirascope/api/_generated/organization_memberships/raw_client.py +736 -0
  58. mirascope/api/_generated/organization_memberships/types/__init__.py +27 -0
  59. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item.py +33 -0
  60. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item_role.py +7 -0
  61. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_request_role.py +7 -0
  62. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response.py +31 -0
  63. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response_role.py +7 -0
  64. mirascope/api/_generated/organizations/__init__.py +26 -2
  65. mirascope/api/_generated/organizations/client.py +442 -20
  66. mirascope/api/_generated/organizations/raw_client.py +1763 -164
  67. mirascope/api/_generated/organizations/types/__init__.py +48 -2
  68. mirascope/api/_generated/organizations/types/organizations_create_payment_intent_response.py +24 -0
  69. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_request_target_plan.py +7 -0
  70. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response.py +47 -0
  71. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item.py +33 -0
  72. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item_resource.py +7 -0
  73. mirascope/api/_generated/organizations/types/organizations_router_balance_response.py +24 -0
  74. mirascope/api/_generated/organizations/types/organizations_subscription_response.py +53 -0
  75. mirascope/api/_generated/organizations/types/organizations_subscription_response_current_plan.py +7 -0
  76. mirascope/api/_generated/organizations/types/organizations_subscription_response_payment_method.py +26 -0
  77. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change.py +34 -0
  78. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change_target_plan.py +7 -0
  79. mirascope/api/_generated/organizations/types/organizations_update_subscription_request_target_plan.py +7 -0
  80. mirascope/api/_generated/organizations/types/organizations_update_subscription_response.py +35 -0
  81. mirascope/api/_generated/project_memberships/__init__.py +25 -0
  82. mirascope/api/_generated/project_memberships/client.py +437 -0
  83. mirascope/api/_generated/project_memberships/raw_client.py +1039 -0
  84. mirascope/api/_generated/project_memberships/types/__init__.py +29 -0
  85. mirascope/api/_generated/project_memberships/types/project_memberships_create_request_role.py +7 -0
  86. mirascope/api/_generated/project_memberships/types/project_memberships_create_response.py +35 -0
  87. mirascope/api/_generated/project_memberships/types/project_memberships_create_response_role.py +7 -0
  88. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item.py +33 -0
  89. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item_role.py +7 -0
  90. mirascope/api/_generated/project_memberships/types/project_memberships_update_request_role.py +7 -0
  91. mirascope/api/_generated/project_memberships/types/project_memberships_update_response.py +35 -0
  92. mirascope/api/_generated/project_memberships/types/project_memberships_update_response_role.py +7 -0
  93. mirascope/api/_generated/projects/raw_client.py +415 -58
  94. mirascope/api/_generated/reference.md +2767 -397
  95. mirascope/api/_generated/tags/__init__.py +19 -0
  96. mirascope/api/_generated/tags/client.py +504 -0
  97. mirascope/api/_generated/tags/raw_client.py +1288 -0
  98. mirascope/api/_generated/tags/types/__init__.py +17 -0
  99. mirascope/api/_generated/tags/types/tags_create_response.py +41 -0
  100. mirascope/api/_generated/tags/types/tags_get_response.py +41 -0
  101. mirascope/api/_generated/tags/types/tags_list_response.py +23 -0
  102. mirascope/api/_generated/tags/types/tags_list_response_tags_item.py +41 -0
  103. mirascope/api/_generated/tags/types/tags_update_response.py +41 -0
  104. mirascope/api/_generated/token_cost/__init__.py +7 -0
  105. mirascope/api/_generated/token_cost/client.py +160 -0
  106. mirascope/api/_generated/token_cost/raw_client.py +264 -0
  107. mirascope/api/_generated/token_cost/types/__init__.py +8 -0
  108. mirascope/api/_generated/token_cost/types/token_cost_calculate_request_usage.py +54 -0
  109. mirascope/api/_generated/token_cost/types/token_cost_calculate_response.py +52 -0
  110. mirascope/api/_generated/traces/__init__.py +20 -0
  111. mirascope/api/_generated/traces/client.py +543 -0
  112. mirascope/api/_generated/traces/raw_client.py +1366 -96
  113. mirascope/api/_generated/traces/types/__init__.py +28 -0
  114. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py +6 -0
  115. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response.py +33 -0
  116. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response_spans_item.py +88 -0
  117. mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py +0 -2
  118. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response.py +25 -0
  119. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response_traces_item.py +44 -0
  120. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item.py +26 -0
  121. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item_operator.py +7 -0
  122. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_by.py +7 -0
  123. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_order.py +7 -0
  124. mirascope/api/_generated/traces/types/traces_search_by_env_response.py +26 -0
  125. mirascope/api/_generated/traces/types/traces_search_by_env_response_spans_item.py +50 -0
  126. mirascope/api/_generated/traces/types/traces_search_response_spans_item.py +10 -1
  127. mirascope/api/_generated/types/__init__.py +32 -2
  128. mirascope/api/_generated/types/bad_request_error_body.py +50 -0
  129. mirascope/api/_generated/types/date.py +3 -0
  130. mirascope/api/_generated/types/immutable_resource_error.py +22 -0
  131. mirascope/api/_generated/types/internal_server_error_body.py +3 -3
  132. mirascope/api/_generated/types/plan_limit_exceeded_error.py +32 -0
  133. mirascope/api/_generated/types/plan_limit_exceeded_error_tag.py +7 -0
  134. mirascope/api/_generated/types/pricing_unavailable_error.py +23 -0
  135. mirascope/api/_generated/types/rate_limit_error.py +31 -0
  136. mirascope/api/_generated/types/rate_limit_error_tag.py +5 -0
  137. mirascope/api/_generated/types/service_unavailable_error_body.py +24 -0
  138. mirascope/api/_generated/types/service_unavailable_error_tag.py +7 -0
  139. mirascope/api/_generated/types/subscription_past_due_error.py +31 -0
  140. mirascope/api/_generated/types/subscription_past_due_error_tag.py +7 -0
  141. mirascope/api/settings.py +19 -1
  142. mirascope/llm/__init__.py +53 -10
  143. mirascope/llm/calls/__init__.py +2 -1
  144. mirascope/llm/calls/calls.py +3 -1
  145. mirascope/llm/calls/decorator.py +21 -7
  146. mirascope/llm/content/tool_output.py +22 -5
  147. mirascope/llm/exceptions.py +284 -71
  148. mirascope/llm/formatting/__init__.py +17 -0
  149. mirascope/llm/formatting/format.py +112 -35
  150. mirascope/llm/formatting/output_parser.py +178 -0
  151. mirascope/llm/formatting/partial.py +80 -7
  152. mirascope/llm/formatting/primitives.py +192 -0
  153. mirascope/llm/formatting/types.py +20 -8
  154. mirascope/llm/messages/__init__.py +3 -0
  155. mirascope/llm/messages/_utils.py +34 -0
  156. mirascope/llm/models/__init__.py +5 -0
  157. mirascope/llm/models/models.py +137 -69
  158. mirascope/llm/{providers/base → models}/params.py +7 -57
  159. mirascope/llm/models/thinking_config.py +61 -0
  160. mirascope/llm/prompts/_utils.py +0 -32
  161. mirascope/llm/prompts/decorator.py +16 -5
  162. mirascope/llm/prompts/prompts.py +131 -68
  163. mirascope/llm/providers/__init__.py +1 -4
  164. mirascope/llm/providers/anthropic/_utils/__init__.py +2 -0
  165. mirascope/llm/providers/anthropic/_utils/beta_decode.py +18 -9
  166. mirascope/llm/providers/anthropic/_utils/beta_encode.py +62 -13
  167. mirascope/llm/providers/anthropic/_utils/decode.py +18 -9
  168. mirascope/llm/providers/anthropic/_utils/encode.py +26 -7
  169. mirascope/llm/providers/anthropic/_utils/errors.py +2 -2
  170. mirascope/llm/providers/anthropic/beta_provider.py +64 -18
  171. mirascope/llm/providers/anthropic/provider.py +91 -33
  172. mirascope/llm/providers/base/__init__.py +0 -4
  173. mirascope/llm/providers/base/_utils.py +55 -6
  174. mirascope/llm/providers/base/base_provider.py +116 -37
  175. mirascope/llm/providers/google/_utils/__init__.py +2 -0
  176. mirascope/llm/providers/google/_utils/decode.py +20 -7
  177. mirascope/llm/providers/google/_utils/encode.py +26 -7
  178. mirascope/llm/providers/google/_utils/errors.py +3 -2
  179. mirascope/llm/providers/google/provider.py +64 -18
  180. mirascope/llm/providers/mirascope/_utils.py +13 -17
  181. mirascope/llm/providers/mirascope/provider.py +49 -18
  182. mirascope/llm/providers/mlx/_utils.py +7 -2
  183. mirascope/llm/providers/mlx/encoding/base.py +5 -2
  184. mirascope/llm/providers/mlx/encoding/transformers.py +5 -2
  185. mirascope/llm/providers/mlx/mlx.py +23 -6
  186. mirascope/llm/providers/mlx/provider.py +42 -13
  187. mirascope/llm/providers/openai/_utils/errors.py +2 -2
  188. mirascope/llm/providers/openai/completions/_utils/encode.py +20 -16
  189. mirascope/llm/providers/openai/completions/base_provider.py +40 -11
  190. mirascope/llm/providers/openai/provider.py +40 -10
  191. mirascope/llm/providers/openai/responses/_utils/__init__.py +2 -0
  192. mirascope/llm/providers/openai/responses/_utils/decode.py +19 -6
  193. mirascope/llm/providers/openai/responses/_utils/encode.py +22 -10
  194. mirascope/llm/providers/openai/responses/provider.py +56 -18
  195. mirascope/llm/providers/provider_registry.py +93 -19
  196. mirascope/llm/responses/__init__.py +6 -1
  197. mirascope/llm/responses/_utils.py +102 -12
  198. mirascope/llm/responses/base_response.py +5 -2
  199. mirascope/llm/responses/base_stream_response.py +115 -25
  200. mirascope/llm/responses/response.py +2 -1
  201. mirascope/llm/responses/root_response.py +89 -17
  202. mirascope/llm/responses/stream_response.py +6 -9
  203. mirascope/llm/tools/decorator.py +9 -4
  204. mirascope/llm/tools/tool_schema.py +12 -6
  205. mirascope/llm/tools/toolkit.py +35 -27
  206. mirascope/llm/tools/tools.py +45 -20
  207. mirascope/ops/__init__.py +4 -0
  208. mirascope/ops/_internal/configuration.py +82 -31
  209. mirascope/ops/_internal/exporters/exporters.py +64 -11
  210. mirascope/ops/_internal/instrumentation/llm/common.py +530 -0
  211. mirascope/ops/_internal/instrumentation/llm/cost.py +190 -0
  212. mirascope/ops/_internal/instrumentation/llm/encode.py +1 -1
  213. mirascope/ops/_internal/instrumentation/llm/llm.py +116 -1242
  214. mirascope/ops/_internal/instrumentation/llm/model.py +1798 -0
  215. mirascope/ops/_internal/instrumentation/llm/response.py +521 -0
  216. mirascope/ops/_internal/instrumentation/llm/serialize.py +300 -0
  217. mirascope/ops/_internal/protocols.py +83 -1
  218. mirascope/ops/_internal/traced_calls.py +4 -0
  219. mirascope/ops/_internal/traced_functions.py +118 -8
  220. mirascope/ops/_internal/tracing.py +78 -1
  221. mirascope/ops/_internal/utils.py +52 -4
  222. {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/METADATA +12 -11
  223. mirascope-2.0.1.dist-info/RECORD +423 -0
  224. {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/licenses/LICENSE +1 -1
  225. mirascope-2.0.0a6.dist-info/RECORD +0 -316
  226. {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,530 @@
1
+ """Shared utilities and types for OpenTelemetry GenAI instrumentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable, Iterator, Mapping, Sequence
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING, Protocol, TypeAlias, runtime_checkable
10
+ from typing_extensions import TypeIs
11
+
12
+ from opentelemetry import trace as otel_trace
13
+ from opentelemetry.semconv._incubating.attributes import (
14
+ gen_ai_attributes as GenAIAttributes,
15
+ )
16
+ from opentelemetry.semconv.attributes import error_attributes as ErrorAttributes
17
+ from opentelemetry.trace import SpanKind, Status, StatusCode
18
+
19
+ from .....llm import (
20
+ AnyToolFn,
21
+ AnyToolSchema,
22
+ BaseToolkit,
23
+ Format,
24
+ FormattableT,
25
+ Jsonable,
26
+ Message,
27
+ Model,
28
+ ModelId,
29
+ Params,
30
+ ProviderId,
31
+ RootResponse,
32
+ ToolkitT,
33
+ ToolSchema,
34
+ )
35
+ from ...configuration import get_tracer
36
+ from ...utils import json_dumps
37
+ from .cost import calculate_cost_async, calculate_cost_sync
38
+ from .encode import (
39
+ map_finish_reason,
40
+ snapshot_from_root_response,
41
+ split_request_messages,
42
+ )
43
+ from .serialize import (
44
+ serialize_mirascope_content,
45
+ serialize_mirascope_cost,
46
+ serialize_mirascope_messages,
47
+ serialize_mirascope_usage,
48
+ )
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+ # Re-export for backwards compatibility
53
+ _calculate_cost_sync = calculate_cost_sync
54
+ _calculate_cost_async = calculate_cost_async
55
+
56
+ if TYPE_CHECKING:
57
+ from opentelemetry.trace import Span, Tracer
58
+ from opentelemetry.util.types import AttributeValue
59
+
60
+ from . import gen_ai_types
61
+ else:
62
+ AttributeValue = None
63
+ Span = None
64
+ Tracer = None
65
+
66
+
67
+ ToolsParam: TypeAlias = (
68
+ Sequence[ToolSchema[AnyToolFn]] | BaseToolkit[AnyToolSchema] | None
69
+ )
70
+ FormatParam: TypeAlias = Format[FormattableT] | None
71
+ ParamsDict: TypeAlias = Mapping[str, str | int | float | bool | Sequence[str] | None]
72
+ SpanAttributes: TypeAlias = Mapping[str, AttributeValue]
73
+ AttributeSetter: TypeAlias = Callable[[str, AttributeValue], None]
74
+ ParamsValue = str | int | float | bool | Sequence[str] | None
75
+
76
+
77
+ @dataclass(slots=True)
78
+ class SpanContext:
79
+ """Container for a GenAI span and its associated dropped parameters."""
80
+
81
+ span: Span | None
82
+ """The active span, if any."""
83
+
84
+ dropped_params: dict[str, Jsonable]
85
+ """Parameters that could not be recorded as span attributes."""
86
+
87
+
88
+ @runtime_checkable
89
+ class Identifiable(Protocol):
90
+ """Protocol for objects with an optional ID attribute."""
91
+
92
+ id: str | None
93
+ """Optional ID attribute."""
94
+
95
+
96
+ _PARAM_ATTRIBUTE_MAP: Mapping[str, str] = {
97
+ "temperature": GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE,
98
+ "max_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
99
+ "max_output_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
100
+ "max_completion_tokens": GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS,
101
+ "top_p": GenAIAttributes.GEN_AI_REQUEST_TOP_P,
102
+ "top_k": GenAIAttributes.GEN_AI_REQUEST_TOP_K,
103
+ "frequency_penalty": GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY,
104
+ "presence_penalty": GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY,
105
+ "seed": GenAIAttributes.GEN_AI_REQUEST_SEED,
106
+ "stop_sequences": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES,
107
+ "stop": GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES,
108
+ "n": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT,
109
+ "choice_count": GenAIAttributes.GEN_AI_REQUEST_CHOICE_COUNT,
110
+ }
111
+
112
+
113
+ def record_exception(span: Span, exc: Exception) -> None:
114
+ """Record exception details on span following OpenTelemetry semantic conventions."""
115
+ span.record_exception(exc)
116
+ span.set_attribute(ErrorAttributes.ERROR_TYPE, exc.__class__.__name__)
117
+ error_message = str(exc)
118
+ if error_message:
119
+ span.set_attribute("error.message", error_message)
120
+ span.set_status(Status(StatusCode.ERROR, error_message))
121
+
122
+
123
+ def _infer_output_type(format_obj: FormatParam) -> str:
124
+ """Infer the GenAI output type from the format parameter."""
125
+ if format_obj is None:
126
+ return GenAIAttributes.GenAiOutputTypeValues.TEXT.value
127
+ return GenAIAttributes.GenAiOutputTypeValues.JSON.value
128
+
129
+
130
+ def _apply_param_attributes(
131
+ attrs: dict[str, AttributeValue], params: ParamsDict
132
+ ) -> None:
133
+ """Apply model parameters as span attributes."""
134
+ if not params:
135
+ return
136
+
137
+ for key, attr_key in _PARAM_ATTRIBUTE_MAP.items():
138
+ if key not in params:
139
+ continue
140
+ value = params[key]
141
+ if value is None:
142
+ continue
143
+ if key in {"stop", "stop_sequences"} and isinstance(value, str):
144
+ value = [value]
145
+ attrs[attr_key] = value
146
+
147
+
148
+ def _set_json_attribute(
149
+ setter: AttributeSetter,
150
+ *,
151
+ key: str,
152
+ payload: (
153
+ gen_ai_types.SystemInstructions
154
+ | gen_ai_types.InputMessages
155
+ | gen_ai_types.OutputMessages
156
+ ),
157
+ ) -> None:
158
+ """Assign a JSON-serialized attribute to a span."""
159
+ if not payload:
160
+ return
161
+ setter(key, json_dumps(payload))
162
+
163
+
164
+ def _assign_request_message_attributes(
165
+ setter: AttributeSetter,
166
+ *,
167
+ messages: Sequence[Message],
168
+ ) -> None:
169
+ """Assign request message attributes to a span."""
170
+ system_payload, input_payload = split_request_messages(messages)
171
+ _set_json_attribute(
172
+ setter,
173
+ key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
174
+ payload=system_payload,
175
+ )
176
+ _set_json_attribute(
177
+ setter,
178
+ key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
179
+ payload=input_payload,
180
+ )
181
+
182
+
183
+ def _collect_tool_schemas(
184
+ tools: Sequence[ToolSchema[AnyToolFn]] | BaseToolkit[AnyToolSchema],
185
+ ) -> list[ToolSchema[AnyToolFn]]:
186
+ """Collect ToolSchema instances from a tools parameter."""
187
+ iterable = list(tools.tools) if isinstance(tools, BaseToolkit) else list(tools)
188
+ schemas: list[ToolSchema[AnyToolFn]] = []
189
+ for tool in iterable:
190
+ if isinstance(tool, ToolSchema):
191
+ schemas.append(tool)
192
+ return schemas
193
+
194
+
195
+ def _serialize_tool_definitions(
196
+ tools: ToolsParam,
197
+ format: FormatParam = None,
198
+ ) -> str | None:
199
+ """Serialize tool definitions to JSON for span attributes."""
200
+ if tools is None:
201
+ tool_schemas: list[ToolSchema[AnyToolFn]] = []
202
+ else:
203
+ tool_schemas = _collect_tool_schemas(tools)
204
+
205
+ if isinstance(format, Format) and format.mode == "tool":
206
+ tool_schemas.append(format.create_tool_schema())
207
+
208
+ if not tool_schemas:
209
+ return None
210
+ definitions: list[dict[str, str | int | bool | dict[str, str | int | bool]]] = []
211
+ for tool in tool_schemas:
212
+ tool_def: dict[str, str | int | bool | dict[str, str | int | bool]] = {
213
+ "name": tool.name,
214
+ "description": tool.description,
215
+ "parameters": tool.parameters.model_dump(by_alias=True, mode="json"),
216
+ }
217
+ if tool.strict is not None:
218
+ tool_def["strict"] = tool.strict
219
+ definitions.append(tool_def)
220
+ return json_dumps(definitions)
221
+
222
+
223
+ def _build_request_attributes(
224
+ *,
225
+ operation: str,
226
+ provider: ProviderId,
227
+ model_id: ModelId,
228
+ messages: Sequence[Message],
229
+ tools: ToolsParam,
230
+ format: FormatParam,
231
+ params: ParamsDict,
232
+ ) -> dict[str, AttributeValue]:
233
+ """Build GenAI request attributes for a span."""
234
+ attrs: dict[str, AttributeValue] = {
235
+ GenAIAttributes.GEN_AI_OPERATION_NAME: operation,
236
+ GenAIAttributes.GEN_AI_PROVIDER_NAME: provider,
237
+ GenAIAttributes.GEN_AI_REQUEST_MODEL: model_id,
238
+ GenAIAttributes.GEN_AI_OUTPUT_TYPE: _infer_output_type(format),
239
+ }
240
+ _apply_param_attributes(attrs, params)
241
+
242
+ _assign_request_message_attributes(
243
+ attrs.__setitem__,
244
+ messages=messages,
245
+ )
246
+
247
+ tool_payload = _serialize_tool_definitions(tools, format=format)
248
+ if tool_payload:
249
+ # The incubating semconv module does not yet expose a constant for this key.
250
+ attrs["gen_ai.tool.definitions"] = tool_payload
251
+
252
+ return attrs
253
+
254
+
255
+ def _extract_response_id(
256
+ raw: dict[str, str | int] | str | Identifiable | None,
257
+ ) -> str | None:
258
+ """Extract response ID from raw response data."""
259
+ if isinstance(raw, dict):
260
+ for key in ("id", "response_id", "responseId"):
261
+ value = raw.get(key)
262
+ if isinstance(value, str):
263
+ return value
264
+ elif isinstance(raw, Identifiable):
265
+ return raw.id
266
+ return None
267
+
268
+
269
+ def attach_response(
270
+ span: Span,
271
+ response: RootResponse[ToolkitT, FormattableT | None],
272
+ *,
273
+ request_messages: Sequence[Message],
274
+ ) -> None:
275
+ """Attach response attributes to a GenAI span."""
276
+ span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.model_id)
277
+ span.set_attribute(
278
+ GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS,
279
+ [map_finish_reason(response.finish_reason)],
280
+ )
281
+ response_id = _extract_response_id(getattr(response, "raw", None))
282
+ if response_id:
283
+ span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_ID, response_id)
284
+
285
+ snapshot = snapshot_from_root_response(
286
+ response,
287
+ request_messages=request_messages,
288
+ )
289
+ _set_json_attribute(
290
+ span.set_attribute,
291
+ key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
292
+ payload=snapshot.system_instructions,
293
+ )
294
+ _set_json_attribute(
295
+ span.set_attribute,
296
+ key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
297
+ payload=snapshot.inputs,
298
+ )
299
+ _set_json_attribute(
300
+ span.set_attribute,
301
+ key=GenAIAttributes.GEN_AI_OUTPUT_MESSAGES,
302
+ payload=snapshot.outputs,
303
+ )
304
+ if response.usage is not None:
305
+ span.set_attribute(
306
+ GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, response.usage.input_tokens
307
+ )
308
+ span.set_attribute(
309
+ GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, response.usage.output_tokens
310
+ )
311
+
312
+ # Mirascope-specific attributes
313
+ span.set_attribute(
314
+ "mirascope.response.messages", serialize_mirascope_messages(request_messages)
315
+ )
316
+ span.set_attribute(
317
+ "mirascope.response.content", serialize_mirascope_content(response.content)
318
+ )
319
+ if (usage_json := serialize_mirascope_usage(response.usage)) is not None:
320
+ span.set_attribute("mirascope.response.usage", usage_json)
321
+
322
+ # Calculate and attach cost if usage is available
323
+ if response.usage is not None:
324
+ cost = _calculate_cost_sync(
325
+ response.provider_id, response.model_id, response.usage
326
+ )
327
+ if cost is not None:
328
+ span.set_attribute(
329
+ "mirascope.response.cost",
330
+ serialize_mirascope_cost(
331
+ input_cost=cost.input_cost_centicents,
332
+ output_cost=cost.output_cost_centicents,
333
+ total_cost=cost.total_cost_centicents,
334
+ cache_read_cost=cost.cache_read_cost_centicents,
335
+ cache_write_cost=cost.cache_write_cost_centicents,
336
+ ),
337
+ )
338
+
339
+
340
+ async def attach_response_async(
341
+ span: Span,
342
+ response: RootResponse[ToolkitT, FormattableT | None],
343
+ *,
344
+ request_messages: Sequence[Message],
345
+ ) -> None:
346
+ """Attach response attributes to a GenAI span (async version for cost calculation)."""
347
+ span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.model_id)
348
+ span.set_attribute(
349
+ GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS,
350
+ [map_finish_reason(response.finish_reason)],
351
+ )
352
+ response_id = _extract_response_id(getattr(response, "raw", None))
353
+ if response_id:
354
+ span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_ID, response_id)
355
+
356
+ snapshot = snapshot_from_root_response(
357
+ response,
358
+ request_messages=request_messages,
359
+ )
360
+ _set_json_attribute(
361
+ span.set_attribute,
362
+ key=GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS,
363
+ payload=snapshot.system_instructions,
364
+ )
365
+ _set_json_attribute(
366
+ span.set_attribute,
367
+ key=GenAIAttributes.GEN_AI_INPUT_MESSAGES,
368
+ payload=snapshot.inputs,
369
+ )
370
+ _set_json_attribute(
371
+ span.set_attribute,
372
+ key=GenAIAttributes.GEN_AI_OUTPUT_MESSAGES,
373
+ payload=snapshot.outputs,
374
+ )
375
+ if response.usage is not None:
376
+ span.set_attribute(
377
+ GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, response.usage.input_tokens
378
+ )
379
+ span.set_attribute(
380
+ GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, response.usage.output_tokens
381
+ )
382
+
383
+ # Mirascope-specific attributes
384
+ span.set_attribute(
385
+ "mirascope.response.messages", serialize_mirascope_messages(request_messages)
386
+ )
387
+ span.set_attribute(
388
+ "mirascope.response.content", serialize_mirascope_content(response.content)
389
+ )
390
+ if (usage_json := serialize_mirascope_usage(response.usage)) is not None:
391
+ span.set_attribute("mirascope.response.usage", usage_json)
392
+
393
+ # Calculate and attach cost if usage is available (async)
394
+ if response.usage is not None:
395
+ cost = await _calculate_cost_async(
396
+ response.provider_id, response.model_id, response.usage
397
+ )
398
+ if cost is not None:
399
+ span.set_attribute(
400
+ "mirascope.response.cost",
401
+ serialize_mirascope_cost(
402
+ input_cost=cost.input_cost_centicents,
403
+ output_cost=cost.output_cost_centicents,
404
+ total_cost=cost.total_cost_centicents,
405
+ cache_read_cost=cost.cache_read_cost_centicents,
406
+ cache_write_cost=cost.cache_write_cost_centicents,
407
+ ),
408
+ )
409
+
410
+
411
+ def _is_supported_param_value(value: object) -> TypeIs[ParamsValue]:
412
+ """Returns True if the value can be exported as an OTEL attribute."""
413
+ if isinstance(value, str | int | float | bool) or value is None:
414
+ return True
415
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes):
416
+ return all(isinstance(item, str) for item in value)
417
+ return False
418
+
419
+
420
+ def _normalize_dropped_value(value: object) -> Jsonable:
421
+ """Returns a JSON-safe representation for unsupported param values."""
422
+ if isinstance(value, str | int | float | bool) or value is None:
423
+ return value
424
+ if isinstance(value, Mapping):
425
+ normalized: dict[str, Jsonable] = {}
426
+ for key, item in value.items():
427
+ normalized[str(key)] = _normalize_dropped_value(item)
428
+ return normalized
429
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
430
+ return [_normalize_dropped_value(item) for item in value]
431
+ try:
432
+ return str(value)
433
+ except Exception: # pragma: no cover
434
+ return f"<{type(value).__name__}>"
435
+
436
+
437
+ def _params_as_mapping(params: Params) -> tuple[ParamsDict, dict[str, Jsonable]]:
438
+ """Returns supported params and a mapping of dropped params."""
439
+ filtered: dict[str, ParamsValue] = {}
440
+ dropped: dict[str, Jsonable] = {}
441
+ for key, value in params.items():
442
+ if _is_supported_param_value(value):
443
+ filtered[key] = value
444
+ else:
445
+ dropped[key] = _normalize_dropped_value(value)
446
+ return filtered, dropped
447
+
448
+
449
+ def record_dropped_params(
450
+ span: Span,
451
+ dropped_params: Mapping[str, Jsonable],
452
+ ) -> None:
453
+ """Emit an event with JSON-encoded params that cannot become attributes.
454
+
455
+ See https://opentelemetry.io/docs/specs/otel/common/ for the attribute type limits,
456
+ https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/ for the GenAI
457
+ guidance on recording richer payloads via events, and
458
+ https://opentelemetry.io/blog/2025/complex-attribute-types/ for the recommendation
459
+ to serialize unsupported complex types instead of dropping them outright.
460
+ """
461
+ if not dropped_params:
462
+ return None
463
+ payload = json_dumps(dropped_params)
464
+ span.add_event(
465
+ "gen_ai.request.params.untracked",
466
+ attributes={
467
+ "gen_ai.untracked_params.count": len(dropped_params),
468
+ "gen_ai.untracked_params.keys": list(dropped_params.keys()),
469
+ "gen_ai.untracked_params.json": payload,
470
+ },
471
+ )
472
+ return None
473
+
474
+
475
+ @contextmanager
476
+ def start_model_span(
477
+ model: Model,
478
+ *,
479
+ messages: Sequence[Message],
480
+ tools: ToolsParam,
481
+ format: FormatParam,
482
+ activate: bool = True,
483
+ ) -> Iterator[SpanContext]:
484
+ """Context manager that yields a SpanContext for a model call."""
485
+ params, dropped_params = _params_as_mapping(model.params)
486
+ tracer = get_tracer()
487
+
488
+ if tracer is None or otel_trace is None:
489
+ yield SpanContext(None, dropped_params)
490
+ return
491
+
492
+ operation = GenAIAttributes.GenAiOperationNameValues.CHAT.value
493
+ attrs = _build_request_attributes(
494
+ operation=operation,
495
+ provider=model.provider_id,
496
+ model_id=model.model_id,
497
+ messages=messages,
498
+ tools=tools,
499
+ format=format,
500
+ params=params,
501
+ )
502
+ span_name = f"{operation} {model.model_id}"
503
+
504
+ if activate:
505
+ with tracer.start_as_current_span(
506
+ name=span_name,
507
+ kind=SpanKind.CLIENT,
508
+ ) as active_span:
509
+ for key, value in attrs.items():
510
+ active_span.set_attribute(key, value)
511
+ try:
512
+ yield SpanContext(active_span, dropped_params)
513
+ except Exception as exc:
514
+ record_exception(active_span, exc)
515
+ raise
516
+ return
517
+
518
+ span = tracer.start_span(
519
+ name=span_name,
520
+ kind=SpanKind.CLIENT,
521
+ )
522
+ for key, value in attrs.items():
523
+ span.set_attribute(key, value)
524
+ try:
525
+ yield SpanContext(span, dropped_params)
526
+ except Exception as exc:
527
+ record_exception(span, exc)
528
+ raise
529
+ finally:
530
+ span.end()