Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

AI SDK Integration

Learn how to use oRPC as a transport for AI SDK streams and turn oRPC procedures and contracts into AI SDK tools.

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)

Last updated on August 7, 2026

Was this page helpful?