Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
22d4737
Realtime chat basically working
jherr Jan 20, 2026
71ad066
Realtime chat basically working
jherr Jan 20, 2026
5ad2d2c
more updates
jherr Jan 22, 2026
f91425c
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr Feb 19, 2026
41a5e97
chore: update pnpm-lock.yaml and latest realtime chat changes
jherr Feb 19, 2026
aa48ba2
ci: apply automated fixes
autofix-ci[bot] Feb 19, 2026
18ce05c
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr Feb 20, 2026
008a824
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr Feb 21, 2026
e9fe67b
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr Feb 21, 2026
a611ad4
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr Mar 1, 2026
c7c3520
chore: merge origin/main into pr-300
jherr Mar 6, 2026
8bc5a93
API improved
jherr Mar 6, 2026
dc7c45f
API further improved
jherr Mar 6, 2026
ddca0d6
API further improved
jherr Mar 6, 2026
4025e87
ci: apply automated fixes
autofix-ci[bot] Mar 6, 2026
a5f9879
Updating the docs
jherr Mar 6, 2026
2d8990e
text chat
jherr Mar 6, 2026
26cd4bd
Merge branch 'realtime-chat' of github.com:TanStack/ai into realtime-…
jherr Mar 6, 2026
62301f6
ci: apply automated fixes
autofix-ci[bot] Mar 6, 2026
14fed75
Merge branch 'main' into realtime-chat
jherr Mar 6, 2026
2ca3efc
Getting close to releasable
jherr Mar 6, 2026
d0233b5
updating changeset
jherr Mar 7, 2026
1b4e0f1
ci: apply automated fixes
autofix-ci[bot] Mar 7, 2026
f4e62ac
Realtime chat basically working
jherr Jan 20, 2026
929b4a8
Realtime chat basically working
jherr Jan 20, 2026
882c2be
more updates
jherr Jan 22, 2026
5298fa8
chore: update pnpm-lock.yaml and latest realtime chat changes
jherr Feb 19, 2026
dbb9cab
ci: apply automated fixes
autofix-ci[bot] Feb 19, 2026
8dbdddf
API improved
jherr Mar 6, 2026
44d0740
API further improved
jherr Mar 6, 2026
0165d97
API further improved
jherr Mar 6, 2026
1f5eaab
Updating the docs
jherr Mar 6, 2026
98831f8
text chat
jherr Mar 6, 2026
88a308f
ci: apply automated fixes
autofix-ci[bot] Mar 6, 2026
bd8f479
ci: apply automated fixes
autofix-ci[bot] Mar 6, 2026
3580092
Getting close to releasable
jherr Mar 6, 2026
5ff0f68
updating changeset
jherr Mar 7, 2026
3d6ce16
Resolve merge conflicts in realtime adapters and lockfile
jherr Mar 10, 2026
32844fb
Merge main into realtime-chat
jherr Mar 10, 2026
fb148cc
docs: add missing RealtimeStateChangeCallback type alias doc
jherr Mar 10, 2026
c7a57c6
ci: apply automated fixes
autofix-ci[bot] Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/realtime-chat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@tanstack/ai': minor
'@tanstack/ai-client': minor
'@tanstack/ai-openai': minor
'@tanstack/ai-elevenlabs': minor
'@tanstack/ai-react': minor
---

feat: add realtime chat support with OpenAI and ElevenLabs adapters

Adds realtime voice/text chat capabilities including a provider-agnostic realtime client, OpenAI Realtime API adapter, ElevenLabs conversational AI adapter, React `useRealtimeChat` hook, and shared realtime types across the core, client, and framework packages.
393 changes: 393 additions & 0 deletions docs/architecture/realtime-chat.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion examples/ts-react-chat/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# OpenAI API Key
# Get yours at: https://platform.openai.com/api-keys
OPENAI_API_KEY=sk-...
OPENAI_API_KEY=sk-...

# ElevenLabs API Key (for realtime voice)
# Get yours at: https://elevenlabs.io/app/settings/api-keys
ELEVENLABS_API_KEY=xi-...

