@kernl-sdk/openai 0.1.0

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.
@@ -0,0 +1,592 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import {
4
+ TURN_DETECTION,
5
+ SESSION_CONFIG,
6
+ ITEM,
7
+ CLIENT_EVENT,
8
+ SERVER_EVENT,
9
+ } from "../event";
10
+
11
+ describe("TURN_DETECTION codec", () => {
12
+ describe("encode", () => {
13
+ it("should encode server_vad mode", () => {
14
+ const result = TURN_DETECTION.encode({
15
+ mode: "server_vad",
16
+ threshold: 0.5,
17
+ silenceDurationMs: 500,
18
+ prefixPaddingMs: 300,
19
+ createResponse: true,
20
+ interruptResponse: true,
21
+ });
22
+
23
+ expect(result).toEqual({
24
+ type: "server_vad",
25
+ threshold: 0.5,
26
+ silence_duration_ms: 500,
27
+ prefix_padding_ms: 300,
28
+ create_response: true,
29
+ interrupt_response: true,
30
+ });
31
+ });
32
+
33
+ it("should encode manual mode as none", () => {
34
+ const result = TURN_DETECTION.encode({ mode: "manual" });
35
+
36
+ expect(result).toEqual({
37
+ type: "none",
38
+ threshold: undefined,
39
+ silence_duration_ms: undefined,
40
+ prefix_padding_ms: undefined,
41
+ create_response: undefined,
42
+ interrupt_response: undefined,
43
+ });
44
+ });
45
+ });
46
+
47
+ describe("decode", () => {
48
+ it("should decode server_vad type", () => {
49
+ const result = TURN_DETECTION.decode({
50
+ type: "server_vad",
51
+ threshold: 0.5,
52
+ silence_duration_ms: 500,
53
+ prefix_padding_ms: 300,
54
+ create_response: true,
55
+ interrupt_response: true,
56
+ });
57
+
58
+ expect(result).toEqual({
59
+ mode: "server_vad",
60
+ threshold: 0.5,
61
+ silenceDurationMs: 500,
62
+ prefixPaddingMs: 300,
63
+ createResponse: true,
64
+ interruptResponse: true,
65
+ });
66
+ });
67
+
68
+ it("should decode none type as manual", () => {
69
+ const result = TURN_DETECTION.decode({ type: "none" });
70
+
71
+ expect(result).toEqual({
72
+ mode: "manual",
73
+ threshold: undefined,
74
+ silenceDurationMs: undefined,
75
+ prefixPaddingMs: undefined,
76
+ createResponse: undefined,
77
+ interruptResponse: undefined,
78
+ });
79
+ });
80
+ });
81
+ });
82
+
83
+ describe("SESSION_CONFIG codec", () => {
84
+ describe("encode", () => {
85
+ it("should encode basic session config", () => {
86
+ const result = SESSION_CONFIG.encode({
87
+ instructions: "You are a helpful assistant",
88
+ modalities: ["text", "audio"],
89
+ voice: { voiceId: "alloy" },
90
+ });
91
+
92
+ expect(result).toEqual({
93
+ instructions: "You are a helpful assistant",
94
+ modalities: ["text", "audio"],
95
+ voice: "alloy",
96
+ input_audio_format: undefined,
97
+ output_audio_format: undefined,
98
+ turn_detection: undefined,
99
+ tools: undefined,
100
+ });
101
+ });
102
+
103
+ it("should encode session config with tools", () => {
104
+ const result = SESSION_CONFIG.encode({
105
+ tools: [
106
+ {
107
+ kind: "function",
108
+ name: "get_weather",
109
+ description: "Get weather",
110
+ parameters: { type: "object", properties: {} },
111
+ },
112
+ ],
113
+ });
114
+
115
+ expect(result.tools).toEqual([
116
+ {
117
+ type: "function",
118
+ name: "get_weather",
119
+ description: "Get weather",
120
+ parameters: { type: "object", properties: {} },
121
+ },
122
+ ]);
123
+ });
124
+
125
+ it("should encode session config with turn detection", () => {
126
+ const result = SESSION_CONFIG.encode({
127
+ turnDetection: {
128
+ mode: "server_vad",
129
+ threshold: 0.5,
130
+ },
131
+ });
132
+
133
+ expect(result.turn_detection).toEqual({
134
+ type: "server_vad",
135
+ threshold: 0.5,
136
+ silence_duration_ms: undefined,
137
+ prefix_padding_ms: undefined,
138
+ create_response: undefined,
139
+ interrupt_response: undefined,
140
+ });
141
+ });
142
+ });
143
+
144
+ describe("decode", () => {
145
+ it("should decode basic session config", () => {
146
+ const result = SESSION_CONFIG.decode({
147
+ instructions: "You are a helpful assistant",
148
+ modalities: ["text", "audio"],
149
+ voice: "alloy",
150
+ });
151
+
152
+ expect(result).toEqual({
153
+ instructions: "You are a helpful assistant",
154
+ modalities: ["text", "audio"],
155
+ voice: { voiceId: "alloy" },
156
+ turnDetection: undefined,
157
+ });
158
+ });
159
+ });
160
+ });
161
+
162
+ describe("ITEM codec", () => {
163
+ describe("encode", () => {
164
+ it("should encode user message with text", () => {
165
+ const result = ITEM.encode({
166
+ kind: "message",
167
+ id: "msg-1",
168
+ role: "user",
169
+ content: [{ kind: "text", text: "Hello" }],
170
+ });
171
+
172
+ expect(result).toEqual({
173
+ type: "message",
174
+ role: "user",
175
+ content: [{ type: "input_text", text: "Hello" }],
176
+ });
177
+ });
178
+
179
+ it("should encode assistant message with output_text", () => {
180
+ const result = ITEM.encode({
181
+ kind: "message",
182
+ id: "msg-2",
183
+ role: "assistant",
184
+ content: [{ kind: "text", text: "Hi there" }],
185
+ });
186
+
187
+ expect(result).toEqual({
188
+ type: "message",
189
+ role: "assistant",
190
+ content: [{ type: "output_text", text: "Hi there" }],
191
+ });
192
+ });
193
+
194
+ it("should encode tool call", () => {
195
+ const result = ITEM.encode({
196
+ kind: "tool-call",
197
+ callId: "call-1",
198
+ toolId: "get_weather",
199
+ state: "completed",
200
+ arguments: '{"city": "NYC"}',
201
+ });
202
+
203
+ expect(result).toEqual({
204
+ type: "function_call",
205
+ call_id: "call-1",
206
+ name: "get_weather",
207
+ arguments: '{"city": "NYC"}',
208
+ });
209
+ });
210
+
211
+ it("should encode tool result", () => {
212
+ const result = ITEM.encode({
213
+ kind: "tool-result",
214
+ callId: "call-1",
215
+ toolId: "get_weather",
216
+ state: "completed",
217
+ result: { temp: 72 },
218
+ error: null,
219
+ });
220
+
221
+ expect(result).toEqual({
222
+ type: "function_call_output",
223
+ call_id: "call-1",
224
+ output: '{"temp":72}',
225
+ });
226
+ });
227
+
228
+ it("should encode tool result with error", () => {
229
+ const result = ITEM.encode({
230
+ kind: "tool-result",
231
+ callId: "call-1",
232
+ toolId: "get_weather",
233
+ state: "completed",
234
+ result: null,
235
+ error: "City not found",
236
+ });
237
+
238
+ expect(result).toEqual({
239
+ type: "function_call_output",
240
+ call_id: "call-1",
241
+ output: "City not found",
242
+ });
243
+ });
244
+ });
245
+
246
+ describe("decode", () => {
247
+ it("should decode message item", () => {
248
+ const result = ITEM.decode({
249
+ type: "message",
250
+ role: "user",
251
+ content: [{ type: "input_text", text: "Hello" }],
252
+ });
253
+
254
+ expect(result.kind).toBe("message");
255
+ if (result.kind === "message") {
256
+ expect(result.role).toBe("user");
257
+ expect(result.content).toEqual([{ kind: "text", text: "Hello" }]);
258
+ }
259
+ });
260
+
261
+ it("should decode function call item", () => {
262
+ const result = ITEM.decode({
263
+ type: "function_call",
264
+ call_id: "call-1",
265
+ name: "get_weather",
266
+ arguments: '{"city": "NYC"}',
267
+ });
268
+
269
+ expect(result).toEqual({
270
+ kind: "tool-call",
271
+ callId: "call-1",
272
+ toolId: "get_weather",
273
+ state: "completed",
274
+ arguments: '{"city": "NYC"}',
275
+ });
276
+ });
277
+
278
+ it("should decode function call output item", () => {
279
+ const result = ITEM.decode({
280
+ type: "function_call_output",
281
+ call_id: "call-1",
282
+ output: '{"temp": 72}',
283
+ });
284
+
285
+ expect(result).toEqual({
286
+ kind: "tool-result",
287
+ callId: "call-1",
288
+ toolId: "",
289
+ state: "completed",
290
+ result: '{"temp": 72}',
291
+ error: null,
292
+ });
293
+ });
294
+ });
295
+ });
296
+
297
+ describe("CLIENT_EVENT codec", () => {
298
+ describe("encode", () => {
299
+ it("should encode session.update", () => {
300
+ const result = CLIENT_EVENT.encode({
301
+ kind: "session.update",
302
+ config: { instructions: "Be helpful" },
303
+ });
304
+
305
+ expect(result).toEqual({
306
+ type: "session.update",
307
+ session: {
308
+ instructions: "Be helpful",
309
+ modalities: undefined,
310
+ voice: undefined,
311
+ input_audio_format: undefined,
312
+ output_audio_format: undefined,
313
+ turn_detection: undefined,
314
+ tools: undefined,
315
+ },
316
+ });
317
+ });
318
+
319
+ it("should encode audio.input.append", () => {
320
+ const result = CLIENT_EVENT.encode({
321
+ kind: "audio.input.append",
322
+ audio: "base64data",
323
+ });
324
+
325
+ expect(result).toEqual({
326
+ type: "input_audio_buffer.append",
327
+ audio: "base64data",
328
+ });
329
+ });
330
+
331
+ it("should encode audio.input.commit", () => {
332
+ const result = CLIENT_EVENT.encode({ kind: "audio.input.commit" });
333
+ expect(result).toEqual({ type: "input_audio_buffer.commit" });
334
+ });
335
+
336
+ it("should encode audio.input.clear", () => {
337
+ const result = CLIENT_EVENT.encode({ kind: "audio.input.clear" });
338
+ expect(result).toEqual({ type: "input_audio_buffer.clear" });
339
+ });
340
+
341
+ it("should encode response.create", () => {
342
+ const result = CLIENT_EVENT.encode({
343
+ kind: "response.create",
344
+ config: { instructions: "Override", modalities: ["text"] },
345
+ });
346
+
347
+ expect(result).toEqual({
348
+ type: "response.create",
349
+ response: { instructions: "Override", modalities: ["text"] },
350
+ });
351
+ });
352
+
353
+ it("should encode response.cancel", () => {
354
+ const result = CLIENT_EVENT.encode({
355
+ kind: "response.cancel",
356
+ responseId: "resp-1",
357
+ });
358
+
359
+ expect(result).toEqual({
360
+ type: "response.cancel",
361
+ response_id: "resp-1",
362
+ });
363
+ });
364
+
365
+ it("should encode tool.result", () => {
366
+ const result = CLIENT_EVENT.encode({
367
+ kind: "tool.result",
368
+ callId: "call-1",
369
+ result: '{"temp": 72}',
370
+ });
371
+
372
+ expect(result).toEqual({
373
+ type: "conversation.item.create",
374
+ item: {
375
+ type: "function_call_output",
376
+ call_id: "call-1",
377
+ output: '{"temp": 72}',
378
+ },
379
+ });
380
+ });
381
+
382
+ it("should return null for activity events", () => {
383
+ expect(CLIENT_EVENT.encode({ kind: "activity.start" })).toBeNull();
384
+ expect(CLIENT_EVENT.encode({ kind: "activity.end" })).toBeNull();
385
+ });
386
+ });
387
+ });
388
+
389
+ describe("SERVER_EVENT codec", () => {
390
+ describe("decode", () => {
391
+ it("should decode session.created", () => {
392
+ const result = SERVER_EVENT.decode({
393
+ type: "session.created",
394
+ session: { id: "sess-1", instructions: "Be helpful" },
395
+ });
396
+
397
+ expect(result).toEqual({
398
+ kind: "session.created",
399
+ session: {
400
+ id: "sess-1",
401
+ config: {
402
+ instructions: "Be helpful",
403
+ modalities: undefined,
404
+ voice: undefined,
405
+ turnDetection: undefined,
406
+ },
407
+ },
408
+ });
409
+ });
410
+
411
+ it("should decode error", () => {
412
+ const result = SERVER_EVENT.decode({
413
+ type: "error",
414
+ error: { code: "invalid_request", message: "Bad request" },
415
+ });
416
+
417
+ expect(result).toEqual({
418
+ kind: "session.error",
419
+ error: { code: "invalid_request", message: "Bad request" },
420
+ });
421
+ });
422
+
423
+ it("should decode response.output_audio.delta", () => {
424
+ const result = SERVER_EVENT.decode({
425
+ type: "response.output_audio.delta",
426
+ response_id: "resp-1",
427
+ item_id: "item-1",
428
+ content_index: 0,
429
+ delta: "base64audio",
430
+ });
431
+
432
+ expect(result).toEqual({
433
+ kind: "audio.output.delta",
434
+ responseId: "resp-1",
435
+ itemId: "item-1",
436
+ audio: "base64audio",
437
+ });
438
+ });
439
+
440
+ it("should decode response.output_audio.done", () => {
441
+ const result = SERVER_EVENT.decode({
442
+ type: "response.output_audio.done",
443
+ response_id: "resp-1",
444
+ item_id: "item-1",
445
+ content_index: 0,
446
+ });
447
+
448
+ expect(result).toEqual({
449
+ kind: "audio.output.done",
450
+ responseId: "resp-1",
451
+ itemId: "item-1",
452
+ });
453
+ });
454
+
455
+ it("should decode response.text.delta", () => {
456
+ const result = SERVER_EVENT.decode({
457
+ type: "response.text.delta",
458
+ response_id: "resp-1",
459
+ item_id: "item-1",
460
+ content_index: 0,
461
+ delta: "Hello",
462
+ });
463
+
464
+ expect(result).toEqual({
465
+ kind: "text.output.delta",
466
+ responseId: "resp-1",
467
+ itemId: "item-1",
468
+ delta: "Hello",
469
+ });
470
+ });
471
+
472
+ it("should decode response.text.done as text.output", () => {
473
+ const result = SERVER_EVENT.decode({
474
+ type: "response.text.done",
475
+ response_id: "resp-1",
476
+ item_id: "item-1",
477
+ content_index: 0,
478
+ text: "Hello world",
479
+ });
480
+
481
+ expect(result).toEqual({
482
+ kind: "text.output",
483
+ responseId: "resp-1",
484
+ itemId: "item-1",
485
+ text: "Hello world",
486
+ });
487
+ });
488
+
489
+ it("should decode input transcription completed as transcript.input", () => {
490
+ const result = SERVER_EVENT.decode({
491
+ type: "conversation.item.input_audio_transcription.completed",
492
+ item_id: "item-1",
493
+ content_index: 0,
494
+ transcript: "Hello there",
495
+ });
496
+
497
+ expect(result).toEqual({
498
+ kind: "transcript.input",
499
+ itemId: "item-1",
500
+ text: "Hello there",
501
+ });
502
+ });
503
+
504
+ it("should decode output audio transcript done as transcript.output", () => {
505
+ const result = SERVER_EVENT.decode({
506
+ type: "response.output_audio_transcript.done",
507
+ response_id: "resp-1",
508
+ item_id: "item-1",
509
+ content_index: 0,
510
+ transcript: "Hi there",
511
+ });
512
+
513
+ expect(result).toEqual({
514
+ kind: "transcript.output",
515
+ responseId: "resp-1",
516
+ itemId: "item-1",
517
+ text: "Hi there",
518
+ });
519
+ });
520
+
521
+ it("should decode response.done with completed status", () => {
522
+ const result = SERVER_EVENT.decode({
523
+ type: "response.done",
524
+ response: {
525
+ id: "resp-1",
526
+ status: "completed",
527
+ usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 },
528
+ },
529
+ });
530
+
531
+ expect(result).toEqual({
532
+ kind: "response.done",
533
+ responseId: "resp-1",
534
+ status: "completed",
535
+ usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
536
+ });
537
+ });
538
+
539
+ it("should decode response.done with in_progress status as failed", () => {
540
+ const result = SERVER_EVENT.decode({
541
+ type: "response.done",
542
+ response: { id: "resp-1", status: "in_progress" },
543
+ });
544
+
545
+ expect(result?.kind).toBe("response.done");
546
+ if (result?.kind === "response.done") {
547
+ expect(result.status).toBe("failed");
548
+ }
549
+ });
550
+
551
+ it("should decode function call arguments done as tool.call", () => {
552
+ const result = SERVER_EVENT.decode({
553
+ type: "response.function_call_arguments.done",
554
+ response_id: "resp-1",
555
+ item_id: "item-1",
556
+ call_id: "call-1",
557
+ name: "get_weather",
558
+ arguments: '{"city": "NYC"}',
559
+ });
560
+
561
+ expect(result).toEqual({
562
+ kind: "tool.call",
563
+ callId: "call-1",
564
+ toolId: "get_weather",
565
+ arguments: '{"city": "NYC"}',
566
+ });
567
+ });
568
+
569
+ it("should decode conversation.item.created as item.created", () => {
570
+ const result = SERVER_EVENT.decode({
571
+ type: "conversation.item.created",
572
+ item: {
573
+ type: "message",
574
+ role: "user",
575
+ content: [{ type: "input_text", text: "Hi" }],
576
+ },
577
+ previous_item_id: "prev-1",
578
+ });
579
+
580
+ expect(result?.kind).toBe("item.created");
581
+ });
582
+
583
+ it("should return null for conversation.item.done", () => {
584
+ const result = SERVER_EVENT.decode({
585
+ type: "conversation.item.done",
586
+ item: { type: "message", role: "user", content: [] },
587
+ });
588
+
589
+ expect(result).toBeNull();
590
+ });
591
+ });
592
+ });