Skip to content

Commit b4721f4

Browse files
Vapi Taskerclaude
andcommitted
fix: return full API response from get_call including messages/transcript
The get_call tool was only returning a subset of fields from the Vapi API. This change updates transformCallOutput to pass through the complete API response, including: - messages (full transcript with role, content, timestamps) - transcript (plain text transcript) - recordingUrl and stereoRecordingUrl - cost and costBreakdown - analysis and artifact data - Additional call metadata The CallOutputSchema has been updated to document these fields while using passthrough() to allow any additional fields from the API. Fixes: PRISM-5 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c644117 commit b4721f4

3 files changed

Lines changed: 159 additions & 14 deletions

File tree

src/__tests__/transformers.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { transformCallOutput } from '../transformers/index.js';
2+
import { Vapi } from '@vapi-ai/server-sdk';
3+
4+
describe('transformCallOutput', () => {
5+
// Use type assertion to allow setting all fields including those returned by API
6+
const mockCallWithMessages = {
7+
id: 'call-123',
8+
orgId: 'org-456',
9+
createdAt: '2025-01-15T10:00:00.000Z',
10+
updatedAt: '2025-01-15T10:05:00.000Z',
11+
status: 'ended',
12+
endedReason: 'assistant-ended-call',
13+
assistantId: 'asst-789',
14+
phoneNumberId: 'pn-012',
15+
customer: {
16+
number: '+15551234567',
17+
},
18+
messages: [
19+
{
20+
role: 'assistant',
21+
message: 'Hello! How can I help you today?',
22+
time: 1705312800000,
23+
secondsFromStart: 0,
24+
},
25+
{
26+
role: 'user',
27+
message: 'I need help with my order.',
28+
time: 1705312805000,
29+
secondsFromStart: 5,
30+
},
31+
{
32+
role: 'assistant',
33+
message: 'Of course! Can you provide your order number?',
34+
time: 1705312810000,
35+
secondsFromStart: 10,
36+
},
37+
],
38+
transcript: 'Hello! How can I help you today?\nI need help with my order.\nOf course! Can you provide your order number?',
39+
recordingUrl: 'https://example.com/recording.mp3',
40+
stereoRecordingUrl: 'https://example.com/stereo-recording.mp3',
41+
costBreakdown: {
42+
stt: 0.01,
43+
llm: 0.05,
44+
tts: 0.02,
45+
vapi: 0.03,
46+
total: 0.11,
47+
},
48+
cost: 0.11,
49+
} as Vapi.Call;
50+
51+
it('should include messages array in the output', () => {
52+
const result = transformCallOutput(mockCallWithMessages);
53+
54+
// This test verifies that messages are included in the output
55+
expect(result.messages).toBeDefined();
56+
expect(Array.isArray(result.messages)).toBe(true);
57+
expect(result.messages!).toHaveLength(3);
58+
expect(result.messages![0]).toMatchObject({
59+
role: 'assistant',
60+
message: 'Hello! How can I help you today?',
61+
});
62+
});
63+
64+
it('should include transcript in the output', () => {
65+
const result = transformCallOutput(mockCallWithMessages);
66+
67+
expect(result.transcript).toBeDefined();
68+
expect(result.transcript).toContain('Hello! How can I help you today?');
69+
});
70+
71+
it('should include recording URLs in the output', () => {
72+
const result = transformCallOutput(mockCallWithMessages);
73+
74+
expect(result.recordingUrl).toBe('https://example.com/recording.mp3');
75+
expect(result.stereoRecordingUrl).toBe('https://example.com/stereo-recording.mp3');
76+
});
77+
78+
it('should include cost information in the output', () => {
79+
const result = transformCallOutput(mockCallWithMessages);
80+
81+
expect(result.cost).toBe(0.11);
82+
expect(result.costBreakdown).toBeDefined();
83+
expect(result.costBreakdown!.total).toBe(0.11);
84+
});
85+
86+
it('should still include basic call fields', () => {
87+
const result = transformCallOutput(mockCallWithMessages);
88+
89+
expect(result.id).toBe('call-123');
90+
expect(result.createdAt).toBe('2025-01-15T10:00:00.000Z');
91+
expect(result.updatedAt).toBe('2025-01-15T10:05:00.000Z');
92+
expect(result.status).toBe('ended');
93+
expect(result.endedReason).toBe('assistant-ended-call');
94+
expect(result.assistantId).toBe('asst-789');
95+
expect(result.phoneNumberId).toBe('pn-012');
96+
expect(result.customer?.number).toBe('+15551234567');
97+
});
98+
99+
it('should handle calls without messages gracefully', () => {
100+
const callWithoutMessages: Vapi.Call = {
101+
id: 'call-no-messages',
102+
orgId: 'org-456',
103+
createdAt: '2025-01-15T10:00:00.000Z',
104+
updatedAt: '2025-01-15T10:00:00.000Z',
105+
status: 'queued',
106+
};
107+
108+
const result = transformCallOutput(callWithoutMessages);
109+
110+
expect(result.id).toBe('call-no-messages');
111+
expect(result.messages).toBeUndefined();
112+
});
113+
});

