Skip to content

Commit 428ad93

Browse files
dinwwwhclaude
andauthored
feat(ai-sdk): AI SDK integration (#1707)
## Summary Brings the AI SDK integration (`@orpc/ai-sdk`) to v2, targeting AI SDK v7+. You can now turn procedures and contracts into [AI SDK Tools](https://ai-sdk.dev/docs/foundations/tools): ```ts const createTool = createToolFactory({ context: {} }) const implementTool = implementToolFactory() const tools = { getWeather: createTool(getWeatherProcedure), createTask: implementTool(createTaskContract, { execute: ... }), // or omit execute for client-side / human-in-the-loop tools } ``` - `createToolFactory` converts procedures into tools; `implementToolFactory` implements procedure contracts as tools - `aiSdkTool` metadata plugin attaches base AI SDK tool options to procedures/contracts, with input/output types inferred from the contract - Procedures with `asyncIteratorObject` outputs stream each event as a [preliminary tool result](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling#preliminary-tool-results) - Multiple input/output schemas are combined into a single schema (`$defs`/`$ref`-aware `allOf` composition) - New [AI SDK Integration](https://orpc.dev/docs/integrations/ai-sdk) docs page - 100% test coverage --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b57f86c commit 428ad93

15 files changed

Lines changed: 1562 additions & 2 deletions

File tree

apps/content/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export default withMermaid(defineConfig({
187187
text: 'Integrations',
188188
collapsed: true,
189189
items: [
190+
{ text: 'AI SDK', link: '/docs/integrations/ai-sdk' },
190191
{ text: 'ArkType', link: '/docs/integrations/arktype' },
191192
{ text: 'Effect', link: '/docs/integrations/effect' },
192193
{ text: 'Evlog', link: '/docs/integrations/evlog' },
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# AI SDK Integration
2+
3+
[AI SDK](https://ai-sdk.dev/) is a free open-source library for building AI-powered products. You can seamlessly integrate it with oRPC without any extra overhead.
4+
5+
::: warning
6+
This documentation requires AI SDK v7.0.0 or later. For a refresher, review the [AI SDK documentation](https://ai-sdk.dev/docs).
7+
:::
8+
9+
## Transport
10+
11+
Use oRPC as the transport for AI SDK streams, sending them as either an [AsyncIteratorObject](/docs/async-iterator-object) or a [ReadableStream\<Uint8Array\>](/docs/binary-data#readablestreamuint8array). The examples below use the `AsyncIteratorObject` approach.
12+
13+
### Server
14+
15+
Use `streamToAsyncIteratorObject` to convert AI SDK streams into [AsyncIteratorObject](/docs/async-iterator-object)s.
16+
17+
```ts
18+
import { os, streamToAsyncIteratorObject, type } from '@orpc/server'
19+
import { convertToModelMessages, streamText, toUIMessageStream, UIMessage } from 'ai'
20+
import { google } from '@ai-sdk/google'
21+
22+
export const chat = os
23+
.input(type<{ chatId: string, messages: UIMessage[] }>())
24+
.handler(async ({ input }) => {
25+
const result = streamText({
26+
model: google('gemini-2.5-flash'),
27+
system: 'You are a helpful assistant.',
28+
messages: await convertToModelMessages(input.messages),
29+
})
30+
31+
return streamToAsyncIteratorObject(
32+
toUIMessageStream(result),
33+
)
34+
})
35+
```
36+
37+
### Client
38+
39+
On the client side, convert the `AsyncIteratorObject` back to a stream using `asyncIteratorToUnproxiedDataStream` or `asyncIteratorToStream`.
40+
41+
```tsx
42+
import { useState } from 'react'
43+
import { useChat } from '@ai-sdk/react'
44+
import { asyncIteratorToUnproxiedDataStream } from '@orpc/client'
45+
import { client } from './client'
46+
47+
export function Example() {
48+
const { messages, sendMessage, status } = useChat({
49+
transport: {
50+
async sendMessages(options) {
51+
return asyncIteratorToUnproxiedDataStream(await client.chat({
52+
chatId: options.chatId,
53+
messages: options.messages,
54+
}, { signal: options.abortSignal }))
55+
},
56+
reconnectToStream(options) {
57+
throw new Error('Unsupported')
58+
},
59+
},
60+
})
61+
const [input, setInput] = useState('')
62+
63+
return (
64+
<>
65+
{messages.map(message => (
66+
<div key={message.id}>
67+
{message.role === 'user' ? 'User: ' : 'AI: '}
68+
{message.parts.map((part, index) =>
69+
part.type === 'text' ? <span key={index}>{part.text}</span> : null,
70+
)}
71+
</div>
72+
))}
73+
74+
<form
75+
onSubmit={(e) => {
76+
e.preventDefault()
77+
if (input.trim()) {
78+
sendMessage({ text: input })
79+
setInput('')
80+
}
81+
}}
82+
>
83+
<input
84+
value={input}
85+
onChange={e => setInput(e.target.value)}
86+
disabled={status !== 'ready'}
87+
placeholder="Say something..."
88+
/>
89+
<button type="submit" disabled={status !== 'ready'}>
90+
Submit
91+
</button>
92+
</form>
93+
</>
94+
)
95+
}
96+
```
97+
98+
::: info
99+
The `reconnectToStream` function is not supported by default, which is fine for most use cases. If you need reconnection support, implement it similar to `sendMessages` with custom reconnection logic.
100+
:::
101+
102+
::: info
103+
Prefer `asyncIteratorToUnproxiedDataStream` over `asyncIteratorToStream`.
104+
AI SDK internally uses `structuredClone`, which doesn't support proxied data.
105+
oRPC may proxy events for [metadata](/docs/client/async-iterator-object#event-metadata), so unproxy before passing to AI SDK.
106+
:::
107+
108+
## Tool Implementer
109+
110+
Implements a [procedure contract](/docs/contract/procedure) as an [AI SDK Tool](https://ai-sdk.dev/docs/foundations/tools) by leveraging existing contract definitions.
111+
112+
```ts
113+
import { aiSdkTool, implementToolFactory } from '@orpc/ai-sdk'
114+
115+
const getWeatherContract = oc
116+
.meta(aiSdkTool({ // Base AI SDK tool options
117+
description: 'Get the weather in a location',
118+
metadata: { source: 'weather-service' }
119+
}))
120+
.input(z.object({
121+
location: z.string().describe('The location to get the weather for'),
122+
}))
123+
.output(z.object({
124+
location: z.string().describe('The location the weather is for'),
125+
temperature: z.number().describe('The temperature in Celsius'),
126+
}))
127+
128+
const implementTool = implementToolFactory()
129+
130+
const getWeatherTool = implementTool(getWeatherContract, {
131+
execute: async ({ location }) => ({
132+
location,
133+
temperature: 72 + Math.floor(Math.random() * 21) - 10,
134+
}),
135+
// ...add any additional AI SDK tool options or overrides here
136+
})
137+
```
138+
139+
::: info
140+
Standard [procedures](/docs/procedure) are also compatible with [procedure contracts](/docs/contract/procedure).
141+
:::
142+
143+
::: info
144+
The `aiSdkTool` [metadata](/docs/metadata) attaches base AI SDK tool options that every tool created from the procedure/contract inherits. If applied multiple times, later calls override matching keys from earlier ones.
145+
:::
146+
147+
## Tool Factory
148+
149+
Converts a [procedure](/docs/procedure) into an [AI SDK Tool](https://ai-sdk.dev/docs/foundations/tools) by leveraging existing procedure definitions.
150+
151+
```ts
152+
import { aiSdkTool, createToolFactory } from '@orpc/ai-sdk'
153+
import { os } from '@orpc/server'
154+
import { z } from 'zod'
155+
156+
const getWeatherProcedure = os
157+
.meta(aiSdkTool({ // Base AI SDK tool options
158+
description: 'Get the weather in a location',
159+
metadata: { source: 'weather-service' }
160+
}))
161+
.input(z.object({
162+
location: z.string().describe('The location to get the weather for'),
163+
}))
164+
.output(z.object({
165+
location: z.string().describe('The location the weather is for'),
166+
temperature: z.number().describe('The temperature in Celsius'),
167+
}))
168+
.handler(async ({ input }) => ({
169+
location: input.location,
170+
temperature: 72 + Math.floor(Math.random() * 21) - 10,
171+
}))
172+
173+
const createTool = createToolFactory({
174+
context: {}, // provide initial context if needed
175+
interceptors: [], // oRPC interceptors if needed
176+
})
177+
178+
const getWeatherTool = createTool(getWeatherProcedure, {
179+
// ...add any additional AI SDK tool options or overrides here
180+
})
181+
```
182+
183+
### Streaming Tool Outputs
184+
185+
When a procedure outputs an [AsyncIteratorObject](/docs/async-iterator-object) validated with `asyncIteratorObject`, the resulting tool streams every event as a [preliminary tool result](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling#preliminary-tool-results): each event replaces the tool output in the UI, and the last event becomes the final tool result sent to the model.
186+
187+
```ts
188+
import { asyncIteratorObject, os } from '@orpc/server'
189+
190+
const deployProcedure = os
191+
.input(z.object({ app: z.string() }))
192+
.output(asyncIteratorObject(
193+
z.object({
194+
status: z.string(),
195+
url: z.string().optional().describe('Available once the deploy finishes'),
196+
}),
197+
))
198+
.handler(async function* ({ input }) {
199+
yield { status: 'building' }
200+
yield { status: 'uploading' }
201+
yield { status: 'ready', url: `https://${input.app}.example.com` }
202+
})
203+
204+
const deployTool = createTool(deployProcedure)
205+
```

packages/ai-sdk/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Hidden folders and files
2+
.*
3+
!.gitignore
4+
!.*.example
5+
6+
# Common generated folders
7+
logs/
8+
node_modules/
9+
out/
10+
dist/
11+
dist-ssr/
12+
build/
13+
coverage/
14+
temp/
15+
16+
# Common generated files
17+
*.log
18+
*.log.*
19+
*.tsbuildinfo
20+
*.vitest-temp.json
21+
vite.config.ts.timestamp-*
22+
vitest.config.ts.timestamp-*
23+
24+
# Common manual ignore files
25+
*.local
26+
*.pem

0 commit comments

Comments
 (0)