AI SDK Integration
AI SDK is a free open-source library for building AI-powered products. You can seamlessly integrate it with oRPC without any extra overhead.
Transport
Use oRPC as the transport for AI SDK streams, sending them as either an AsyncIteratorObject or a ReadableStream<Uint8Array>. The examples below use the AsyncIteratorObject approach.
Server
Use streamToAsyncIteratorObject to convert AI SDK streams into AsyncIteratorObjects.
import { os, streamToAsyncIteratorObject, type } from '@orpc/server'
import { convertToModelMessages, streamText, toUIMessageStream, UIMessage } from 'ai'
import { google } from '@ai-sdk/google'
export const chat = os
.input(type<{ chatId: string, messages: UIMessage[] }>())
.handler(async ({ input }) => {
const result = streamText({
model: google('gemini-2.5-flash'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(input.messages),
})
return streamToAsyncIteratorObject(
toUIMessageStream(result),
)
})
Client
On the client side, convert the AsyncIteratorObject back to a stream using asyncIteratorToUnproxiedDataStream or asyncIteratorToStream.
import { useState } from 'react'
import { useChat } from '@ai-sdk/react'
import { asyncIteratorToUnproxiedDataStream } from '@orpc/client'
import { client } from './client'
export function Example() {
const { messages, sendMessage, status } = useChat({
transport: {
async sendMessages(options) {
return asyncIteratorToUnproxiedDataStream(await client.chat({
chatId: options.chatId,
messages: options.messages,
}, { signal: options.abortSignal }))
},
reconnectToStream(options) {
throw new Error('Unsupported')
},
},
})
const [input, setInput] = useState('')
return (
<>
{messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, index) =>
part.type === 'text' ? <span key={index}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault()
if (input.trim()) {
sendMessage({ text: input })
setInput('')
}
}}
>
<input
value={input}
onChange={e => setInput(e.target.value)}
disabled={status !== 'ready'}
placeholder="Say something..."
/>
<button type="submit" disabled={status !== 'ready'}>
Submit
</button>
</form>
</>
)
}
Tool Implementer
Implements a procedure contract as an AI SDK Tool by leveraging existing contract definitions.
import { aiSdkTool, implementToolFactory } from '@orpc/ai-sdk'
const getWeatherContract = oc
.meta(aiSdkTool({ // Base AI SDK tool options
description: 'Get the weather in a location',
metadata: { source: 'weather-service' }
}))
.input(z.object({
location: z.string().describe('The location to get the weather for'),
}))
.output(z.object({
location: z.string().describe('The location the weather is for'),
temperature: z.number().describe('The temperature in Celsius'),
}))
const implementTool = implementToolFactory()
const getWeatherTool = implementTool(getWeatherContract, {
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
// ...add any additional AI SDK tool options or overrides here
})
Tool Factory
Converts a procedure into an AI SDK Tool by leveraging existing procedure definitions.
import { aiSdkTool, createToolFactory } from '@orpc/ai-sdk'
import { os } from '@orpc/server'
import { z } from 'zod'
const getWeatherProcedure = os
.meta(aiSdkTool({ // Base AI SDK tool options
description: 'Get the weather in a location',
metadata: { source: 'weather-service' }
}))
.input(z.object({
location: z.string().describe('The location to get the weather for'),
}))
.output(z.object({
location: z.string().describe('The location the weather is for'),
temperature: z.number().describe('The temperature in Celsius'),
}))
.handler(async ({ input }) => ({
location: input.location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}))
const createTool = createToolFactory({
context: {}, // provide initial context if needed
interceptors: [], // oRPC interceptors if needed
})
const getWeatherTool = createTool(getWeatherProcedure, {
// ...add any additional AI SDK tool options or overrides here
})
Streaming Tool Outputs
When a procedure outputs an AsyncIteratorObject validated with asyncIteratorObject, the resulting tool streams every event as a preliminary tool result: each event replaces the tool output in the UI, and the last event becomes the final tool result sent to the model.
import { asyncIteratorObject, os } from '@orpc/server'
const deployProcedure = os
.input(z.object({ app: z.string() }))
.output(asyncIteratorObject(
z.object({
status: z.string(),
url: z.string().optional().describe('Available once the deploy finishes'),
}),
))
.handler(async function* ({ input }) {
yield { status: 'building' }
yield { status: 'uploading' }
yield { status: 'ready', url: `https://${input.app}.example.com` }
})
const deployTool = createTool(deployProcedure)