runtype-pydantic-ai 0.2.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ .venv*/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ .pytest_cache/
6
+ .probe/
@@ -0,0 +1,7 @@
1
+ # @runtypelabs/pydantic-ai-otel
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9f1faab: Add `runtype-pydantic-ai`, a Python adapter that sends Pydantic AI 1.x and 2.x agent runs to Runtype at full fidelity through one `instrument()` call. It composes with Logfire and any OpenTelemetry provider.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.5
2
+ Name: runtype-pydantic-ai
3
+ Version: 0.2.0
4
+ Summary: Send Pydantic AI agent runs to Runtype at full fidelity: one call, composes with Logfire and any OpenTelemetry setup.
5
+ Project-URL: Homepage, https://runtype.com
6
+ Project-URL: Documentation, https://docs.runtype.com
7
+ Author-email: Runtype Labs <dev@runtype.com>
8
+ License-Expression: MIT
9
+ Keywords: gen-ai,observability,opentelemetry,otel,pydantic-ai,runtype
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: System :: Monitoring
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.30
22
+ Requires-Dist: opentelemetry-sdk>=1.30
23
+ Requires-Dist: pydantic-ai-slim<3,>=1.90
24
+ Provides-Extra: dev
25
+ Requires-Dist: logfire>=4.40; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # runtype-pydantic-ai
31
+
32
+ Send [Pydantic AI](https://ai.pydantic.dev) agent runs to [Runtype](https://runtype.com). Each run
33
+ appears as a first-class execution: transcript, system prompt, model calls, tool calls with arguments
34
+ and results, token usage, cache usage, cost, loop iterations, stop reason and sub-agents.
35
+
36
+ - Works with `pydantic-ai` 1.90+ and 2.x from one entry point.
37
+ - Composes with Logfire and any OpenTelemetry setup. Your other exporters keep receiving the original
38
+ spans, and only agent-run spans reach Runtype.
39
+ - Sends everything Runtype can show by default. Every content kind has its own off switch, and
40
+ `content=False` sends shape and cost only. Exception messages and stack traces never leave the
41
+ process; a failed run reports the exception class name.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install runtype-pydantic-ai
47
+ ```
48
+
49
+ ## Quick start
50
+
51
+ Set `RUNTYPE_API_KEY` to a key with the `TELEMETRY:WRITE` scope and `RUNTYPE_AGENT_ID` to the Runtype
52
+ agent that owns the runs. Then add one call at startup:
53
+
54
+ ```python
55
+ import runtype_pydantic_ai
56
+
57
+ runtype_pydantic_ai.instrument()
58
+ ```
59
+
60
+ Every `Agent` in the process is now traced. In a short-lived script, keep the handle and flush before
61
+ exit:
62
+
63
+ ```python
64
+ handle = runtype_pydantic_ai.instrument()
65
+ ...
66
+ handle.force_flush()
67
+ ```
68
+
69
+ ## With Logfire
70
+
71
+ Call `instrument()` after `logfire.configure()` and it joins Logfire's provider. Alternatively, pass
72
+ the processor in:
73
+
74
+ ```python
75
+ import logfire
76
+ import runtype_pydantic_ai
77
+
78
+ logfire.configure(additional_span_processors=[runtype_pydantic_ai.span_processor()])
79
+ logfire.instrument_pydantic_ai(include_content=True, version=5)
80
+ ```
81
+
82
+ Use `version=5` on `pydantic-ai` 1.x. The 1.x default, format 2, is still understood, but it reports
83
+ less.
84
+
85
+ ## With another OpenTelemetry provider
86
+
87
+ `instrument()` leaves a global non-Logfire provider alone and traces through a private one, because
88
+ prompt content would otherwise reach every exporter on it. To share a provider, pass it:
89
+
90
+ ```python
91
+ runtype_pydantic_ai.instrument(tracer_provider=provider)
92
+ ```
93
+
94
+ Its other exporters then receive raw content, and `redact` does not apply to them. Use `content=False`
95
+ to keep content off every exporter.
96
+
97
+ If `Agent.instrument_all()` already ran, `instrument()` keeps those settings and joins the provider
98
+ they use, and raises if it cannot reach that provider. An agent with its own instrumentation, such as
99
+ `Agent(instrument=True)`, traces to its own provider, so pass that provider too.
100
+
101
+ ## Several agents in one process
102
+
103
+ Map Pydantic AI agent names to Runtype agents, or pin a block of code to one:
104
+
105
+ ```python
106
+ runtype_pydantic_ai.instrument(agent_ids={"chat": "agent_a", "run": "agent_b"})
107
+
108
+ with runtype_pydantic_ai.runtype_agent("agent_c"):
109
+ await agent.run("...")
110
+ ```
111
+
112
+ A sub-agent called from a tool stays a branch of the outer run, under the outer run's Runtype agent.
113
+
114
+ Every top-level run is its own Runtype execution, even when several share a web request's span. A run
115
+ under such a span gets a new trace id in Runtype, so it will not match the id in your other backend.
116
+
117
+ ## One agent only
118
+
119
+ To trace a single agent rather than all of them:
120
+
121
+ ```python
122
+ handle = runtype_pydantic_ai.instrument()
123
+ Agent.instrument_all(False)
124
+ agent = Agent("openai:gpt-5-mini", capabilities=[runtype_pydantic_ai.capability(handle)])
125
+ ```
126
+
127
+ Early 1.x releases have no public `Instrumentation` capability, and `capability()` says so. Use
128
+ `Agent(instrument=handle.settings)` there.
129
+
130
+ ## Content
131
+
132
+ ```python
133
+ from runtype_pydantic_ai import ContentOptions
134
+
135
+ runtype_pydantic_ai.instrument(content=False)
136
+ runtype_pydantic_ai.instrument(content=ContentOptions(tool_results=False, max_chars=16_384))
137
+ runtype_pydantic_ai.instrument(content=ContentOptions(redact=lambda value, attribute: scrub(value)))
138
+ ```
139
+
140
+ Each switch also applies inside message lists. `tool_arguments=False` and `tool_results=False` remove
141
+ those payloads from tool call parts, `system_instructions=False` removes system messages, and
142
+ `output_messages=False` removes earlier assistant turns from `gen_ai.input.messages`.
143
+
144
+ `redact` receives the parsed value and the attribute name. Returning `None`, or raising, drops that one
145
+ attribute and keeps the span.
146
+
147
+ Values are capped to fit ingest's limits. Oversized message lists stay valid JSON: large parts become a
148
+ marker, then the oldest turns are dropped and the newest kept.
149
+
150
+ ## Custom attributes
151
+
152
+ Attributes are allow-listed. To send your own, name their prefixes:
153
+
154
+ ```python
155
+ runtype_pydantic_ai.instrument(passthrough_attribute_prefixes=["myapp."])
156
+ ```
157
+
158
+ ## Options
159
+
160
+ | Environment variable | Purpose |
161
+ | -------------------- | ------------------------------------------------------------------------ |
162
+ | `RUNTYPE_API_KEY` | Runtype key with the `TELEMETRY:WRITE` scope. Required. |
163
+ | `RUNTYPE_AGENT_ID` | Default Runtype agent for runs. |
164
+ | `RUNTYPE_OTLP_URL` | Full trace URL. Defaults to `https://api.runtype.com/v1/otel/v1/traces`. |
@@ -0,0 +1,135 @@
1
+ # runtype-pydantic-ai
2
+
3
+ Send [Pydantic AI](https://ai.pydantic.dev) agent runs to [Runtype](https://runtype.com). Each run
4
+ appears as a first-class execution: transcript, system prompt, model calls, tool calls with arguments
5
+ and results, token usage, cache usage, cost, loop iterations, stop reason and sub-agents.
6
+
7
+ - Works with `pydantic-ai` 1.90+ and 2.x from one entry point.
8
+ - Composes with Logfire and any OpenTelemetry setup. Your other exporters keep receiving the original
9
+ spans, and only agent-run spans reach Runtype.
10
+ - Sends everything Runtype can show by default. Every content kind has its own off switch, and
11
+ `content=False` sends shape and cost only. Exception messages and stack traces never leave the
12
+ process; a failed run reports the exception class name.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install runtype-pydantic-ai
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ Set `RUNTYPE_API_KEY` to a key with the `TELEMETRY:WRITE` scope and `RUNTYPE_AGENT_ID` to the Runtype
23
+ agent that owns the runs. Then add one call at startup:
24
+
25
+ ```python
26
+ import runtype_pydantic_ai
27
+
28
+ runtype_pydantic_ai.instrument()
29
+ ```
30
+
31
+ Every `Agent` in the process is now traced. In a short-lived script, keep the handle and flush before
32
+ exit:
33
+
34
+ ```python
35
+ handle = runtype_pydantic_ai.instrument()
36
+ ...
37
+ handle.force_flush()
38
+ ```
39
+
40
+ ## With Logfire
41
+
42
+ Call `instrument()` after `logfire.configure()` and it joins Logfire's provider. Alternatively, pass
43
+ the processor in:
44
+
45
+ ```python
46
+ import logfire
47
+ import runtype_pydantic_ai
48
+
49
+ logfire.configure(additional_span_processors=[runtype_pydantic_ai.span_processor()])
50
+ logfire.instrument_pydantic_ai(include_content=True, version=5)
51
+ ```
52
+
53
+ Use `version=5` on `pydantic-ai` 1.x. The 1.x default, format 2, is still understood, but it reports
54
+ less.
55
+
56
+ ## With another OpenTelemetry provider
57
+
58
+ `instrument()` leaves a global non-Logfire provider alone and traces through a private one, because
59
+ prompt content would otherwise reach every exporter on it. To share a provider, pass it:
60
+
61
+ ```python
62
+ runtype_pydantic_ai.instrument(tracer_provider=provider)
63
+ ```
64
+
65
+ Its other exporters then receive raw content, and `redact` does not apply to them. Use `content=False`
66
+ to keep content off every exporter.
67
+
68
+ If `Agent.instrument_all()` already ran, `instrument()` keeps those settings and joins the provider
69
+ they use, and raises if it cannot reach that provider. An agent with its own instrumentation, such as
70
+ `Agent(instrument=True)`, traces to its own provider, so pass that provider too.
71
+
72
+ ## Several agents in one process
73
+
74
+ Map Pydantic AI agent names to Runtype agents, or pin a block of code to one:
75
+
76
+ ```python
77
+ runtype_pydantic_ai.instrument(agent_ids={"chat": "agent_a", "run": "agent_b"})
78
+
79
+ with runtype_pydantic_ai.runtype_agent("agent_c"):
80
+ await agent.run("...")
81
+ ```
82
+
83
+ A sub-agent called from a tool stays a branch of the outer run, under the outer run's Runtype agent.
84
+
85
+ Every top-level run is its own Runtype execution, even when several share a web request's span. A run
86
+ under such a span gets a new trace id in Runtype, so it will not match the id in your other backend.
87
+
88
+ ## One agent only
89
+
90
+ To trace a single agent rather than all of them:
91
+
92
+ ```python
93
+ handle = runtype_pydantic_ai.instrument()
94
+ Agent.instrument_all(False)
95
+ agent = Agent("openai:gpt-5-mini", capabilities=[runtype_pydantic_ai.capability(handle)])
96
+ ```
97
+
98
+ Early 1.x releases have no public `Instrumentation` capability, and `capability()` says so. Use
99
+ `Agent(instrument=handle.settings)` there.
100
+
101
+ ## Content
102
+
103
+ ```python
104
+ from runtype_pydantic_ai import ContentOptions
105
+
106
+ runtype_pydantic_ai.instrument(content=False)
107
+ runtype_pydantic_ai.instrument(content=ContentOptions(tool_results=False, max_chars=16_384))
108
+ runtype_pydantic_ai.instrument(content=ContentOptions(redact=lambda value, attribute: scrub(value)))
109
+ ```
110
+
111
+ Each switch also applies inside message lists. `tool_arguments=False` and `tool_results=False` remove
112
+ those payloads from tool call parts, `system_instructions=False` removes system messages, and
113
+ `output_messages=False` removes earlier assistant turns from `gen_ai.input.messages`.
114
+
115
+ `redact` receives the parsed value and the attribute name. Returning `None`, or raising, drops that one
116
+ attribute and keeps the span.
117
+
118
+ Values are capped to fit ingest's limits. Oversized message lists stay valid JSON: large parts become a
119
+ marker, then the oldest turns are dropped and the newest kept.
120
+
121
+ ## Custom attributes
122
+
123
+ Attributes are allow-listed. To send your own, name their prefixes:
124
+
125
+ ```python
126
+ runtype_pydantic_ai.instrument(passthrough_attribute_prefixes=["myapp."])
127
+ ```
128
+
129
+ ## Options
130
+
131
+ | Environment variable | Purpose |
132
+ | -------------------- | ------------------------------------------------------------------------ |
133
+ | `RUNTYPE_API_KEY` | Runtype key with the `TELEMETRY:WRITE` scope. Required. |
134
+ | `RUNTYPE_AGENT_ID` | Default Runtype agent for runs. |
135
+ | `RUNTYPE_OTLP_URL` | Full trace URL. Defaults to `https://api.runtype.com/v1/otel/v1/traces`. |
@@ -0,0 +1,229 @@
1
+ """A small support-desk Pydantic AI app, for proving the adapter end to end.
2
+
3
+ Runs offline on FunctionModel, so the same scenarios work on every supported pydantic-ai version:
4
+
5
+ RUNTYPE_API_KEY=... RUNTYPE_AGENT_ID=agent_... python examples/test_app.py
6
+
7
+ `TEST_APP_MODEL=openai:gpt-5-mini` swaps in a real model for the plain tool scenario.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import os
14
+
15
+ from opentelemetry import trace
16
+ from pydantic import BaseModel
17
+ from pydantic_ai import Agent, ModelRetry
18
+ from pydantic_ai.messages import (
19
+ ModelMessage,
20
+ ModelResponse,
21
+ RequestUsage,
22
+ TextPart,
23
+ ToolCallPart,
24
+ ToolReturnPart,
25
+ )
26
+ from pydantic_ai.models.function import AgentInfo, DeltaToolCall, FunctionModel
27
+
28
+ import runtype_pydantic_ai
29
+
30
+
31
+ class Ticket(BaseModel):
32
+ summary: str
33
+ priority: int
34
+
35
+
36
+ def _tool_returns(messages: list[ModelMessage]) -> list[ToolReturnPart]:
37
+ return [
38
+ part
39
+ for message in messages
40
+ for part in message.parts
41
+ if isinstance(part, ToolReturnPart)
42
+ ]
43
+
44
+
45
+ def _usage() -> RequestUsage:
46
+ return RequestUsage(input_tokens=120, output_tokens=30, cache_read_tokens=40)
47
+
48
+
49
+ def weather_model(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
50
+ if not _tool_returns(messages):
51
+ call = ToolCallPart(
52
+ "lookup_order", {"order_id": "ord_42"}, tool_call_id="call_lookup"
53
+ )
54
+ return ModelResponse(
55
+ parts=[call],
56
+ usage=_usage(),
57
+ model_name="test-model",
58
+ finish_reason="tool_call",
59
+ )
60
+ return ModelResponse(
61
+ parts=[TextPart("Order ord_42 shipped on Monday.")],
62
+ usage=_usage(),
63
+ model_name="test-model",
64
+ finish_reason="stop",
65
+ )
66
+
67
+
68
+ async def weather_stream(messages: list[ModelMessage], info: AgentInfo):
69
+ if not _tool_returns(messages):
70
+ yield {
71
+ 0: DeltaToolCall(
72
+ name="lookup_order",
73
+ json_args='{"order_id":"ord_42"}',
74
+ tool_call_id="call_lookup",
75
+ )
76
+ }
77
+ else:
78
+ yield "Order ord_42 "
79
+ yield "shipped on Monday."
80
+
81
+
82
+ def structured_model(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
83
+ output_tool = info.output_tools[0].name
84
+ call = ToolCallPart(
85
+ output_tool,
86
+ {"summary": "Refund request", "priority": 2},
87
+ tool_call_id="call_out",
88
+ )
89
+ return ModelResponse(
90
+ parts=[call], usage=_usage(), model_name="test-model", finish_reason="tool_call"
91
+ )
92
+
93
+
94
+ def retry_model(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
95
+ attempts = sum(1 for message in messages if isinstance(message, ModelResponse))
96
+ if attempts < 2:
97
+ call = ToolCallPart(
98
+ "flaky_lookup", {"attempt": attempts}, tool_call_id=f"call_retry_{attempts}"
99
+ )
100
+ return ModelResponse(
101
+ parts=[call],
102
+ usage=_usage(),
103
+ model_name="test-model",
104
+ finish_reason="tool_call",
105
+ )
106
+ return ModelResponse(
107
+ parts=[TextPart("Recovered.")],
108
+ usage=_usage(),
109
+ model_name="test-model",
110
+ finish_reason="stop",
111
+ )
112
+
113
+
114
+ def delegating_model(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
115
+ if not _tool_returns(messages):
116
+ call = ToolCallPart(
117
+ "ask_specialist",
118
+ {"question": "refund policy?"},
119
+ tool_call_id="call_delegate",
120
+ )
121
+ return ModelResponse(
122
+ parts=[call],
123
+ usage=_usage(),
124
+ model_name="test-model",
125
+ finish_reason="tool_call",
126
+ )
127
+ return ModelResponse(
128
+ parts=[TextPart("30 days.")],
129
+ usage=_usage(),
130
+ model_name="test-model",
131
+ finish_reason="stop",
132
+ )
133
+
134
+
135
+ def specialist_model(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
136
+ return ModelResponse(
137
+ parts=[TextPart("Refunds: 30 days")],
138
+ usage=_usage(),
139
+ model_name="specialist",
140
+ finish_reason="stop",
141
+ )
142
+
143
+
144
+ def failing_model(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
145
+ raise RuntimeError("secret upstream detail: sk-should-never-export")
146
+
147
+
148
+ def build_support_agent() -> Agent:
149
+ model = os.environ.get("TEST_APP_MODEL") or FunctionModel(
150
+ weather_model, stream_function=weather_stream, model_name="test-model"
151
+ )
152
+ agent = Agent(
153
+ model, name="support", instructions="You are the support desk. Be brief."
154
+ )
155
+
156
+ @agent.tool_plain
157
+ def lookup_order(order_id: str) -> str:
158
+ """Find an order's status."""
159
+ return f"{order_id}: shipped Monday"
160
+
161
+ return agent
162
+
163
+
164
+ def build_agents() -> dict[str, Agent]:
165
+ structured = Agent(
166
+ FunctionModel(structured_model),
167
+ name="triage",
168
+ output_type=Ticket,
169
+ instructions="Triage.",
170
+ )
171
+ retrying = Agent(
172
+ FunctionModel(retry_model), name="retrier", instructions="Retry on failure."
173
+ )
174
+
175
+ @retrying.tool_plain(retries=3)
176
+ def flaky_lookup(attempt: int) -> str:
177
+ if attempt == 0:
178
+ raise ModelRetry("try again")
179
+ return "ok"
180
+
181
+ specialist = Agent(
182
+ FunctionModel(specialist_model), name="specialist", instructions="Know refunds."
183
+ )
184
+ delegating = Agent(
185
+ FunctionModel(delegating_model), name="frontdesk", instructions="Delegate."
186
+ )
187
+
188
+ @delegating.tool_plain
189
+ async def ask_specialist(question: str) -> str:
190
+ result = await specialist.run(question)
191
+ return result.output
192
+
193
+ failing = Agent(FunctionModel(failing_model), name="failing", instructions="Fail.")
194
+ return {
195
+ "support": build_support_agent(),
196
+ "triage": structured,
197
+ "retrier": retrying,
198
+ "frontdesk": delegating,
199
+ "failing": failing,
200
+ }
201
+
202
+
203
+ async def run_all(agents: dict[str, Agent]) -> None:
204
+ first = await agents["support"].run("Where is order ord_42?")
205
+ await agents["support"].run(
206
+ "Thanks, and when did it ship?", message_history=first.all_messages()
207
+ )
208
+ await agents["triage"].run("I want a refund")
209
+ await agents["retrier"].run("Look it up")
210
+ await agents["frontdesk"].run("What is the refund policy?")
211
+ async with agents["support"].run_stream("Stream the status of ord_42") as stream:
212
+ await stream.get_output()
213
+ with trace.get_tracer("test-app").start_as_current_span("POST /chat"):
214
+ await agents["support"].run("Inside a web request")
215
+ try:
216
+ await agents["failing"].run("This one fails")
217
+ except RuntimeError:
218
+ pass
219
+
220
+
221
+ def main() -> None:
222
+ handle = runtype_pydantic_ai.instrument()
223
+ asyncio.run(run_all(build_agents()))
224
+ handle.force_flush()
225
+ print("sent 8 runs to", os.environ.get("RUNTYPE_OTLP_URL", "api.runtype.com"))
226
+
227
+
228
+ if __name__ == "__main__":
229
+ main()
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@runtypelabs/pydantic-ai-otel",
3
+ "version": "0.2.0",
4
+ "private": true,
5
+ "description": "Changesets anchor for the Pydantic AI adapter (published to PyPI as runtype-pydantic-ai, not npm). The version here is mirrored into pyproject.toml and runtype_pydantic_ai/_version.py by scripts/sync-sdk-versions.mjs.",
6
+ "license": "MIT"
7
+ }
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "runtype-pydantic-ai"
7
+ version = "0.2.0"
8
+ description = "Send Pydantic AI agent runs to Runtype at full fidelity: one call, composes with Logfire and any OpenTelemetry setup."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Runtype Labs", email = "dev@runtype.com" }
14
+ ]
15
+ keywords = ["runtype", "pydantic-ai", "opentelemetry", "otel", "gen-ai", "observability"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: System :: Monitoring",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "pydantic-ai-slim>=1.90,<3",
30
+ "opentelemetry-sdk>=1.30",
31
+ "opentelemetry-exporter-otlp-proto-http>=1.30",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=8",
37
+ "pytest-asyncio>=0.23",
38
+ "logfire>=4.40",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://runtype.com"
43
+ Documentation = "https://docs.runtype.com"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["runtype_pydantic_ai"]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ testpaths = ["tests"]