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
@@ -6,6 +6,38 @@ from dataclasses import dataclass
6
6
  from typing import Any, Literal
7
7
 
8
8
 
9
+ @dataclass(kw_only=True)
10
+ class ProviderToolUsage:
11
+ """Usage data for a provider's server-side tool.
12
+
13
+ Tracks usage of tools executed by the provider that have separate pricing
14
+ beyond standard token costs (e.g., web search, code execution).
15
+ """
16
+
17
+ name: str
18
+ """Tool name matching our ProviderTool.name (e.g., "web_search").
19
+
20
+ This is the consistent cross-provider identifier that matches
21
+ the corresponding Mirascope ProviderTool class (e.g., WebSearchTool.name).
22
+ """
23
+
24
+ call_count: int = 0
25
+ """Number of invocations/calls of this tool."""
26
+
27
+ duration_seconds: float | None = None
28
+ """Duration in seconds for time-based tools (e.g., code execution).
29
+
30
+ None if not applicable to this tool type.
31
+ """
32
+
33
+ metadata: dict[str, Any] | None = None
34
+ """Provider-specific metadata for debugging/analytics.
35
+
36
+ Examples:
37
+ - Google grounding: {"queries": ["query1", "query2"]}
38
+ """
39
+
40
+
9
41
  @dataclass(kw_only=True)
10
42
  class UsageDeltaChunk:
11
43
  """A chunk containing incremental token usage information from a streaming response.
@@ -31,6 +63,9 @@ class UsageDeltaChunk:
31
63
  reasoning_tokens: int = 0
32
64
  """Delta in reasoning/thinking tokens."""
33
65
 
66
+ provider_tool_usage: list[ProviderToolUsage] | None = None
67
+ """Provider tool usage (emitted in final chunk only)."""
68
+
34
69
 
35
70
  @dataclass(kw_only=True)
36
71
  class Usage:
@@ -86,6 +121,15 @@ class Usage:
86
121
  Will be 0 if not reported by the provider or if the model does not support reasoning.
87
122
  """
88
123
 
124
+ provider_tool_usage: list[ProviderToolUsage] | None = None
125
+ """Provider tool usage data for tools with separate pricing.
126
+
127
+ Tracks usage of server-side tools executed by the provider (e.g., web search,
128
+ code execution) that have pricing beyond standard token costs.
129
+
130
+ Will be None if no provider tools were used.
131
+ """
132
+
89
133
  raw: Any = None
90
134
  """The raw usage object from the provider."""
91
135
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .decorator import ToolDecorator, tool
4
4
  from .protocols import AsyncContextToolFn, AsyncToolFn, ContextToolFn, ToolFn
