The screenshot API for developers -
Try ScreenshotOne
Skip to content

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.

WARNING

This documentation requires AI SDK v7.0.0 or later. For a refresher, review the AI SDK documentation.

Transport

Use oRPC as the transport for AI SDK streams: the server sends them through regular oRPC procedures, and the client converts them back. For example, you can use it as a useChat transport.

Server

Use streamToAsyncIteratorObject to convert AI SDK streams into AsyncIteratorObjects.

ts
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.

tsx
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>
    </>
  )
}

INFO

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.

INFO

Prefer asyncIteratorToUnproxiedDataStream over asyncIteratorToStream. AI SDK internally uses structuredClone, which doesn't support proxied data. oRPC may proxy events for metadata, so unproxy before passing to AI SDK.

Tool Implementer

Implements a procedure contract as an AI SDK Tool by leveraging existing contract definitions.

ts
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
})

INFO

Standard procedures are also compatible with procedure contracts.

INFO

The aiSdkTool 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.

Tool Factory

Converts a procedure into an AI SDK Tool by leveraging existing procedure definitions.

ts
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.

ts
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)

Released under the MIT License.