mirascope 2.0.2__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 (64) hide show
  1. mirascope/_stubs.py +39 -18
  2. mirascope/api/_generated/__init__.py +4 -0
  3. mirascope/api/_generated/project_memberships/__init__.py +4 -0
  4. mirascope/api/_generated/project_memberships/client.py +91 -0
  5. mirascope/api/_generated/project_memberships/raw_client.py +239 -0
  6. mirascope/api/_generated/project_memberships/types/__init__.py +4 -0
  7. mirascope/api/_generated/project_memberships/types/project_memberships_get_response.py +33 -0
  8. mirascope/api/_generated/project_memberships/types/project_memberships_get_response_role.py +7 -0
  9. mirascope/api/_generated/reference.md +72 -0
  10. mirascope/llm/__init__.py +19 -0
  11. mirascope/llm/calls/decorator.py +17 -24
  12. mirascope/llm/formatting/__init__.py +2 -2
  13. mirascope/llm/formatting/format.py +2 -4
  14. mirascope/llm/formatting/types.py +19 -2
  15. mirascope/llm/models/models.py +66 -146
  16. mirascope/llm/prompts/decorator.py +5 -16
  17. mirascope/llm/prompts/prompts.py +5 -13
  18. mirascope/llm/providers/anthropic/_utils/beta_decode.py +22 -7
  19. mirascope/llm/providers/anthropic/_utils/beta_encode.py +22 -16
  20. mirascope/llm/providers/anthropic/_utils/decode.py +45 -7
  21. mirascope/llm/providers/anthropic/_utils/encode.py +28 -15
  22. mirascope/llm/providers/anthropic/beta_provider.py +33 -69
  23. mirascope/llm/providers/anthropic/provider.py +52 -91
  24. mirascope/llm/providers/base/_utils.py +4 -9
  25. mirascope/llm/providers/base/base_provider.py +89 -205
  26. mirascope/llm/providers/google/_utils/decode.py +51 -1
  27. mirascope/llm/providers/google/_utils/encode.py +38 -21
  28. mirascope/llm/providers/google/provider.py +33 -69
  29. mirascope/llm/providers/mirascope/provider.py +25 -61
  30. mirascope/llm/providers/mlx/encoding/base.py +3 -6
  31. mirascope/llm/providers/mlx/encoding/transformers.py +4 -8
  32. mirascope/llm/providers/mlx/mlx.py +9 -21
  33. mirascope/llm/providers/mlx/provider.py +33 -69
  34. mirascope/llm/providers/openai/completions/_utils/encode.py +39 -20
  35. mirascope/llm/providers/openai/completions/base_provider.py +34 -75
  36. mirascope/llm/providers/openai/provider.py +25 -61
  37. mirascope/llm/providers/openai/responses/_utils/decode.py +31 -2
  38. mirascope/llm/providers/openai/responses/_utils/encode.py +32 -17
  39. mirascope/llm/providers/openai/responses/provider.py +34 -75
  40. mirascope/llm/responses/__init__.py +2 -1
  41. mirascope/llm/responses/base_stream_response.py +4 -0
  42. mirascope/llm/responses/response.py +8 -12
  43. mirascope/llm/responses/stream_response.py +8 -12
  44. mirascope/llm/responses/usage.py +44 -0
  45. mirascope/llm/tools/__init__.py +24 -0
  46. mirascope/llm/tools/provider_tools.py +18 -0
  47. mirascope/llm/tools/tool_schema.py +4 -2
  48. mirascope/llm/tools/toolkit.py +24 -6
  49. mirascope/llm/tools/types.py +112 -0
  50. mirascope/llm/tools/web_search_tool.py +32 -0
  51. mirascope/ops/__init__.py +19 -1
  52. mirascope/ops/_internal/instrumentation/__init__.py +20 -0
  53. mirascope/ops/_internal/instrumentation/llm/common.py +19 -49
  54. mirascope/ops/_internal/instrumentation/llm/model.py +61 -82
  55. mirascope/ops/_internal/instrumentation/llm/serialize.py +36 -12
  56. mirascope/ops/_internal/instrumentation/providers/__init__.py +29 -0
  57. mirascope/ops/_internal/instrumentation/providers/anthropic.py +78 -0
  58. mirascope/ops/_internal/instrumentation/providers/base.py +179 -0
  59. mirascope/ops/_internal/instrumentation/providers/google_genai.py +85 -0
  60. mirascope/ops/_internal/instrumentation/providers/openai.py +82 -0
  61. {mirascope-2.0.2.dist-info → mirascope-2.1.0.dist-info}/METADATA +96 -68
  62. {mirascope-2.0.2.dist-info → mirascope-2.1.0.dist-info}/RECORD +64 -54
  63. {mirascope-2.0.2.dist-info → mirascope-2.1.0.dist-info}/WHEEL +0 -0
  64. {mirascope-2.0.2.dist-info → mirascope-2.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirascope
3
- Version: 2.0.2
3
+ Version: 2.1.0
4
4
  Summary: Every frontier LLM. One unified interface.
5
5
  Project-URL: Homepage, https://mirascope.com
6
6
  Project-URL: Documentation, https://mirascope.com/docs/mirascope/v2
@@ -60,6 +60,9 @@ Requires-Dist: mlx-lm<1,>=0.28.4; extra == 'all'
60
60
  Requires-Dist: openai<3,>=2.15.0; extra == 'all'
61
61
  Requires-Dist: opentelemetry-api<2,>=1.38.0; extra == 'all'
62
62
  Requires-Dist: opentelemetry-exporter-otlp<2,>=1.38.0; extra == 'all'
63
+ Requires-Dist: opentelemetry-instrumentation-anthropic<1,>=0.50.0; extra == 'all'
64
+ Requires-Dist: opentelemetry-instrumentation-google-genai<1,>=0.3b0; extra == 'all'
65
+ Requires-Dist: opentelemetry-instrumentation-openai-v2<3,>=2.0b0; extra == 'all'
63
66
  Requires-Dist: opentelemetry-instrumentation<1,>=0.59b0; extra == 'all'
64
67
  Requires-Dist: opentelemetry-propagator-b3<2,>=1.38.0; extra == 'all'
65
68
  Requires-Dist: opentelemetry-propagator-b3>=1.38.0; extra == 'all'
@@ -88,6 +91,9 @@ Provides-Extra: ops
88
91
  Requires-Dist: libcst>=1.8.6; extra == 'ops'
89
92
  Requires-Dist: opentelemetry-api<2,>=1.38.0; extra == 'ops'
90
93
  Requires-Dist: opentelemetry-exporter-otlp<2,>=1.38.0; extra == 'ops'
94
+ Requires-Dist: opentelemetry-instrumentation-anthropic<1,>=0.50.0; extra == 'ops'
95
+ Requires-Dist: opentelemetry-instrumentation-google-genai<1,>=0.3b0; extra == 'ops'
96
+ Requires-Dist: opentelemetry-instrumentation-openai-v2<3,>=2.0b0; extra == 'ops'
91
97
  Requires-Dist: opentelemetry-instrumentation<1,>=0.59b0; extra == 'ops'
92
98
  Requires-Dist: opentelemetry-propagator-b3<2,>=1.38.0; extra == 'ops'
93
99
  Requires-Dist: opentelemetry-propagator-b3>=1.38.0; extra == 'ops'
@@ -97,9 +103,66 @@ Requires-Dist: orjson>=3.11.4; extra == 'ops'
97
103
  Requires-Dist: packaging>=25.0; extra == 'ops'
98
104
  Description-Content-Type: text/markdown
99
105
 
100
- ## Mirascope v2 Python
106
+ # Mirascope Python
101
107
 
102
- This directory contains the Python implementation of Mirascope.
108
+ This directory contains the Python implementation of Mirascope: The LLM Anti-Framework. It's intended as a "Goldilocks API" that affords the fine-grained control you'd get from using raw provider APIs, as well as the type safety and easy ergonomics that are offered by higher-level agent frameworks. Think of Mirascope as the "React" of LLM development, where provider native APIs are HTML/CSS, and the agent frameworks are Angular.
109
+
110
+ ## Documentation
111
+
112
+ - [Why use Mirascope?](https://mirascope.com/docs/why)
113
+ - [Mirascope Quickstart](https://mirascope.com/docs/quickstart)
114
+ - [Mirascope Concepts](https://mirascope.com/docs/learn/llm)
115
+
116
+ ## Installation
117
+
118
+ ```bash
119
+ # using uv, with all provider deps
120
+ uv add "mirascope[all]"
121
+
122
+ # using uv, with just Anthropic
123
+ uv add "mirascope[anthropic]"
124
+
125
+ # using pip, with all deps
126
+ pip install "mirascope[all]"
127
+
128
+ # using pip, just OpenAI
129
+ pip install "mirascope[openai]"
130
+ ```
131
+
132
+ ## Usage
133
+
134
+ Here's an example of creating a simple agent, with tool-calling and streaming, using Mirascope. For many more examples, [read the docs](https://mirascope.com/docs).
135
+
136
+ ```python
137
+ from mirascope import llm
138
+
139
+ @llm.tool
140
+ def exp(a: float, b: float) -> float:
141
+ """Compute an exponent"""
142
+ return a ** b
143
+
144
+ @llm.tool
145
+ def add(a: float, b: float) -> float:
146
+ """Add two numbers"""
147
+ return a + b
148
+
149
+ model = llm.Model("anthropic/claude-haiku-4-5")
150
+ response = model.stream("What is 42 ** 4 + 37 ** 3?", tools=[exp, add])
151
+
152
+ while True:
153
+ for stream in response.streams():
154
+ if stream.content_type == "text":
155
+ for delta in stream:
156
+ print(delta, end="", flush=True)
157
+ elif stream.content_type == "tool_call":
158
+ stream.collect() # consume the stream
159
+ print(f"\n> Calling {stream.tool_name}({stream.partial_args})")
160
+ print()
161
+ if response.tool_calls:
162
+ response = response.resume(response.execute_tools())
163
+ else:
164
+ break
165
+ ```
103
166
 
104
167
  ## Development Setup
105
168
 
@@ -107,97 +170,62 @@ This directory contains the Python implementation of Mirascope.
107
170
  ```bash
108
171
  cp .env.example .env
109
172
  # Edit .env with your API keys
173
+ # Necessary for updating e2e snapshot tests
110
174
  ```
111
175
 
112
176
  2. **Install Dependencies**:
113
177
 
114
178
  ```bash
179
+ cd python
115
180
  uv sync --all-extras --dev
116
181
  ```
117
182
 
118
- 3. **Run Tests**:
119
- ```bash
120
- uv run pytest
121
- ```
183
+ ## Helpful Commands:
122
184
 
123
- ## `ops.span` and Session Tracing
185
+ Here are helpful development commands. All must be run from within the `python` directory.
124
186
 
125
- `mirascope.ops` provides tracing helpers to trace any Python function, not just
126
- `llm.Model` calls.
187
+ - `uv run pyright .`
127
188
 
128
- 1. Install the OTEL extra and set up a tracer provider exactly as shown above.
129
- `ops.span` automatically reuses the active provider, so spans from manual
130
- instrumentation and GenAI instrumentation end up in the same trace tree.
131
- 2. Use `ops.session` to group related spans and attach metadata:
132
- ```python
133
- from mirascope import ops
189
+ Run typechecking.
134
190
 
135
- with ops.session(id="req-42", attributes={"team": "core"}):
136
- with ops.span("load-data") as span:
137
- span.set(stage="ingest")
138
- # expensive work here
139
- ```
140
- 3. The span exposes `span_id`/`trace_id`, logging helpers, and graceful no-op
141
- behavior when OTEL is not configured. When OTEL is active, session metadata is
142
- attached to every span, and additional tools like `ops.trace`/`ops.version`
143
- (planned) can build on the same context.
191
+ - `uv run ruff check --fix .`
144
192
 
145
- ## `ops.trace` Decorator
193
+ Check ruff linter (fixing issues where possible).
146
194
 
147
- `@ops.trace` adds span instrumentation to any Python callable (including
148
- `llm.Model` helpers) so you can capture argument/return metadata alongside the
149
- GenAI spans emitted by `llm.instrument_opentelemetry`.
195
+ - `uv run ruff format .`
150
196
 
151
- ```python
152
- from mirascope import ops
197
+ Run ruff formatter.
153
198
 
154
- @ops.trace(tags=["ingest"])
155
- def normalize(record: dict[str, str]) -> dict[str, str]:
156
- return {k: v.strip() for k, v in record.items()}
199
+ - `uv run pytest`
157
200
 
158
- result = normalize({"foo": " bar "})
159
- wrapped = normalize.wrapped({"foo": " bar "})
160
- print(wrapped.span_id, wrapped.trace_id)
161
- ```
201
+ Run all Python unit tests.
162
202
 
163
- - The decorator automatically handles sync/async functions and reuses `ops.span`
164
- serialization logic for arguments/results.
165
- - Combine with `ops.session` to tag spans with contextual metadata, and with
166
- `ops.instrument_opentelemetry` to obtain both model-level GenAI spans
167
- and method-level spans like `recommend_book.__call__`.
168
- - For now we focus on Mirascope-layer entry points (e.g., decorated functions or
169
- `llm.Model` wrappers) and do not auto-instrument underlying provider SDK calls.
203
+ - `uv run pytest --cov --cov-config=.coverargc --cov-report=term-missing`
170
204
 
171
- ## Testing
205
+ Run all Python unit tests, and report code coverage. (We require 100% coverage in CI.)
172
206
 
173
- ### VCR Cassettes
207
+ - `uv run pytest --fix`
174
208
 
175
- The project uses [VCR.py](https://vcrpy.readthedocs.io/) to record and replay HTTP interactions with LLM APIs. This allows tests to run quickly and consistently without making actual API calls.
209
+ Run all Python unit tests, and update any changed snapshots.
176
210
 
177
- - **Cassettes Location**: Test cassettes are stored in `tests/llm/clients/*/cassettes/`
178
- - **Recording Mode**: Tests use `"once"` mode - records new interactions if no cassette exists, replays existing cassettes otherwise
179
- - **CI/CD**: In CI environments, tests use existing cassettes and never make real API calls
211
+ - `uvx codespell --config ../.codespellrc`
180
212
 
181
- ### Inline Snapshots
213
+ Run codespell, identifying many common spelling mistakes.
182
214
 
183
- The project uses [inline-snapshot](https://15r10nk.github.io/inline-snapshot/) to capture expected test outputs directly in the test code.
215
+ ## Typechecking
184
216
 
185
- - **Update Snapshots**: Run `uv run pytest --inline-snapshot=fix` to update all snapshots with actual values
186
- - **Review Changes**: Run `uv run pytest --inline-snapshot=review` to preview what would change
187
- - **Formatted Output**: Snapshots are automatically formatted with `ruff` for consistency
217
+ We prize type safety, both on the API surface and internally. You can run typechecking via `uv run pyright .` within this `python/` directory. (We're looking into supporting `ty` so as to speed up our typechecking.)
188
218
 
189
- Example:
190
- ```python
191
- def test_api_response():
192
- response = client.call(messages=[user("Hello")])
193
- assert response.content == snapshot([Text(text="Hi there!")])
194
- ```
219
+ ## Testing
220
+
221
+ This project makes extensive use of e2e tests, which replay real interactions with various LLM providers to ensure that Mirascope truly works on the providers' APIs. We use [VCR.py](https://vcrpy.readthedocs.io/) and [inline-snapshot](https://15r10nk.github.io/inline-snapshot/) to maintain these e2e tests. For more details, read [tests/e2e/README.md](./tests/e2e/README.md).
222
+
223
+ If you make changes to Mirascope that require new snapshots, you should manually delete the outdated cassette files, and then run `uv run pytest python/tests/e2e --fix`. Note that doing so requires real API keys in your `.env` file.
224
+
225
+ We require 100% code coverage in CI. You can get a code coverage report via:
195
226
 
196
- ### Recording New Test Interactions
227
+ `uv run pytest --cov --cov-config=.coverargc --cov-report=term-missing`
197
228
 
198
- To record new test interactions:
229
+ ## Read More
199
230
 
200
- 1. Ensure your API keys are set in `.env`
201
- 2. Delete the relevant cassette file (if updating an existing test)
202
- 3. Run the specific test: `uv run pytest tests/path/to/test.py::test_name`
203
- 4. The cassette will be automatically created/updated
231
+ For more info on contributing, read [the contributing page in our docs](https://mirascope.com/docs/contributing).