Skip to content

Commit bb5823e

Browse files
committed
Add a ToolLike protocol to decouple tool schema serialization boundary
1 parent 88d50b3 commit bb5823e

8 files changed

Lines changed: 28 additions & 15 deletions

File tree

examples/temporal-durable/workflow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ def __init__(
2828
async def stream(
2929
self,
3030
messages: list[ai.Message],
31-
tools: Sequence[ai.ToolSchema] | None = None,
31+
tools: Sequence[ai.ToolLike] | None = None,
3232
) -> AsyncGenerator[ai.Message, None]:
3333
result = await self.call_fn(
3434
activities.LLMCallParams(
3535
messages=[m.model_dump() for m in messages],
36-
tool_schemas=[t.model_dump() for t in (tools or [])],
36+
tool_schemas=[t.schema.model_dump() for t in (tools or [])],
3737
)
3838
)
3939
yield ai.Message.model_validate(result.message)

src/vercel_ai_sdk/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
HookPart,
1313
make_messages,
1414
)
15-
from .core.tools import ToolSchema, Tool, tool
15+
from .core.tools import ToolLike, ToolSchema, Tool, tool
1616
from .core.llm import LanguageModel
1717
from .core.streams import StreamResult, stream
1818
from .core.runtime import (
@@ -37,6 +37,7 @@
3737
"ToolPart",
3838
"ToolDelta",
3939
"ReasoningPart",
40+
"ToolLike",
4041
"ToolSchema",
4142
"Tool",
4243
"LanguageModel",

src/vercel_ai_sdk/anthropic/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .. import core
1111

1212

13-
def _tools_to_anthropic(tools: Sequence[core.tools.Tool]) -> list[dict[str, Any]]:
13+
def _tools_to_anthropic(tools: Sequence[core.tools.ToolLike]) -> list[dict[str, Any]]:
1414
"""Convert internal Tool objects to Anthropic tool schema format."""
1515
return [
1616
{
@@ -117,7 +117,7 @@ def __init__(
117117
async def stream_events(
118118
self,
119119
messages: list[core.messages.Message],
120-
tools: Sequence[core.tools.Tool] | None = None,
120+
tools: Sequence[core.tools.ToolLike] | None = None,
121121
) -> AsyncGenerator[core.llm.StreamEvent, None]:
122122
"""Yield raw stream events from Anthropic API."""
123123
system_prompt, anthropic_messages = _messages_to_anthropic(messages)
@@ -206,7 +206,7 @@ async def stream_events(
206206
async def stream(
207207
self,
208208
messages: list[core.messages.Message],
209-
tools: Sequence[core.tools.Tool] | None = None,
209+
tools: Sequence[core.tools.ToolLike] | None = None,
210210
) -> AsyncGenerator[core.messages.Message, None]:
211211
"""Stream Messages (uses StreamProcessor internally)."""
212212
handler = core.llm.StreamHandler()

src/vercel_ai_sdk/core/llm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,15 @@ class LanguageModel(abc.ABC):
215215
async def stream(
216216
self,
217217
messages: list[messages_.Message],
218-
tools: Sequence[tools_.Tool] | None = None,
218+
tools: Sequence[tools_.ToolLike] | None = None,
219219
) -> AsyncGenerator[messages_.Message, None]:
220220
raise NotImplementedError
221221
yield
222222

223223
async def buffer(
224224
self,
225225
messages: list[messages_.Message],
226-
tools: Sequence[tools_.Tool] | None = None,
226+
tools: Sequence[tools_.ToolLike] | None = None,
227227
) -> messages_.Message:
228228
"""Drain the stream and return the final message."""
229229
final = None

src/vercel_ai_sdk/core/runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def _find_runtime_param(fn: Callable[..., Any]) -> str | None:
187187
async def stream_step(
188188
llm: llm_.LanguageModel,
189189
messages: list[messages_.Message],
190-
tools: Sequence[tools_.Tool] | None = None,
190+
tools: Sequence[tools_.ToolLike] | None = None,
191191
label: str | None = None,
192192
) -> AsyncGenerator[messages_.Message, None]:
193193
"""Single LLM call that streams to Runtime."""
@@ -243,7 +243,7 @@ async def execute_tool(
243243
async def stream_loop(
244244
llm: llm_.LanguageModel,
245245
messages: list[messages_.Message],
246-
tools: Sequence[tools_.Tool],
246+
tools: Sequence[tools_.ToolLike],
247247
label: str | None = None,
248248
) -> streams_.StreamResult:
249249
"""Agent loop: stream LLM, execute tools, repeat until done."""

src/vercel_ai_sdk/core/tools.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import inspect
44
import json
55
from collections.abc import Awaitable, Callable
6-
from typing import TYPE_CHECKING, Any, get_type_hints
6+
from typing import TYPE_CHECKING, Any, Protocol, get_type_hints, runtime_checkable
77

88
import pydantic
99

@@ -27,6 +27,18 @@ def _is_runtime_type(hint: Any) -> bool:
2727
return hint is Runtime
2828

2929

30+
@runtime_checkable
31+
class ToolLike(Protocol):
32+
"""Anything the LLM layer can use as a tool definition."""
33+
34+
@property
35+
def name(self) -> str: ...
36+
@property
37+
def description(self) -> str: ...
38+
@property
39+
def param_schema(self) -> dict[str, Any]: ...
40+
41+
3042
class ToolSchema(pydantic.BaseModel):
3143
"""What the LLM sees: name, description, and JSON Schema for parameters."""
3244

src/vercel_ai_sdk/openai/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .. import core
1010

1111

12-
def _tools_to_openai(tools: Sequence[core.tools.Tool]) -> list[dict[str, Any]]:
12+
def _tools_to_openai(tools: Sequence[core.tools.ToolLike]) -> list[dict[str, Any]]:
1313
"""Convert internal Tool objects to OpenAI tool schema format."""
1414
return [
1515
{
@@ -132,7 +132,7 @@ def __init__(
132132
async def stream_events(
133133
self,
134134
messages: list[core.messages.Message],
135-
tools: Sequence[core.tools.Tool] | None = None,
135+
tools: Sequence[core.tools.ToolLike] | None = None,
136136
) -> AsyncGenerator[core.llm.StreamEvent, None]:
137137
"""Yield raw stream events from OpenAI API."""
138138
openai_messages = _messages_to_openai(messages)
@@ -243,7 +243,7 @@ async def stream_events(
243243
async def stream(
244244
self,
245245
messages: list[core.messages.Message],
246-
tools: Sequence[core.tools.Tool] | None = None,
246+
tools: Sequence[core.tools.ToolLike] | None = None,
247247
) -> AsyncGenerator[core.messages.Message, None]:
248248
"""Stream Messages (uses StreamHandler internally)."""
249249
handler = core.llm.StreamHandler()

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(self, responses: list[list[messages.Message]]) -> None:
1717
async def stream(
1818
self,
1919
messages: list[messages.Message],
20-
tools: Sequence[ai.Tool] | None = None,
20+
tools: Sequence[ai.ToolLike] | None = None,
2121
) -> AsyncGenerator[messages.Message, None]:
2222
if self._call_index >= len(self._responses):
2323
raise RuntimeError("MockLLM: no more responses configured")

0 commit comments

Comments
 (0)