# ElevenLabs Agent ID (for realtime voice)
# Create an agent at: https://elevenlabs.io/app/conversational-ai
ELEVENLABS_AGENT_ID=...
1 change: 1 addition & 0 deletions examples/ts-react-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@tanstack/ai": "workspace:*",
"@tanstack/ai-anthropic": "workspace:*",
"@tanstack/ai-client": "workspace:*",
"@tanstack/ai-elevenlabs": "workspace:*",
"@tanstack/ai-gemini": "workspace:*",
"@tanstack/ai-grok": "workspace:*",
"@tanstack/ai-ollama": "workspace:*",
Expand Down
81 changes: 81 additions & 0 deletions examples/ts-react-chat/src/components/AudioSparkline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useEffect, useRef } from 'react'

export function AudioSparkline({
getData,
color,
label,
}: {
getData: () => Uint8Array
color: string
label: string
}) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animationRef = useRef<number | null>(null)

useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return

const ctx = canvas.getContext('2d')
if (!ctx) return

function draw() {
const data = getData()
const width = canvas!.width
const height = canvas!.height

ctx!.fillStyle = '#1f2937'
ctx!.fillRect(0, 0, width, height)

ctx!.strokeStyle = color
ctx!.lineWidth = 1
ctx!.beginPath()

const step = Math.max(1, Math.floor(data.length / width))

for (let i = 0; i < width; i++) {
const dataIndex = Math.min(i * step, data.length - 1)
const value = data[dataIndex] ?? 128
const y = height - (value / 255) * height

if (i === 0) {
ctx!.moveTo(i, y)
} else {
ctx!.lineTo(i, y)
}
}

ctx!.stroke()

ctx!.strokeStyle = '#4b5563'
ctx!.setLineDash([2, 2])
ctx!.beginPath()
ctx!.moveTo(0, height / 2)
ctx!.lineTo(width, height / 2)
ctx!.stroke()
ctx!.setLineDash([])

animationRef.current = requestAnimationFrame(draw)
}

draw()

return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [getData, color])

return (
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 w-12">{label}</span>
<canvas
ref={canvasRef}
width={200}
height={40}
className="rounded border border-gray-700"
/>
</div>
)
}
15 changes: 14 additions & 1 deletion examples/ts-react-chat/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link } from '@tanstack/react-router'

import { useState } from 'react'
import { Guitar, Home, Menu, X } from 'lucide-react'
import { Guitar, Home, Menu, Mic, X } from 'lucide-react'

export default function Header() {
const [isOpen, setIsOpen] = useState(false)
Expand Down Expand Up @@ -71,6 +71,19 @@ export default function Header() {
<Guitar size={20} />
<span className="font-medium">Guitar Demo</span>
</Link>

<Link
to="/realtime"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<Mic size={20} />
<span className="font-medium">Voice Chat (Realtime)</span>
</Link>
</nav>
</aside>
</>
Expand Down
167 changes: 167 additions & 0 deletions examples/ts-react-chat/src/lib/realtime-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'

// Tool to get current time - useful for voice assistants
export const getCurrentTimeToolDef = toolDefinition({
name: 'getCurrentTime',
description:
'Get the current date and time. Use this when the user asks what time it is or the current date.',
inputSchema: z.object({
timezone: z
.string()
.optional()
.describe('Optional timezone like "America/New_York" or "Europe/London"'),
}),
outputSchema: z.object({
time: z.string(),
date: z.string(),
timezone: z.string(),
}),
})

// Tool to get weather - common voice assistant use case
export const getWeatherToolDef = toolDefinition({
name: 'getWeather',
description:
'Get the current weather for a location. Use this when the user asks about the weather.',
inputSchema: z.object({
location: z
.string()
.describe(
'The city and state/country, e.g. "San Francisco, CA" or "London, UK"',
),
}),
outputSchema: z.object({
location: z.string(),
temperature: z.number(),
unit: z.string(),
condition: z.string(),
humidity: z.number(),
}),
})

// Tool to set a reminder - demonstrates user interaction
export const setReminderToolDef = toolDefinition({
name: 'setReminder',
description:
'Set a reminder for the user. Use this when the user asks to be reminded about something.',
inputSchema: z.object({
message: z.string().describe('What to remind the user about'),
inMinutes: z.number().describe('How many minutes from now to remind'),
}),
outputSchema: z.object({
success: z.boolean(),
message: z.string(),
remindAt: z.string(),
}),
})

// Tool to search knowledge base - useful for assistants with specific knowledge
export const searchKnowledgeToolDef = toolDefinition({
name: 'searchKnowledge',
description:
'Search a knowledge base for information. Use this to find specific facts or documentation.',
inputSchema: z.object({
query: z.string().describe('The search query'),
}),
outputSchema: z.object({
results: z.array(
z.object({
title: z.string(),
snippet: z.string(),
}),
),
}),
})

// Client-side implementation of getCurrentTime
export const getCurrentTimeClient = getCurrentTimeToolDef.client(
({ timezone }) => {
const now = new Date()
const tz = timezone || Intl.DateTimeFormat().resolvedOptions().timeZone

return {
time: now.toLocaleTimeString('en-US', { timeZone: tz }),
date: now.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: tz,
}),
timezone: tz,
}
},
)

