Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

TanStack AI Integration

Learn how to use oRPC as a transport for TanStack AI chat streams, through the oRPC client or as a plain Server-Sent Events endpoint.

Transport

TanStack AI’s chat returns an AsyncIteratorObject of stream chunks, so a procedure can return it directly and oRPC streams every chunk to the client.

Server

import type { UIMessage } from '@tanstack/ai'
import { os, type } from '@orpc/server'
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export const streamChat = os
  .input(type<{ messages: UIMessage[] }>())
  .handler(({ input, signal }) => {
    const abortController = new AbortController()
    signal?.addEventListener('abort', () => abortController.abort(), { once: true })

    return chat({
      adapter: openaiText('gpt-5.5'),
      systemPrompts: ['You are a helpful assistant.'],
      messages: input.messages,
      abortController,
    })
  })

Client

Pass an oRPC client call as the fetcher of useChat. The fetcher option accepts a promise of an AsyncIterable of stream chunks, which is exactly what an oRPC client call returns.

import { useState } from 'react'
import { useChat } from '@tanstack/ai-react'
import { client } from './client'

export function Example() {
  const { messages, sendMessage, isLoading } = useChat({
    fetcher: ({ messages }, { signal }) =>
      client.streamChat({ messages }, { signal }),
  })
  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.content}</span> : null,
          )}
        </div>
      ))}

      <form
        onSubmit={(e) => {
          e.preventDefault()
          if (input.trim() && !isLoading) {
            sendMessage(input)
            setInput('')
          }
        }}
      >
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          disabled={isLoading}
          placeholder="Say something..."
        />
        <button type="submit" disabled={isLoading}>
          Submit
        </button>
      </form>
    </>
  )
}

Last updated on August 19, 2026

Was this page helpful?