@kuralle-syrinx/grok 4.4.1 → 4.5.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,320 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { WebSocketServer, type WebSocket } from "ws";
5
+ import {
6
+ PipelineBusImpl,
7
+ Route,
8
+ type TextToSpeechAudioPacket,
9
+ type TextToSpeechEndPacket,
10
+ type UsageRecordedPacket,
11
+ } from "@kuralle-syrinx/core";
12
+
13
+ import { bytesToBase64 } from "@kuralle-syrinx/realtime";
14
+ import { GrokTTSPlugin } from "./tts.js";
15
+
16
+ let servers: WebSocketServer[] = [];
17
+
18
+ afterEach(async () => {
19
+ await Promise.all(
20
+ servers.splice(0).map(
21
+ (server) =>
22
+ new Promise<void>((resolve) => {
23
+ for (const client of server.clients) client.terminate();
24
+ server.close(() => resolve());
25
+ }),
26
+ ),
27
+ );
28
+ });
29
+
30
+ async function createLocalServer(
31
+ onConnection: (socket: WebSocket, requestUrl: string, authHeader: string) => void,
32
+ ): Promise<string> {
33
+ const server = await new Promise<WebSocketServer>((resolve) => {
34
+ let next: WebSocketServer;
35
+ next = new WebSocketServer({ port: 0 }, () => resolve(next));
36
+ });
37
+ servers.push(server);
38
+ server.on("connection", (socket, request) => {
39
+ const header = request.headers["authorization"];
40
+ onConnection(
41
+ socket,
42
+ request.url ?? "",
43
+ Array.isArray(header) ? header[0] ?? "" : header ?? "",
44
+ );
45
+ });
46
+ const address = server.address();
47
+ if (!address || typeof address === "string") throw new Error("Expected TCP server address");
48
+ return `ws://127.0.0.1:${address.port}/v1/tts`;
49
+ }
50
+
51
+ async function waitForCondition(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
52
+ const startedAt = Date.now();
53
+ while (Date.now() - startedAt < timeoutMs) {
54
+ if (predicate()) return;
55
+ await new Promise((resolve) => setTimeout(resolve, 10));
56
+ }
57
+ throw new Error("Timed out waiting for Grok TTS test condition");
58
+ }
59
+
60
+ describe("GrokTTSPlugin", () => {
61
+ it("streams text.delta and maps audio.delta to tts.audio", async () => {
62
+ const received: Array<Record<string, unknown>> = [];
63
+ const endpointUrl = await createLocalServer((socket, requestUrl, authHeader) => {
64
+ expect(requestUrl).toContain("voice=eve");
65
+ expect(requestUrl).toContain("codec=pcm");
66
+ expect(requestUrl).toContain("sample_rate=16000");
67
+ expect(authHeader).toBe("Bearer test-xai-key");
68
+ socket.on("message", (data) => {
69
+ const msg = JSON.parse(data.toString()) as Record<string, unknown>;
70
+ received.push(msg);
71
+ if (msg["type"] === "text.delta") {
72
+ socket.send(JSON.stringify({
73
+ type: "audio.delta",
74
+ delta: bytesToBase64(new Uint8Array([1, 2, 3, 4])),
75
+ }));
76
+ }
77
+ if (msg["type"] === "text.done") {
78
+ socket.send(JSON.stringify({ type: "audio.done" }));
79
+ }
80
+ });
81
+ });
82
+
83
+ const bus = new PipelineBusImpl();
84
+ const started = bus.start();
85
+ const plugin = new GrokTTSPlugin();
86
+ const audio: TextToSpeechAudioPacket[] = [];
87
+ const ends: TextToSpeechEndPacket[] = [];
88
+ bus.on("tts.audio", (pkt) => {
89
+ audio.push(pkt as TextToSpeechAudioPacket);
90
+ });
91
+ bus.on("tts.end", (pkt) => {
92
+ ends.push(pkt as TextToSpeechEndPacket);
93
+ });
94
+
95
+ await plugin.initialize(bus, {
96
+ api_key: "test-xai-key",
97
+ endpoint_url: endpointUrl,
98
+ voice_id: "eve",
99
+ sample_rate: 16000,
100
+ });
101
+ bus.push(Route.Main, {
102
+ kind: "tts.text",
103
+ contextId: "turn-1",
104
+ timestampMs: Date.now(),
105
+ text: "Hello there.",
106
+ });
107
+ bus.push(Route.Main, { kind: "tts.done", contextId: "turn-1", timestampMs: Date.now() });
108
+ await waitForCondition(() => ends.length >= 1);
109
+
110
+ expect(received).toEqual([
111
+ expect.objectContaining({ type: "text.delta", delta: "Hello there." }),
112
+ expect.objectContaining({ type: "text.done" }),
113
+ ]);
114
+ expect(audio).toEqual([
115
+ expect.objectContaining({
116
+ contextId: "turn-1",
117
+ sampleRateHz: 16000,
118
+ audio: new Uint8Array([1, 2, 3, 4]),
119
+ provider: expect.objectContaining({ name: "grok", model: "eve" }),
120
+ }),
121
+ ]);
122
+ expect(ends).toEqual([expect.objectContaining({ contextId: "turn-1" })]);
123
+
124
+ await plugin.close();
125
+ bus.stop();
126
+ await started;
127
+ });
128
+
129
+ it("honors codec and bit_rate overrides on the TTS websocket URL", async () => {
130
+ let requestUrl = "";
131
+ const endpointUrl = await createLocalServer((socket, url) => {
132
+ requestUrl = url;
133
+ socket.on("message", (data) => {
134
+ const msg = JSON.parse(data.toString()) as Record<string, unknown>;
135
+ if (msg["type"] === "text.delta") {
136
+ socket.send(JSON.stringify({
137
+ type: "audio.delta",
138
+ delta: bytesToBase64(new Uint8Array([1, 2, 3, 4])),
139
+ }));
140
+ }
141
+ if (msg["type"] === "text.done") {
142
+ socket.send(JSON.stringify({ type: "audio.done" }));
143
+ }
144
+ });
145
+ });
146
+
147
+ const bus = new PipelineBusImpl();
148
+ const started = bus.start();
149
+ const plugin = new GrokTTSPlugin();
150
+ const ends: TextToSpeechEndPacket[] = [];
151
+ bus.on("tts.end", (pkt) => {
152
+ ends.push(pkt as TextToSpeechEndPacket);
153
+ });
154
+
155
+ await plugin.initialize(bus, {
156
+ api_key: "test-xai-key",
157
+ endpoint_url: endpointUrl,
158
+ voice_id: "ara",
159
+ language: "es",
160
+ codec: "mp3",
161
+ bit_rate: 128000,
162
+ sample_rate: 24000,
163
+ });
164
+ bus.push(Route.Main, {
165
+ kind: "tts.text",
166
+ contextId: "turn-cfg",
167
+ timestampMs: Date.now(),
168
+ text: "Hola.",
169
+ });
170
+ bus.push(Route.Main, { kind: "tts.done", contextId: "turn-cfg", timestampMs: Date.now() });
171
+ await waitForCondition(() => ends.length >= 1 || requestUrl.length > 0);
172
+
173
+ expect(requestUrl).toContain("voice=ara");
174
+ expect(requestUrl).toContain("language=es");
175
+ expect(requestUrl).toContain("codec=mp3");
176
+ expect(requestUrl).toContain("sample_rate=24000");
177
+ expect(requestUrl).toContain("bit_rate=128000");
178
+
179
+ await plugin.close();
180
+ bus.stop();
181
+ await started;
182
+ });
183
+
184
+ it("sends text.clear on interrupt", async () => {
185
+ const received: Array<Record<string, unknown>> = [];
186
+ const endpointUrl = await createLocalServer((socket) => {
187
+ socket.on("message", (data) => {
188
+ received.push(JSON.parse(data.toString()) as Record<string, unknown>);
189
+ });
190
+ });
191
+
192
+ const bus = new PipelineBusImpl();
193
+ const started = bus.start();
194
+ const plugin = new GrokTTSPlugin();
195
+
196
+ await plugin.initialize(bus, { api_key: "test-xai-key", endpoint_url: endpointUrl });
197
+ bus.push(Route.Main, {
198
+ kind: "tts.text",
199
+ contextId: "turn-x",
200
+ timestampMs: Date.now(),
201
+ text: "Interrupt me.",
202
+ });
203
+ await waitForCondition(() => received.some((m) => m["type"] === "text.delta"));
204
+ bus.push(Route.Critical, { kind: "interrupt.tts", contextId: "turn-x", timestampMs: Date.now() });
205
+ await waitForCondition(() => received.some((m) => m["type"] === "text.clear"));
206
+
207
+ expect(received).toEqual([
208
+ expect.objectContaining({ type: "text.delta", delta: "Interrupt me." }),
209
+ expect.objectContaining({ type: "text.clear" }),
210
+ ]);
211
+
212
+ await plugin.close();
213
+ bus.stop();
214
+ await started;
215
+ });
216
+
217
+ it("emits usage.recorded with stage tts and characters equal to synthesized text length", async () => {
218
+ const text = "Hello there.";
219
+ const endpointUrl = await createLocalServer((socket) => {
220
+ socket.on("message", (data) => {
221
+ const msg = JSON.parse(data.toString()) as Record<string, unknown>;
222
+ if (msg["type"] === "text.delta") {
223
+ socket.send(JSON.stringify({
224
+ type: "audio.delta",
225
+ delta: bytesToBase64(new Uint8Array([1, 2, 3, 4])),
226
+ }));
227
+ }
228
+ if (msg["type"] === "text.done") {
229
+ socket.send(JSON.stringify({ type: "audio.done" }));
230
+ }
231
+ });
232
+ });
233
+
234
+ const bus = new PipelineBusImpl();
235
+ const started = bus.start();
236
+ const plugin = new GrokTTSPlugin();
237
+ const ends: TextToSpeechEndPacket[] = [];
238
+ const usage: UsageRecordedPacket[] = [];
239
+ bus.on("tts.end", (pkt) => {
240
+ ends.push(pkt as TextToSpeechEndPacket);
241
+ });
242
+ bus.on("usage.recorded", (pkt) => {
243
+ usage.push(pkt as UsageRecordedPacket);
244
+ });
245
+
246
+ await plugin.initialize(bus, {
247
+ api_key: "test-xai-key",
248
+ endpoint_url: endpointUrl,
249
+ voice_id: "eve",
250
+ sample_rate: 16000,
251
+ });
252
+ bus.push(Route.Main, {
253
+ kind: "tts.text",
254
+ contextId: "turn-usage",
255
+ timestampMs: Date.now(),
256
+ text,
257
+ });
258
+ bus.push(Route.Main, { kind: "tts.done", contextId: "turn-usage", timestampMs: Date.now() });
259
+ await waitForCondition(() => ends.length >= 1 && usage.length >= 1);
260
+
261
+ expect(usage).toEqual([
262
+ expect.objectContaining({
263
+ kind: "usage.recorded",
264
+ contextId: "turn-usage",
265
+ stage: "tts",
266
+ provider: "grok",
267
+ model: "eve",
268
+ characters: text.length,
269
+ }),
270
+ ]);
271
+
272
+ await plugin.close();
273
+ bus.stop();
274
+ await started;
275
+ });
276
+
277
+ it("does not emit usage.recorded for a cancelled TTS turn", async () => {
278
+ const received: Array<Record<string, unknown>> = [];
279
+ const endpointUrl = await createLocalServer((socket) => {
280
+ socket.on("message", (data) => {
281
+ const msg = JSON.parse(data.toString()) as Record<string, unknown>;
282
+ received.push(msg);
283
+ if (msg["type"] === "text.clear") {
284
+ // Late done after cancel must not bill.
285
+ socket.send(JSON.stringify({ type: "audio.done" }));
286
+ }
287
+ });
288
+ });
289
+
290
+ const bus = new PipelineBusImpl();
291
+ const started = bus.start();
292
+ const plugin = new GrokTTSPlugin();
293
+ const usage: UsageRecordedPacket[] = [];
294
+ bus.on("usage.recorded", (pkt) => {
295
+ usage.push(pkt as UsageRecordedPacket);
296
+ });
297
+
298
+ await plugin.initialize(bus, { api_key: "test-xai-key", endpoint_url: endpointUrl, voice_id: "eve" });
299
+ bus.push(Route.Main, {
300
+ kind: "tts.text",
301
+ contextId: "turn-cancel",
302
+ timestampMs: Date.now(),
303
+ text: "This will be cancelled.",
304
+ });
305
+ await waitForCondition(() => received.some((m) => m["type"] === "text.delta"));
306
+ bus.push(Route.Critical, {
307
+ kind: "interrupt.tts",
308
+ contextId: "turn-cancel",
309
+ timestampMs: Date.now(),
310
+ });
311
+ await waitForCondition(() => received.some((m) => m["type"] === "text.clear"));
312
+ await new Promise((resolve) => setTimeout(resolve, 40));
313
+
314
+ expect(usage).toEqual([]);
315
+
316
+ await plugin.close();
317
+ bus.stop();
318
+ await started;
319
+ });
320
+ });