Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

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)

Last updated on August 6, 2026

Was this page helpful?