Skip to content

Commit 34a28c4

Browse files
Implement client-side vapi tool handler (#769)
1 parent 6f3d78c commit 34a28c4

File tree

2 files changed

+299
-0
lines changed

2 files changed

+299
-0
lines changed

fern/docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ navigation:
185185
- page: Custom tools
186186
path: tools/custom-tools.mdx
187187
icon: fa-light fa-screwdriver-wrench
188+
- page: Client-side tools (Web SDK)
189+
path: tools/client-side-websdk.mdx
190+
icon: fa-light fa-browser
188191
- page: Tool rejection plan
189192
path: tools/tool-rejection-plan.mdx
190193
icon: fa-light fa-shield-xmark

fern/tools/client-side-websdk.mdx

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
---
2+
title: Client-side Tools (Web SDK)
3+
subtitle: Handle tool-calls in the browser without a server URL
4+
slug: tools/client-side-websdk
5+
---
6+
7+
## Overview
8+
9+
Use the Web SDK to handle tool-calls entirely on the client. This lets your assistant trigger UI-side effects (like showing notifications or changing state) directly in the browser.
10+
11+
**In this guide, you'll learn to:**
12+
- Define a client-side tool with the Web SDK
13+
- Receive and handle `tool-calls` events on the client
14+
- Inject extra context during a call with `addMessage`
15+
16+
<Warning>
17+
Client-side tools cannot send a tool "result" back to the model. If the model must use the output of a tool to continue reasoning, implement a server-based tool instead. See: <a href="https://docs.vapi.ai/tools/custom-tools" target="_blank">Server-based Custom Tools</a>.
18+
</Warning>
19+
20+
<Info>
21+
To make a tool client-side, simply <b>do not provide a server URL</b>. The tool specification is delivered to the browser, and the Web SDK emits <code>tool-calls</code> messages that your frontend can handle.
22+
</Info>
23+
24+
## Quickstart
25+
26+
1. Install the Web SDK:
27+
28+
```bash
29+
npm install @vapi-ai/web
30+
```
31+
32+
2. Start a call with your tool defined in the <code>model.tools</code> array and subscribe to <code>clientMessages: ['tool-calls']</code>.
33+
3. Listen for <code>message.type === 'tool-calls'</code> and perform the desired UI update. No response is sent back to the model.
34+
4. (Optional) Inject context mid-call using <code>vapi.addMessage(...)</code>.
35+
36+
## Complete example (React + Web SDK)
37+
38+
```tsx
39+
import Vapi from '@vapi-ai/web';
40+
import { useCallback, useState } from 'react';
41+
42+
const vapi = new Vapi('<YOUR_PUBLIC_KEY>');
43+
44+
function App() {
45+
const [notification, setNotification] = useState<string | null>(null);
46+
47+
const handleUIUpdate = useCallback((message?: string) => {
48+
setNotification(message || 'UI Update Triggered!');
49+
setTimeout(() => setNotification(null), 3000);
50+
}, []);
51+
52+
// 1) Listen for client tool-calls and update the UI
53+
vapi.on('message', (message) => {
54+
console.log('Message:', message);
55+
56+
if (message.type === 'tool-calls') {
57+
const toolCalls = message.toolCallList;
58+
59+
toolCalls.forEach((toolCall) => {
60+
const functionName = toolCall.function?.name;
61+
let parameters: Record<string, unknown> = {};
62+
63+
try {
64+
const args = toolCall.function?.arguments;
65+
if (typeof args === 'string') {
66+
parameters = JSON.parse(args || '{}');
67+
} else if (typeof args === 'object' && args !== null) {
68+
parameters = args as Record<string, unknown>;
69+
} else {
70+
parameters = {};
71+
}
72+
} catch (err) {
73+
console.error('Failed to parse toolCall arguments:', err);
74+
return;
75+
}
76+
77+
if (functionName === 'updateUI') {
78+
handleUIUpdate((parameters as any).message);
79+
}
80+
});
81+
}
82+
});
83+
84+
// 2) Start the call with a client-side tool (no server URL)
85+
const startCall = useCallback(() => {
86+
vapi.start({
87+
model: {
88+
provider: 'openai',
89+
model: 'gpt-4.1',
90+
messages: [
91+
{
92+
role: 'system',
93+
content:
94+
"You are an attentive assistant who can interact with the application's user interface by calling available tools. Whenever the user asks to update, refresh, change, or otherwise modify the UI, or hints that some UI update should occur, always use the 'updateUI' tool call with the requested action and relevant data. Use tool calls proactively if you determine that a UI update would be helpful.",
95+
},
96+
],
97+
tools: [
98+
{
99+
type: 'function',
100+
async: true,
101+
function: {
102+
name: 'updateUI',
103+
description:
104+
'Call this function to initiate any UI update whenever the user requests or implies they want the user interface to change (for example: show a message, highlight something, trigger an animation, etc). Provide an \'action\' describing the update and an optional \'data\' object with specifics.',
105+
parameters: {
106+
type: 'object',
107+
properties: {
108+
message: {
109+
description:
110+
'Feel free to start with any brief introduction message in 10 words.',
111+
type: 'string',
112+
default: '',
113+
},
114+
},
115+
required: ['message'],
116+
},
117+
},
118+
messages: [
119+
{
120+
type: 'request-start',
121+
content: 'Updating UI...',
122+
blocking: false,
123+
},
124+
],
125+
},
126+
],
127+
},
128+
voice: { provider: 'vapi', voiceId: 'Elliot' },
129+
transcriber: { provider: 'deepgram', model: 'nova-2', language: 'en' },
130+
name: 'Alex - Test',
131+
firstMessage: 'Hello.',
132+
voicemailMessage: "Please call back when you're available.",
133+
endCallMessage: 'Goodbye.',
134+
clientMessages: ['tool-calls'], // subscribe to client-side tool calls
135+
});
136+
}, []);
137+
138+
const stopCall = useCallback(() => {
139+
vapi.stop();
140+
}, []);
141+
142+
return (
143+
<div style={{
144+
minHeight: '100vh',
145+
display: 'flex',
146+
alignItems: 'center',
147+
justifyContent: 'center',
148+
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
149+
fontFamily:
150+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
151+
}}>
152+
{notification && (
153+
<div style={{
154+
position: 'fixed',
155+
top: 20,
156+
left: '50%',
157+
transform: 'translateX(-50%)',
158+
background: '#10b981',
159+
color: '#fff',
160+
padding: '16px 24px',
161+
textAlign: 'center',
162+
borderRadius: '12px',
163+
zIndex: 1000,
164+
maxWidth: 400,
165+
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.2)',
166+
fontSize: '14px',
167+
fontWeight: 500,
168+
}}>
169+
{notification}
170+
</div>
171+
)}
172+
173+
<div style={{
174+
background: 'white',
175+
padding: '48px',
176+
borderRadius: '20px',
177+
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
178+
textAlign: 'center',
179+
maxWidth: '400px',
180+
width: '100%',
181+
}}>
182+
<h1 style={{
183+
fontSize: '32px',
184+
fontWeight: 700,
185+
color: '#1f2937',
186+
marginBottom: '12px',
187+
marginTop: 0,
188+
}}>
189+
Vapi Client Tool Calls
190+
</h1>
191+
192+
<p style={{
193+
fontSize: '16px',
194+
color: '#6b7280',
195+
marginBottom: '32px',
196+
marginTop: 0,
197+
}}>
198+
Start a call and ask the assistant to trigger UI updates
199+
</p>
200+
201+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
202+
<button
203+
onClick={startCall}
204+
style={{
205+
background:
206+
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
207+
color: 'white',
208+
border: 'none',
209+
padding: '16px 32px',
210+
borderRadius: '12px',
211+
fontSize: '16px',
212+
fontWeight: 600,
213+
cursor: 'pointer',
214+
transition: 'transform 0.2s, box-shadow 0.2s',
215+
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.4)',
216+
}}
217+
onMouseEnter={(e) => {
218+
(e.target as HTMLButtonElement).style.transform = 'translateY(-2px)';
219+
(e.target as HTMLButtonElement).style.boxShadow =
220+
'0 6px 20px rgba(102, 126, 234, 0.5)';
221+
}}
222+
onMouseLeave={(e) => {
223+
(e.target as HTMLButtonElement).style.transform = 'translateY(0)';
224+
(e.target as HTMLButtonElement).style.boxShadow =
225+
'0 4px 12px rgba(102, 126, 234, 0.4)';
226+
}}
227+
>
228+
Start Call
229+
</button>
230+
231+
<button
232+
onClick={stopCall}
233+
style={{
234+
background: 'white',
235+
color: '#ef4444',
236+
border: '2px solid #ef4444',
237+
padding: '16px 32px',
238+
borderRadius: '12px',
239+
fontSize: '16px',
240+
fontWeight: 600,
241+
cursor: 'pointer',
242+
transition: 'all 0.2s',
243+
}}
244+
onMouseEnter={(e) => {
245+
(e.target as HTMLButtonElement).style.background = '#ef4444';
246+
(e.target as HTMLButtonElement).style.color = 'white';
247+
}}
248+
onMouseLeave={(e) => {
249+
(e.target as HTMLButtonElement).style.background = 'white';
250+
(e.target as HTMLButtonElement).style.color = '#ef4444';
251+
}}
252+
>
253+
Stop Call
254+
</button>
255+
</div>
256+
</div>
257+
</div>
258+
);
259+
}
260+
261+
export default App;
262+
```
263+
264+
## Inject data during the call
265+
266+
Use <code>addMessage</code> to provide extra context mid-call. This does not return results for a tool; it adds messages the model can see.
267+
268+
```ts
269+
// Inject system-level context
270+
vapi.addMessage({
271+
role: 'system',
272+
content: 'Context: userId=123, plan=premium, theme=dark',
273+
});
274+
275+
// Inject a user message
276+
vapi.addMessage({
277+
role: 'user',
278+
content: 'FYI: I switched to the settings tab.',
279+
});
280+
```
281+
282+
<Note>
283+
If you need the model to <b>consume tool outputs</b> (e.g., fetch data and continue reasoning with it), implement a server-based tool. See <a href="https://docs.vapi.ai/tools/custom-tools" target="_blank">Custom Tools</a>.
284+
</Note>
285+
286+
## Key points
287+
288+
- **Client-only execution**: Omit the server URL to run tools on the client.
289+
- **One-way side effects**: Client tools do not send results back to the model.
290+
- **Subscribe to events**: Use <code>clientMessages: ['tool-calls']</code> and handle <code>message.type === 'tool-calls'</code>.
291+
- **Add context**: Use <code>vapi.addMessage</code> to inject data mid-call.
292+
293+
## Next steps
294+
295+
- **Server-based tools**: Learn how to return results back to the model with <a href="/tools/custom-tools">Custom Tools</a>.
296+
- **API reference**: See <a href="/api-reference/tools/create">Tools API</a> for full configuration options.

0 commit comments

Comments
 (0)