src/schemas/index.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,18 +287,54 @@ export const CallInputSchema = z.object({
287287
.describe('Overrides for the assistant configuration'),
288288
});
289289

290+
// Message schema for call transcript messages
291+
const CallMessageSchema = z.object({
292+
role: z.string(),
293+
message: z.string().optional(),
294+
time: z.number().optional(),
295+
secondsFromStart: z.number().optional(),
296+
content: z.string().optional(),
297+
}).passthrough();
298+
299+
// Cost breakdown schema
300+
const CostBreakdownSchema = z.object({
301+
stt: z.number().optional(),
302+
llm: z.number().optional(),
303+
tts: z.number().optional(),
304+
vapi: z.number().optional(),
305+
total: z.number().optional(),
306+
}).passthrough();
307+
290308
export const CallOutputSchema = BaseResponseSchema.extend({
291309
status: z.string(),
292310
endedReason: z.string().optional(),
293311
assistantId: z.string().optional(),
294312
phoneNumberId: z.string().optional(),
295313
customer: z
296314
.object({
297-
number: z.string(),
315+
number: z.string().optional(),
298316
})
317+
.passthrough()
299318
.optional(),
300319
scheduledAt: z.string().optional(),
301-
});
320+
// Full transcript fields
321+
messages: z.array(CallMessageSchema).optional(),
322+
transcript: z.string().optional(),
323+
// Recording URLs
324+
recordingUrl: z.string().optional(),
325+
stereoRecordingUrl: z.string().optional(),
326+
// Cost information
327+
cost: z.number().optional(),
328+
costBreakdown: CostBreakdownSchema.optional(),
329+
// Analysis and artifact data
330+
analysis: z.any().optional(),
331+
artifact: z.any().optional(),
332+
// Additional call metadata
333+
startedAt: z.string().optional(),
334+
endedAt: z.string().optional(),
335+
type: z.string().optional(),
336+
// Allow any additional fields from the API
337+
}).passthrough();
302338

303339
export const GetCallInputSchema = z.object({
304340
callId: z.string().describe('ID of the call to get'),

src/transformers/index.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,21 +211,17 @@ export function transformCallInput(
211211
export function transformCallOutput(
212212
call: Vapi.Call
213213
): z.infer<typeof CallOutputSchema> {
214+
// Return the full call object to include all fields from the API response,
215+
// including messages (transcript), cost, recording URLs, etc.
216+
// Using type assertion since Vapi.Call contains all the fields we need
217+
// and we want to pass through the complete response.
214218
return {
215-
id: call.id,
216-
createdAt: call.createdAt,
217-
updatedAt: call.updatedAt,
219+
...call,
220+
// Ensure required fields have default values
218221
status: call.status || '',
219-
endedReason: call.endedReason,
220-
assistantId: call.assistantId,
221-
phoneNumberId: call.phoneNumberId,
222-
customer: call.customer
223-
? {
224-
number: call.customer.number || '',
225-
}
226-
: undefined,
222+
// Map schedulePlan.earliestAt to scheduledAt for backward compatibility
227223
scheduledAt: call.schedulePlan?.earliestAt,
228-
};
224+
} as z.infer<typeof CallOutputSchema>;
229225
}
230226

231227
// ===== Phone Number Transformers =====

0 commit comments

Comments
 (0)