Repository navigation
feat: add realtime chat support with OpenAI and ElevenLabs adapters #300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 71ad066
Realtime chat basically working
jherr 5ad2d2c
more updates
jherr f91425c
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr 41a5e97
chore: update pnpm-lock.yaml and latest realtime chat changes
jherr aa48ba2
ci: apply automated fixes
autofix-ci[bot] 18ce05c
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr 008a824
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr e9fe67b
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr a611ad4
Merge remote-tracking branch 'origin/main' into realtime-chat
jherr c7c3520
chore: merge origin/main into pr-300
jherr 8bc5a93
API improved
jherr dc7c45f
API further improved
jherr ddca0d6
API further improved
jherr 4025e87
ci: apply automated fixes
autofix-ci[bot] a5f9879
Updating the docs
jherr 2d8990e
text chat
jherr 26cd4bd
Merge branch 'realtime-chat' of github.com:TanStack/ai into realtime-…
jherr 62301f6
ci: apply automated fixes
autofix-ci[bot] 14fed75
Merge branch 'main' into realtime-chat
jherr 2ca3efc
Getting close to releasable
jherr d0233b5
updating changeset
jherr 1b4e0f1
ci: apply automated fixes
autofix-ci[bot] f4e62ac
Realtime chat basically working
jherr 929b4a8
Realtime chat basically working
jherr 882c2be
more updates
jherr 5298fa8
chore: update pnpm-lock.yaml and latest realtime chat changes
jherr dbb9cab
ci: apply automated fixes
autofix-ci[bot] 8dbdddf
API improved
jherr 44d0740
API further improved
jherr 0165d97
API further improved
jherr 1f5eaab
Updating the docs
jherr 98831f8
text chat
jherr 88a308f
ci: apply automated fixes
autofix-ci[bot] bd8f479
ci: apply automated fixes
autofix-ci[bot] 3580092
Getting close to releasable
jherr 5ff0f68
updating changeset
jherr 3d6ce16
Resolve merge conflicts in realtime adapters and lockfile
jherr 32844fb
Merge main into realtime-chat
jherr fb148cc
docs: add missing RealtimeStateChangeCallback type alias doc
jherr c7a57c6
ci: apply automated fixes
autofix-ci[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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=... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', | ||
| }), | ||
| }) | ||
| } | ||
|
|
||
| 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) | ||
| }, | ||
| }) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: TanStack/ai
Length of output: 1272
🌐 Web query:
OpenAI gpt-realtime model availability deprecation 2026💡 Result:
gpt-realtimeis generally available in the OpenAI Realtime API (audio + text real-time over WebRTC/WebSocket/SIP), with an aliasgpt-realtimeand a published snapshot (e.g.,gpt-realtime-2025-08-28). (developers.openai.com)gpt-4o-realtime-previewfamily and scheduled it for shutdown on 2026-02-27, withgpt-realtimeas the recommended replacement. (platform.openai.com)gpt-realtimeitself deprecated in 2026? I found no OpenAI notice indicating a 2026 deprecation/shutdown forgpt-realtimeitself—only for the older preview realtime models. (developers.openai.com)Citations:
🌐 Web query:
OpenAI gpt-4o-realtime-preview model status💡 Result:
gpt-4o-realtime-previewis deprecated (preview-only) and is scheduled to be shut down on March 24, 2026 (along withgpt-4o-realtime-preview-2025-06-03andgpt-4o-realtime-preview-2024-12-17). The recommended replacement isgpt-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:
Repository: TanStack/ai
Length of output: 82
🏁 Script executed:
Repository: TanStack/ai
Length of output: 908
Use
gpt-realtimeinstead of the deprecated preview model.gpt-4o-realtime-previewis scheduled for shutdown on 2026-03-24. Replace withgpt-realtime, which is now generally available and the recommended model for the OpenAI Realtime API.🤖 Prompt for AI Agents