- )}
)}
diff --git a/packages/typescript/ai-openai/src/realtime/adapter.ts b/packages/typescript/ai-openai/src/realtime/adapter.ts
index 51745a26c..645a5e716 100644
--- a/packages/typescript/ai-openai/src/realtime/adapter.ts
+++ b/packages/typescript/ai-openai/src/realtime/adapter.ts
@@ -513,33 +513,38 @@ async function createWebRTCConnection(
// Log analyser state for debugging
console.log('[Realtime] getAudioVisualization called, inputAnalyser:', !!inputAnalyser, 'outputAnalyser:', !!outputAnalyser)
- // Helper to calculate RMS (Root Mean Square) from time domain data
- // This gives a better measure of perceived loudness than frequency data
- function calculateRMS(analyser: AnalyserNode): number {
+ // Helper to calculate audio level from time domain data
+ // Uses peak amplitude which is more responsive for voice audio meters
+ function calculateLevel(analyser: AnalyserNode): number {
const data = new Uint8Array(analyser.fftSize)
analyser.getByteTimeDomainData(data)
- // Calculate RMS - values are 0-255 with 128 being silence
- let sumSquares = 0
+ // Find peak deviation from center (128 is silence)
+ // This is more responsive than RMS for voice level meters
+ let maxDeviation = 0
for (const sample of data) {
- const normalized = (sample - 128) / 128 // Convert to -1 to 1 range
- sumSquares += normalized * normalized
+ const deviation = Math.abs(sample - 128)
+ if (deviation > maxDeviation) {
+ maxDeviation = deviation
+ }
}
- const rms = Math.sqrt(sumSquares / data.length)
- // Scale and clamp to 0-1 range (RMS of full-scale sine is ~0.707)
- return Math.min(1, rms * 1.5)
+ // Normalize to 0-1 range (max deviation is 128)
+ // Scale by 1.5x so that ~66% amplitude reads as full scale
+ // This provides good visual feedback without pegging too early
+ const normalized = maxDeviation / 128
+ return Math.min(1, normalized * 1.5)
}
return {
get inputLevel() {
if (!inputAnalyser) return 0
- return calculateRMS(inputAnalyser)
+ return calculateLevel(inputAnalyser)
},
get outputLevel() {
if (!outputAnalyser) return 0
- return calculateRMS(outputAnalyser)
+ return calculateLevel(outputAnalyser)
},
getInputFrequencyData() {
From 41a5e97ff987c6992a847535db54ca16f768b4f0 Mon Sep 17 00:00:00 2001
From: Jack Herrington
Date: Thu, 19 Feb 2026 10:18:57 -0800
Subject: [PATCH 4/5] chore: update pnpm-lock.yaml and latest realtime chat
changes
Co-Authored-By: Warp
---
.changeset/realtime-chat.md | 11 ++
examples/ts-react-chat/package.json | 2 +-
.../ai-client/src/realtime-client.ts | 2 +-
.../ai-client/src/realtime-types.ts | 28 ++---
.../typescript/ai-elevenlabs/package.json | 2 +-
.../ai-elevenlabs/src/realtime/adapter.ts | 16 +--
.../ai-elevenlabs/src/realtime/token.ts | 2 +-
.../ai-elevenlabs/src/realtime/types.ts | 7 --
.../ai-openai/src/realtime/adapter.ts | 14 +--
.../ai-openai/src/realtime/token.ts | 4 +-
.../typescript/ai-react/src/realtime-types.ts | 2 +-
packages/typescript/ai/src/realtime/types.ts | 22 ++--
pnpm-lock.yaml | 115 ++++++++++++++++++
13 files changed, 173 insertions(+), 54 deletions(-)
create mode 100644 .changeset/realtime-chat.md
diff --git a/.changeset/realtime-chat.md b/.changeset/realtime-chat.md
new file mode 100644
index 000000000..11cc3cca1
--- /dev/null
+++ b/.changeset/realtime-chat.md
@@ -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.
diff --git a/examples/ts-react-chat/package.json b/examples/ts-react-chat/package.json
index cd160a2f8..9580b8e76 100644
--- a/examples/ts-react-chat/package.json
+++ b/examples/ts-react-chat/package.json
@@ -13,11 +13,11 @@
"@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:*",
"@tanstack/ai-openai": "workspace:*",
- "@tanstack/ai-elevenlabs": "workspace:*",
"@tanstack/ai-openrouter": "workspace:*",
"@tanstack/ai-react": "workspace:*",
"@tanstack/ai-react-ui": "workspace:*",
diff --git a/packages/typescript/ai-client/src/realtime-client.ts b/packages/typescript/ai-client/src/realtime-client.ts
index 0672fe23e..f49d4d25e 100644
--- a/packages/typescript/ai-client/src/realtime-client.ts
+++ b/packages/typescript/ai-client/src/realtime-client.ts
@@ -1,11 +1,11 @@
import type {
+ AnyClientTool,
AudioVisualization,
RealtimeMessage,
RealtimeMode,
RealtimeStatus,
RealtimeToken,
} from '@tanstack/ai'
-import type { AnyClientTool } from '@tanstack/ai'
import type {
RealtimeClientOptions,
RealtimeClientState,
diff --git a/packages/typescript/ai-client/src/realtime-types.ts b/packages/typescript/ai-client/src/realtime-types.ts
index 3393ee92c..4ff6bf9bd 100644
--- a/packages/typescript/ai-client/src/realtime-types.ts
+++ b/packages/typescript/ai-client/src/realtime-types.ts
@@ -1,4 +1,5 @@
import type {
+ AnyClientTool,
AudioVisualization,
RealtimeEvent,
RealtimeEventHandler,
@@ -8,7 +9,6 @@ import type {
RealtimeStatus,
RealtimeToken,
} from '@tanstack/ai'
-import type { AnyClientTool } from '@tanstack/ai'
// ============================================================================
// Adapter Interface
@@ -27,7 +27,7 @@ export interface RealtimeAdapter {
* @param token - The ephemeral token from the server
* @returns A connection instance
*/
- connect(token: RealtimeToken): Promise
+ connect: (token: RealtimeToken) => Promise
}
/**
@@ -37,38 +37,38 @@ export interface RealtimeAdapter {
export interface RealtimeConnection {
// Lifecycle
/** Disconnect from the realtime session */
- disconnect(): Promise
+ disconnect: () => Promise
// Audio I/O
/** Start capturing audio from the microphone */
- startAudioCapture(): Promise
+ startAudioCapture: () => Promise
/** Stop capturing audio */
- stopAudioCapture(): void
+ stopAudioCapture: () => void
// Text input
/** Send a text message (fallback for when voice isn't available) */
- sendText(text: string): void
+ sendText: (text: string) => void
// Tool results
/** Send a tool execution result back to the provider */
- sendToolResult(callId: string, result: string): void
+ sendToolResult: (callId: string, result: string) => void
// Session management
/** Update session configuration */
- updateSession(config: Partial): void
+ updateSession: (config: Partial) => void
/** Interrupt the current response */
- interrupt(): void
+ interrupt: () => void
// Events
/** Subscribe to connection events */
- on(
- event: E,
- handler: RealtimeEventHandler,
- ): () => void
+ on: (
+ event: TEvent,
+ handler: RealtimeEventHandler,
+ ) => () => void
// Audio visualization
/** Get audio visualization data */
- getAudioVisualization(): AudioVisualization
+ getAudioVisualization: () => AudioVisualization
}
// ============================================================================
diff --git a/packages/typescript/ai-elevenlabs/package.json b/packages/typescript/ai-elevenlabs/package.json
index 0edafe92a..4ff1754e8 100644
--- a/packages/typescript/ai-elevenlabs/package.json
+++ b/packages/typescript/ai-elevenlabs/package.json
@@ -36,7 +36,7 @@
"lint:fix": "eslint ./src --fix",
"test:build": "publint --strict",
"test:eslint": "eslint ./src",
- "test:lib": "vitest",
+ "test:lib": "vitest --passWithNoTests",
"test:lib:dev": "pnpm test:lib --watch",
"test:types": "tsc"
},
diff --git a/packages/typescript/ai-elevenlabs/src/realtime/adapter.ts b/packages/typescript/ai-elevenlabs/src/realtime/adapter.ts
index cb193485c..39ee355bd 100644
--- a/packages/typescript/ai-elevenlabs/src/realtime/adapter.ts
+++ b/packages/typescript/ai-elevenlabs/src/realtime/adapter.ts
@@ -59,9 +59,9 @@ async function createElevenLabsConnection(
const emptyTimeDomainData = new Uint8Array(128).fill(128)
// Helper to emit events
- function emit(
- event: E,
- payload: Parameters>[0],
+ function emit(
+ event: TEvent,
+ payload: Parameters>[0],
) {
const handlers = eventHandlers.get(event)
if (handlers) {
@@ -145,7 +145,7 @@ async function createElevenLabsConnection(
emit('mode_change', { mode: 'idle' })
},
- sendText(text: string) {
+ sendText(_text: string) {
// ElevenLabs doesn't support direct text input in the same way
// The SDK is voice-first. Log a warning.
console.warn(
@@ -153,7 +153,7 @@ async function createElevenLabsConnection(
)
},
- sendToolResult(callId: string, result: string) {
+ sendToolResult(_callId: string, _result: string) {
// ElevenLabs handles client tools differently - they're registered at session start
console.warn(
'ElevenLabs tool results are handled via clientTools option during session creation.',
@@ -174,9 +174,9 @@ async function createElevenLabsConnection(
emit('interrupted', {})
},
- on(
- event: E,
- handler: RealtimeEventHandler,
+ on(
+ event: TEvent,
+ handler: RealtimeEventHandler,
): () => void {
if (!eventHandlers.has(event)) {
eventHandlers.set(event, new Set())
diff --git a/packages/typescript/ai-elevenlabs/src/realtime/token.ts b/packages/typescript/ai-elevenlabs/src/realtime/token.ts
index 99989b06b..030d0c9a9 100644
--- a/packages/typescript/ai-elevenlabs/src/realtime/token.ts
+++ b/packages/typescript/ai-elevenlabs/src/realtime/token.ts
@@ -8,7 +8,7 @@ const ELEVENLABS_API_URL = 'https://api.elevenlabs.io/v1'
*/
function getElevenLabsApiKey(): string {
// Check process.env (Node.js)
- if (typeof process !== 'undefined' && process.env?.ELEVENLABS_API_KEY) {
+ if (typeof process !== 'undefined' && process.env.ELEVENLABS_API_KEY) {
return process.env.ELEVENLABS_API_KEY
}
diff --git a/packages/typescript/ai-elevenlabs/src/realtime/types.ts b/packages/typescript/ai-elevenlabs/src/realtime/types.ts
index ff2166f08..c3f5227f7 100644
--- a/packages/typescript/ai-elevenlabs/src/realtime/types.ts
+++ b/packages/typescript/ai-elevenlabs/src/realtime/types.ts
@@ -53,10 +53,3 @@ export interface ElevenLabsClientTool {
/** Tool handler function */
handler: (params: TParams) => Promise | TResult
}
-
-/**
- * ElevenLabs signed URL response
- */
-export interface ElevenLabsSignedUrlResponse {
- signed_url: string
-}
diff --git a/packages/typescript/ai-openai/src/realtime/adapter.ts b/packages/typescript/ai-openai/src/realtime/adapter.ts
index 645a5e716..6245a3c05 100644
--- a/packages/typescript/ai-openai/src/realtime/adapter.ts
+++ b/packages/typescript/ai-openai/src/realtime/adapter.ts
@@ -88,9 +88,9 @@ async function createWebRTCConnection(
const emptyTimeDomainData = new Uint8Array(2048).fill(128) // 128 is silence
// Helper to emit events (defined early so it can be used during setup)
- function emit(
- event: E,
- payload: Parameters>[0],
+ function emit(
+ event: TEvent,
+ payload: Parameters>[0],
) {
const handlers = eventHandlers.get(event)
if (handlers) {
@@ -264,7 +264,7 @@ async function createWebRTCConnection(
// Emit message complete if we have a current message
if (currentMessageId) {
const response = event.response as Record
- const output = response.output as Array>
+ const output = response.output as Array> | undefined
const message: RealtimeMessage = {
id: currentMessageId,
@@ -495,9 +495,9 @@ async function createWebRTCConnection(
emit('interrupted', { messageId: currentMessageId ?? undefined })
},
- on(
- event: E,
- handler: RealtimeEventHandler,
+ on(
+ event: TEvent,
+ handler: RealtimeEventHandler,
): () => void {
if (!eventHandlers.has(event)) {
eventHandlers.set(event, new Set())
diff --git a/packages/typescript/ai-openai/src/realtime/token.ts b/packages/typescript/ai-openai/src/realtime/token.ts
index d226cacbb..e09d8c766 100644
--- a/packages/typescript/ai-openai/src/realtime/token.ts
+++ b/packages/typescript/ai-openai/src/realtime/token.ts
@@ -1,5 +1,5 @@
+import { getOpenAIApiKeyFromEnv } from '../utils/client'
import type { RealtimeToken, RealtimeTokenAdapter, Tool } from '@tanstack/ai'
-import { getOpenAIApiKeyFromEnv } from '../utils'
import type {
OpenAIRealtimeModel,
OpenAIRealtimeSessionResponse,
@@ -111,7 +111,7 @@ export function openaiRealtimeToken(
const sessionData: OpenAIRealtimeSessionResponse = await response.json()
// Convert tools to our format
- const tools: Array = (sessionData.tools || []).map((t) => ({
+ const tools: Array = sessionData.tools.map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.parameters,
diff --git a/packages/typescript/ai-react/src/realtime-types.ts b/packages/typescript/ai-react/src/realtime-types.ts
index f3a403464..9b44269d9 100644
--- a/packages/typescript/ai-react/src/realtime-types.ts
+++ b/packages/typescript/ai-react/src/realtime-types.ts
@@ -1,11 +1,11 @@
import type {
+ AnyClientTool,
RealtimeMessage,
RealtimeMode,
RealtimeStatus,
RealtimeToken,
} from '@tanstack/ai'
import type { RealtimeAdapter } from '@tanstack/ai-client'
-import type { AnyClientTool } from '@tanstack/ai'
/**
* Options for the useRealtimeChat hook.
diff --git a/packages/typescript/ai/src/realtime/types.ts b/packages/typescript/ai/src/realtime/types.ts
index e563a2ae7..8a4be3a98 100644
--- a/packages/typescript/ai/src/realtime/types.ts
+++ b/packages/typescript/ai/src/realtime/types.ts
@@ -57,7 +57,7 @@ export interface RealtimeTokenAdapter {
/** Provider identifier */
provider: string
/** Generate an ephemeral token for client use */
- generateToken(): Promise
+ generateToken: () => Promise
}
/**
@@ -176,14 +176,14 @@ export interface AudioVisualization {
readonly outputLevel: number
/** Get frequency data for input audio visualization */
- getInputFrequencyData(): Uint8Array
+ getInputFrequencyData: () => Uint8Array
/** Get frequency data for output audio visualization */
- getOutputFrequencyData(): Uint8Array
+ getOutputFrequencyData: () => Uint8Array
/** Get time domain data for input waveform */
- getInputTimeDomainData(): Uint8Array
+ getInputTimeDomainData: () => Uint8Array
/** Get time domain data for output waveform */
- getOutputTimeDomainData(): Uint8Array
+ getOutputTimeDomainData: () => Uint8Array
/** Input sample rate */
readonly inputSampleRate: number
@@ -191,13 +191,13 @@ export interface AudioVisualization {
readonly outputSampleRate: number
/** Subscribe to raw input audio samples */
- onInputAudio?(
+ onInputAudio?: (
callback: (samples: Float32Array, sampleRate: number) => void,
- ): () => void
+ ) => () => void
/** Subscribe to raw output audio samples */
- onOutputAudio?(
+ onOutputAudio?: (
callback: (samples: Float32Array, sampleRate: number) => void,
- ): () => void
+ ) => () => void
}
// ============================================================================
@@ -238,8 +238,8 @@ export interface RealtimeEventPayloads {
/**
* Handler type for realtime events
*/
-export type RealtimeEventHandler = (
- payload: RealtimeEventPayloads[E],
+export type RealtimeEventHandler = (
+ payload: RealtimeEventPayloads[TEvent],
) => void
// ============================================================================
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9b669fda9..cb260e569 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -201,6 +201,9 @@ importers:
'@tanstack/ai-client':
specifier: workspace:*
version: link:../../packages/typescript/ai-client
+ '@tanstack/ai-elevenlabs':
+ specifier: workspace:*
+ version: link:../../packages/typescript/ai-elevenlabs
'@tanstack/ai-gemini':
specifier: workspace:*
version: link:../../packages/typescript/ai-gemini
@@ -743,6 +746,22 @@ importers:
specifier: ^2.11.10
version: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
+ packages/typescript/ai-elevenlabs:
+ dependencies:
+ '@11labs/client':
+ specifier: ^0.2.0
+ version: 0.2.0(@types/dom-mediacapture-record@1.0.22)
+ devDependencies:
+ '@tanstack/ai':
+ specifier: workspace:*
+ version: link:../ai
+ '@tanstack/ai-client':
+ specifier: workspace:*
+ version: link:../ai-client
+ '@vitest/coverage-v8':
+ specifier: 4.0.14
+ version: 4.0.14(vitest@4.0.18(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
+
packages/typescript/ai-fal:
dependencies:
'@fal-ai/client':
@@ -819,6 +838,9 @@ importers:
'@tanstack/ai':
specifier: workspace:*
version: link:../ai
+ '@tanstack/ai-client':
+ specifier: workspace:*
+ version: link:../ai-client
'@vitest/coverage-v8':
specifier: 4.0.14
version: 4.0.14(vitest@4.0.18(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
@@ -1409,6 +1431,10 @@ importers:
packages:
+ '@11labs/client@0.2.0':
+ resolution: {integrity: sha512-GBplAV4WDbcoThsIzdSDPN3xbcitK0ZZ4iJfJZKfltqvgvS6Uw8GZxHwVgiPwnQoA3uosYyY3L9TuPwmel18xQ==}
+ deprecated: This package is no longer maintained. Please use @elevenlabs/client for the latest version
+
'@acemir/cssom@0.9.29':
resolution: {integrity: sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA==}
@@ -1601,6 +1627,9 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
+ '@bufbuild/protobuf@1.10.1':
+ resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==}
+
'@changesets/apply-release-plan@7.0.14':
resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==}
@@ -2474,6 +2503,12 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@livekit/mutex@1.1.1':
+ resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==}
+
+ '@livekit/protocol@1.44.0':
+ resolution: {integrity: sha512-/vfhDUGcUKO8Q43r6i+5FrDhl5oZjm/X3U4x2Iciqvgn5C8qbj+57YPcWSJ1kyIZm5Cm6AV2nAPjMm3ETD/iyg==}
+
'@manypkg/find-root@1.1.0':
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
@@ -4367,6 +4402,9 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+ '@types/dom-mediacapture-record@1.0.22':
+ resolution: {integrity: sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==}
+
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@@ -6555,6 +6593,9 @@ packages:
jju@1.4.0:
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
+ jose@6.1.3:
+ resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
+
js-beautify@1.15.4:
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
engines: {node: '>=14'}
@@ -6758,6 +6799,11 @@ packages:
resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==}
hasBin: true
+ livekit-client@2.17.2:
+ resolution: {integrity: sha512-+67y2EtAWZabARlY7kANl/VT1Uu1EJYR5a8qwpT2ub/uBCltsEgEDOxCIMwE9HFR5w+z41HR6GL9hyEvW/y6CQ==}
+ peerDependencies:
+ '@types/dom-mediacapture-record': ^1
+
local-pkg@0.5.1:
resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
engines: {node: '>=14'}
@@ -6796,6 +6842,10 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
+ loglevel@1.9.2:
+ resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
+ engines: {node: '>= 0.6.0'}
+
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -7861,6 +7911,13 @@ packages:
scule@1.3.0:
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
+ sdp-transform@2.15.0:
+ resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==}
+ hasBin: true
+
+ sdp@3.2.1:
+ resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==}
+
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -8339,6 +8396,9 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
+ ts-debounce@4.0.0:
+ resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==}
+
ts-declaration-location@1.0.7:
resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==}
peerDependencies:
@@ -8408,6 +8468,9 @@ packages:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
+ typed-emitter@2.1.0:
+ resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
+
typedoc-plugin-frontmatter@1.3.0:
resolution: {integrity: sha512-xYQFMAecMlsRUjmf9oM/Sq2FVz4zlgcbIeVFNLdO118CHTN06gIKJNSlyExh9+Xl8sK0YhIvoQwViUURxritWA==}
peerDependencies:
@@ -9105,6 +9168,10 @@ packages:
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+ webrtc-adapter@9.0.3:
+ resolution: {integrity: sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==}
+ engines: {node: '>=6.0.0', npm: '>=3.10.0'}
+
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
@@ -9259,6 +9326,12 @@ packages:
snapshots:
+ '@11labs/client@0.2.0(@types/dom-mediacapture-record@1.0.22)':
+ dependencies:
+ livekit-client: 2.17.2(@types/dom-mediacapture-record@1.0.22)
+ transitivePeerDependencies:
+ - '@types/dom-mediacapture-record'
+
'@acemir/cssom@0.9.29': {}
'@alcyone-labs/zod-to-json-schema@4.0.10(zod@4.2.1)':
@@ -9508,6 +9581,8 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
+ '@bufbuild/protobuf@1.10.1': {}
+
'@changesets/apply-release-plan@7.0.14':
dependencies:
'@changesets/config': 3.1.2
@@ -10180,6 +10255,12 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@livekit/mutex@1.1.1': {}
+
+ '@livekit/protocol@1.44.0':
+ dependencies:
+ '@bufbuild/protobuf': 1.10.1
+
'@manypkg/find-root@1.1.0':
dependencies:
'@babel/runtime': 7.28.4
@@ -12648,6 +12729,8 @@ snapshots:
'@types/deep-eql@4.0.2': {}
+ '@types/dom-mediacapture-record@1.0.22': {}
+
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.8
@@ -15291,6 +15374,8 @@ snapshots:
jju@1.4.0: {}
+ jose@6.1.3: {}
+
js-beautify@1.15.4:
dependencies:
config-chain: 1.1.13
@@ -15510,6 +15595,20 @@ snapshots:
untun: 0.1.3
uqr: 0.1.2
+ livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22):
+ dependencies:
+ '@livekit/mutex': 1.1.1
+ '@livekit/protocol': 1.44.0
+ '@types/dom-mediacapture-record': 1.0.22
+ events: 3.3.0
+ jose: 6.1.3
+ loglevel: 1.9.2
+ sdp-transform: 2.15.0
+ ts-debounce: 4.0.0
+ tslib: 2.8.1
+ typed-emitter: 2.1.0
+ webrtc-adapter: 9.0.3
+
local-pkg@0.5.1:
dependencies:
mlly: 1.8.0
@@ -15546,6 +15645,8 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
+ loglevel@1.9.2: {}
+
longest-streak@3.1.0: {}
lowlight@3.3.0:
@@ -17166,6 +17267,10 @@ snapshots:
scule@1.3.0: {}
+ sdp-transform@2.15.0: {}
+
+ sdp@3.2.1: {}
+
semver@6.3.1: {}
semver@7.5.4:
@@ -17667,6 +17772,8 @@ snapshots:
dependencies:
typescript: 5.9.3
+ ts-debounce@4.0.0: {}
+
ts-declaration-location@1.0.7(typescript@5.9.3):
dependencies:
picomatch: 4.0.3
@@ -17734,6 +17841,10 @@ snapshots:
media-typer: 1.1.0
mime-types: 3.0.2
+ typed-emitter@2.1.0:
+ optionalDependencies:
+ rxjs: 7.8.2
+
typedoc-plugin-frontmatter@1.3.0(typedoc-plugin-markdown@4.9.0(typedoc@0.28.14(typescript@5.9.3))):
dependencies:
typedoc-plugin-markdown: 4.9.0(typedoc@0.28.14(typescript@5.9.3))
@@ -18509,6 +18620,10 @@ snapshots:
webpack-virtual-modules@0.6.2: {}
+ webrtc-adapter@9.0.3:
+ dependencies:
+ sdp: 3.2.1
+
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
From aa48ba285f9db6b511a9d54c67d9643572829356 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Thu, 19 Feb 2026 18:21:02 +0000
Subject: [PATCH 5/5] ci: apply automated fixes
---
.../ts-react-chat/src/lib/realtime-tools.ts | 146 +++++++++++-------
.../src/routes/api.realtime-token.ts | 13 +-
.../ts-react-chat/src/routes/realtime.tsx | 40 ++---
.../ai-client/src/realtime-client.ts | 51 +++---
packages/typescript/ai-elevenlabs/README.md | 2 +-
.../typescript/ai-elevenlabs/src/index.ts | 5 +-
.../ai-elevenlabs/src/realtime/adapter.ts | 4 +-
packages/typescript/ai-openai/src/index.ts | 5 +-
.../ai-openai/src/realtime/adapter.ts | 35 +++--
.../ai-openai/src/realtime/token.ts | 11 +-
.../ai-react/src/use-realtime-chat.ts | 35 ++++-
11 files changed, 208 insertions(+), 139 deletions(-)
diff --git a/examples/ts-react-chat/src/lib/realtime-tools.ts b/examples/ts-react-chat/src/lib/realtime-tools.ts
index 2cf28d7bd..e19c52226 100644
--- a/examples/ts-react-chat/src/lib/realtime-tools.ts
+++ b/examples/ts-react-chat/src/lib/realtime-tools.ts
@@ -4,9 +4,13 @@ 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.',
+ 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"'),
+ timezone: z
+ .string()
+ .optional()
+ .describe('Optional timezone like "America/New_York" or "Europe/London"'),
}),
outputSchema: z.object({
time: z.string(),
@@ -18,9 +22,14 @@ export const getCurrentTimeToolDef = toolDefinition({
// 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.',
+ 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"'),
+ location: z
+ .string()
+ .describe(
+ 'The city and state/country, e.g. "San Francisco, CA" or "London, UK"',
+ ),
}),
outputSchema: z.object({
location: z.string(),
@@ -34,7 +43,8 @@ export const getWeatherToolDef = toolDefinition({
// 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.',
+ 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'),
@@ -49,44 +59,50 @@ export const setReminderToolDef = toolDefinition({
// 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.',
+ 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(),
- })),
+ 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,
- }
-})
+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 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,
@@ -97,40 +113,50 @@ export const getWeatherClient = getWeatherToolDef.client(({ location }) => {
})
// 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(),
- }
-})
+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 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 = [
diff --git a/examples/ts-react-chat/src/routes/api.realtime-token.ts b/examples/ts-react-chat/src/routes/api.realtime-token.ts
index c9367ad19..807623ecf 100644
--- a/examples/ts-react-chat/src/routes/api.realtime-token.ts
+++ b/examples/ts-react-chat/src/routes/api.realtime-token.ts
@@ -13,9 +13,13 @@ import * as z from 'zod'
type Provider = 'openai' | 'elevenlabs'
// Convert tool definitions to OpenAI's format using Zod's native toJSONSchema
-function toolDefToOpenAI(toolDef: { name: string; description: string; inputSchema?: unknown }) {
+function toolDefToOpenAI(toolDef: {
+ name: string
+ description: string
+ inputSchema?: unknown
+}) {
let parameters: Record = { type: 'object', properties: {} }
-
+
if (toolDef.inputSchema) {
// Use Zod's native toJSONSchema for Zod v4+
const jsonSchema = z.toJSONSchema(toolDef.inputSchema as z.ZodType)
@@ -23,7 +27,7 @@ function toolDefToOpenAI(toolDef: { name: string; description: string; inputSche
const { $schema, ...rest } = jsonSchema as Record
parameters = rest
}
-
+
return {
type: 'function' as const,
name: toolDef.name,
@@ -85,7 +89,8 @@ Be friendly and engaging!`,
if (!agentId) {
return new Response(
JSON.stringify({
- error: 'ElevenLabs agent ID is required. Set ELEVENLABS_AGENT_ID or pass agentId in request body.',
+ error:
+ 'ElevenLabs agent ID is required. Set ELEVENLABS_AGENT_ID or pass agentId in request body.',
}),
{
status: 400,
diff --git a/examples/ts-react-chat/src/routes/realtime.tsx b/examples/ts-react-chat/src/routes/realtime.tsx
index 4be974a8f..107b839e5 100644
--- a/examples/ts-react-chat/src/routes/realtime.tsx
+++ b/examples/ts-react-chat/src/routes/realtime.tsx
@@ -14,11 +14,11 @@ const PROVIDER_OPTIONS: Array<{ value: Provider; label: string }> = [
]
// Sparkline component to visualize audio waveform
-function AudioSparkline({
- getData,
+function AudioSparkline({
+ getData,
color,
label,
-}: {
+}: {
getData: () => Uint8Array
color: string
label: string
@@ -49,20 +49,20 @@ function AudioSparkline({
// Sample the data to fit the canvas width
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
// Convert 0-255 to canvas height (128 is center/silence)
- const y = height - ((value / 255) * height)
-
+ const y = height - (value / 255) * height
+
if (i === 0) {
ctx!.moveTo(i, y)
} else {
ctx!.lineTo(i, y)
}
}
-
+
ctx!.stroke()
// Draw center line (silence level)
@@ -89,10 +89,10 @@ function AudioSparkline({
return (
{label}
-
@@ -105,7 +105,8 @@ function RealtimePage() {
const messagesEndRef = useRef(null)
// Get the appropriate adapter based on provider
- const adapter = provider === 'openai' ? openaiRealtime() : elevenlabsRealtime()
+ const adapter =
+ provider === 'openai' ? openaiRealtime() : elevenlabsRealtime()
const {
status,
@@ -284,7 +285,8 @@ function RealtimePage() {
Click "Start Conversation" to begin talking with the AI
- Try asking: "What time is it?" or "What's the weather in San Francisco?"
+ Try asking: "What time is it?" or "What's the weather in San
+ Francisco?"
)}
@@ -388,9 +390,9 @@ function RealtimePage() {
{Math.round(inputLevel * 100)}%
-
@@ -406,9 +408,9 @@ function RealtimePage() {
{Math.round(outputLevel * 100)}%
-
diff --git a/packages/typescript/ai-client/src/realtime-client.ts b/packages/typescript/ai-client/src/realtime-client.ts
index f49d4d25e..170a4d585 100644
--- a/packages/typescript/ai-client/src/realtime-client.ts
+++ b/packages/typescript/ai-client/src/realtime-client.ts
@@ -61,7 +61,7 @@ export class RealtimeClient {
constructor(options: RealtimeClientOptions) {
this.instanceId = ++clientIdCounter
console.log(`[RealtimeClient #${this.instanceId}] Created`)
-
+
this.options = {
autoPlayback: true,
autoCapture: true,
@@ -102,7 +102,10 @@ export class RealtimeClient {
// Connect via adapter
this.connection = await this.options.adapter.connect(this.token)
- console.log(`[RealtimeClient #${this.instanceId}] Connection established:`, !!this.connection)
+ console.log(
+ `[RealtimeClient #${this.instanceId}] Connection established:`,
+ !!this.connection,
+ )
// Subscribe to connection events
this.subscribeToConnectionEvents()
@@ -251,7 +254,10 @@ export class RealtimeClient {
/** Get audio visualization data */
get audio(): AudioVisualization | null {
- console.log(`[RealtimeClient #${this.instanceId}] audio getter, connection:`, !!this.connection)
+ console.log(
+ `[RealtimeClient #${this.instanceId}] audio getter, connection:`,
+ !!this.connection,
+ )
return this.connection?.getAudioVisualization() ?? null
}
@@ -381,25 +387,28 @@ export class RealtimeClient {
// Tool calls
this.unsubscribers.push(
- this.connection.on('tool_call', async ({ toolCallId, toolName, input }) => {
- const tool = this.clientTools.get(toolName)
- if (tool?.execute) {
- try {
- const output = await tool.execute(input)
- this.connection?.sendToolResult(
- toolCallId,
- typeof output === 'string' ? output : JSON.stringify(output),
- )
- } catch (error) {
- const errMsg =
- error instanceof Error ? error.message : String(error)
- this.connection?.sendToolResult(
- toolCallId,
- JSON.stringify({ error: errMsg }),
- )
+ this.connection.on(
+ 'tool_call',
+ async ({ toolCallId, toolName, input }) => {
+ const tool = this.clientTools.get(toolName)
+ if (tool?.execute) {
+ try {
+ const output = await tool.execute(input)
+ this.connection?.sendToolResult(
+ toolCallId,
+ typeof output === 'string' ? output : JSON.stringify(output),
+ )
+ } catch (error) {
+ const errMsg =
+ error instanceof Error ? error.message : String(error)
+ this.connection?.sendToolResult(
+ toolCallId,
+ JSON.stringify({ error: errMsg }),
+ )
+ }
}
- }
- }),
+ },
+ ),
)
// Message complete
diff --git a/packages/typescript/ai-elevenlabs/README.md b/packages/typescript/ai-elevenlabs/README.md
index 6f85bb8ed..71b0d979b 100644
--- a/packages/typescript/ai-elevenlabs/README.md
+++ b/packages/typescript/ai-elevenlabs/README.md
@@ -31,7 +31,7 @@ import { RealtimeClient } from '@tanstack/ai-client'
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
const client = new RealtimeClient({
- getToken: () => fetch('/api/realtime-token').then(r => r.json()),
+ getToken: () => fetch('/api/realtime-token').then((r) => r.json()),
adapter: elevenlabsRealtime(),
})
diff --git a/packages/typescript/ai-elevenlabs/src/index.ts b/packages/typescript/ai-elevenlabs/src/index.ts
index 14702a1da..8f3789e84 100644
--- a/packages/typescript/ai-elevenlabs/src/index.ts
+++ b/packages/typescript/ai-elevenlabs/src/index.ts
@@ -2,10 +2,7 @@
// ElevenLabs Realtime (Voice) Adapters
// ============================================================================
-export {
- elevenlabsRealtimeToken,
- elevenlabsRealtime,
-} from './realtime/index'
+export { elevenlabsRealtimeToken, elevenlabsRealtime } from './realtime/index'
export type {
ElevenLabsRealtimeTokenOptions,
diff --git a/packages/typescript/ai-elevenlabs/src/realtime/adapter.ts b/packages/typescript/ai-elevenlabs/src/realtime/adapter.ts
index 39ee355bd..dfe33d318 100644
--- a/packages/typescript/ai-elevenlabs/src/realtime/adapter.ts
+++ b/packages/typescript/ai-elevenlabs/src/realtime/adapter.ts
@@ -51,7 +51,9 @@ async function createElevenLabsConnection(
_options: ElevenLabsRealtimeOptions,
): Promise {
const eventHandlers = new Map>>()
- let conversation: Awaited> | null = null
+ let conversation: Awaited<
+ ReturnType
+ > | null = null
let messageIdCounter = 0
// Empty arrays for when visualization isn't available
diff --git a/packages/typescript/ai-openai/src/index.ts b/packages/typescript/ai-openai/src/index.ts
index 2b7916a37..afadc4529 100644
--- a/packages/typescript/ai-openai/src/index.ts
+++ b/packages/typescript/ai-openai/src/index.ts
@@ -105,10 +105,7 @@ export type { OpenAIClientConfig } from './utils/client'
// Realtime (Voice) Adapters
// ============================================================================
-export {
- openaiRealtimeToken,
- openaiRealtime,
-} from './realtime/index'
+export { openaiRealtimeToken, openaiRealtime } from './realtime/index'
export type {
OpenAIRealtimeVoice,
diff --git a/packages/typescript/ai-openai/src/realtime/adapter.ts b/packages/typescript/ai-openai/src/realtime/adapter.ts
index 6245a3c05..e25a7dbd3 100644
--- a/packages/typescript/ai-openai/src/realtime/adapter.ts
+++ b/packages/typescript/ai-openai/src/realtime/adapter.ts
@@ -8,10 +8,7 @@ import type {
RealtimeStatus,
RealtimeToken,
} from '@tanstack/ai'
-import type {
- RealtimeAdapter,
- RealtimeConnection,
-} from '@tanstack/ai-client'
+import type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client'
import type { OpenAIRealtimeOptions } from './types'
const OPENAI_REALTIME_URL = 'https://api.openai.com/v1/realtime'
@@ -175,7 +172,10 @@ async function createWebRTCConnection(
await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp })
// Set up input audio analysis now that we have the stream
- console.log('[Realtime] Setting up input audio analysis, localStream:', localStream)
+ console.log(
+ '[Realtime] Setting up input audio analysis, localStream:',
+ localStream,
+ )
setupInputAudioAnalysis(localStream)
console.log('[Realtime] Input analyser created:', inputAnalyser)
@@ -224,7 +224,11 @@ async function createWebRTCConnection(
case 'response.audio_transcript.delta': {
const delta = event.delta as string
- emit('transcript', { role: 'assistant', transcript: delta, isFinal: false })
+ emit('transcript', {
+ role: 'assistant',
+ transcript: delta,
+ isFinal: false,
+ })
break
}
@@ -264,8 +268,10 @@ async function createWebRTCConnection(
// Emit message complete if we have a current message
if (currentMessageId) {
const response = event.response as Record
- const output = response.output as Array> | undefined
-
+ const output = response.output as
+ | Array>
+ | undefined
+
const message: RealtimeMessage = {
id: currentMessageId,
role: 'assistant',
@@ -511,14 +517,19 @@ async function createWebRTCConnection(
getAudioVisualization(): AudioVisualization {
// Log analyser state for debugging
- console.log('[Realtime] getAudioVisualization called, inputAnalyser:', !!inputAnalyser, 'outputAnalyser:', !!outputAnalyser)
-
+ console.log(
+ '[Realtime] getAudioVisualization called, inputAnalyser:',
+ !!inputAnalyser,
+ 'outputAnalyser:',
+ !!outputAnalyser,
+ )
+
// Helper to calculate audio level from time domain data
// Uses peak amplitude which is more responsive for voice audio meters
function calculateLevel(analyser: AnalyserNode): number {
const data = new Uint8Array(analyser.fftSize)
analyser.getByteTimeDomainData(data)
-
+
// Find peak deviation from center (128 is silence)
// This is more responsive than RMS for voice level meters
let maxDeviation = 0
@@ -528,7 +539,7 @@ async function createWebRTCConnection(
maxDeviation = deviation
}
}
-
+
// Normalize to 0-1 range (max deviation is 128)
// Scale by 1.5x so that ~66% amplitude reads as full scale
// This provides good visual feedback without pegging too early
diff --git a/packages/typescript/ai-openai/src/realtime/token.ts b/packages/typescript/ai-openai/src/realtime/token.ts
index e09d8c766..832adaa79 100644
--- a/packages/typescript/ai-openai/src/realtime/token.ts
+++ b/packages/typescript/ai-openai/src/realtime/token.ts
@@ -126,11 +126,12 @@ export function openaiRealtimeToken(
voice: sessionData.voice,
instructions: sessionData.instructions,
tools,
- vadMode: sessionData.turn_detection?.type === 'semantic_vad'
- ? 'semantic'
- : sessionData.turn_detection?.type === 'server_vad'
- ? 'server'
- : 'manual',
+ vadMode:
+ sessionData.turn_detection?.type === 'semantic_vad'
+ ? 'semantic'
+ : sessionData.turn_detection?.type === 'server_vad'
+ ? 'server'
+ : 'manual',
vadConfig: sessionData.turn_detection
? {
threshold: sessionData.turn_detection.threshold,
diff --git a/packages/typescript/ai-react/src/use-realtime-chat.ts b/packages/typescript/ai-react/src/use-realtime-chat.ts
index 3434db607..9d3f0af41 100644
--- a/packages/typescript/ai-react/src/use-realtime-chat.ts
+++ b/packages/typescript/ai-react/src/use-realtime-chat.ts
@@ -1,7 +1,14 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { RealtimeClient } from '@tanstack/ai-client'
-import type { RealtimeMessage, RealtimeMode, RealtimeStatus } from '@tanstack/ai'
-import type { UseRealtimeChatOptions, UseRealtimeChatReturn } from './realtime-types'
+import type {
+ RealtimeMessage,
+ RealtimeMode,
+ RealtimeStatus,
+} from '@tanstack/ai'
+import type {
+ UseRealtimeChatOptions,
+ UseRealtimeChatReturn,
+} from './realtime-types'
// Empty frequency data for when client is not connected
const emptyFrequencyData = new Uint8Array(128)
@@ -54,8 +61,12 @@ export function useRealtimeChat(
const [status, setStatus] = useState('idle')
const [mode, setMode] = useState('idle')
const [messages, setMessages] = useState>([])
- const [pendingUserTranscript, setPendingUserTranscript] = useState(null)
- const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState(null)
+ const [pendingUserTranscript, setPendingUserTranscript] = useState<
+ string | null
+ >(null)
+ const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState<
+ string | null
+ >(null)
const [error, setError] = useState(null)
const [inputLevel, setInputLevel] = useState(0)
const [outputLevel, setOutputLevel] = useState(0)
@@ -181,19 +192,27 @@ export function useRealtimeChat(
// Audio visualization
const getInputFrequencyData = useCallback(() => {
- return clientRef.current?.audio?.getInputFrequencyData() ?? emptyFrequencyData
+ return (
+ clientRef.current?.audio?.getInputFrequencyData() ?? emptyFrequencyData
+ )
}, [])
const getOutputFrequencyData = useCallback(() => {
- return clientRef.current?.audio?.getOutputFrequencyData() ?? emptyFrequencyData
+ return (
+ clientRef.current?.audio?.getOutputFrequencyData() ?? emptyFrequencyData
+ )
}, [])
const getInputTimeDomainData = useCallback(() => {
- return clientRef.current?.audio?.getInputTimeDomainData() ?? emptyTimeDomainData
+ return (
+ clientRef.current?.audio?.getInputTimeDomainData() ?? emptyTimeDomainData
+ )
}, [])
const getOutputTimeDomainData = useCallback(() => {
- return clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData
+ return (
+ clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData
+ )
}, [])
// VAD mode control