// Client-side implementation of getWeather (mock data for demo)
export const getWeatherClient = getWeatherToolDef.client(({ location }) => {
// Mock weather data for demo purposes
const conditions = ['Sunny', 'Partly Cloudy', 'Cloudy', 'Rainy', 'Snowy']
const randomCondition =
conditions[Math.floor(Math.random() * conditions.length)]!
const randomTemp = Math.floor(Math.random() * 30) + 50 // 50-80°F
const randomHumidity = Math.floor(Math.random() * 50) + 30 // 30-80%

return {
location,
temperature: randomTemp,
unit: 'F',
condition: randomCondition,
humidity: randomHumidity,
}
})

// Client-side implementation of setReminder
export const setReminderClient = setReminderToolDef.client(
({ message, inMinutes }) => {
const remindAt = new Date(Date.now() + inMinutes * 60 * 1000)

// In a real app, you'd schedule a notification here
console.log(
`[Reminder] Will remind about "${message}" at ${remindAt.toLocaleTimeString()}`,
)

// For demo purposes, show an alert after the specified time
setTimeout(
() => {
alert(`Reminder: ${message}`)
},
inMinutes * 60 * 1000,
)

return {
success: true,
message: `Reminder set: "${message}"`,
remindAt: remindAt.toLocaleTimeString(),
}
},
)

// Client-side implementation of searchKnowledge (mock data for demo)
export const searchKnowledgeClient = searchKnowledgeToolDef.client(
({ query }) => {
// Mock search results for demo
const mockResults = [
{
title: `Result for: ${query}`,
snippet: `This is a mock search result for the query "${query}". In a real application, this would return actual search results from a knowledge base.`,
},
{
title: 'Additional Information',
snippet:
'More relevant information would appear here based on your search query.',
},
]

return { results: mockResults }
},
)

// Export all client tools as an array for easy use
export const realtimeClientTools = [
getCurrentTimeClient,
getWeatherClient,
setReminderClient,
searchKnowledgeClient,
] as const
78 changes: 78 additions & 0 deletions examples/ts-react-chat/src/lib/use-realtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { createServerFn } from '@tanstack/react-start'
import { realtimeToken } from '@tanstack/ai'
import { useRealtimeChat } from '@tanstack/ai-react'
import { openaiRealtime, openaiRealtimeToken } from '@tanstack/ai-openai'
import {
elevenlabsRealtime,
elevenlabsRealtimeToken,
} from '@tanstack/ai-elevenlabs'
import { realtimeClientTools } from '@/lib/realtime-tools'

type Provider = 'openai' | 'elevenlabs'

