@tanstack/ai-elevenlabs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @tanstack/ai-elevenlabs
2
+
3
+ ElevenLabs adapter for TanStack AI realtime voice conversations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @tanstack/ai-elevenlabs @tanstack/ai @tanstack/ai-client
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Server-Side Token Generation
14
+
15
+ ```typescript
16
+ import { realtimeToken } from '@tanstack/ai'
17
+ import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'
18
+
19
+ // Generate a signed URL for client use
20
+ const token = await realtimeToken({
21
+ adapter: elevenlabsRealtimeToken({
22
+ agentId: 'your-agent-id',
23
+ }),
24
+ })
25
+ ```
26
+
27
+ ### Client-Side Usage
28
+
29
+ ```typescript
30
+ import { RealtimeClient } from '@tanstack/ai-client'
31
+ import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
32
+
33
+ const client = new RealtimeClient({
34
+ getToken: () => fetch('/api/realtime-token').then((r) => r.json()),
35
+ adapter: elevenlabsRealtime(),
36
+ })
37
+
38
+ await client.connect()
39
+ ```
40
+
41
+ ### With React
42
+
43
+ ```typescript
44
+ import { useRealtimeChat } from '@tanstack/ai-react'
45
+ import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
46
+
47
+ function VoiceChat() {
48
+ const { status, mode, messages, connect, disconnect } = useRealtimeChat({
49
+ getToken: () => fetch('/api/realtime-token').then(r => r.json()),
50
+ adapter: elevenlabsRealtime(),
51
+ })
52
+
53
+ return (
54
+ <div>
55
+ <p>Status: {status}</p>
56
+ <p>Mode: {mode}</p>
57
+ <button onClick={status === 'idle' ? connect : disconnect}>
58
+ {status === 'idle' ? 'Start' : 'Stop'}
59
+ </button>
60
+ </div>
61
+ )
62
+ }
63
+ ```
64
+
65
+ ## Environment Variables
66
+
67
+ Set `ELEVENLABS_API_KEY` in your environment for server-side token generation.
68
+
69
+ ## Requirements
70
+
71
+ - ElevenLabs account with Conversational AI agent configured
72
+ - Agent ID from ElevenLabs dashboard
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,2 @@
1
+ export { elevenlabsRealtimeToken, elevenlabsRealtime } from './realtime/index.js';
2
+ export type { ElevenLabsRealtimeTokenOptions, ElevenLabsRealtimeOptions, ElevenLabsConversationMode, ElevenLabsVADConfig, ElevenLabsClientTool, } from './realtime/index.js';
@@ -0,0 +1,7 @@
1
+ import { elevenlabsRealtimeToken } from "./realtime/token.js";
2
+ import { elevenlabsRealtime } from "./realtime/adapter.js";
3
+ export {
4
+ elevenlabsRealtime,
5
+ elevenlabsRealtimeToken
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,22 @@
1
+ import { RealtimeAdapter } from '@tanstack/ai-client';
2
+ import { ElevenLabsRealtimeOptions } from './types.js';
3
+ /**
4
+ * Creates an ElevenLabs realtime adapter for client-side use.
5
+ *
6
+ * Wraps the @11labs/client SDK for voice conversations.
7
+ *
8
+ * @param options - Optional configuration
9
+ * @returns A RealtimeAdapter for use with RealtimeClient
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { RealtimeClient } from '@tanstack/ai-client'
14
+ * import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
15
+ *
16
+ * const client = new RealtimeClient({
17
+ * getToken: () => fetch('/api/realtime-token').then(r => r.json()),
18
+ * adapter: elevenlabsRealtime(),
19
+ * })
20
+ * ```
21
+ */
22
+ export declare function elevenlabsRealtime(options?: ElevenLabsRealtimeOptions): RealtimeAdapter;
@@ -0,0 +1,185 @@
1
+ import { Conversation } from "@11labs/client";
2
+ function elevenlabsRealtime(options = {}) {
3
+ return {
4
+ provider: "elevenlabs",
5
+ async connect(token, clientToolDefs) {
6
+ return createElevenLabsConnection(token, options, clientToolDefs);
7
+ }
8
+ };
9
+ }
10
+ async function createElevenLabsConnection(token, _options, clientToolDefs) {
11
+ const eventHandlers = /* @__PURE__ */ new Map();
12
+ let conversation = null;
13
+ let messageIdCounter = 0;
14
+ const emptyFrequencyData = new Uint8Array(128);
15
+ const emptyTimeDomainData = new Uint8Array(128).fill(128);
16
+ function emit(event, payload) {
17
+ const handlers = eventHandlers.get(event);
18
+ if (handlers) {
19
+ for (const handler of handlers) {
20
+ handler(payload);
21
+ }
22
+ }
23
+ }
24
+ function generateMessageId() {
25
+ return `el-msg-${Date.now()}-${++messageIdCounter}`;
26
+ }
27
+ const elevenLabsClientTools = {};
28
+ if (clientToolDefs) {
29
+ for (const tool of clientToolDefs) {
30
+ elevenLabsClientTools[tool.name] = {
31
+ handler: async (params) => {
32
+ if (tool.execute) {
33
+ const result = await tool.execute(params);
34
+ return typeof result === "string" ? result : JSON.stringify(result);
35
+ }
36
+ return JSON.stringify({
37
+ error: `No execute function for tool ${tool.name}`
38
+ });
39
+ },
40
+ description: tool.description,
41
+ parameters: tool.inputSchema ? tool.inputSchema : { type: "object", properties: {} }
42
+ };
43
+ }
44
+ }
45
+ const sessionOptions = {
46
+ signedUrl: token.token,
47
+ onConnect: () => {
48
+ emit("status_change", { status: "connected" });
49
+ emit("mode_change", { mode: "listening" });
50
+ },
51
+ onDisconnect: () => {
52
+ emit("status_change", { status: "idle" });
53
+ emit("mode_change", { mode: "idle" });
54
+ },
55
+ onModeChange: ({ mode }) => {
56
+ const mappedMode = mode === "speaking" ? "speaking" : "listening";
57
+ emit("mode_change", { mode: mappedMode });
58
+ },
59
+ onMessage: ({ message, source }) => {
60
+ const role = source === "user" ? "user" : "assistant";
61
+ emit("transcript", {
62
+ role,
63
+ transcript: message,
64
+ isFinal: true
65
+ });
66
+ const realtimeMessage = {
67
+ id: generateMessageId(),
68
+ role,
69
+ timestamp: Date.now(),
70
+ parts: [{ type: "audio", transcript: message }]
71
+ };
72
+ emit("message_complete", { message: realtimeMessage });
73
+ },
74
+ onError: (error) => {
75
+ emit("error", {
76
+ error: new Error(
77
+ typeof error === "string" ? error : error.message || "Unknown error"
78
+ )
79
+ });
80
+ }
81
+ };
82
+ if (Object.keys(elevenLabsClientTools).length > 0) {
83
+ sessionOptions.clientTools = elevenLabsClientTools;
84
+ }
85
+ conversation = await Conversation.startSession(
86
+ sessionOptions
87
+ );
88
+ const connection = {
89
+ async disconnect() {
90
+ if (conversation) {
91
+ await conversation.endSession();
92
+ conversation = null;
93
+ }
94
+ emit("status_change", { status: "idle" });
95
+ },
96
+ async startAudioCapture() {
97
+ emit("mode_change", { mode: "listening" });
98
+ },
99
+ stopAudioCapture() {
100
+ emit("mode_change", { mode: "idle" });
101
+ },
102
+ sendText(text) {
103
+ if (!conversation) return;
104
+ conversation.sendUserMessage(text);
105
+ },
106
+ sendImage(_imageData, _mimeType) {
107
+ console.warn(
108
+ "ElevenLabs realtime does not support sending images directly."
109
+ );
110
+ },
111
+ sendToolResult(_callId, _result) {
112
+ },
113
+ updateSession(_config) {
114
+ console.warn(
115
+ "ElevenLabs does not support runtime session updates. Configure at connection time."
116
+ );
117
+ },
118
+ interrupt() {
119
+ emit("mode_change", { mode: "listening" });
120
+ emit("interrupted", {});
121
+ },
122
+ on(event, handler) {
123
+ if (!eventHandlers.has(event)) {
124
+ eventHandlers.set(event, /* @__PURE__ */ new Set());
125
+ }
126
+ eventHandlers.get(event).add(handler);
127
+ return () => {
128
+ eventHandlers.get(event)?.delete(handler);
129
+ };
130
+ },
131
+ getAudioVisualization() {
132
+ return {
133
+ get inputLevel() {
134
+ if (!conversation) return 0;
135
+ try {
136
+ return conversation.getInputVolume();
137
+ } catch {
138
+ return 0;
139
+ }
140
+ },
141
+ get outputLevel() {
142
+ if (!conversation) return 0;
143
+ try {
144
+ return conversation.getOutputVolume();
145
+ } catch {
146
+ return 0;
147
+ }
148
+ },
149
+ getInputFrequencyData() {
150
+ if (!conversation) return emptyFrequencyData;
151
+ try {
152
+ return conversation.getInputByteFrequencyData();
153
+ } catch {
154
+ return emptyFrequencyData;
155
+ }
156
+ },
157
+ getOutputFrequencyData() {
158
+ if (!conversation) return emptyFrequencyData;
159
+ try {
160
+ return conversation.getOutputByteFrequencyData();
161
+ } catch {
162
+ return emptyFrequencyData;
163
+ }
164
+ },
165
+ getInputTimeDomainData() {
166
+ return emptyTimeDomainData;
167
+ },
168
+ getOutputTimeDomainData() {
169
+ return emptyTimeDomainData;
170
+ },
171
+ get inputSampleRate() {
172
+ return 16e3;
173
+ },
174
+ get outputSampleRate() {
175
+ return 16e3;
176
+ }
177
+ };
178
+ }
179
+ };
180
+ return connection;
181
+ }
182
+ export {
183
+ elevenlabsRealtime
184
+ };
185
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sources":["../../../src/realtime/adapter.ts"],"sourcesContent":["import { Conversation } from '@11labs/client'\nimport type {\n AnyClientTool,\n AudioVisualization,\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeStatus,\n RealtimeToken,\n} from '@tanstack/ai'\nimport type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client'\nimport type { ElevenLabsRealtimeOptions } from './types'\n\n/**\n * Creates an ElevenLabs realtime adapter for client-side use.\n *\n * Wraps the @11labs/client SDK for voice conversations.\n *\n * @param options - Optional configuration\n * @returns A RealtimeAdapter for use with RealtimeClient\n *\n * @example\n * ```typescript\n * import { RealtimeClient } from '@tanstack/ai-client'\n * import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'\n *\n * const client = new RealtimeClient({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: elevenlabsRealtime(),\n * })\n * ```\n */\nexport function elevenlabsRealtime(\n options: ElevenLabsRealtimeOptions = {},\n): RealtimeAdapter {\n return {\n provider: 'elevenlabs',\n\n async connect(\n token: RealtimeToken,\n clientToolDefs?: ReadonlyArray<AnyClientTool>,\n ): Promise<RealtimeConnection> {\n return createElevenLabsConnection(token, options, clientToolDefs)\n },\n }\n}\n\n/**\n * Creates a connection to ElevenLabs conversational AI\n */\nasync function createElevenLabsConnection(\n token: RealtimeToken,\n _options: ElevenLabsRealtimeOptions,\n clientToolDefs?: ReadonlyArray<AnyClientTool>,\n): Promise<RealtimeConnection> {\n const eventHandlers = new Map<RealtimeEvent, Set<RealtimeEventHandler<any>>>()\n let conversation: Awaited<\n ReturnType<typeof Conversation.startSession>\n > | null = null\n let messageIdCounter = 0\n\n // Empty arrays for when visualization isn't available\n const emptyFrequencyData = new Uint8Array(128)\n const emptyTimeDomainData = new Uint8Array(128).fill(128)\n\n // Helper to emit events\n function emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: Parameters<RealtimeEventHandler<TEvent>>[0],\n ) {\n const handlers = eventHandlers.get(event)\n if (handlers) {\n for (const handler of handlers) {\n handler(payload)\n }\n }\n }\n\n function generateMessageId(): string {\n return `el-msg-${Date.now()}-${++messageIdCounter}`\n }\n\n // Convert TanStack tool definitions to ElevenLabs clientTools format\n const elevenLabsClientTools: Record<\n string,\n {\n handler: (params: unknown) => Promise<string>\n description: string\n parameters: Record<string, unknown>\n }\n > = {}\n\n if (clientToolDefs) {\n for (const tool of clientToolDefs) {\n elevenLabsClientTools[tool.name] = {\n handler: async (params: unknown) => {\n if (tool.execute) {\n const result = await tool.execute(params)\n return typeof result === 'string' ? result : JSON.stringify(result)\n }\n return JSON.stringify({\n error: `No execute function for tool ${tool.name}`,\n })\n },\n description: tool.description,\n parameters: tool.inputSchema\n ? (tool.inputSchema as Record<string, unknown>)\n : { type: 'object', properties: {} },\n }\n }\n }\n\n // Build session options\n const sessionOptions: Record<string, unknown> = {\n signedUrl: token.token,\n\n onConnect: () => {\n emit('status_change', { status: 'connected' as RealtimeStatus })\n emit('mode_change', { mode: 'listening' })\n },\n\n onDisconnect: () => {\n emit('status_change', { status: 'idle' as RealtimeStatus })\n emit('mode_change', { mode: 'idle' })\n },\n\n onModeChange: ({ mode }: { mode: string }) => {\n const mappedMode: RealtimeMode =\n mode === 'speaking' ? 'speaking' : 'listening'\n emit('mode_change', { mode: mappedMode })\n },\n\n onMessage: ({ message, source }: { message: string; source: string }) => {\n const role = source === 'user' ? 'user' : 'assistant'\n\n // Emit transcript update\n emit('transcript', {\n role,\n transcript: message,\n isFinal: true,\n })\n\n // Create and emit message\n const realtimeMessage: RealtimeMessage = {\n id: generateMessageId(),\n role,\n timestamp: Date.now(),\n parts: [{ type: 'audio', transcript: message }],\n }\n emit('message_complete', { message: realtimeMessage })\n },\n\n onError: (error: string | Error) => {\n emit('error', {\n error: new Error(\n typeof error === 'string' ? error : error.message || 'Unknown error',\n ),\n })\n },\n }\n\n // Only add clientTools if we have any\n if (Object.keys(elevenLabsClientTools).length > 0) {\n sessionOptions.clientTools = elevenLabsClientTools\n }\n\n // Start the conversation session\n conversation = await Conversation.startSession(\n sessionOptions as Parameters<typeof Conversation.startSession>[0],\n )\n\n // Connection implementation\n const connection: RealtimeConnection = {\n async disconnect() {\n if (conversation) {\n await conversation.endSession()\n conversation = null\n }\n emit('status_change', { status: 'idle' as RealtimeStatus })\n },\n\n async startAudioCapture() {\n // ElevenLabs SDK handles audio capture automatically\n // This is called when the session starts\n emit('mode_change', { mode: 'listening' })\n },\n\n stopAudioCapture() {\n // ElevenLabs SDK handles this\n emit('mode_change', { mode: 'idle' })\n },\n\n sendText(text: string) {\n if (!conversation) return\n conversation.sendUserMessage(text)\n },\n\n sendImage(_imageData: string, _mimeType: string) {\n // ElevenLabs does not support direct image input in the conversation API\n console.warn(\n 'ElevenLabs realtime does not support sending images directly.',\n )\n },\n\n sendToolResult(_callId: string, _result: string) {\n // ElevenLabs client tools are handled via the clientTools handlers\n // registered at session start — results are returned automatically\n },\n\n updateSession(_config: Partial<RealtimeSessionConfig>) {\n // ElevenLabs session config is set at creation time\n console.warn(\n 'ElevenLabs does not support runtime session updates. Configure at connection time.',\n )\n },\n\n interrupt() {\n // ElevenLabs handles interruption automatically via barge-in\n // No explicit API to call\n emit('mode_change', { mode: 'listening' })\n emit('interrupted', {})\n },\n\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n if (!eventHandlers.has(event)) {\n eventHandlers.set(event, new Set())\n }\n eventHandlers.get(event)!.add(handler)\n\n return () => {\n eventHandlers.get(event)?.delete(handler)\n }\n },\n\n getAudioVisualization(): AudioVisualization {\n return {\n get inputLevel() {\n if (!conversation) return 0\n try {\n return conversation.getInputVolume()\n } catch {\n return 0\n }\n },\n\n get outputLevel() {\n if (!conversation) return 0\n try {\n return conversation.getOutputVolume()\n } catch {\n return 0\n }\n },\n\n getInputFrequencyData() {\n if (!conversation) return emptyFrequencyData\n try {\n return conversation.getInputByteFrequencyData()\n } catch {\n return emptyFrequencyData\n }\n },\n\n getOutputFrequencyData() {\n if (!conversation) return emptyFrequencyData\n try {\n return conversation.getOutputByteFrequencyData()\n } catch {\n return emptyFrequencyData\n }\n },\n\n getInputTimeDomainData() {\n // ElevenLabs SDK doesn't expose time domain data\n return emptyTimeDomainData\n },\n\n getOutputTimeDomainData() {\n // ElevenLabs SDK doesn't expose time domain data\n return emptyTimeDomainData\n },\n\n get inputSampleRate() {\n return 16000\n },\n\n get outputSampleRate() {\n return 16000\n },\n }\n },\n }\n\n return connection\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { elevenlabsRealtimeToken } from './token.js';
2
+ export { elevenlabsRealtime } from './adapter.js';
3
+ export type { ElevenLabsRealtimeTokenOptions, ElevenLabsRealtimeOptions, ElevenLabsConversationMode, ElevenLabsVADConfig, ElevenLabsClientTool, } from './types.js';
@@ -0,0 +1,24 @@
1
+ import { RealtimeTokenAdapter } from '@tanstack/ai';
2
+ import { ElevenLabsRealtimeTokenOptions } from './types.js';
3
+ /**
4
+ * Creates an ElevenLabs realtime token adapter.
5
+ *
6
+ * This adapter generates signed URLs for client-side connections.
7
+ * The signed URL is valid for 30 minutes.
8
+ *
9
+ * @param options - Configuration options including agentId
10
+ * @returns A RealtimeTokenAdapter for use with realtimeToken()
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { realtimeToken } from '@tanstack/ai'
15
+ * import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'
16
+ *
17
+ * const token = await realtimeToken({
18
+ * adapter: elevenlabsRealtimeToken({
19
+ * agentId: 'your-agent-id',
20
+ * }),
21
+ * })
22
+ * ```
23
+ */
24
+ export declare function elevenlabsRealtimeToken(options: ElevenLabsRealtimeTokenOptions): RealtimeTokenAdapter;
@@ -0,0 +1,57 @@
1
+ const ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1";
2
+ function getElevenLabsApiKey() {
3
+ if (typeof process !== "undefined" && process.env.ELEVENLABS_API_KEY) {
4
+ return process.env.ELEVENLABS_API_KEY;
5
+ }
6
+ if (typeof window !== "undefined" && window.env?.ELEVENLABS_API_KEY) {
7
+ return window.env.ELEVENLABS_API_KEY;
8
+ }
9
+ throw new Error(
10
+ "ELEVENLABS_API_KEY not found in environment variables. Please set ELEVENLABS_API_KEY in your environment."
11
+ );
12
+ }
13
+ function elevenlabsRealtimeToken(options) {
14
+ const apiKey = getElevenLabsApiKey();
15
+ return {
16
+ provider: "elevenlabs",
17
+ async generateToken() {
18
+ const { agentId, overrides } = options;
19
+ const response = await fetch(
20
+ `${ELEVENLABS_API_URL}/convai/conversation/get_signed_url?agent_id=${agentId}`,
21
+ {
22
+ method: "GET",
23
+ headers: {
24
+ "xi-api-key": apiKey
25
+ }
26
+ }
27
+ );
28
+ if (!response.ok) {
29
+ const errorText = await response.text();
30
+ throw new Error(
31
+ `ElevenLabs signed URL request failed: ${response.status} ${errorText}`
32
+ );
33
+ }
34
+ const data = await response.json();
35
+ const signedUrl = data.signed_url;
36
+ const expiresAt = Date.now() + 30 * 60 * 1e3;
37
+ return {
38
+ provider: "elevenlabs",
39
+ token: signedUrl,
40
+ expiresAt,
41
+ config: {
42
+ voice: overrides?.voiceId,
43
+ instructions: overrides?.systemPrompt,
44
+ providerOptions: {
45
+ agentId,
46
+ firstMessage: overrides?.firstMessage,
47
+ language: overrides?.language
48
+ }
49
+ }
50
+ };
51
+ }
52
+ };
53
+ }
54
+ export {
55
+ elevenlabsRealtimeToken
56
+ };
57
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.js","sources":["../../../src/realtime/token.ts"],"sourcesContent":["import type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'\nimport type { ElevenLabsRealtimeTokenOptions } from './types'\n\nconst ELEVENLABS_API_URL = 'https://api.elevenlabs.io/v1'\n\n/**\n * Get ElevenLabs API key from environment\n */\nfunction getElevenLabsApiKey(): string {\n // Check process.env (Node.js)\n if (typeof process !== 'undefined' && process.env.ELEVENLABS_API_KEY) {\n return process.env.ELEVENLABS_API_KEY\n }\n\n // Check window.env (Browser with injected env)\n if (\n typeof window !== 'undefined' &&\n (window as unknown as { env?: { ELEVENLABS_API_KEY?: string } }).env\n ?.ELEVENLABS_API_KEY\n ) {\n return (window as unknown as { env: { ELEVENLABS_API_KEY: string } }).env\n .ELEVENLABS_API_KEY\n }\n\n throw new Error(\n 'ELEVENLABS_API_KEY not found in environment variables. ' +\n 'Please set ELEVENLABS_API_KEY in your environment.',\n )\n}\n\n/**\n * Creates an ElevenLabs realtime token adapter.\n *\n * This adapter generates signed URLs for client-side connections.\n * The signed URL is valid for 30 minutes.\n *\n * @param options - Configuration options including agentId\n * @returns A RealtimeTokenAdapter for use with realtimeToken()\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'\n *\n * const token = await realtimeToken({\n * adapter: elevenlabsRealtimeToken({\n * agentId: 'your-agent-id',\n * }),\n * })\n * ```\n */\nexport function elevenlabsRealtimeToken(\n options: ElevenLabsRealtimeTokenOptions,\n): RealtimeTokenAdapter {\n const apiKey = getElevenLabsApiKey()\n\n return {\n provider: 'elevenlabs',\n\n async generateToken(): Promise<RealtimeToken> {\n const { agentId, overrides } = options\n\n // Get signed URL from ElevenLabs\n const response = await fetch(\n `${ELEVENLABS_API_URL}/convai/conversation/get_signed_url?agent_id=${agentId}`,\n {\n method: 'GET',\n headers: {\n 'xi-api-key': apiKey,\n },\n },\n )\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `ElevenLabs signed URL request failed: ${response.status} ${errorText}`,\n )\n }\n\n const data = await response.json()\n const signedUrl = data.signed_url as string\n\n // Signed URLs are valid for 30 minutes\n const expiresAt = Date.now() + 30 * 60 * 1000\n\n return {\n provider: 'elevenlabs',\n token: signedUrl,\n expiresAt,\n config: {\n voice: overrides?.voiceId,\n instructions: overrides?.systemPrompt,\n providerOptions: {\n agentId,\n firstMessage: overrides?.firstMessage,\n language: overrides?.language,\n },\n },\n }\n },\n }\n}\n"],"names":[],"mappings":"AAGA,MAAM,qBAAqB;AAK3B,SAAS,sBAA8B;AAErC,MAAI,OAAO,YAAY,eAAe,QAAQ,IAAI,oBAAoB;AACpE,WAAO,QAAQ,IAAI;AAAA,EACrB;AAGA,MACE,OAAO,WAAW,eACjB,OAAgE,KAC7D,oBACJ;AACA,WAAQ,OAA8D,IACnE;AAAA,EACL;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EAAA;AAGJ;AAuBO,SAAS,wBACd,SACsB;AACtB,QAAM,SAAS,oBAAA;AAEf,SAAO;AAAA,IACL,UAAU;AAAA,IAEV,MAAM,gBAAwC;AAC5C,YAAM,EAAE,SAAS,UAAA,IAAc;AAG/B,YAAM,WAAW,MAAM;AAAA,QACrB,GAAG,kBAAkB,gDAAgD,OAAO;AAAA,QAC5E;AAAA,UACE,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,cAAc;AAAA,UAAA;AAAA,QAChB;AAAA,MACF;AAGF,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAY,MAAM,SAAS,KAAA;AACjC,cAAM,IAAI;AAAA,UACR,yCAAyC,SAAS,MAAM,IAAI,SAAS;AAAA,QAAA;AAAA,MAEzE;AAEA,YAAM,OAAO,MAAM,SAAS,KAAA;AAC5B,YAAM,YAAY,KAAK;AAGvB,YAAM,YAAY,KAAK,IAAA,IAAQ,KAAK,KAAK;AAEzC,aAAO;AAAA,QACL,UAAU;AAAA,QACV,OAAO;AAAA,QACP;AAAA,QACA,QAAQ;AAAA,UACN,OAAO,WAAW;AAAA,UAClB,cAAc,WAAW;AAAA,UACzB,iBAAiB;AAAA,YACf;AAAA,YACA,cAAc,WAAW;AAAA,YACzB,UAAU,WAAW;AAAA,UAAA;AAAA,QACvB;AAAA,MACF;AAAA,IAEJ;AAAA,EAAA;AAEJ;"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Options for the ElevenLabs realtime token adapter
3
+ */
4
+ export interface ElevenLabsRealtimeTokenOptions {
5
+ /** Agent ID configured in ElevenLabs dashboard */
6
+ agentId: string;
7
+ /** Optional override values for the agent */
8
+ overrides?: {
9
+ /** Custom voice ID to use */
10
+ voiceId?: string;
11
+ /** Custom system prompt */
12
+ systemPrompt?: string;
13
+ /** First message the agent should speak */
14
+ firstMessage?: string;
15
+ /** Language code (e.g., 'en') */
16
+ language?: string;
17
+ };
18
+ }
19
+ /**
20
+ * Options for the ElevenLabs realtime client adapter
21
+ */
22
+ export interface ElevenLabsRealtimeOptions {
23
+ /** Connection mode (default: auto-detect) */
24
+ connectionMode?: 'websocket' | 'webrtc';
25
+ /** Enable debug logging */
26
+ debug?: boolean;
27
+ }
28
+ /**
29
+ * ElevenLabs conversation mode
30
+ */
31
+ export type ElevenLabsConversationMode = 'speaking' | 'listening';
32
+ /**
33
+ * ElevenLabs voice activity detection configuration
34
+ */
35
+ export interface ElevenLabsVADConfig {
36
+ /** VAD threshold (0.1-0.9) */
37
+ vadThreshold?: number;
38
+ /** Silence threshold in seconds (0.3-3.0) */
39
+ vadSilenceThresholdSecs?: number;
40
+ /** Minimum speech duration in ms */
41
+ minSpeechDurationMs?: number;
42
+ /** Minimum silence duration in ms */
43
+ minSilenceDurationMs?: number;
44
+ }
45
+ /**
46
+ * Client tool definition for ElevenLabs
47
+ */
48
+ export interface ElevenLabsClientTool<TParams = unknown, TResult = unknown> {
49
+ /** Tool handler function */
50
+ handler: (params: TParams) => Promise<TResult> | TResult;
51
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@tanstack/ai-elevenlabs",
3
+ "version": "0.0.1",
4
+ "description": "ElevenLabs adapter for TanStack AI realtime voice",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/ai.git",
10
+ "directory": "packages/typescript/ai-elevenlabs"
11
+ },
12
+ "keywords": [
13
+ "ai",
14
+ "elevenlabs",
15
+ "voice",
16
+ "realtime",
17
+ "tanstack",
18
+ "adapter"
19
+ ],
20
+ "type": "module",
21
+ "module": "./dist/esm/index.js",
22
+ "types": "./dist/esm/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/esm/index.d.ts",
26
+ "import": "./dist/esm/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src"
32
+ ],
33
+ "scripts": {
34
+ "build": "vite build",
35
+ "clean": "premove ./build ./dist",
36
+ "lint:fix": "eslint ./src --fix",
37
+ "test:build": "publint --strict",
38
+ "test:eslint": "eslint ./src",
39
+ "test:lib": "vitest --passWithNoTests",
40
+ "test:lib:dev": "pnpm test:lib --watch",
41
+ "test:types": "tsc"
42
+ },
43
+ "dependencies": {
44
+ "@11labs/client": "^0.2.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@tanstack/ai": "^0.6.3",
48
+ "@tanstack/ai-client": "^0.6.0"
49
+ },
50
+ "devDependencies": {
51
+ "@tanstack/ai": "^0.6.3",
52
+ "@tanstack/ai-client": "^0.6.0",
53
+ "@vitest/coverage-v8": "4.0.14"
54
+ }
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ // ============================================================================
2
+ // ElevenLabs Realtime (Voice) Adapters
3
+ // ============================================================================
4
+
5
+ export { elevenlabsRealtimeToken, elevenlabsRealtime } from './realtime/index'
6
+
7
+ export type {
8
+ ElevenLabsRealtimeTokenOptions,
9
+ ElevenLabsRealtimeOptions,
10
+ ElevenLabsConversationMode,
11
+ ElevenLabsVADConfig,
12
+ ElevenLabsClientTool,
13
+ } from './realtime/index'
@@ -0,0 +1,300 @@
1
+ import { Conversation } from '@11labs/client'
2
+ import type {
3
+ AnyClientTool,
4
+ AudioVisualization,
5
+ RealtimeEvent,
6
+ RealtimeEventHandler,
7
+ RealtimeMessage,
8
+ RealtimeMode,
9
+ RealtimeSessionConfig,
10
+ RealtimeStatus,
11
+ RealtimeToken,
12
+ } from '@tanstack/ai'
13
+ import type { RealtimeAdapter, RealtimeConnection } from '@tanstack/ai-client'
14
+ import type { ElevenLabsRealtimeOptions } from './types'
15
+
16
+ /**
17
+ * Creates an ElevenLabs realtime adapter for client-side use.
18
+ *
19
+ * Wraps the @11labs/client SDK for voice conversations.
20
+ *
21
+ * @param options - Optional configuration
22
+ * @returns A RealtimeAdapter for use with RealtimeClient
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * import { RealtimeClient } from '@tanstack/ai-client'
27
+ * import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
28
+ *
29
+ * const client = new RealtimeClient({
30
+ * getToken: () => fetch('/api/realtime-token').then(r => r.json()),
31
+ * adapter: elevenlabsRealtime(),
32
+ * })
33
+ * ```
34
+ */
35
+ export function elevenlabsRealtime(
36
+ options: ElevenLabsRealtimeOptions = {},
37
+ ): RealtimeAdapter {
38
+ return {
39
+ provider: 'elevenlabs',
40
+
41
+ async connect(
42
+ token: RealtimeToken,
43
+ clientToolDefs?: ReadonlyArray<AnyClientTool>,
44
+ ): Promise<RealtimeConnection> {
45
+ return createElevenLabsConnection(token, options, clientToolDefs)
46
+ },
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Creates a connection to ElevenLabs conversational AI
52
+ */
53
+ async function createElevenLabsConnection(
54
+ token: RealtimeToken,
55
+ _options: ElevenLabsRealtimeOptions,
56
+ clientToolDefs?: ReadonlyArray<AnyClientTool>,
57
+ ): Promise<RealtimeConnection> {
58
+ const eventHandlers = new Map<RealtimeEvent, Set<RealtimeEventHandler<any>>>()
59
+ let conversation: Awaited<
60
+ ReturnType<typeof Conversation.startSession>
61
+ > | null = null
62
+ let messageIdCounter = 0
63
+
64
+ // Empty arrays for when visualization isn't available
65
+ const emptyFrequencyData = new Uint8Array(128)
66
+ const emptyTimeDomainData = new Uint8Array(128).fill(128)
67
+
68
+ // Helper to emit events
69
+ function emit<TEvent extends RealtimeEvent>(
70
+ event: TEvent,
71
+ payload: Parameters<RealtimeEventHandler<TEvent>>[0],
72
+ ) {
73
+ const handlers = eventHandlers.get(event)
74
+ if (handlers) {
75
+ for (const handler of handlers) {
76
+ handler(payload)
77
+ }
78
+ }
79
+ }
80
+
81
+ function generateMessageId(): string {
82
+ return `el-msg-${Date.now()}-${++messageIdCounter}`
83
+ }
84
+
85
+ // Convert TanStack tool definitions to ElevenLabs clientTools format
86
+ const elevenLabsClientTools: Record<
87
+ string,
88
+ {
89
+ handler: (params: unknown) => Promise<string>
90
+ description: string
91
+ parameters: Record<string, unknown>
92
+ }
93
+ > = {}
94
+
95
+ if (clientToolDefs) {
96
+ for (const tool of clientToolDefs) {
97
+ elevenLabsClientTools[tool.name] = {
98
+ handler: async (params: unknown) => {
99
+ if (tool.execute) {
100
+ const result = await tool.execute(params)
101
+ return typeof result === 'string' ? result : JSON.stringify(result)
102
+ }
103
+ return JSON.stringify({
104
+ error: `No execute function for tool ${tool.name}`,
105
+ })
106
+ },
107
+ description: tool.description,
108
+ parameters: tool.inputSchema
109
+ ? (tool.inputSchema as Record<string, unknown>)
110
+ : { type: 'object', properties: {} },
111
+ }
112
+ }
113
+ }
114
+
115
+ // Build session options
116
+ const sessionOptions: Record<string, unknown> = {
117
+ signedUrl: token.token,
118
+
119
+ onConnect: () => {
120
+ emit('status_change', { status: 'connected' as RealtimeStatus })
121
+ emit('mode_change', { mode: 'listening' })
122
+ },
123
+
124
+ onDisconnect: () => {
125
+ emit('status_change', { status: 'idle' as RealtimeStatus })
126
+ emit('mode_change', { mode: 'idle' })
127
+ },
128
+
129
+ onModeChange: ({ mode }: { mode: string }) => {
130
+ const mappedMode: RealtimeMode =
131
+ mode === 'speaking' ? 'speaking' : 'listening'
132
+ emit('mode_change', { mode: mappedMode })
133
+ },
134
+
135
+ onMessage: ({ message, source }: { message: string; source: string }) => {
136
+ const role = source === 'user' ? 'user' : 'assistant'
137
+
138
+ // Emit transcript update
139
+ emit('transcript', {
140
+ role,
141
+ transcript: message,
142
+ isFinal: true,
143
+ })
144
+
145
+ // Create and emit message
146
+ const realtimeMessage: RealtimeMessage = {
147
+ id: generateMessageId(),
148
+ role,
149
+ timestamp: Date.now(),
150
+ parts: [{ type: 'audio', transcript: message }],
151
+ }
152
+ emit('message_complete', { message: realtimeMessage })
153
+ },
154
+
155
+ onError: (error: string | Error) => {
156
+ emit('error', {
157
+ error: new Error(
158
+ typeof error === 'string' ? error : error.message || 'Unknown error',
159
+ ),
160
+ })
161
+ },
162
+ }
163
+
164
+ // Only add clientTools if we have any
165
+ if (Object.keys(elevenLabsClientTools).length > 0) {
166
+ sessionOptions.clientTools = elevenLabsClientTools
167
+ }
168
+
169
+ // Start the conversation session
170
+ conversation = await Conversation.startSession(
171
+ sessionOptions as Parameters<typeof Conversation.startSession>[0],
172
+ )
173
+
174
+ // Connection implementation
175
+ const connection: RealtimeConnection = {
176
+ async disconnect() {
177
+ if (conversation) {
178
+ await conversation.endSession()
179
+ conversation = null
180
+ }
181
+ emit('status_change', { status: 'idle' as RealtimeStatus })
182
+ },
183
+
184
+ async startAudioCapture() {
185
+ // ElevenLabs SDK handles audio capture automatically
186
+ // This is called when the session starts
187
+ emit('mode_change', { mode: 'listening' })
188
+ },
189
+
190
+ stopAudioCapture() {
191
+ // ElevenLabs SDK handles this
192
+ emit('mode_change', { mode: 'idle' })
193
+ },
194
+
195
+ sendText(text: string) {
196
+ if (!conversation) return
197
+ conversation.sendUserMessage(text)
198
+ },
199
+
200
+ sendImage(_imageData: string, _mimeType: string) {
201
+ // ElevenLabs does not support direct image input in the conversation API
202
+ console.warn(
203
+ 'ElevenLabs realtime does not support sending images directly.',
204
+ )
205
+ },
206
+
207
+ sendToolResult(_callId: string, _result: string) {
208
+ // ElevenLabs client tools are handled via the clientTools handlers
209
+ // registered at session start — results are returned automatically
210
+ },
211
+
212
+ updateSession(_config: Partial<RealtimeSessionConfig>) {
213
+ // ElevenLabs session config is set at creation time
214
+ console.warn(
215
+ 'ElevenLabs does not support runtime session updates. Configure at connection time.',
216
+ )
217
+ },
218
+
219
+ interrupt() {
220
+ // ElevenLabs handles interruption automatically via barge-in
221
+ // No explicit API to call
222
+ emit('mode_change', { mode: 'listening' })
223
+ emit('interrupted', {})
224
+ },
225
+
226
+ on<TEvent extends RealtimeEvent>(
227
+ event: TEvent,
228
+ handler: RealtimeEventHandler<TEvent>,
229
+ ): () => void {
230
+ if (!eventHandlers.has(event)) {
231
+ eventHandlers.set(event, new Set())
232
+ }
233
+ eventHandlers.get(event)!.add(handler)
234
+
235
+ return () => {
236
+ eventHandlers.get(event)?.delete(handler)
237
+ }
238
+ },
239
+
240
+ getAudioVisualization(): AudioVisualization {
241
+ return {
242
+ get inputLevel() {
243
+ if (!conversation) return 0
244
+ try {
245
+ return conversation.getInputVolume()
246
+ } catch {
247
+ return 0
248
+ }
249
+ },
250
+
251
+ get outputLevel() {
252
+ if (!conversation) return 0
253
+ try {
254
+ return conversation.getOutputVolume()
255
+ } catch {
256
+ return 0
257
+ }
258
+ },
259
+
260
+ getInputFrequencyData() {
261
+ if (!conversation) return emptyFrequencyData
262
+ try {
263
+ return conversation.getInputByteFrequencyData()
264
+ } catch {
265
+ return emptyFrequencyData
266
+ }
267
+ },
268
+
269
+ getOutputFrequencyData() {
270
+ if (!conversation) return emptyFrequencyData
271
+ try {
272
+ return conversation.getOutputByteFrequencyData()
273
+ } catch {
274
+ return emptyFrequencyData
275
+ }
276
+ },
277
+
278
+ getInputTimeDomainData() {
279
+ // ElevenLabs SDK doesn't expose time domain data
280
+ return emptyTimeDomainData
281
+ },
282
+
283
+ getOutputTimeDomainData() {
284
+ // ElevenLabs SDK doesn't expose time domain data
285
+ return emptyTimeDomainData
286
+ },
287
+
288
+ get inputSampleRate() {
289
+ return 16000
290
+ },
291
+
292
+ get outputSampleRate() {
293
+ return 16000
294
+ },
295
+ }
296
+ },
297
+ }
298
+
299
+ return connection
300
+ }
@@ -0,0 +1,14 @@
1
+ // Token adapter for server-side use
2
+ export { elevenlabsRealtimeToken } from './token'
3
+
4
+ // Client adapter for browser use
5
+ export { elevenlabsRealtime } from './adapter'
6
+
7
+ // Types
8
+ export type {
9
+ ElevenLabsRealtimeTokenOptions,
10
+ ElevenLabsRealtimeOptions,
11
+ ElevenLabsConversationMode,
12
+ ElevenLabsVADConfig,
13
+ ElevenLabsClientTool,
14
+ } from './types'
@@ -0,0 +1,103 @@
1
+ import type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'
2
+ import type { ElevenLabsRealtimeTokenOptions } from './types'
3
+
4
+ const ELEVENLABS_API_URL = 'https://api.elevenlabs.io/v1'
5
+
6
+ /**
7
+ * Get ElevenLabs API key from environment
8
+ */
9
+ function getElevenLabsApiKey(): string {
10
+ // Check process.env (Node.js)
11
+ if (typeof process !== 'undefined' && process.env.ELEVENLABS_API_KEY) {
12
+ return process.env.ELEVENLABS_API_KEY
13
+ }
14
+
15
+ // Check window.env (Browser with injected env)
16
+ if (
17
+ typeof window !== 'undefined' &&
18
+ (window as unknown as { env?: { ELEVENLABS_API_KEY?: string } }).env
19
+ ?.ELEVENLABS_API_KEY
20
+ ) {
21
+ return (window as unknown as { env: { ELEVENLABS_API_KEY: string } }).env
22
+ .ELEVENLABS_API_KEY
23
+ }
24
+
25
+ throw new Error(
26
+ 'ELEVENLABS_API_KEY not found in environment variables. ' +
27
+ 'Please set ELEVENLABS_API_KEY in your environment.',
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Creates an ElevenLabs realtime token adapter.
33
+ *
34
+ * This adapter generates signed URLs for client-side connections.
35
+ * The signed URL is valid for 30 minutes.
36
+ *
37
+ * @param options - Configuration options including agentId
38
+ * @returns A RealtimeTokenAdapter for use with realtimeToken()
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import { realtimeToken } from '@tanstack/ai'
43
+ * import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'
44
+ *
45
+ * const token = await realtimeToken({
46
+ * adapter: elevenlabsRealtimeToken({
47
+ * agentId: 'your-agent-id',
48
+ * }),
49
+ * })
50
+ * ```
51
+ */
52
+ export function elevenlabsRealtimeToken(
53
+ options: ElevenLabsRealtimeTokenOptions,
54
+ ): RealtimeTokenAdapter {
55
+ const apiKey = getElevenLabsApiKey()
56
+
57
+ return {
58
+ provider: 'elevenlabs',
59
+
60
+ async generateToken(): Promise<RealtimeToken> {
61
+ const { agentId, overrides } = options
62
+
63
+ // Get signed URL from ElevenLabs
64
+ const response = await fetch(
65
+ `${ELEVENLABS_API_URL}/convai/conversation/get_signed_url?agent_id=${agentId}`,
66
+ {
67
+ method: 'GET',
68
+ headers: {
69
+ 'xi-api-key': apiKey,
70
+ },
71
+ },
72
+ )
73
+
74
+ if (!response.ok) {
75
+ const errorText = await response.text()
76
+ throw new Error(
77
+ `ElevenLabs signed URL request failed: ${response.status} ${errorText}`,
78
+ )
79
+ }
80
+
81
+ const data = await response.json()
82
+ const signedUrl = data.signed_url as string
83
+
84
+ // Signed URLs are valid for 30 minutes
85
+ const expiresAt = Date.now() + 30 * 60 * 1000
86
+
87
+ return {
88
+ provider: 'elevenlabs',
89
+ token: signedUrl,
90
+ expiresAt,
91
+ config: {
92
+ voice: overrides?.voiceId,
93
+ instructions: overrides?.systemPrompt,
94
+ providerOptions: {
95
+ agentId,
96
+ firstMessage: overrides?.firstMessage,
97
+ language: overrides?.language,
98
+ },
99
+ },
100
+ }
101
+ },
102
+ }
103
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Options for the ElevenLabs realtime token adapter
3
+ */
4
+ export interface ElevenLabsRealtimeTokenOptions {
5
+ /** Agent ID configured in ElevenLabs dashboard */
6
+ agentId: string
7
+ /** Optional override values for the agent */
8
+ overrides?: {
9
+ /** Custom voice ID to use */
10
+ voiceId?: string
11
+ /** Custom system prompt */
12
+ systemPrompt?: string
13
+ /** First message the agent should speak */
14
+ firstMessage?: string
15
+ /** Language code (e.g., 'en') */
16
+ language?: string
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Options for the ElevenLabs realtime client adapter
22
+ */
23
+ export interface ElevenLabsRealtimeOptions {
24
+ /** Connection mode (default: auto-detect) */
25
+ connectionMode?: 'websocket' | 'webrtc'
26
+ /** Enable debug logging */
27
+ debug?: boolean
28
+ }
29
+
30
+ /**
31
+ * ElevenLabs conversation mode
32
+ */
33
+ export type ElevenLabsConversationMode = 'speaking' | 'listening'
34
+
35
+ /**
36
+ * ElevenLabs voice activity detection configuration
37
+ */
38
+ export interface ElevenLabsVADConfig {
39
+ /** VAD threshold (0.1-0.9) */
40
+ vadThreshold?: number
41
+ /** Silence threshold in seconds (0.3-3.0) */
42
+ vadSilenceThresholdSecs?: number
43
+ /** Minimum speech duration in ms */
44
+ minSpeechDurationMs?: number
45
+ /** Minimum silence duration in ms */
46
+ minSilenceDurationMs?: number
47
+ }
48
+
49
+ /**
50
+ * Client tool definition for ElevenLabs
51
+ */
52
+ export interface ElevenLabsClientTool<TParams = unknown, TResult = unknown> {
53
+ /** Tool handler function */
54
+ handler: (params: TParams) => Promise<TResult> | TResult
55
+ }