5
+ from .provider_tools import ProviderTool
5
6
  from .tool_schema import (
6
7
  FORMAT_TOOL_NAME,
7
8
  AnyToolFn,
@@ -19,21 +20,38 @@ from .toolkit import (
19
20
  ToolkitT,
20
21
  )
21
22
  from .tools import AsyncContextTool, AsyncTool, ContextTool, Tool, ToolT
23
+ from .types import (
24
+ AnyTools,
25
+ AsyncContextTools,
26
+ AsyncTools,
27
+ ContextTools,
28
+ Tools,
29
+ normalize_async_context_tools,
30
+ normalize_async_tools,
31
+ normalize_context_tools,
32
+ normalize_tools,
33
+ )
34
+ from .web_search_tool import WebSearchTool
22
35
 
23
36
  __all__ = [
24
37
  "FORMAT_TOOL_NAME",
25
38
  "AnyToolFn",
26
39
  "AnyToolSchema",
40
+ "AnyTools",
27
41
  "AsyncContextTool",
28
42
  "AsyncContextToolFn",
29
43
  "AsyncContextToolkit",
44
+ "AsyncContextTools",
30
45
  "AsyncTool",
31
46
  "AsyncToolFn",
32
47
  "AsyncToolkit",
48
+ "AsyncTools",
33
49
  "BaseToolkit",
34
50
  "ContextTool",
35
51
  "ContextToolFn",
36
52
  "ContextToolkit",
53
+ "ContextTools",
54
+ "ProviderTool",
37
55
  "Tool",
38
56
  "ToolDecorator",
39
57
  "ToolFn",
@@ -43,5 +61,11 @@ __all__ = [
43
61
  "ToolT",
44
62
  "Toolkit",
45
63
  "ToolkitT",
64
+ "Tools",
65
+ "WebSearchTool",
66
+ "normalize_async_context_tools",
67
+ "normalize_async_tools",
68
+ "normalize_context_tools",
69
+ "normalize_tools",
46
70
  "tool",
47
71
  ]
@@ -0,0 +1,18 @@
1
+ """Base class for provider-native tools."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class ProviderTool:
8
+ """Base class for tools executed natively by providers.
9
+
10
+ Unlike regular tools which define functions that you execute locally,
11
+ provider tools are capabilities built into the provider's API.
12
+ The provider handles execution entirely server-side.
13
+
14
+ Provider tools have no sync/async distinction since they are not
15
+ executed by your code - they are configuration passed to the provider.
16
+ """
17
+
18
+ name: str
@@ -22,6 +22,7 @@ from docstring_parser import parse
22
22
  from pydantic import BaseModel, Field, create_model
23
23
  from pydantic.fields import FieldInfo
24
24
 
25
+ from ..._utils import copy_function_metadata
25
26
  from ..content import ToolCall
26
27
  from ..types import Jsonable
27
28
  from .protocols import AsyncContextToolFn, AsyncToolFn, ContextToolFn, ToolFn
@@ -39,8 +40,7 @@ ToolFnT = TypeVar(
39
40
  covariant=True,
40
41
  )
41
42
 
42
- AnyToolSchema: TypeAlias = "ToolSchema[AnyToolFn]"
43
- ToolSchemaT = TypeVar("ToolSchemaT", bound=AnyToolSchema, covariant=True)
43
+ ToolSchemaT = TypeVar("ToolSchemaT", bound="ToolSchema[AnyToolFn]", covariant=True)
44
44
 
45
45
 
46
46
  ModelJsonSchema = TypedDict(
@@ -159,11 +159,14 @@ class ToolSchema(Generic[ToolFnT]):
159
159
 
160
160
  strict: bool | None
161
161
  """Whether the tool should use strict mode when supported by the model.
162
-
163
- If set to None, will use the provider's default setting (usually as strict as
162
+
163
+ If set to None, will use the provider's default setting (usually as strict as
164
164
  possible).
165
165
  """
166
166
 
167
+ __name__: str
168
+ """The name of the underlying function (preserved for decorator stacking)."""
169
+
167
170
  def __hash__(self) -> int:
168
171
  if not hasattr(self, "_hash"):
169
172
  self._hash = hash(
@@ -200,6 +203,7 @@ class ToolSchema(Generic[ToolFnT]):
200
203
  self.description = description
201
204
  self.parameters = parameters
202
205
  self.strict = strict
206
+ copy_function_metadata(self, fn)
203
207
 
204
208
  @classmethod
205
209
  def from_function(
@@ -312,3 +316,6 @@ class ToolSchema(Generic[ToolFnT]):
312
316
  is performed.
313
317
  """
314
318
  return tool_call.name == self.name
319
+
320
+
321
+ AnyToolSchema: TypeAlias = ToolSchema[AnyToolFn]
@@ -6,6 +6,7 @@ from ..content import ToolCall, ToolOutput
6
6
  from ..context import Context, DepsT
7
7
  from ..exceptions import ToolNotFoundError
8
8
  from ..types import Jsonable
9
+ from .provider_tools import ProviderTool
9
10
  from .tool_schema import ToolSchemaT
10
11
  from .tools import AsyncContextTool, AsyncTool, ContextTool, Tool
11
12
 
@@ -24,13 +25,24 @@ class BaseToolkit(Generic[ToolSchemaT]):
24
25
  including name validation and tool lookup.
25
26
  """
26
27
 
27
- tools: Sequence[ToolSchemaT]
28
+ tools: Sequence[ToolSchemaT | ProviderTool]
28
29
  """The tools included in the toolkit."""
29
30
 
30
31
  tools_dict: dict[str, ToolSchemaT]
31
- """A mapping from tool names to tools in the toolkit."""
32
+ """A mapping from tool names to tools in the toolkit.
32
33
 
33
- def __init__(self, tools: Sequence[ToolSchemaT] | None) -> None:
34
+ This dict does not include any `ProviderTool`s, since they do not correspond
35
+ to tool calls that your code executes.
36
+ """
37
+
38
+ provider_tools_dict: dict[str, ProviderTool]
39
+ """A mapping from provider tool names to provider tools in the toolkit.
40
+
41
+ Provider tools are capabilities built into the provider's API (like web search)
42
+ that are executed server-side, not by your code.
43
+ """
44
+
45
+ def __init__(self, tools: Sequence[ToolSchemaT | ProviderTool] | None) -> None:
34
46
  """Initialize the toolkit with a collection of tools.
35
47
 
36
48
  Args:
@@ -41,10 +53,16 @@ class BaseToolkit(Generic[ToolSchemaT]):
41
53
  """
42
54
  self.tools = tools or []
43
55
  self.tools_dict = {}
56
+ self.provider_tools_dict = {}
44
57
  for tool in self.tools:
45
- if tool.name in self.tools_dict:
46
- raise ValueError(f"Multiple tools with name: {tool.name}")
47
- self.tools_dict[tool.name] = tool
58
+ if isinstance(tool, ProviderTool):
59
+ if tool.name in self.provider_tools_dict:
60
+ raise ValueError(f"Multiple provider tools with name: {tool.name}")
61
+ self.provider_tools_dict[tool.name] = tool
62
+ else:
63
+ if tool.name in self.tools_dict:
64
+ raise ValueError(f"Multiple tools with name: {tool.name}")
65
+ self.tools_dict[tool.name] = tool
48
66
 
49
67
  def get(self, tool_call: ToolCall) -> ToolSchemaT:
50
68
  """Get a tool that can execute a specific tool call.
@@ -0,0 +1,112 @@
1
+ """Type aliases for tool parameter types used in provider signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import TypeAlias
7
+ from typing_extensions import TypeAliasType
8
+
9
+ from ..context import DepsT
10
+ from .provider_tools import ProviderTool
11
+ from .tool_schema import AnyToolSchema
12
+ from .toolkit import (
13
+ AsyncContextToolkit,
14
+ AsyncToolkit,
15
+ BaseToolkit,
16
+ ContextToolkit,
17
+ Toolkit,
18
+ )
19
+ from .tools import AsyncContextTool, AsyncTool, ContextTool, Tool
20
+
21
+ AnyTools: TypeAlias = (
22
+ Sequence[AnyToolSchema | ProviderTool] | BaseToolkit[AnyToolSchema]
23
+ )
24
+
25
+ Tools: TypeAlias = Sequence[Tool | ProviderTool] | Toolkit
26
+ """Type alias for sync tool parameters: a sequence of Tools or a Toolkit."""
27
+
28
+ AsyncTools: TypeAlias = Sequence[AsyncTool | ProviderTool] | AsyncToolkit
29
+ """Type alias for async tool parameters: a sequence of AsyncTools or an AsyncToolkit."""
30
+
31
+ ContextTools = TypeAliasType(
32
+ "ContextTools",
33
+ Sequence[Tool | ContextTool[DepsT] | ProviderTool] | ContextToolkit[DepsT],
34
+ type_params=(DepsT,),
35
+ )
36
+ """Type alias for sync context tool parameters: a sequence of Tools/ContextTools or a ContextToolkit."""
37
+
38
+ AsyncContextTools = TypeAliasType(
39
+ "AsyncContextTools",
40
+ Sequence[AsyncTool | AsyncContextTool[DepsT] | ProviderTool]
41
+ | AsyncContextToolkit[DepsT],
42
+ type_params=(DepsT,),
43
+ )
44
+ """Type alias for async context tool parameters: a sequence of AsyncTools/AsyncContextTools or an AsyncContextToolkit."""
45
+
46
+
47
+ def normalize_tools(tools: Tools | None) -> Toolkit:
48
+ """Normalize tools input to a Toolkit.
49
+
50
+ Args:
51
+ tools: A sequence of Tools, a Toolkit, or None.
52
+
53
+ Returns:
54
+ A Toolkit containing the tools (or an empty Toolkit if None).
55
+ """
56
+ if tools is None:
57
+ return Toolkit(None)
58
+ if isinstance(tools, Toolkit):
59
+ return tools
60
+ return Toolkit(tools)
61
+
62
+
63
+ def normalize_async_tools(tools: AsyncTools | None) -> AsyncToolkit:
64
+ """Normalize async tools input to an AsyncToolkit.
65
+
66
+ Args:
67
+ tools: A sequence of AsyncTools, an AsyncToolkit, or None.
68
+
69
+ Returns:
70
+ An AsyncToolkit containing the tools (or an empty AsyncToolkit if None).
71
+ """
72
+ if tools is None:
73
+ return AsyncToolkit(None)
74
+ if isinstance(tools, AsyncToolkit):
75
+ return tools
76
+ return AsyncToolkit(tools)
77
+
78
+
79
+ def normalize_context_tools(
80
+ tools: ContextTools[DepsT] | None,
81
+ ) -> ContextToolkit[DepsT]:
82
+ """Normalize context tools input to a ContextToolkit.
83
+
84
+ Args:
85
+ tools: A sequence of Tools/ContextTools, a ContextToolkit, or None.
86
+
87
+ Returns:
88
+ A ContextToolkit containing the tools (or an empty ContextToolkit if None).
89
+ """
90
+ if tools is None:
91
+ return ContextToolkit(None)
92
+ if isinstance(tools, ContextToolkit):
93
+ return tools
94
+ return ContextToolkit(tools)
95
+
96
+
97
+ def normalize_async_context_tools(
98
+ tools: AsyncContextTools[DepsT] | None,
99
+ ) -> AsyncContextToolkit[DepsT]:
100
+ """Normalize async context tools input to an AsyncContextToolkit.
101
+
102
+ Args:
103
+ tools: A sequence of AsyncTools/AsyncContextTools, an AsyncContextToolkit, or None.
104
+
105
+ Returns:
106
+ An AsyncContextToolkit containing the tools (or an empty AsyncContextToolkit if None).
107
+ """
108
+ if tools is None:
109
+ return AsyncContextToolkit(None)
110
+ if isinstance(tools, AsyncContextToolkit):
111
+ return tools
112
+ return AsyncContextToolkit(tools)
@@ -0,0 +1,32 @@
1
+ """Web search tool for provider-native web search capabilities."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from .provider_tools import ProviderTool
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class WebSearchTool(ProviderTool):
10
+ """Web search tool that allows the model to search the internet.
11
+
12
+ This is a provider tool - the search is executed server-side by the provider,
13
+ not by your code. The model decides when to search based on the prompt,
14
+ and the provider returns search results with citations.
15
+
16
+ Supported providers include Anthropic, Google, and OpenAI (when using the Responses API).
17
+
18
+ Example:
19
+ ```python
20
+ from mirascope import llm
21
+
22
+ @llm.call("anthropic/claude-sonnet-4-5", tools=[llm.WebSearchTool()])
23
+ def search_web(query: str) -> str:
24
+ return f"Search the web for: {query}"
25
+
26
+ response = search_web("Who won the 2024 Super Bowl?")
27
+ print(response.text()) # Response includes citations from web search
28
+ ```
29
+ """
30
+
31
+ name: str = field(default="web_search", init=False)
32
+ """The tool name. Always "web_search" for this tool type."""
mirascope/ops/__init__.py CHANGED
@@ -12,9 +12,18 @@ stub_module_if_missing("mirascope.ops", "ops")
12
12
  # ruff: noqa: E402
13
13
  from ._internal.configuration import configure, tracer_context
14
14
  from ._internal.context import propagated_context
15
- from ._internal.instrumentation.llm import (
15
+ from ._internal.instrumentation import (
16
+ instrument_anthropic,
17
+ instrument_google_genai,
16
18
  instrument_llm,
19
+ instrument_openai,
20
+ is_anthropic_instrumented,
21
+ is_google_genai_instrumented,
22
+ is_openai_instrumented,
23
+ uninstrument_anthropic,
24
+ uninstrument_google_genai,
17
25
  uninstrument_llm,
26
+ uninstrument_openai,
18
27
  )
19
28
  from ._internal.propagation import (
20
29
  ContextPropagator,
@@ -99,13 +108,22 @@ __all__ = [
99
108
  "extract_session_id",
100
109
  "get_propagator",
101
110
  "inject_context",
111
+ "instrument_anthropic",
112
+ "instrument_google_genai",
102
113
  "instrument_llm",
114
+ "instrument_openai",
115
+ "is_anthropic_instrumented",
116
+ "is_google_genai_instrumented",
117
+ "is_openai_instrumented",
103
118
  "propagated_context",
104
119
  "reset_propagator",
105
120
  "session",
106
121
  "span",
107
122
  "trace",
108
123
  "tracer_context",
124
+ "uninstrument_anthropic",
125
+ "uninstrument_google_genai",
109
126
  "uninstrument_llm",
127
+ "uninstrument_openai",
110
128
  "version",
111
129
  ]
@@ -738,12 +738,15 @@ class _DependencyCollector:
738
738
  # For Python 3.13+
739
739
  return definition.func # pyright: ignore[reportFunctionMemberAccess] # pragma: no cover
740
740
 
741
+ # Handle objects with .fn but no __qualname__ (e.g., old-style wrappers).
742
+ # With copy_function_metadata() now copying __qualname__ to ToolSchema, Prompt,
743
+ # Call, etc., this branch is no longer reached in normal usage.
741
744
  if (
742
745
  (wrapped_function := getattr(definition, "fn", None)) is not None
743
746
  and not hasattr(definition, "__qualname__")
744
747
  and callable(wrapped_function)
745
748
  ):
746
- return wrapped_function
749
+ return wrapped_function # pragma: no cover
747
750
 
748
751
  return definition
749
752
 
@@ -20,14 +20,17 @@ from ....api._generated.traces.types import (
20
20
  TracesCreateRequestResourceSpansItemResource,
21
21
  TracesCreateRequestResourceSpansItemResourceAttributesItem,
22
22
  TracesCreateRequestResourceSpansItemResourceAttributesItemValue,
23
+ TracesCreateRequestResourceSpansItemResourceAttributesItemValueArrayValue,
23
24
  TracesCreateRequestResourceSpansItemScopeSpansItem,
24
25
  TracesCreateRequestResourceSpansItemScopeSpansItemScope,
25
26
  TracesCreateRequestResourceSpansItemScopeSpansItemSpansItem,
26
27
  TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItem,
27
28
  TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue,
29
+ TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValueArrayValue,
28
30
  TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemStatus,
29
31
  )
30
32
  from ....api.client import Mirascope
33
+ from .utils import to_otlp_any_value
31
34
 
32
35
  logger = logging.getLogger(__name__)
33
36
 
@@ -282,17 +285,7 @@ class MirascopeOTLPExporter(SpanExporter):
282
285
  def _convert_attribute_value(
283
286
  self, value: AttributeValue
284
287
  ) -> TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue:
285
- """Convert OpenTelemetry AttributeValue to Mirascope API's KeyValueValue.
286
-
287
- This conversion is necessary because the Fern-generated API client
288
- expects KeyValueValue objects, not OpenTelemetry's AttributeValue types.
289
-
290
- Args:
291
- value: An OpenTelemetry AttributeValue (bool, int, float, str, or Sequence)
292
-
293
- Returns:
294
- A KeyValueValue object for the Mirascope API
295
- """
288
+ """Convert OpenTelemetry AttributeValue to Mirascope API's KeyValueValue."""
296
289
  match value:
297
290
  case str():
298
291
  return TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue(
@@ -312,49 +305,21 @@ class MirascopeOTLPExporter(SpanExporter):
312
305
  )
313
306
  case _:
314
307
  return TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValue(
315
- string_value=str(list(value))
308
+ array_value=TracesCreateRequestResourceSpansItemScopeSpansItemSpansItemAttributesItemValueArrayValue(
309
+ values=[to_otlp_any_value(v) for v in value]
310
+ )
316
311
  )
317
312
 
318
313
  def _convert_event_attribute_value(
319
314
  self, value: AttributeValue
320
315
  ) -> dict[str, object]:
321
- """Convert OpenTelemetry AttributeValue to OTLP event attribute value format.
322
-
323
- This uses the OTLP JSON format with typed value wrappers (e.g., stringValue, intValue).
324
-
325
- Args:
326
- value: An OpenTelemetry AttributeValue (bool, int, float, str, or Sequence)
327
-
328
- Returns:
329
- A dict with the typed value (e.g., {"stringValue": "..."})
330
- """
331
- match value:
332
- case str():
333
- return {"stringValue": value}
334
- case bool():
335
- return {"boolValue": value}
336
- case int():
337
- return {"intValue": str(value)}
338
- case float():
339
- return {"doubleValue": value}
340
- case _:
341
- # Sequences - convert to string representation
342
- return {"stringValue": str(list(value))}
316
+ """Convert OpenTelemetry AttributeValue to OTLP event attribute value format."""
317
+ return to_otlp_any_value(value)
343
318
 
344
319
  def _convert_resource_attribute_value(
345
320
  self, value: AttributeValue
346
321
  ) -> TracesCreateRequestResourceSpansItemResourceAttributesItemValue:
347
- """Convert OpenTelemetry AttributeValue to Mirascope API's resource KeyValueValue.
348
-
349
- This conversion is necessary because the Fern-generated API client
350
- expects KeyValueValue objects, not OpenTelemetry's AttributeValue types.
351
-
352
- Args:
353
- value: An OpenTelemetry AttributeValue (bool, int, float, str, or Sequence)
354
-
355
- Returns:
356
- A KeyValueValue object for the Mirascope API resource attributes
357
- """
322
+ """Convert OpenTelemetry AttributeValue to Mirascope API's resource KeyValueValue."""
358
323
  match value:
359
324
  case str():
360
325
  return TracesCreateRequestResourceSpansItemResourceAttributesItemValue(
@@ -374,7 +339,9 @@ class MirascopeOTLPExporter(SpanExporter):
374
339
  )
375
340
  case _:
376
341
  return TracesCreateRequestResourceSpansItemResourceAttributesItemValue(
377
- string_value=str(list(value))
342
+ array_value=TracesCreateRequestResourceSpansItemResourceAttributesItemValueArrayValue(
343
+ values=[to_otlp_any_value(v) for v in value]
344
+ )
378
345
  )
379
346
 
380
347
  def shutdown(self) -> None:
@@ -4,6 +4,43 @@ This module provides helper functions for formatting and converting
4
4
  OpenTelemetry data types for export.
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping, Sequence
10
+
11
+ from opentelemetry.util.types import AttributeValue
12
+
13
+
14
+ def to_otlp_any_value(value: AttributeValue) -> dict[str, object]:
15
+ """Convert AttributeValue to OTLP AnyValue (dict form).
16
+
17
+ - string/bool/int/float are converted to stringValue/boolValue/intValue/doubleValue
18
+ - Sequence (excluding str/bytes/Mapping) is converted to arrayValue.values
19
+ - Unsupported types fallback to stringValue=str(value)
20
+
21
+ Args:
22
+ value: An OpenTelemetry AttributeValue (bool, int, float, str, or Sequence)
23
+
24
+ Returns:
25
+ A dict representing OTLP AnyValue (e.g., {"stringValue": "..."})
26
+ """
27
+ match value:
28
+ case str():
29
+ return {"stringValue": value}
30
+ case bool():
31
+ return {"boolValue": value}
32
+ case int():
33
+ return {"intValue": str(value)}
34
+ case float():
35
+ return {"doubleValue": value}
36
+ case _ if isinstance(value, bytes | bytearray | memoryview | Mapping):
37
+ return {"stringValue": str(value)}
38
+ case _ if isinstance(value, Sequence):
39
+ values = [to_otlp_any_value(v) for v in value]
40
+ return {"arrayValue": {"values": values}}
41
+ case _:
42
+ return {"stringValue": str(value)}
43
+
7
44
 
8
45
  def format_trace_id(trace_id: int) -> str:
9
46
  """Format a trace ID as a 32-character hex string.
@@ -1,8 +1,28 @@
1
1
  """Instrumentation modules for various frameworks and libraries."""
2
2
 
3
3
  from .llm import instrument_llm, uninstrument_llm
4
+ from .providers import (
5
+ instrument_anthropic,
6
+ instrument_google_genai,
7
+ instrument_openai,
8
+ is_anthropic_instrumented,
9
+ is_google_genai_instrumented,
10
+ is_openai_instrumented,
11
+ uninstrument_anthropic,
12
+ uninstrument_google_genai,
13
+ uninstrument_openai,
14
+ )
4
15
 
5
16
  __all__ = [
17
+ "instrument_anthropic",
18
+ "instrument_google_genai",
6
19
  "instrument_llm",
20
+ "instrument_openai",
21
+ "is_anthropic_instrumented",
22
+ "is_google_genai_instrumented",
23
+ "is_openai_instrumented",
24
+ "uninstrument_anthropic",
25
+ "uninstrument_google_genai",
7
26
  "uninstrument_llm",
27
+ "uninstrument_openai",
8
28
  ]