const getRealtimeTokenFn = createServerFn({ method: 'POST' })
.inputValidator((data: { provider: Provider; agentId?: string }) => {
if (!data.provider) throw new Error('Provider is required')
return data
})
.handler(async ({ data }) => {
if (data.provider === 'openai') {
return realtimeToken({
adapter: openaiRealtimeToken({
model: 'gpt-4o-realtime-preview',
}),
})
Comment on lines +19 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, examine the file to confirm the model being used
cat -n examples/ts-react-chat/src/lib/use-realtime.ts | head -30

Repository: TanStack/ai

Length of output: 1272


🌐 Web query:

OpenAI gpt-realtime model availability deprecation 2026

💡 Result:

  • Current availability (as of Mar 6, 2026): gpt-realtime is generally available in the OpenAI Realtime API (audio + text real-time over WebRTC/WebSocket/SIP), with an alias gpt-realtime and a published snapshot (e.g., gpt-realtime-2025-08-28). (developers.openai.com)
  • Deprecations affecting “realtime” models in 2026: OpenAI deprecated the older gpt-4o-realtime-preview family and scheduled it for shutdown on 2026-02-27, with gpt-realtime as the recommended replacement. (platform.openai.com)
  • Is gpt-realtime itself deprecated in 2026? I found no OpenAI notice indicating a 2026 deprecation/shutdown for gpt-realtime itself—only for the older preview realtime models. (developers.openai.com)

Citations:


🌐 Web query:

OpenAI gpt-4o-realtime-preview model status

💡 Result:

gpt-4o-realtime-preview is deprecated (preview-only) and is scheduled to be shut down on March 24, 2026 (along with gpt-4o-realtime-preview-2025-06-03 and gpt-4o-realtime-preview-2024-12-17). The recommended replacement is gpt-realtime. [1]

As of today (March 6, 2026), that shutdown date is still in the future, so the model may still be callable, but you should migrate now to avoid disruption. [1][2]

References:
[1] OpenAI API Deprecations (gpt-4o-realtime-preview models shutdown schedule) (platform.openai.com)
[2] OpenAI blog: introducing gpt-realtime (GA replacement model) (openai.com)

Citations:


🏁 Script executed:

# Check if there are other uses of gpt-4o-realtime-preview in the codebase
rg 'gpt-4o-realtime-preview' --type ts --type tsx --type js --type jsx

Repository: TanStack/ai

Length of output: 82


🏁 Script executed:

# Search for the deprecated model string across the codebase
rg 'gpt-4o-realtime-preview'

Repository: TanStack/ai

Length of output: 908


Use gpt-realtime instead of the deprecated preview model.

gpt-4o-realtime-preview is scheduled for shutdown on 2026-03-24. Replace with gpt-realtime, which is now generally available and the recommended model for the OpenAI Realtime API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/ts-react-chat/src/lib/use-realtime.ts` around lines 19 - 24, The
code is using the deprecated model string 'gpt-4o-realtime-preview' in the
openaiRealtimeToken call; update the model to the supported name 'gpt-realtime'
inside the openaiRealtimeToken options (the call site is the realtimeToken
wrapper around openaiRealtimeToken in use-realtime.ts) so the realtimeToken({
adapter: openaiRealtimeToken({ model: ... }) }) uses 'gpt-realtime' instead of
the preview model.

}

if (data.provider === 'elevenlabs') {
const agentId = data.agentId || process.env.ELEVENLABS_AGENT_ID
if (!agentId) {
throw new Error(
'ElevenLabs agent ID is required. Set ELEVENLABS_AGENT_ID or pass agentId in request body.',
)
}
return realtimeToken({
adapter: elevenlabsRealtimeToken({ agentId }),
})
}

throw new Error(`Unknown provider: ${data.provider}`)
})

export function useRealtime({
provider,
agentId,
}: {
provider: Provider
agentId: string
}) {
const adapter =
provider === 'openai' ? openaiRealtime() : elevenlabsRealtime()

return useRealtimeChat({
getToken: () =>
getRealtimeTokenFn({
data: {
provider,
...(provider === 'elevenlabs' && agentId ? { agentId } : {}),
},
}),
adapter,
instructions: `You are a helpful, friendly voice assistant with access to several tools.

You can:
- Tell the user the current time and date (getCurrentTime)
- Get weather information for any location (getWeather)
- Set reminders for the user (setReminder)
- Search a knowledge base for information (searchKnowledge)

Keep your responses concise and conversational since this is a voice interface.
When using tools, briefly explain what you're doing and then share the results naturally.
Be friendly and engaging!`,
voice: 'alloy',
tools: realtimeClientTools,
onError: (err) => {
console.error('Realtime error:', err)
},
})
}
Loading
Loading