mirascope 2.0.0a6__py3-none-any.whl → 2.0.2__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 (230) hide show
  1. mirascope/_utils.py +34 -0
  2. mirascope/api/_generated/__init__.py +186 -5
  3. mirascope/api/_generated/annotations/client.py +38 -6
  4. mirascope/api/_generated/annotations/raw_client.py +366 -47
  5. mirascope/api/_generated/annotations/types/annotations_create_response.py +19 -6
  6. mirascope/api/_generated/annotations/types/annotations_get_response.py +19 -6
  7. mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item.py +22 -7
  8. mirascope/api/_generated/annotations/types/annotations_update_response.py +19 -6
  9. mirascope/api/_generated/api_keys/__init__.py +12 -2
  10. mirascope/api/_generated/api_keys/client.py +107 -6
  11. mirascope/api/_generated/api_keys/raw_client.py +486 -38
  12. mirascope/api/_generated/api_keys/types/__init__.py +7 -1
  13. mirascope/api/_generated/api_keys/types/api_keys_list_all_for_org_response_item.py +40 -0
  14. mirascope/api/_generated/client.py +36 -0
  15. mirascope/api/_generated/docs/raw_client.py +71 -9
  16. mirascope/api/_generated/environment.py +3 -3
  17. mirascope/api/_generated/environments/__init__.py +6 -0
  18. mirascope/api/_generated/environments/client.py +158 -9
  19. mirascope/api/_generated/environments/raw_client.py +620 -52
  20. mirascope/api/_generated/environments/types/__init__.py +10 -0
  21. mirascope/api/_generated/environments/types/environments_get_analytics_response.py +60 -0
  22. mirascope/api/_generated/environments/types/environments_get_analytics_response_top_functions_item.py +24 -0
  23. mirascope/api/_generated/{organizations/types/organizations_credits_response.py → environments/types/environments_get_analytics_response_top_models_item.py} +6 -3
  24. mirascope/api/_generated/errors/__init__.py +6 -0
  25. mirascope/api/_generated/errors/bad_request_error.py +5 -2
  26. mirascope/api/_generated/errors/conflict_error.py +5 -2
  27. mirascope/api/_generated/errors/payment_required_error.py +15 -0
  28. mirascope/api/_generated/errors/service_unavailable_error.py +14 -0
  29. mirascope/api/_generated/errors/too_many_requests_error.py +15 -0
  30. mirascope/api/_generated/functions/__init__.py +10 -0
  31. mirascope/api/_generated/functions/client.py +222 -8
  32. mirascope/api/_generated/functions/raw_client.py +975 -134
  33. mirascope/api/_generated/functions/types/__init__.py +28 -4
  34. mirascope/api/_generated/functions/types/functions_get_by_env_response.py +53 -0
  35. mirascope/api/_generated/functions/types/functions_get_by_env_response_dependencies_value.py +22 -0
  36. mirascope/api/_generated/functions/types/functions_list_by_env_response.py +25 -0
  37. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +56 -0
  38. mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item_dependencies_value.py +22 -0
  39. mirascope/api/_generated/health/raw_client.py +74 -10
  40. mirascope/api/_generated/organization_invitations/__init__.py +33 -0
  41. mirascope/api/_generated/organization_invitations/client.py +546 -0
  42. mirascope/api/_generated/organization_invitations/raw_client.py +1519 -0
  43. mirascope/api/_generated/organization_invitations/types/__init__.py +53 -0
  44. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response.py +34 -0
  45. mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response_role.py +7 -0
  46. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_request_role.py +7 -0
  47. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response.py +48 -0
  48. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_role.py +7 -0
  49. mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_status.py +7 -0
  50. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response.py +48 -0
  51. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_role.py +7 -0
  52. mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_status.py +7 -0
  53. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item.py +48 -0
  54. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_role.py +7 -0
  55. mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_status.py +7 -0
  56. mirascope/api/_generated/organization_memberships/__init__.py +19 -0
  57. mirascope/api/_generated/organization_memberships/client.py +302 -0
  58. mirascope/api/_generated/organization_memberships/raw_client.py +736 -0
  59. mirascope/api/_generated/organization_memberships/types/__init__.py +27 -0
  60. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item.py +33 -0
  61. mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item_role.py +7 -0
  62. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_request_role.py +7 -0
  63. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response.py +31 -0
  64. mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response_role.py +7 -0
  65. mirascope/api/_generated/organizations/__init__.py +26 -2
  66. mirascope/api/_generated/organizations/client.py +442 -20
  67. mirascope/api/_generated/organizations/raw_client.py +1763 -164
  68. mirascope/api/_generated/organizations/types/__init__.py +48 -2
  69. mirascope/api/_generated/organizations/types/organizations_create_payment_intent_response.py +24 -0
  70. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_request_target_plan.py +7 -0
  71. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response.py +47 -0
  72. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item.py +33 -0
  73. mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item_resource.py +7 -0
  74. mirascope/api/_generated/organizations/types/organizations_router_balance_response.py +24 -0
  75. mirascope/api/_generated/organizations/types/organizations_subscription_response.py +53 -0
  76. mirascope/api/_generated/organizations/types/organizations_subscription_response_current_plan.py +7 -0
  77. mirascope/api/_generated/organizations/types/organizations_subscription_response_payment_method.py +26 -0
  78. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change.py +34 -0
  79. mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change_target_plan.py +7 -0
  80. mirascope/api/_generated/organizations/types/organizations_update_subscription_request_target_plan.py +7 -0
  81. mirascope/api/_generated/organizations/types/organizations_update_subscription_response.py +35 -0
  82. mirascope/api/_generated/project_memberships/__init__.py +25 -0
  83. mirascope/api/_generated/project_memberships/client.py +437 -0
  84. mirascope/api/_generated/project_memberships/raw_client.py +1039 -0
  85. mirascope/api/_generated/project_memberships/types/__init__.py +29 -0
  86. mirascope/api/_generated/project_memberships/types/project_memberships_create_request_role.py +7 -0
  87. mirascope/api/_generated/project_memberships/types/project_memberships_create_response.py +35 -0
  88. mirascope/api/_generated/project_memberships/types/project_memberships_create_response_role.py +7 -0
  89. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item.py +33 -0
  90. mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item_role.py +7 -0
  91. mirascope/api/_generated/project_memberships/types/project_memberships_update_request_role.py +7 -0
  92. mirascope/api/_generated/project_memberships/types/project_memberships_update_response.py +35 -0
  93. mirascope/api/_generated/project_memberships/types/project_memberships_update_response_role.py +7 -0
  94. mirascope/api/_generated/projects/raw_client.py +415 -58
  95. mirascope/api/_generated/reference.md +2767 -397
  96. mirascope/api/_generated/tags/__init__.py +19 -0
  97. mirascope/api/_generated/tags/client.py +504 -0
  98. mirascope/api/_generated/tags/raw_client.py +1288 -0
  99. mirascope/api/_generated/tags/types/__init__.py +17 -0
  100. mirascope/api/_generated/tags/types/tags_create_response.py +41 -0
  101. mirascope/api/_generated/tags/types/tags_get_response.py +41 -0
  102. mirascope/api/_generated/tags/types/tags_list_response.py +23 -0
  103. mirascope/api/_generated/tags/types/tags_list_response_tags_item.py +41 -0
  104. mirascope/api/_generated/tags/types/tags_update_response.py +41 -0
  105. mirascope/api/_generated/token_cost/__init__.py +7 -0
  106. mirascope/api/_generated/token_cost/client.py +160 -0
  107. mirascope/api/_generated/token_cost/raw_client.py +264 -0
  108. mirascope/api/_generated/token_cost/types/__init__.py +8 -0
  109. mirascope/api/_generated/token_cost/types/token_cost_calculate_request_usage.py +54 -0
  110. mirascope/api/_generated/token_cost/types/token_cost_calculate_response.py +52 -0
  111. mirascope/api/_generated/traces/__init__.py +20 -0
  112. mirascope/api/_generated/traces/client.py +543 -0
  113. mirascope/api/_generated/traces/raw_client.py +1366 -96
  114. mirascope/api/_generated/traces/types/__init__.py +28 -0
  115. mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py +6 -0
  116. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response.py +33 -0
  117. mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response_spans_item.py +88 -0
  118. mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py +0 -2
  119. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response.py +25 -0
  120. mirascope/api/_generated/traces/types/traces_list_by_function_hash_response_traces_item.py +44 -0
  121. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item.py +26 -0
  122. mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item_operator.py +7 -0
  123. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_by.py +7 -0
  124. mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_order.py +7 -0
  125. mirascope/api/_generated/traces/types/traces_search_by_env_response.py +26 -0
  126. mirascope/api/_generated/traces/types/traces_search_by_env_response_spans_item.py +50 -0
  127. mirascope/api/_generated/traces/types/traces_search_response_spans_item.py +10 -1
  128. mirascope/api/_generated/types/__init__.py +32 -2
  129. mirascope/api/_generated/types/bad_request_error_body.py +50 -0
  130. mirascope/api/_generated/types/date.py +3 -0
  131. mirascope/api/_generated/types/immutable_resource_error.py +22 -0
  132. mirascope/api/_generated/types/internal_server_error_body.py +3 -3
  133. mirascope/api/_generated/types/plan_limit_exceeded_error.py +32 -0
  134. mirascope/api/_generated/types/plan_limit_exceeded_error_tag.py +7 -0
  135. mirascope/api/_generated/types/pricing_unavailable_error.py +23 -0
  136. mirascope/api/_generated/types/rate_limit_error.py +31 -0
  137. mirascope/api/_generated/types/rate_limit_error_tag.py +5 -0
  138. mirascope/api/_generated/types/service_unavailable_error_body.py +24 -0
  139. mirascope/api/_generated/types/service_unavailable_error_tag.py +7 -0
  140. mirascope/api/_generated/types/subscription_past_due_error.py +31 -0
  141. mirascope/api/_generated/types/subscription_past_due_error_tag.py +7 -0
  142. mirascope/api/settings.py +19 -1
  143. mirascope/llm/__init__.py +53 -10
  144. mirascope/llm/calls/__init__.py +2 -1
  145. mirascope/llm/calls/calls.py +29 -20
  146. mirascope/llm/calls/decorator.py +21 -7
  147. mirascope/llm/content/tool_output.py +22 -5
  148. mirascope/llm/exceptions.py +284 -71
  149. mirascope/llm/formatting/__init__.py +17 -0
  150. mirascope/llm/formatting/format.py +112 -35
  151. mirascope/llm/formatting/output_parser.py +178 -0
  152. mirascope/llm/formatting/partial.py +80 -7
  153. mirascope/llm/formatting/primitives.py +192 -0
  154. mirascope/llm/formatting/types.py +20 -8
  155. mirascope/llm/messages/__init__.py +3 -0
  156. mirascope/llm/messages/_utils.py +34 -0
  157. mirascope/llm/models/__init__.py +5 -0
  158. mirascope/llm/models/models.py +137 -69
  159. mirascope/llm/{providers/base → models}/params.py +7 -57
  160. mirascope/llm/models/thinking_config.py +61 -0
  161. mirascope/llm/prompts/_utils.py +0 -32
  162. mirascope/llm/prompts/decorator.py +16 -5
  163. mirascope/llm/prompts/prompts.py +160 -92
  164. mirascope/llm/providers/__init__.py +1 -4
  165. mirascope/llm/providers/anthropic/_utils/__init__.py +2 -0
  166. mirascope/llm/providers/anthropic/_utils/beta_decode.py +18 -9
  167. mirascope/llm/providers/anthropic/_utils/beta_encode.py +62 -13
  168. mirascope/llm/providers/anthropic/_utils/decode.py +18 -9
  169. mirascope/llm/providers/anthropic/_utils/encode.py +26 -7
  170. mirascope/llm/providers/anthropic/_utils/errors.py +2 -2
  171. mirascope/llm/providers/anthropic/beta_provider.py +64 -18
  172. mirascope/llm/providers/anthropic/provider.py +91 -33
  173. mirascope/llm/providers/base/__init__.py +0 -4
  174. mirascope/llm/providers/base/_utils.py +55 -6
  175. mirascope/llm/providers/base/base_provider.py +116 -37
  176. mirascope/llm/providers/google/_utils/__init__.py +2 -0
  177. mirascope/llm/providers/google/_utils/decode.py +20 -7
  178. mirascope/llm/providers/google/_utils/encode.py +26 -7
  179. mirascope/llm/providers/google/_utils/errors.py +3 -2
  180. mirascope/llm/providers/google/provider.py +64 -18
  181. mirascope/llm/providers/mirascope/_utils.py +13 -17
  182. mirascope/llm/providers/mirascope/provider.py +49 -18
  183. mirascope/llm/providers/mlx/_utils.py +7 -2
  184. mirascope/llm/providers/mlx/encoding/base.py +5 -2
  185. mirascope/llm/providers/mlx/encoding/transformers.py +5 -2
  186. mirascope/llm/providers/mlx/mlx.py +23 -6
  187. mirascope/llm/providers/mlx/provider.py +42 -13
  188. mirascope/llm/providers/openai/_utils/errors.py +2 -2
  189. mirascope/llm/providers/openai/completions/_utils/encode.py +20 -16
  190. mirascope/llm/providers/openai/completions/base_provider.py +40 -11
  191. mirascope/llm/providers/openai/provider.py +40 -10
  192. mirascope/llm/providers/openai/responses/_utils/__init__.py +2 -0
  193. mirascope/llm/providers/openai/responses/_utils/decode.py +19 -6
  194. mirascope/llm/providers/openai/responses/_utils/encode.py +22 -10
  195. mirascope/llm/providers/openai/responses/provider.py +56 -18
  196. mirascope/llm/providers/provider_registry.py +93 -19
  197. mirascope/llm/responses/__init__.py +6 -1
  198. mirascope/llm/responses/_utils.py +102 -12
  199. mirascope/llm/responses/base_response.py +5 -2
  200. mirascope/llm/responses/base_stream_response.py +115 -25
  201. mirascope/llm/responses/response.py +2 -1
  202. mirascope/llm/responses/root_response.py +89 -17
  203. mirascope/llm/responses/stream_response.py +6 -9
  204. mirascope/llm/tools/decorator.py +9 -4
  205. mirascope/llm/tools/tool_schema.py +17 -6
  206. mirascope/llm/tools/toolkit.py +35 -27
  207. mirascope/llm/tools/tools.py +45 -20
  208. mirascope/ops/__init__.py +4 -0
  209. mirascope/ops/_internal/closure.py +4 -1
  210. mirascope/ops/_internal/configuration.py +82 -31
  211. mirascope/ops/_internal/exporters/exporters.py +55 -35
  212. mirascope/ops/_internal/exporters/utils.py +37 -0
  213. mirascope/ops/_internal/instrumentation/llm/common.py +530 -0
  214. mirascope/ops/_internal/instrumentation/llm/cost.py +190 -0
  215. mirascope/ops/_internal/instrumentation/llm/encode.py +1 -1
  216. mirascope/ops/_internal/instrumentation/llm/llm.py +116 -1242
  217. mirascope/ops/_internal/instrumentation/llm/model.py +1798 -0
  218. mirascope/ops/_internal/instrumentation/llm/response.py +521 -0
  219. mirascope/ops/_internal/instrumentation/llm/serialize.py +300 -0
  220. mirascope/ops/_internal/protocols.py +83 -1
  221. mirascope/ops/_internal/traced_calls.py +18 -0
  222. mirascope/ops/_internal/traced_functions.py +125 -10
  223. mirascope/ops/_internal/tracing.py +78 -1
  224. mirascope/ops/_internal/utils.py +60 -4
  225. mirascope/ops/_internal/versioned_functions.py +1 -1
  226. {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/METADATA +12 -11
  227. mirascope-2.0.2.dist-info/RECORD +424 -0
  228. {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/licenses/LICENSE +1 -1
  229. mirascope-2.0.0a6.dist-info/RECORD +0 -316
  230. {mirascope-2.0.0a6.dist-info → mirascope-2.0.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,300 @@
1
+ """Mirascope-specific serialization for span attributes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Sequence
7
+ from typing import TYPE_CHECKING, Any, Protocol
8
+
9
+ from .....llm.content import (
10
+ Audio,
11
+ Base64ImageSource,
12
+ Document,
13
+ Image,
14
+ Text,
15
+ Thought,
16
+ ToolCall,
17
+ ToolOutput,
18
+ )
19
+ from .....llm.content.document import Base64DocumentSource, TextDocumentSource
20
+ from .....llm.messages import AssistantMessage, Message, SystemMessage, UserMessage
21
+ from .....llm.responses.usage import Usage
22
+ from ...utils import json_dumps
23
+ from .cost import calculate_cost_async, calculate_cost_sync
24
+
25
+ if TYPE_CHECKING:
26
+ from opentelemetry.util.types import AttributeValue
27
+
28
+ from .....llm.responses.root_response import RootResponse
29
+ from .....llm.types import Jsonable
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class SpanProtocol(Protocol):
35
+ """Protocol for span objects that support setting attributes."""
36
+
37
+ def set(self, **attributes: AttributeValue) -> None:
38
+ """Set attributes on the span."""
39
+ ...
40
+
41
+
42
+ def _serialize_content_part(
43
+ part: Text | ToolCall | Thought | Image | Audio | Document | ToolOutput[Jsonable],
44
+ ) -> dict[str, Any]:
45
+ """Serialize a single content part to a dict matching the Mirascope dataclass structure."""
46
+ if isinstance(part, Text):
47
+ return {"type": "text", "text": part.text}
48
+ elif isinstance(part, ToolCall):
49
+ return {
50
+ "type": "tool_call",
51
+ "id": part.id,
52
+ "name": part.name,
53
+ "args": part.args,
54
+ }
55
+ elif isinstance(part, Thought):
56
+ return {"type": "thought", "thought": part.thought}
57
+ elif isinstance(part, ToolOutput):
58
+ return {
59
+ "type": "tool_output",
60
+ "id": part.id,
61
+ "name": part.name,
62
+ "result": part.result,
63
+ }
64
+ elif isinstance(part, Image):
65
+ if isinstance(part.source, Base64ImageSource):
66
+ return {
67
+ "type": "image",
68
+ "source": {
69
+ "type": "base64_image_source",
70
+ "mime_type": part.source.mime_type,
71
+ "data": part.source.data,
72
+ },
73
+ }
74
+ else: # URLImageSource
75
+ return {
76
+ "type": "image",
77
+ "source": {"type": "url_image_source", "url": part.source.url},
78
+ }
79
+ elif isinstance(part, Audio):
80
+ return {
81
+ "type": "audio",
82
+ "source": {
83
+ "type": "base64_audio_source",
84
+ "mime_type": part.source.mime_type,
85
+ "data": part.source.data,
86
+ },
87
+ }
88
+ elif isinstance(part, Document):
89
+ # Document has multiple source types - serialize based on actual type
90
+ if isinstance(part.source, Base64DocumentSource):
91
+ return {
92
+ "type": "document",
93
+ "source": {
94
+ "type": "base64_document_source",
95
+ "data": part.source.data,
96
+ "media_type": part.source.media_type,
97
+ },
98
+ }
99
+ elif isinstance(part.source, TextDocumentSource):
100
+ return {
101
+ "type": "document",
102
+ "source": {
103
+ "type": "text_document_source",
104
+ "data": part.source.data,
105
+ "media_type": part.source.media_type,
106
+ },
107
+ }
108
+ else: # URLDocumentSource
109
+ return {
110
+ "type": "document",
111
+ "source": {
112
+ "type": "url_document_source",
113
+ "url": part.source.url,
114
+ },
115
+ }
116
+ return {"type": "unknown"} # pragma: no cover
117
+
118
+
119
+ def _serialize_message(message: Message) -> dict[str, Any]:
120
+ """Serialize a Message to a dict matching the Mirascope dataclass structure."""
121
+ if isinstance(message, SystemMessage):
122
+ return {
123
+ "role": "system",
124
+ "content": _serialize_content_part(message.content),
125
+ }
126
+ elif isinstance(message, UserMessage):
127
+ return {
128
+ "role": "user",
129
+ "content": [_serialize_content_part(p) for p in message.content],
130
+ "name": message.name,
131
+ }
132
+ elif isinstance(message, AssistantMessage):
133
+ return {
134
+ "role": "assistant",
135
+ "content": [_serialize_content_part(p) for p in message.content],
136
+ "name": message.name,
137
+ }
138
+ return {"role": "unknown"} # pragma: no cover
139
+
140
+
141
+ def serialize_mirascope_messages(messages: Sequence[Message]) -> str:
142
+ """Serialize input messages to JSON for span attributes."""
143
+ return json_dumps([_serialize_message(m) for m in messages])
144
+
145
+
146
+ def serialize_mirascope_content(
147
+ content: Sequence[Text | ToolCall | Thought],
148
+ ) -> str:
149
+ """Serialize response content to JSON for span attributes."""
150
+ return json_dumps([_serialize_content_part(p) for p in content])
151
+
152
+
153
+ def serialize_mirascope_usage(usage: Usage | None) -> AttributeValue | None:
154
+ """Serialize response usage to JSON for span attributes. Returns None if usage is None."""
155
+ if usage is None:
156
+ return None
157
+ return json_dumps(
158
+ {
159
+ "input_tokens": usage.input_tokens,
160
+ "output_tokens": usage.output_tokens,
161
+ "cache_read_tokens": usage.cache_read_tokens,
162
+ "cache_write_tokens": usage.cache_write_tokens,
163
+ "reasoning_tokens": usage.reasoning_tokens,
164
+ "total_tokens": usage.total_tokens,
165
+ }
166
+ )
167
+
168
+
169
+ def serialize_mirascope_cost(
170
+ input_cost: float,
171
+ output_cost: float,
172
+ total_cost: float,
173
+ cache_read_cost: float | None = None,
174
+ cache_write_cost: float | None = None,
175
+ ) -> str:
176
+ """Serialize cost to JSON for span attributes.
177
+
178
+ All costs are in centicents (1 centicent = $0.0001).
179
+ Consumers can divide by 10,000 to get dollar amounts.
180
+ """
181
+ return json_dumps(
182
+ {
183
+ "input_cost": input_cost,
184
+ "output_cost": output_cost,
185
+ "cache_read_cost": cache_read_cost,
186
+ "cache_write_cost": cache_write_cost,
187
+ "total_cost": total_cost,
188
+ }
189
+ )
190
+
191
+
192
+ def attach_mirascope_response(
193
+ span: SpanProtocol, response: RootResponse[Any, Any]
194
+ ) -> None:
195
+ """Attach Mirascope-specific response attributes to a span.
196
+
197
+ Sets the following attributes:
198
+ - mirascope.trace.output: Pretty-printed response
199
+ - mirascope.response.messages: Serialized input messages (excluding final assistant message)
200
+ - mirascope.response.content: Serialized response content
201
+ - mirascope.response.usage: Serialized usage (if available)
202
+ - mirascope.response.cost: Serialized cost (if available)
203
+ """
204
+ span.set(
205
+ **{
206
+ "mirascope.response.provider_id": response.provider_id,
207
+ "mirascope.response.model_id": response.model_id,
208
+ "mirascope.trace.output": response.pretty(),
209
+ "mirascope.response.messages": serialize_mirascope_messages(
210
+ response.messages[:-1]
211
+ ),
212
+ "mirascope.response.content": serialize_mirascope_content(response.content),
213
+ }
214
+ )
215
+ if (usage_json := serialize_mirascope_usage(response.usage)) is not None:
216
+ span.set(**{"mirascope.response.usage": usage_json})
217
+ logger.debug("Attached usage to span")
218
+ else:
219
+ logger.debug("No usage available, skipping cost calculation")
220
+
221
+ # Calculate and attach cost if usage is available
222
+ if response.usage is not None:
223
+ logger.debug("Attempting cost calculation (sync)")
224
+ cost = calculate_cost_sync(
225
+ response.provider_id, response.model_id, response.usage
226
+ )
227
+ if cost is not None:
228
+ span.set(
229
+ **{
230
+ "mirascope.response.cost": serialize_mirascope_cost(
231
+ input_cost=cost.input_cost_centicents,
232
+ output_cost=cost.output_cost_centicents,
233
+ total_cost=cost.total_cost_centicents,
234
+ cache_read_cost=cost.cache_read_cost_centicents,
235
+ cache_write_cost=cost.cache_write_cost_centicents,
236
+ )
237
+ }
238
+ )
239
+ logger.debug(
240
+ "Attached cost to span: total=%s centicents", cost.total_cost_centicents
241
+ )
242
+ else:
243
+ logger.debug("Cost calculation returned None, not attaching cost to span")
244
+
245
+
246
+ async def attach_mirascope_response_async(
247
+ span: SpanProtocol, response: RootResponse[Any, Any]
248
+ ) -> None:
249
+ """Attach Mirascope-specific response attributes to a span (async version).
250
+
251
+ Sets the following attributes:
252
+ - mirascope.trace.output: Pretty-printed response
253
+ - mirascope.response.messages: Serialized input messages (excluding final assistant message)
254
+ - mirascope.response.content: Serialized response content
255
+ - mirascope.response.usage: Serialized usage (if available)
256
+ - mirascope.response.cost: Serialized cost (if available)
257
+ """
258
+ span.set(
259
+ **{
260
+ "mirascope.response.provider_id": response.provider_id,
261
+ "mirascope.response.model_id": response.model_id,
262
+ "mirascope.trace.output": response.pretty(),
263
+ "mirascope.response.messages": serialize_mirascope_messages(
264
+ response.messages[:-1]
265
+ ),
266
+ "mirascope.response.content": serialize_mirascope_content(response.content),
267
+ }
268
+ )
269
+ if (usage_json := serialize_mirascope_usage(response.usage)) is not None:
270
+ span.set(**{"mirascope.response.usage": usage_json})
271
+ logger.debug("Attached usage to span (async)")
272
+ else:
273
+ logger.debug("No usage available, skipping cost calculation (async)")
274
+
275
+ # Calculate and attach cost if usage is available (async)
276
+ if response.usage is not None:
277
+ logger.debug("Attempting cost calculation (async)")
278
+ cost = await calculate_cost_async(
279
+ response.provider_id, response.model_id, response.usage
280
+ )
281
+ if cost is not None:
282
+ span.set(
283
+ **{
284
+ "mirascope.response.cost": serialize_mirascope_cost(
285
+ input_cost=cost.input_cost_centicents,
286
+ output_cost=cost.output_cost_centicents,
287
+ total_cost=cost.total_cost_centicents,
288
+ cache_read_cost=cost.cache_read_cost_centicents,
289
+ cache_write_cost=cost.cache_write_cost_centicents,
290
+ )
291
+ }
292
+ )
293
+ logger.debug(
294
+ "Attached cost to span (async): total=%s centicents",
295
+ cost.total_cost_centicents,
296
+ )
297
+ else:
298
+ logger.debug(
299
+ "Cost calculation returned None, not attaching cost to span (async)"
300
+ )
@@ -3,12 +3,15 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import inspect
6
- from typing import Protocol
6
+ from typing import TYPE_CHECKING, Protocol
7
7
  from typing_extensions import TypeIs
8
8
 
9
9
  from ...llm.context import Context, DepsT
10
10
  from .types import P, R
11
11
 
12
+ if TYPE_CHECKING:
13
+ from .spans import Span
14
+
12
15
 
13
16
  class SyncFunction(Protocol[P, R]):
14
17
  """Protocol for synchronous callable functions."""
@@ -26,6 +29,30 @@ class AsyncFunction(Protocol[P, R]):
26
29
  ... # pragma: no cover
27
30
 
28
31
 
32
+ class SyncSpanFunction(Protocol[P, R]):
33
+ """Protocol for synchronous functions that receive injected Span.
34
+
35
+ Functions matching this protocol have `trace_ctx: Span` as their first
36
+ parameter. The trace decorator will inject the span automatically.
37
+ """
38
+
39
+ def __call__(self, trace_ctx: Span, *args: P.args, **kwargs: P.kwargs) -> R:
40
+ """The function receives a Span as first parameter."""
41
+ ... # pragma: no cover
42
+
43
+
44
+ class AsyncSpanFunction(Protocol[P, R]):
45
+ """Protocol for asynchronous functions that receive injected Span.
46
+
47
+ Functions matching this protocol have `trace_ctx: Span` as their first
48
+ parameter. The trace decorator will inject the span automatically.
49
+ """
50
+
51
+ async def __call__(self, trace_ctx: Span, *args: P.args, **kwargs: P.kwargs) -> R:
52
+ """The function receives a Span as first parameter."""
53
+ ... # pragma: no cover
54
+
55
+
29
56
  class SyncContextFunction(Protocol[P, DepsT, R]):
30
57
  """Protocol for synchronous callable functions with Context parameter."""
31
58
 
@@ -49,3 +76,58 @@ def fn_is_async(
49
76
  ) -> TypeIs[AsyncFunction[P, R]]:
50
77
  """Type check to determine if a given function is asynchronous."""
51
78
  return inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn)
79
+
80
+
81
+ def fn_wants_span(
82
+ fn: (
83
+ SyncFunction[P, R]
84
+ | AsyncFunction[P, R]
85
+ | SyncSpanFunction[P, R]
86
+ | AsyncSpanFunction[P, R]
87
+ ),
88
+ ) -> TypeIs[SyncSpanFunction[P, R] | AsyncSpanFunction[P, R]]:
89
+ """Check if function wants Span injection as first parameter.
90
+
91
+ Returns True if the function has a first parameter named `trace_ctx`
92
+ with type annotation `Span`.
93
+ """
94
+ # Import here to avoid circular imports
95
+ from .spans import Span
96
+
97
+ try:
98
+ sig = inspect.signature(fn)
99
+ except (ValueError, TypeError):
100
+ return False
101
+
102
+ params = list(sig.parameters.values())
103
+ if not params:
104
+ return False
105
+
106
+ first_param = params[0]
107
+ if first_param.name != "trace_ctx":
108
+ return False
109
+
110
+ # Check annotation
111
+ annotation = first_param.annotation
112
+ if annotation is inspect.Parameter.empty:
113
+ return False
114
+
115
+ # Handle string annotations (forward references)
116
+ # The annotation could be "Span", "ops.Span", "mirascope.ops.Span", etc.
117
+ if isinstance(annotation, str):
118
+ return annotation == "Span" or annotation.endswith(".Span")
119
+
120
+ # Check by identity first
121
+ if annotation is Span:
122
+ return True
123
+
124
+ # Fallback: check by class name and module for robustness
125
+ # This handles cases where the same class might have different identities
126
+ # due to module reloading or import issues in test environments
127
+ if isinstance(annotation, type):
128
+ return (
129
+ annotation.__name__ == "Span"
130
+ and annotation.__module__ == "mirascope.ops._internal.spans"
131
+ )
132
+
133
+ return False
@@ -6,6 +6,7 @@ from dataclasses import dataclass, field
6
6
  from typing import Any, Generic, TypeVar
7
7
  from typing_extensions import TypeIs
8
8
 
9
+ from ..._utils import copy_function_metadata
9
10
  from ...llm.calls import AsyncCall, AsyncContextCall, Call, ContextCall
10
11
  from ...llm.context import Context, DepsT
11
12
  from ...llm.formatting import FormattableT
@@ -22,8 +23,10 @@ from ...llm.responses import (
22
23
  from ...llm.types import P
23
24
  from .protocols import (
24
25
  AsyncFunction,
26
+ AsyncSpanFunction,
25
27
  R,
26
28
  SyncFunction,
29
+ SyncSpanFunction,
27
30
  )
28
31
  from .traced_functions import (
29
32
  AsyncTrace,
@@ -33,6 +36,7 @@ from .traced_functions import (
33
36
  TracedContextFunction,
34
37
  TracedFunction,
35
38
  )
39
+ from .utils import get_original_fn
36
40
 
37
41
  CallT = TypeVar(
38
42
  "CallT",
@@ -47,6 +51,8 @@ def is_call_type(
47
51
  fn: (
48
52
  SyncFunction[P, R]
49
53
  | AsyncFunction[P, R]
54
+ | SyncSpanFunction[P, R]
55
+ | AsyncSpanFunction[P, R]
50
56
  | ContextCall[P, DepsT, FormattableT]
51
57
  | AsyncContextCall[P, DepsT, FormattableT]
52
58
  | Call[P, FormattableT]
@@ -102,6 +108,14 @@ class _BaseTracedCall(Generic[CallT]):
102
108
  metadata: dict[str, str] = field(default_factory=dict)
103
109
  """Arbitrary key-value pairs for additional metadata."""
104
110
 
111
+ __name__: str = field(init=False, repr=False, default="")
112
+ """The name of the underlying function (preserved for decorator stacking)."""
113
+
114
+ def __post_init__(self) -> None:
115
+ """Preserve standard function attributes for decorator stacking."""
116
+ original_fn = get_original_fn(self._call.prompt.fn)
117
+ copy_function_metadata(self, original_fn)
118
+
105
119
 
106
120
  @dataclass(kw_only=True)
107
121
  class TracedCall(_BaseTracedCall[Call[P, FormattableT]]):
@@ -149,6 +163,7 @@ class TracedCall(_BaseTracedCall[Call[P, FormattableT]]):
149
163
 
150
164
  def __post_init__(self) -> None:
151
165
  """Initialize TracedFunction wrappers for call and stream methods."""
166
+ super().__post_init__()
152
167
  self.call = TracedFunction(
153
168
  fn=self._call.call, tags=self.tags, metadata=self.metadata
154
169
  )
@@ -209,6 +224,7 @@ class TracedAsyncCall(_BaseTracedCall[AsyncCall[P, FormattableT]]):
209
224
 
210
225
  def __post_init__(self) -> None:
211
226
  """Initialize AsyncTracedFunction wrappers for call and stream methods."""
227
+ super().__post_init__()
212
228
  self.call = AsyncTracedFunction(
213
229
  fn=self._call.call, tags=self.tags, metadata=self.metadata
214
230
  )
@@ -272,6 +288,7 @@ class TracedContextCall(_BaseTracedCall[ContextCall[P, DepsT, FormattableT]]):
272
288
 
273
289
  def __post_init__(self) -> None:
274
290
  """Initialize TracedContextFunction wrappers for call and stream methods."""
291
+ super().__post_init__()
275
292
  self.call = TracedContextFunction(
276
293
  fn=self._call.call, tags=self.tags, metadata=self.metadata
277
294
  )
@@ -340,6 +357,7 @@ class TracedAsyncContextCall(_BaseTracedCall[AsyncContextCall[P, DepsT, Formatta
340
357
 
341
358
  def __post_init__(self) -> None:
342
359
  """Initialize AsyncTracedContextFunction wrappers for call and stream methods."""
360
+ super().__post_init__()
343
361
  self.call = AsyncTracedContextFunction(
344
362
  fn=self._call.call, tags=self.tags, metadata=self.metadata
345
363
  )
@@ -6,27 +6,32 @@ from abc import ABC, abstractmethod
6
6
  from collections.abc import Generator
7
7
  from contextlib import contextmanager
8
8
  from dataclasses import dataclass, field
9
- from typing import (
10
- Any,
11
- Generic,
12
- Literal,
13
- TypeVar,
14
- )
9
+ from typing import Any, Generic, Literal, TypeVar
15
10
 
16
11
  from opentelemetry.util.types import AttributeValue
17
12
 
13
+ from ..._utils import copy_function_metadata
18
14
  from ...api.client import get_async_client, get_sync_client
19
15
  from ...llm.context import Context, DepsT
20
16
  from ...llm.responses.root_response import RootResponse
17
+ from .instrumentation.llm.serialize import attach_mirascope_response
21
18
  from .protocols import (
22
19
  AsyncContextFunction,
23
20
  AsyncFunction,
21
+ AsyncSpanFunction,
24
22
  SyncContextFunction,
25
23
  SyncFunction,
24
+ SyncSpanFunction,
26
25
  )
27
26
  from .spans import Span
28
27
  from .types import Jsonable, P, R
29
- from .utils import PrimitiveType, extract_arguments, get_qualified_name, json_dumps
28
+ from .utils import (
29
+ PrimitiveType,
30
+ extract_arguments,
31
+ get_original_fn,
32
+ get_qualified_name,
33
+ json_dumps,
34
+ )
30
35
 
31
36
  FunctionT = TypeVar(
32
37
  "FunctionT",
@@ -47,6 +52,7 @@ def record_result_to_span(span: Span, result: object) -> None:
47
52
  output: str | int | float | bool = result
48
53
  elif isinstance(result, RootResponse):
49
54
  output = result.pretty()
55
+ attach_mirascope_response(span, result)
50
56
  else:
51
57
  try:
52
58
  output = json_dumps(result)
@@ -170,10 +176,15 @@ class _BaseFunction(Generic[P, R, FunctionT], ABC):
170
176
  _is_async: bool = field(init=False)
171
177
  """Whether the wrapped function is asynchronous."""
172
178
 
179
+ __name__: str = field(init=False, repr=False, default="")
180
+ """The name of the underlying function (preserved for decorator stacking)."""
181
+
173
182
  def __post_init__(self) -> None:
174
183
  """Initialize additional attributes after dataclass init."""
175
184
  self._qualified_name = get_qualified_name(self.fn)
176
- self._module_name = getattr(self.fn, "__module__", "")
185
+ original_fn = get_original_fn(self.fn)
186
+ self._module_name = getattr(original_fn, "__module__", "")
187
+ copy_function_metadata(self, original_fn)
177
188
 
178
189
 
179
190
  @dataclass(kw_only=True)
@@ -194,7 +205,7 @@ class _BaseTracedFunction(_BaseFunction[P, R, FunctionT]):
194
205
  "mirascope.trace.arg_values": json_dumps(arg_values),
195
206
  }
196
207
  if self.tags:
197
- attributes["mirascope.trace.tags"] = self.tags
208
+ attributes["mirascope.trace.tags"] = list(self.tags)
198
209
  if self.metadata:
199
210
  attributes["mirascope.trace.metadata"] = json_dumps(self.metadata)
200
211
  span.set(**attributes)
@@ -308,7 +319,7 @@ class _BaseTracedContextFunction(
308
319
  "mirascope.trace.arg_values": json_dumps(arg_values),
309
320
  }
310
321
  if self.tags:
311
- attributes["mirascope.trace.tags"] = self.tags
322
+ attributes["mirascope.trace.tags"] = list(self.tags)
312
323
  if self.metadata:
313
324
  attributes["mirascope.trace.metadata"] = json_dumps(self.metadata)
314
325
  span.set(**attributes)
@@ -411,3 +422,107 @@ class AsyncTracedContextFunction(BaseTracedAsyncContextFunction[P, DepsT, R]):
411
422
  result = await self.fn(ctx, *args, **kwargs)
412
423
  record_result_to_span(span, result)
413
424
  return AsyncTrace(result=result, span=span)
425
+
426
+
427
+ @dataclass(kw_only=True)
428
+ class BaseSyncTracedSpanFunction(_BaseTracedFunction[P, R, SyncSpanFunction[P, R]]):
429
+ """Abstract base class for synchronous traced span function wrappers."""
430
+
431
+ _is_async: bool = field(default=False, init=False)
432
+ """Whether the wrapped function is asynchronous."""
433
+
434
+ @abstractmethod
435
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
436
+ """Returns the result of the traced function directly."""
437
+ ...
438
+
439
+ @abstractmethod
440
+ def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> Trace[R]:
441
+ """Returns the trace containing the function result and span info."""
442
+ ...
443
+
444
+
445
+ @dataclass(kw_only=True)
446
+ class TracedSpanFunction(BaseSyncTracedSpanFunction[P, R]):
447
+ """Wrapper for synchronous functions that receive Span as first parameter.
448
+
449
+ The external interface does NOT include `trace_ctx` - it is injected
450
+ automatically by the decorator when calling the inner function.
451
+
452
+ Example:
453
+ ```python
454
+ @ops.trace
455
+ def my_fn(trace_ctx: Span, arg: str) -> str:
456
+ trace_ctx.info(f"Processing: {arg}")
457
+ return arg.upper()
458
+
459
+ # Call without trace_ctx - it's injected
460
+ result = my_fn("hello") # Returns "HELLO"
461
+ ```
462
+ """
463
+
464
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
465
+ """Returns the result of the traced function directly."""
466
+ with self._span(*args, **kwargs) as span:
467
+ result = self.fn(span, *args, **kwargs)
468
+ record_result_to_span(span, result)
469
+ return result
470
+
471
+ def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> Trace[R]:
472
+ """Returns the trace containing the function result and span info."""
473
+ with self._span(*args, **kwargs) as span:
474
+ result = self.fn(span, *args, **kwargs)
475
+ record_result_to_span(span, result)
476
+ return Trace(result=result, span=span)
477
+
478
+
479
+ @dataclass(kw_only=True)
480
+ class BaseAsyncTracedSpanFunction(_BaseTracedFunction[P, R, AsyncSpanFunction[P, R]]):
481
+ """Abstract base class for asynchronous traced span function wrappers."""
482
+
483
+ _is_async: bool = field(default=True, init=False)
484
+ """Whether the wrapped function is asynchronous."""
485
+
486
+ @abstractmethod
487
+ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
488
+ """Returns the result of the traced function directly."""
489
+ ...
490
+
491
+ @abstractmethod
492
+ async def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> AsyncTrace[R]:
493
+ """Returns the trace containing the function result and span info."""
494
+ ...
495
+
496
+
497
+ @dataclass(kw_only=True)
498
+ class AsyncTracedSpanFunction(BaseAsyncTracedSpanFunction[P, R]):
499
+ """Wrapper for asynchronous functions that receive Span as first parameter.
500
+
501
+ The external interface does NOT include `trace_ctx` - it is injected
502
+ automatically by the decorator when calling the inner function.
503
+
504
+ Example:
505
+ ```python
506
+ @ops.trace
507
+ async def my_fn(trace_ctx: Span, arg: str) -> str:
508
+ trace_ctx.info(f"Processing: {arg}")
509
+ return arg.upper()
510
+
511
+ # Call without trace_ctx - it's injected
512
+ result = await my_fn("hello") # Returns "HELLO"
513
+ ```
514
+ """
515
+
516
+ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
517
+ """Returns the result of the traced function directly."""
518
+ with self._span(*args, **kwargs) as span:
519
+ result = await self.fn(span, *args, **kwargs)
520
+ record_result_to_span(span, result)
521
+ return result
522
+
523
+ async def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> AsyncTrace[R]:
524
+ """Returns the trace containing the function result and span info."""
525
+ with self._span(*args, **kwargs) as span:
526
+ result = await self.fn(span, *args, **kwargs)
527
+ record_result_to_span(span, result)
528
+ return AsyncTrace(result=result, span=span)