mirascope 2.0.1__py3-none-any.whl → 2.1.0__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 (75) hide show
  1. mirascope/_stubs.py +39 -18
  2. mirascope/_utils.py +34 -0
  3. mirascope/api/_generated/__init__.py +4 -0
  4. mirascope/api/_generated/organization_invitations/client.py +2 -2
  5. mirascope/api/_generated/organization_invitations/raw_client.py +2 -2
  6. mirascope/api/_generated/project_memberships/__init__.py +4 -0
  7. mirascope/api/_generated/project_memberships/client.py +91 -0
  8. mirascope/api/_generated/project_memberships/raw_client.py +239 -0
  9. mirascope/api/_generated/project_memberships/types/__init__.py +4 -0
  10. mirascope/api/_generated/project_memberships/types/project_memberships_get_response.py +33 -0
  11. mirascope/api/_generated/project_memberships/types/project_memberships_get_response_role.py +7 -0
  12. mirascope/api/_generated/reference.md +73 -1
  13. mirascope/llm/__init__.py +19 -0
  14. mirascope/llm/calls/calls.py +28 -21
  15. mirascope/llm/calls/decorator.py +17 -24
  16. mirascope/llm/formatting/__init__.py +2 -2
  17. mirascope/llm/formatting/format.py +2 -4
  18. mirascope/llm/formatting/types.py +19 -2
  19. mirascope/llm/models/models.py +66 -146
  20. mirascope/llm/prompts/decorator.py +5 -16
  21. mirascope/llm/prompts/prompts.py +35 -38
  22. mirascope/llm/providers/anthropic/_utils/beta_decode.py +22 -7
  23. mirascope/llm/providers/anthropic/_utils/beta_encode.py +22 -16
  24. mirascope/llm/providers/anthropic/_utils/decode.py +45 -7
  25. mirascope/llm/providers/anthropic/_utils/encode.py +28 -15
  26. mirascope/llm/providers/anthropic/beta_provider.py +33 -69
  27. mirascope/llm/providers/anthropic/provider.py +52 -91
  28. mirascope/llm/providers/base/_utils.py +4 -9
  29. mirascope/llm/providers/base/base_provider.py +89 -205
  30. mirascope/llm/providers/google/_utils/decode.py +51 -1
  31. mirascope/llm/providers/google/_utils/encode.py +38 -21
  32. mirascope/llm/providers/google/provider.py +33 -69
  33. mirascope/llm/providers/mirascope/provider.py +25 -61
  34. mirascope/llm/providers/mlx/encoding/base.py +3 -6
  35. mirascope/llm/providers/mlx/encoding/transformers.py +4 -8
  36. mirascope/llm/providers/mlx/mlx.py +9 -21
  37. mirascope/llm/providers/mlx/provider.py +33 -69
  38. mirascope/llm/providers/openai/completions/_utils/encode.py +39 -20
  39. mirascope/llm/providers/openai/completions/base_provider.py +34 -75
  40. mirascope/llm/providers/openai/provider.py +25 -61
  41. mirascope/llm/providers/openai/responses/_utils/decode.py +31 -2
  42. mirascope/llm/providers/openai/responses/_utils/encode.py +32 -17
  43. mirascope/llm/providers/openai/responses/provider.py +34 -75
  44. mirascope/llm/responses/__init__.py +2 -1
  45. mirascope/llm/responses/base_stream_response.py +4 -0
  46. mirascope/llm/responses/response.py +8 -12
  47. mirascope/llm/responses/stream_response.py +8 -12
  48. mirascope/llm/responses/usage.py +44 -0
  49. mirascope/llm/tools/__init__.py +24 -0
  50. mirascope/llm/tools/provider_tools.py +18 -0
  51. mirascope/llm/tools/tool_schema.py +11 -4
  52. mirascope/llm/tools/toolkit.py +24 -6
  53. mirascope/llm/tools/types.py +112 -0
  54. mirascope/llm/tools/web_search_tool.py +32 -0
  55. mirascope/ops/__init__.py +19 -1
  56. mirascope/ops/_internal/closure.py +4 -1
  57. mirascope/ops/_internal/exporters/exporters.py +13 -46
  58. mirascope/ops/_internal/exporters/utils.py +37 -0
  59. mirascope/ops/_internal/instrumentation/__init__.py +20 -0
  60. mirascope/ops/_internal/instrumentation/llm/common.py +19 -49
  61. mirascope/ops/_internal/instrumentation/llm/model.py +61 -82
  62. mirascope/ops/_internal/instrumentation/llm/serialize.py +36 -12
  63. mirascope/ops/_internal/instrumentation/providers/__init__.py +29 -0
  64. mirascope/ops/_internal/instrumentation/providers/anthropic.py +78 -0
  65. mirascope/ops/_internal/instrumentation/providers/base.py +179 -0
  66. mirascope/ops/_internal/instrumentation/providers/google_genai.py +85 -0
  67. mirascope/ops/_internal/instrumentation/providers/openai.py +82 -0
  68. mirascope/ops/_internal/traced_calls.py +14 -0
  69. mirascope/ops/_internal/traced_functions.py +7 -2
  70. mirascope/ops/_internal/utils.py +12 -4
  71. mirascope/ops/_internal/versioned_functions.py +1 -1
  72. {mirascope-2.0.1.dist-info → mirascope-2.1.0.dist-info}/METADATA +96 -68
  73. {mirascope-2.0.1.dist-info → mirascope-2.1.0.dist-info}/RECORD +75 -64
  74. {mirascope-2.0.1.dist-info → mirascope-2.1.0.dist-info}/WHEEL +0 -0
  75. {mirascope-2.0.1.dist-info → mirascope-2.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,78 @@
1
+ """OpenTelemetry instrumentation for Anthropic SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
8
+
9
+ from .base import BaseInstrumentation, ContentCaptureMode
10
+
11
+ if TYPE_CHECKING:
12
+ from opentelemetry.trace import TracerProvider
13
+
14
+
15
+ class AnthropicInstrumentation(BaseInstrumentation[AnthropicInstrumentor]):
16
+ """Manages OpenTelemetry instrumentation lifecycle for the Anthropic SDK."""
17
+
18
+ def _create_instrumentor(self) -> AnthropicInstrumentor:
19
+ """Create a new Anthropic instrumentor instance."""
20
+ return AnthropicInstrumentor()
21
+
22
+ def _configure_capture_content(self, capture_content: ContentCaptureMode) -> None:
23
+ """Configure environment variables for Anthropic content capture."""
24
+ if capture_content == "enabled":
25
+ self._set_env_var("TRACELOOP_TRACE_CONTENT", "true")
26
+ elif capture_content == "disabled":
27
+ self._set_env_var("TRACELOOP_TRACE_CONTENT", "false")
28
+
29
+
30
+ def instrument_anthropic(
31
+ *,
32
+ tracer_provider: TracerProvider | None = None,
33
+ capture_content: ContentCaptureMode = "default",
34
+ ) -> None:
35
+ """Enable OpenTelemetry instrumentation for the Anthropic Python SDK.
36
+
37
+ Uses the provided tracer_provider or the global OpenTelemetry tracer provider.
38
+
39
+ Args:
40
+ tracer_provider: Optional tracer provider to use. If not provided,
41
+ uses the global OpenTelemetry tracer provider.
42
+ capture_content: Controls whether to capture message content in spans.
43
+ - "enabled": Capture message content
44
+ - "disabled": Do not capture message content
45
+ - "default": Use the library's default behavior
46
+
47
+ Example:
48
+
49
+ Enable instrumentation with Mirascope Cloud:
50
+ ```python
51
+ from mirascope import ops
52
+
53
+ ops.configure()
54
+ ops.instrument_anthropic()
55
+ ```
56
+
57
+ Enable instrumentation with content capture:
58
+ ```python
59
+ from mirascope import ops
60
+
61
+ ops.configure()
62
+ ops.instrument_anthropic(capture_content="enabled")
63
+ ```
64
+ """
65
+ AnthropicInstrumentation().instrument(
66
+ tracer_provider=tracer_provider,
67
+ capture_content=capture_content,
68
+ )
69
+
70
+
71
+ def uninstrument_anthropic() -> None:
72
+ """Disable previously configured Anthropic instrumentation."""
73
+ AnthropicInstrumentation().uninstrument()
74
+
75
+
76
+ def is_anthropic_instrumented() -> bool:
77
+ """Return whether Anthropic instrumentation is currently active."""
78
+ return AnthropicInstrumentation().is_instrumented
@@ -0,0 +1,179 @@
1
+ """Base class for OpenTelemetry SDK instrumentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import threading
7
+ from abc import ABC, abstractmethod
8
+ from typing import TYPE_CHECKING, Generic, Literal, Protocol, TypeVar
9
+
10
+ if TYPE_CHECKING:
11
+ from opentelemetry.trace import TracerProvider
12
+
13
+ ContentCaptureMode = Literal["enabled", "disabled", "default"]
14
+
15
+ # OpenTelemetry semantic conventions environment variable
16
+ OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN"
17
+ OTEL_SEMCONV_STABILITY_VALUE = "gen_ai_latest_experimental"
18
+
19
+
20
+ class Instrumentor(Protocol):
21
+ """Protocol for OpenTelemetry instrumentors."""
22
+
23
+ def instrument(self, *, tracer_provider: TracerProvider | None = None) -> None:
24
+ """Enable instrumentation."""
25
+ ...
26
+
27
+ def uninstrument(self) -> None:
28
+ """Disable instrumentation."""
29
+ ...
30
+
31
+
32
+ InstrumentorT = TypeVar("InstrumentorT", bound=Instrumentor)
33
+
34
+
35
+ class BaseInstrumentation(ABC, Generic[InstrumentorT]):
36
+ """Base class for managing OpenTelemetry instrumentation lifecycle.
37
+
38
+ This class provides a thread-safe singleton pattern for SDK instrumentation.
39
+ Subclasses must implement `_create_instrumentor()` and `_configure_capture_content()`.
40
+
41
+ Each subclass gets its own `_instance` and `_instance_lock` via `__init_subclass__`,
42
+ ensuring that different providers can be initialized independently without blocking.
43
+ """
44
+
45
+ _instance: BaseInstrumentation[InstrumentorT] | None = None
46
+ _instance_lock: threading.Lock
47
+ _lock: threading.Lock
48
+ _instrumentor: InstrumentorT | None
49
+ _original_env: dict[str, str | None]
50
+
51
+ def __init_subclass__(cls, **kwargs: object) -> None:
52
+ """Initialize subclass with its own singleton instance and lock."""
53
+ super().__init_subclass__(**kwargs)
54
+ cls._instance = None
55
+ cls._instance_lock = threading.Lock()
56
+
57
+ def __new__(cls) -> BaseInstrumentation[InstrumentorT]:
58
+ """Create or return the singleton instance."""
59
+ if cls._instance is None:
60
+ with cls._instance_lock:
61
+ if cls._instance is None:
62
+ instance = super().__new__(cls)
63
+ instance._lock = threading.Lock()
64
+ instance._instrumentor = None
65
+ instance._original_env = {}
66
+ cls._instance = instance
67
+ return cls._instance
68
+
69
+ @property
70
+ def is_instrumented(self) -> bool:
71
+ """Return whether instrumentation is currently active."""
72
+ return self._instrumentor is not None
73
+
74
+ @abstractmethod
75
+ def _create_instrumentor(self) -> InstrumentorT:
76
+ """Create and return a new instrumentor instance.
77
+
78
+ Returns:
79
+ A new instance of the SDK-specific instrumentor.
80
+ """
81
+ ...
82
+
83
+ @abstractmethod
84
+ def _configure_capture_content(self, capture_content: ContentCaptureMode) -> None:
85
+ """Configure environment variables for content capture.
86
+
87
+ Args:
88
+ capture_content: The capture content mode to configure.
89
+ """
90
+ ...
91
+
92
+ def instrument(
93
+ self,
94
+ *,
95
+ tracer_provider: TracerProvider | None = None,
96
+ capture_content: ContentCaptureMode = "default",
97
+ ) -> None:
98
+ """Enable OpenTelemetry instrumentation for the SDK.
99
+
100
+ Args:
101
+ tracer_provider: Optional tracer provider to use. If not provided,
102
+ uses the global OpenTelemetry tracer provider.
103
+ capture_content: Controls whether to capture message content in spans.
104
+ """
105
+ with self._lock:
106
+ if self.is_instrumented:
107
+ return
108
+
109
+ self._set_env_var(
110
+ OTEL_SEMCONV_STABILITY_OPT_IN,
111
+ OTEL_SEMCONV_STABILITY_VALUE,
112
+ use_setdefault=True,
113
+ )
114
+
115
+ self._configure_capture_content(capture_content)
116
+
117
+ instrumentor = self._create_instrumentor()
118
+ try:
119
+ if tracer_provider is not None:
120
+ instrumentor.instrument(tracer_provider=tracer_provider)
121
+ else:
122
+ instrumentor.instrument()
123
+ except Exception:
124
+ self._restore_env_vars()
125
+ raise
126
+
127
+ self._instrumentor = instrumentor
128
+
129
+ def uninstrument(self) -> None:
130
+ """Disable previously configured instrumentation."""
131
+ with self._lock:
132
+ if self._instrumentor is None:
133
+ return
134
+
135
+ self._instrumentor.uninstrument()
136
+ self._instrumentor = None
137
+ self._restore_env_vars()
138
+
139
+ def _set_env_var(
140
+ self, key: str, value: str, *, use_setdefault: bool = False
141
+ ) -> None:
142
+ """Set an environment variable and track the original value.
143
+
144
+ Args:
145
+ key: The environment variable name.
146
+ value: The value to set.
147
+ use_setdefault: If True, only set if not already present.
148
+ """
149
+ if key not in self._original_env:
150
+ self._original_env[key] = os.environ.get(key)
151
+
152
+ if use_setdefault:
153
+ os.environ.setdefault(key, value)
154
+ else:
155
+ os.environ[key] = value
156
+
157
+ def _restore_env_vars(self) -> None:
158
+ """Restore all environment variables to their original values."""
159
+ for key, value in self._original_env.items():
160
+ if value is None:
161
+ os.environ.pop(key, None)
162
+ else:
163
+ os.environ[key] = value
164
+ self._original_env.clear()
165
+
166
+ @classmethod
167
+ def reset_for_testing(cls) -> None:
168
+ """Reset singleton instance for testing.
169
+
170
+ This properly cleans up the instrumentor and restores environment variables
171
+ before resetting the instance.
172
+ """
173
+ with cls._instance_lock:
174
+ if cls._instance is not None:
175
+ if cls._instance._instrumentor is not None:
176
+ cls._instance._instrumentor.uninstrument()
177
+ cls._instance._instrumentor = None
178
+ cls._instance._restore_env_vars()
179
+ cls._instance = None
@@ -0,0 +1,85 @@
1
+ """OpenTelemetry instrumentation for Google GenAI SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from opentelemetry.instrumentation.google_genai import GoogleGenAiSdkInstrumentor
8
+
9
+ from .base import BaseInstrumentation, ContentCaptureMode
10
+
11
+ if TYPE_CHECKING:
12
+ from opentelemetry.trace import TracerProvider
13
+
14
+
15
+ class GoogleGenAIInstrumentation(BaseInstrumentation[GoogleGenAiSdkInstrumentor]):
16
+ """Manages OpenTelemetry instrumentation lifecycle for the Google GenAI SDK."""
17
+
18
+ def _create_instrumentor(self) -> GoogleGenAiSdkInstrumentor:
19
+ """Create a new Google GenAI instrumentor instance."""
20
+ return GoogleGenAiSdkInstrumentor()
21
+
22
+ def _configure_capture_content(self, capture_content: ContentCaptureMode) -> None:
23
+ """Configure environment variables for Google GenAI content capture."""
24
+ # Google GenAI uses ContentCapturingMode enum instead of true/false.
25
+ # Valid values: NO_CONTENT, SPAN_ONLY, EVENT_ONLY, SPAN_AND_EVENT
26
+ if capture_content == "enabled":
27
+ self._set_env_var(
28
+ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT",
29
+ "SPAN_AND_EVENT",
30
+ )
31
+ elif capture_content == "disabled":
32
+ self._set_env_var(
33
+ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "NO_CONTENT"
34
+ )
35
+
36
+
37
+ def instrument_google_genai(
38
+ *,
39
+ tracer_provider: TracerProvider | None = None,
40
+ capture_content: ContentCaptureMode = "default",
41
+ ) -> None:
42
+ """Enable OpenTelemetry instrumentation for the Google GenAI Python SDK.
43
+
44
+ Uses the provided tracer_provider or the global OpenTelemetry tracer provider.
45
+
46
+ Args:
47
+ tracer_provider: Optional tracer provider to use. If not provided,
48
+ uses the global OpenTelemetry tracer provider.
49
+ capture_content: Controls whether to capture message content in spans.
50
+ - "enabled": Capture message content
51
+ - "disabled": Do not capture message content
52
+ - "default": Use the library's default behavior
53
+
54
+ Example:
55
+
56
+ Enable instrumentation with Mirascope Cloud:
57
+ ```python
58
+ from mirascope import ops
59
+
60
+ ops.configure()
61
+ ops.instrument_google_genai()
62
+ ```
63
+
64
+ Enable instrumentation with content capture:
65
+ ```python
66
+ from mirascope import ops
67
+
68
+ ops.configure()
69
+ ops.instrument_google_genai(capture_content="enabled")
70
+ ```
71
+ """
72
+ GoogleGenAIInstrumentation().instrument(
73
+ tracer_provider=tracer_provider,
74
+ capture_content=capture_content,
75
+ )
76
+
77
+
78
+ def uninstrument_google_genai() -> None:
79
+ """Disable previously configured Google GenAI instrumentation."""
80
+ GoogleGenAIInstrumentation().uninstrument()
81
+
82
+
83
+ def is_google_genai_instrumented() -> bool:
84
+ """Return whether Google GenAI instrumentation is currently active."""
85
+ return GoogleGenAIInstrumentation().is_instrumented
@@ -0,0 +1,82 @@
1
+ """OpenTelemetry instrumentation for OpenAI SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
8
+
9
+ from .base import BaseInstrumentation, ContentCaptureMode
10
+
11
+ if TYPE_CHECKING:
12
+ from opentelemetry.trace import TracerProvider
13
+
14
+
15
+ class OpenAIInstrumentation(BaseInstrumentation[OpenAIInstrumentor]):
16
+ """Manages OpenTelemetry instrumentation lifecycle for the OpenAI SDK."""
17
+
18
+ def _create_instrumentor(self) -> OpenAIInstrumentor:
19
+ """Create a new OpenAI instrumentor instance."""
20
+ return OpenAIInstrumentor()
21
+
22
+ def _configure_capture_content(self, capture_content: ContentCaptureMode) -> None:
23
+ """Configure environment variables for OpenAI content capture."""
24
+ if capture_content == "enabled":
25
+ self._set_env_var(
26
+ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "true"
27
+ )
28
+ elif capture_content == "disabled":
29
+ self._set_env_var(
30
+ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false"
31
+ )
32
+
33
+
34
+ def instrument_openai(
35
+ *,
36
+ tracer_provider: TracerProvider | None = None,
37
+ capture_content: ContentCaptureMode = "default",
38
+ ) -> None:
39
+ """Enable OpenTelemetry instrumentation for the OpenAI Python SDK.
40
+
41
+ Uses the provided tracer_provider or the global OpenTelemetry tracer provider.
42
+
43
+ Args:
44
+ tracer_provider: Optional tracer provider to use. If not provided,
45
+ uses the global OpenTelemetry tracer provider.
46
+ capture_content: Controls whether to capture message content in spans.
47
+ - "enabled": Capture message content
48
+ - "disabled": Do not capture message content
49
+ - "default": Use the library's default behavior
50
+
51
+ Example:
52
+
53
+ Enable instrumentation with Mirascope Cloud:
54
+ ```python
55
+ from mirascope import ops
56
+
57
+ ops.configure()
58
+ ops.instrument_openai()
59
+ ```
60
+
61
+ Enable instrumentation with content capture:
62
+ ```python
63
+ from mirascope import ops
64
+
65
+ ops.configure()
66
+ ops.instrument_openai(capture_content="enabled")
67
+ ```
68
+ """
69
+ OpenAIInstrumentation().instrument(
70
+ tracer_provider=tracer_provider,
71
+ capture_content=capture_content,
72
+ )
73
+
74
+
75
+ def uninstrument_openai() -> None:
76
+ """Disable previously configured OpenAI instrumentation."""
77
+ OpenAIInstrumentation().uninstrument()
78
+
79
+
80
+ def is_openai_instrumented() -> bool:
81
+ """Return whether OpenAI instrumentation is currently active."""
82
+ return OpenAIInstrumentation().is_instrumented
@@ -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
@@ -35,6 +36,7 @@ from .traced_functions import (
35
36
  TracedContextFunction,
36
37
  TracedFunction,
37
38
  )
39
+ from .utils import get_original_fn
38
40
 
39
41
  CallT = TypeVar(
40
42
  "CallT",
@@ -106,6 +108,14 @@ class _BaseTracedCall(Generic[CallT]):
106
108
  metadata: dict[str, str] = field(default_factory=dict)
107
109
  """Arbitrary key-value pairs for additional metadata."""
108
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
+
109
119
 
110
120
  @dataclass(kw_only=True)
111
121
  class TracedCall(_BaseTracedCall[Call[P, FormattableT]]):
@@ -153,6 +163,7 @@ class TracedCall(_BaseTracedCall[Call[P, FormattableT]]):
153
163
 
154
164
  def __post_init__(self) -> None:
155
165
  """Initialize TracedFunction wrappers for call and stream methods."""
166
+ super().__post_init__()
156
167
  self.call = TracedFunction(
157
168
  fn=self._call.call, tags=self.tags, metadata=self.metadata
158
169
  )
@@ -213,6 +224,7 @@ class TracedAsyncCall(_BaseTracedCall[AsyncCall[P, FormattableT]]):
213
224
 
214
225
  def __post_init__(self) -> None:
215
226
  """Initialize AsyncTracedFunction wrappers for call and stream methods."""
227
+ super().__post_init__()
216
228
  self.call = AsyncTracedFunction(
217
229
  fn=self._call.call, tags=self.tags, metadata=self.metadata
218
230
  )
@@ -276,6 +288,7 @@ class TracedContextCall(_BaseTracedCall[ContextCall[P, DepsT, FormattableT]]):
276
288
 
277
289
  def __post_init__(self) -> None:
278
290
  """Initialize TracedContextFunction wrappers for call and stream methods."""
291
+ super().__post_init__()
279
292
  self.call = TracedContextFunction(
280
293
  fn=self._call.call, tags=self.tags, metadata=self.metadata
281
294
  )
@@ -344,6 +357,7 @@ class TracedAsyncContextCall(_BaseTracedCall[AsyncContextCall[P, DepsT, Formatta
344
357
 
345
358
  def __post_init__(self) -> None:
346
359
  """Initialize AsyncTracedContextFunction wrappers for call and stream methods."""
360
+ super().__post_init__()
347
361
  self.call = AsyncTracedContextFunction(
348
362
  fn=self._call.call, tags=self.tags, metadata=self.metadata
349
363
  )
@@ -10,6 +10,7 @@ from typing import Any, Generic, Literal, TypeVar
10
10
 
11
11
  from opentelemetry.util.types import AttributeValue
12
12
 
13
+ from ..._utils import copy_function_metadata
13
14
  from ...api.client import get_async_client, get_sync_client
14
15
  from ...llm.context import Context, DepsT
15
16
  from ...llm.responses.root_response import RootResponse
@@ -175,11 +176,15 @@ class _BaseFunction(Generic[P, R, FunctionT], ABC):
175
176
  _is_async: bool = field(init=False)
176
177
  """Whether the wrapped function is asynchronous."""
177
178
 
179
+ __name__: str = field(init=False, repr=False, default="")
180
+ """The name of the underlying function (preserved for decorator stacking)."""
181
+
178
182
  def __post_init__(self) -> None:
179
183
  """Initialize additional attributes after dataclass init."""
180
184
  self._qualified_name = get_qualified_name(self.fn)
181
185
  original_fn = get_original_fn(self.fn)
182
186
  self._module_name = getattr(original_fn, "__module__", "")
187
+ copy_function_metadata(self, original_fn)
183
188
 
184
189
 
185
190
  @dataclass(kw_only=True)
@@ -200,7 +205,7 @@ class _BaseTracedFunction(_BaseFunction[P, R, FunctionT]):
200
205
  "mirascope.trace.arg_values": json_dumps(arg_values),
201
206
  }
202
207
  if self.tags:
203
- attributes["mirascope.trace.tags"] = self.tags
208
+ attributes["mirascope.trace.tags"] = list(self.tags)
204
209
  if self.metadata:
205
210
  attributes["mirascope.trace.metadata"] = json_dumps(self.metadata)
206
211
  span.set(**attributes)
@@ -314,7 +319,7 @@ class _BaseTracedContextFunction(
314
319
  "mirascope.trace.arg_values": json_dumps(arg_values),
315
320
  }
316
321
  if self.tags:
317
- attributes["mirascope.trace.tags"] = self.tags
322
+ attributes["mirascope.trace.tags"] = list(self.tags)
318
323
  if self.metadata:
319
324
  attributes["mirascope.trace.metadata"] = json_dumps(self.metadata)
320
325
  span.set(**attributes)
@@ -33,15 +33,23 @@ def _is_call_method(fn: Callable[..., Any]) -> bool:
33
33
 
34
34
 
35
35
  def get_original_fn(fn: Callable[..., Any]) -> Callable[..., Any]:
36
- """Get the original function from a Call method or return fn as-is.
36
+ """Get the original unwrapped function.
37
37
 
38
- When fn is a bound method of a Call object (e.g., Call.call or Call.stream),
39
- returns the original decorated function from prompt.fn. Otherwise returns fn.
38
+ Follows the __wrapped__ chain set by copy_function_metadata() to find the
39
+ original function. Falls back to checking for Call methods for bound methods.
40
40
  """
41
+ # Follow __wrapped__ chain if available (set by copy_function_metadata)
42
+ # In practice, get_original_fn is called with original functions or bound methods,
43
+ # so this loop is defensive code for edge cases like @functools.wraps chains.
44
+ while hasattr(fn, "__wrapped__"):
45
+ fn = fn.__wrapped__ # pyright: ignore[reportFunctionMemberAccess] # pragma: no cover
46
+
47
+ # Handle bound methods of Call objects (e.g., Call.call or Call.stream)
41
48
  if _is_call_method(fn):
42
49
  prompt = fn.__self__.prompt # pyright: ignore[reportFunctionMemberAccess]
43
50
  if hasattr(prompt, "fn"):
44
- return prompt.fn
51
+ return get_original_fn(prompt.fn)
52
+
45
53
  return fn
46
54
 
47
55
 
@@ -196,7 +196,7 @@ class _BaseVersionedFunction(_BaseTracedFunction[P, R, Any]):
196
196
  if self.name:
197
197
  span.set(**{"mirascope.version.name": self.name})
198
198
  if self.tags:
199
- span.set(**{"mirascope.version.tags": self.tags})
199
+ span.set(**{"mirascope.version.tags": list(self.tags)})
200
200
  if self.metadata:
201
201
  for key, value in self.metadata.items():
202
202
  span.set(**{f"mirascope.version.meta.{key}": value})