@alexkroman1/aai 1.7.1 → 1.8.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.
Files changed (133) hide show
  1. package/.turbo/turbo-build.log +11 -9
  2. package/CHANGELOG.md +10 -0
  3. package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
  4. package/dist/constants-y68COEGj.js +29 -0
  5. package/dist/host/_base64.d.ts +2 -0
  6. package/dist/host/_mock-ws.d.ts +0 -61
  7. package/dist/host/_pipeline-test-fakes.d.ts +7 -4
  8. package/dist/host/_run-code.d.ts +0 -25
  9. package/dist/host/_runtime-conformance.d.ts +3 -34
  10. package/dist/host/memory-vector.d.ts +0 -11
  11. package/dist/host/providers/resolve-kv.d.ts +0 -7
  12. package/dist/host/providers/resolve-vector.d.ts +0 -8
  13. package/dist/host/providers/stt/assemblyai.d.ts +0 -14
  14. package/dist/host/providers/stt/deepgram.d.ts +2 -14
  15. package/dist/host/providers/stt/soniox.d.ts +0 -22
  16. package/dist/host/providers/tts/rime.d.ts +10 -31
  17. package/dist/host/runtime-barrel.js +619 -630
  18. package/dist/host/runtime-config.d.ts +9 -6
  19. package/dist/host/runtime.d.ts +3 -0
  20. package/dist/host/to-vercel-tools.d.ts +3 -33
  21. package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
  22. package/dist/host/unstorage-kv.d.ts +0 -26
  23. package/dist/index.js +3 -3
  24. package/dist/openai-realtime-cjPAHMMx.js +10 -0
  25. package/dist/sdk/_internal-types.d.ts +6 -55
  26. package/dist/sdk/allowed-hosts.d.ts +4 -3
  27. package/dist/sdk/constants.d.ts +4 -29
  28. package/dist/sdk/define.d.ts +7 -4
  29. package/dist/sdk/kv.d.ts +13 -37
  30. package/dist/sdk/manifest-barrel.js +1 -1
  31. package/dist/sdk/manifest.d.ts +8 -2
  32. package/dist/sdk/protocol.js +1 -1
  33. package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
  34. package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
  35. package/dist/sdk/providers/s2s-barrel.js +2 -0
  36. package/dist/sdk/providers/tts/rime.d.ts +1 -1
  37. package/dist/sdk/providers.d.ts +6 -2
  38. package/dist/sdk/types.d.ts +7 -1
  39. package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
  40. package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
  41. package/host/_base64.ts +9 -0
  42. package/host/_mock-ws.ts +0 -65
  43. package/host/_pipeline-test-fakes.ts +19 -31
  44. package/host/_run-code.ts +10 -53
  45. package/host/_runtime-conformance.ts +3 -44
  46. package/host/_test-utils.ts +20 -42
  47. package/host/builtin-tools.test.ts +127 -222
  48. package/host/builtin-tools.ts +6 -10
  49. package/host/cleanup.test.ts +30 -73
  50. package/host/integration/pipeline-reference.integration.test.ts +12 -17
  51. package/host/integration.test.ts +0 -7
  52. package/host/memory-vector.test.ts +3 -1
  53. package/host/memory-vector.ts +16 -21
  54. package/host/pinecone-vector.test.ts +14 -17
  55. package/host/pinecone-vector.ts +10 -19
  56. package/host/providers/providers.test-d.ts +5 -3
  57. package/host/providers/resolve-kv.ts +23 -41
  58. package/host/providers/resolve-vector.ts +3 -12
  59. package/host/providers/resolve.test.ts +15 -28
  60. package/host/providers/resolve.ts +24 -24
  61. package/host/providers/stt/assemblyai.test.ts +2 -14
  62. package/host/providers/stt/assemblyai.ts +12 -35
  63. package/host/providers/stt/deepgram.test.ts +23 -83
  64. package/host/providers/stt/deepgram.ts +15 -40
  65. package/host/providers/stt/elevenlabs.test.ts +26 -38
  66. package/host/providers/stt/elevenlabs.ts +10 -9
  67. package/host/providers/stt/soniox.test.ts +35 -85
  68. package/host/providers/stt/soniox.ts +8 -53
  69. package/host/providers/tts/cartesia.test.ts +19 -58
  70. package/host/providers/tts/cartesia.ts +36 -66
  71. package/host/providers/tts/rime.test.ts +12 -38
  72. package/host/providers/tts/rime.ts +23 -86
  73. package/host/runtime-config.test.ts +9 -9
  74. package/host/runtime-config.ts +16 -22
  75. package/host/runtime.test.ts +111 -73
  76. package/host/runtime.ts +138 -86
  77. package/host/s2s.test.ts +92 -191
  78. package/host/s2s.ts +55 -49
  79. package/host/server-shutdown.test.ts +9 -30
  80. package/host/server.test.ts +2 -13
  81. package/host/server.ts +85 -100
  82. package/host/session-core.test.ts +15 -30
  83. package/host/session-core.ts +10 -13
  84. package/host/session-prompt.test.ts +1 -5
  85. package/host/to-vercel-tools.test.ts +53 -72
  86. package/host/to-vercel-tools.ts +9 -39
  87. package/host/tool-executor.test.ts +25 -51
  88. package/host/tool-executor.ts +18 -12
  89. package/host/transports/openai-realtime-transport.test.ts +371 -0
  90. package/host/transports/openai-realtime-transport.ts +319 -0
  91. package/host/transports/pipeline-transport.test.ts +125 -298
  92. package/host/transports/pipeline-transport.ts +20 -68
  93. package/host/transports/s2s-transport-fixtures.test.ts +31 -92
  94. package/host/transports/s2s-transport.test.ts +65 -134
  95. package/host/transports/s2s-transport.ts +15 -43
  96. package/host/transports/types.test.ts +4 -8
  97. package/host/unstorage-kv.test.ts +3 -2
  98. package/host/unstorage-kv.ts +5 -35
  99. package/host/ws-handler.test.ts +72 -176
  100. package/host/ws-handler.ts +6 -12
  101. package/package.json +6 -1
  102. package/sdk/__snapshots__/exports.test.ts.snap +7 -0
  103. package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
  104. package/sdk/_internal-types.test.ts +6 -9
  105. package/sdk/_internal-types.ts +16 -57
  106. package/sdk/_test-matchers.ts +25 -15
  107. package/sdk/allowed-hosts.test.ts +50 -114
  108. package/sdk/allowed-hosts.ts +8 -14
  109. package/sdk/constants.ts +5 -52
  110. package/sdk/define.test.ts +7 -6
  111. package/sdk/define.ts +7 -3
  112. package/sdk/exports.test.ts +6 -1
  113. package/sdk/kv.ts +13 -37
  114. package/sdk/manifest.test-d.ts +5 -0
  115. package/sdk/manifest.test.ts +61 -9
  116. package/sdk/manifest.ts +11 -11
  117. package/sdk/protocol-compat.test.ts +66 -98
  118. package/sdk/protocol-snapshot.test.ts +2 -16
  119. package/sdk/protocol.test.ts +13 -22
  120. package/sdk/providers/s2s/openai-realtime.ts +36 -0
  121. package/sdk/providers/s2s-barrel.ts +12 -0
  122. package/sdk/providers/tts/rime.ts +1 -1
  123. package/sdk/providers.ts +24 -5
  124. package/sdk/schema-alignment.test.ts +25 -73
  125. package/sdk/schema-shapes.test.ts +1 -29
  126. package/sdk/system-prompt.test.ts +0 -1
  127. package/sdk/system-prompt.ts +17 -19
  128. package/sdk/types-inference.test.ts +10 -36
  129. package/sdk/types.ts +7 -0
  130. package/sdk/ws-upgrade.test.ts +24 -23
  131. package/sdk/ws-upgrade.ts +2 -3
  132. package/tsdown.config.ts +8 -11
  133. package/dist/constants-C2nirZUI.js +0 -54
@@ -56,17 +56,21 @@ export function resolveApiKey(envVar: string, env: Record<string, string>): stri
56
56
  return env[envVar] ?? process.env[envVar] ?? "";
57
57
  }
58
58
 
59
+ function options<T>(descriptor: { options: Record<string, unknown> }): T {
60
+ return descriptor.options as unknown as T;
61
+ }
62
+
59
63
  /** Resolve an {@link SttProvider} descriptor into a host-side opener. */
60
64
  export function resolveStt(descriptor: SttProvider): SttOpener {
61
65
  switch (descriptor.kind) {
62
66
  case ASSEMBLYAI_KIND:
63
- return openAssemblyAI(descriptor.options as unknown as AssemblyAIOptions);
67
+ return openAssemblyAI(options<AssemblyAIOptions>(descriptor));
64
68
  case DEEPGRAM_KIND:
65
- return openDeepgram(descriptor.options as unknown as DeepgramOptions);
69
+ return openDeepgram(options<DeepgramOptions>(descriptor));
66
70
  case ELEVENLABS_KIND:
67
- return openElevenLabs(descriptor.options as unknown as ElevenLabsOptions);
71
+ return openElevenLabs(options<ElevenLabsOptions>(descriptor));
68
72
  case SONIOX_KIND:
69
- return openSoniox(descriptor.options as unknown as SonioxOptions);
73
+ return openSoniox(options<SonioxOptions>(descriptor));
70
74
  default:
71
75
  throw new Error(
72
76
  `Unknown STT provider kind: "${descriptor.kind}". ` +
@@ -79,9 +83,9 @@ export function resolveStt(descriptor: SttProvider): SttOpener {
79
83
  export function resolveTts(descriptor: TtsProvider): TtsOpener {
80
84
  switch (descriptor.kind) {
81
85
  case CARTESIA_KIND:
82
- return openCartesia(descriptor.options as unknown as CartesiaOptions);
86
+ return openCartesia(options<CartesiaOptions>(descriptor));
83
87
  case RIME_KIND:
84
- return openRime(descriptor.options as unknown as RimeOptions);
88
+ return openRime(options<RimeOptions>(descriptor));
85
89
  default:
86
90
  throw new Error(
87
91
  `Unknown TTS provider kind: "${descriptor.kind}". Supported: ${CARTESIA_KIND}, ${RIME_KIND}.`,
@@ -105,30 +109,28 @@ export function resolveLlm(descriptor: LlmProvider, env: Record<string, string>)
105
109
  // before reading process.env["ANTHROPIC_BASE_URL"]. Without this,
106
110
  // the Deno platform server needs --allow-env to start a session.
107
111
  return createAnthropic({ apiKey, baseURL: "https://api.anthropic.com/v1" })(
108
- (descriptor.options as unknown as AnthropicOptions).model,
112
+ options<AnthropicOptions>(descriptor).model,
109
113
  );
110
114
  }
111
115
  case OPENAI_KIND: {
112
116
  const apiKey = requireKey(env, "OPENAI_API_KEY", "OpenAI");
113
- return createOpenAI({ apiKey })((descriptor.options as unknown as OpenAIOptions).model);
117
+ return createOpenAI({ apiKey })(options<OpenAIOptions>(descriptor).model);
114
118
  }
115
119
  case GOOGLE_KIND: {
116
120
  const apiKey = requireKey(env, "GOOGLE_GENERATIVE_AI_API_KEY", "Google");
117
- return createGoogleGenerativeAI({ apiKey })(
118
- (descriptor.options as unknown as GoogleOptions).model,
119
- );
121
+ return createGoogleGenerativeAI({ apiKey })(options<GoogleOptions>(descriptor).model);
120
122
  }
121
123
  case MISTRAL_KIND: {
122
124
  const apiKey = requireKey(env, "MISTRAL_API_KEY", "Mistral");
123
- return createMistral({ apiKey })((descriptor.options as unknown as MistralOptions).model);
125
+ return createMistral({ apiKey })(options<MistralOptions>(descriptor).model);
124
126
  }
125
127
  case XAI_KIND: {
126
128
  const apiKey = requireKey(env, "XAI_API_KEY", "xAI");
127
- return createXai({ apiKey })((descriptor.options as unknown as XaiOptions).model);
129
+ return createXai({ apiKey })(options<XaiOptions>(descriptor).model);
128
130
  }
129
131
  case GROQ_KIND: {
130
132
  const apiKey = requireKey(env, "GROQ_API_KEY", "Groq");
131
- return createGroq({ apiKey })((descriptor.options as unknown as GroqOptions).model);
133
+ return createGroq({ apiKey })(options<GroqOptions>(descriptor).model);
132
134
  }
133
135
  default:
134
136
  throw new Error(
@@ -151,17 +153,15 @@ export function loadProviderPackage<T>(name: string, label: string): T {
151
153
  try {
152
154
  return requireFromHere(name) as T;
153
155
  } catch (err) {
154
- if (
156
+ const code = (err as NodeJS.ErrnoException | undefined)?.code;
157
+ const isMissing =
155
158
  err instanceof Error &&
156
- ((err as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND" ||
157
- (err as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") &&
158
- err.message.includes(name)
159
- ) {
160
- throw new Error(`${label}: package \`${name}\` is not installed. Run \`pnpm add ${name}\`.`, {
161
- cause: err,
162
- });
163
- }
164
- throw err;
159
+ (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") &&
160
+ err.message.includes(name);
161
+ if (!isMissing) throw err;
162
+ throw new Error(`${label}: package \`${name}\` is not installed. Run \`pnpm add ${name}\`.`, {
163
+ cause: err,
164
+ });
165
165
  }
166
166
  }
167
167
 
@@ -11,15 +11,6 @@ import { type AssemblyAISession, openAssemblyAI } from "./assemblyai.ts";
11
11
 
12
12
  const here = dirname(fileURLToPath(import.meta.url));
13
13
 
14
- // ---------------------------------------------------------------------------
15
- // Mock the `assemblyai` SDK so no real sockets are opened.
16
- //
17
- // Each fake `StreamingTranscriber` keeps its own listener map and exposes
18
- // `_fire(event, payload)` for tests to inject events. The adapter's
19
- // `open()` returns an `AssemblyAISession` with a `_transcriber` pointer,
20
- // which in the test is the fake — giving the test a handle to `_fire`.
21
- // ---------------------------------------------------------------------------
22
-
23
14
  interface FakeTranscriber {
24
15
  on(ev: string, fn: (...args: unknown[]) => void): void;
25
16
  connect(): Promise<void>;
@@ -29,7 +20,7 @@ interface FakeTranscriber {
29
20
  }
30
21
 
31
22
  vi.mock("assemblyai", () => {
32
- const makeFakeTranscriber = (): FakeTranscriber => {
23
+ function makeFakeTranscriber(): FakeTranscriber {
33
24
  const listeners = new Map<string, Array<(...args: unknown[]) => void>>();
34
25
  return {
35
26
  on(ev, fn) {
@@ -50,7 +41,7 @@ vi.mock("assemblyai", () => {
50
41
  for (const fn of listeners.get(ev) ?? []) fn(...args);
51
42
  },
52
43
  };
53
- };
44
+ }
54
45
  return {
55
46
  AssemblyAI: class {
56
47
  streaming = {
@@ -81,9 +72,6 @@ describe("assemblyAI STT adapter — fixture replay", () => {
81
72
  session.on("final", (t) => finals.push(t));
82
73
  session.on("error", (e) => errors.push(e.message));
83
74
 
84
- // Replay fixture through the fake transcriber. The JSON's "type" field
85
- // distinguishes Begin from Turn; we only dispatch turn messages since
86
- // Begin is consumed inside `connect()` by the real SDK.
87
75
  const fake = session._transcriber as unknown as FakeTranscriber;
88
76
  for (const msg of fixture) {
89
77
  if (msg.type === "Turn") fake._fire("turn", msg as TurnEvent);
@@ -1,16 +1,4 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
- /**
3
- * AssemblyAI Universal-Streaming STT opener (host-only).
4
- *
5
- * The user-facing descriptor factory (`assemblyAI(...)`) lives in
6
- * `sdk/providers/stt/assemblyai.ts`. This module is the host-side
7
- * counterpart: it takes the descriptor options + an API key and
8
- * returns an {@link SttOpener} that the pipeline session drives.
9
- *
10
- * Default model: `"u3pro-rt"` (Universal-3 Pro Real-Time). The adapter
11
- * maps that to the SDK's `"u3-rt-pro"` `speechModel` value; any other
12
- * string is forwarded verbatim.
13
- */
14
2
 
15
3
  import { AssemblyAI, type StreamingTranscriber } from "assemblyai";
16
4
  import { createNanoEvents, type Emitter } from "nanoevents";
@@ -23,20 +11,16 @@ import {
23
11
  type SttSession,
24
12
  } from "../../../sdk/providers.ts";
25
13
 
26
- /** Internal: SttSession with a test-only handle to the raw SDK transcriber. */
27
14
  export interface AssemblyAISession extends SttSession {
28
15
  /** @internal Test-only: exposes the underlying SDK transcriber for fixture replay. */
29
16
  readonly _transcriber: StreamingTranscriber;
30
17
  }
31
18
 
32
- /** Translate the descriptor's model alias to the SDK's `speechModel` value. */
33
19
  function resolveSpeechModel(model: string): string {
34
20
  // Plan's public name is "u3pro-rt"; the SDK's enum uses "u3-rt-pro".
35
- if (model === "u3pro-rt") return "u3-rt-pro";
36
- return model;
21
+ return model === "u3pro-rt" ? "u3-rt-pro" : model;
37
22
  }
38
23
 
39
- /** Build an {@link SttOpener} from resolved AssemblyAI descriptor options. */
40
24
  export function openAssemblyAI(opts: AssemblyAIOptions = {}): SttOpener {
41
25
  return {
42
26
  name: "assemblyai",
@@ -53,8 +37,7 @@ export function openAssemblyAI(opts: AssemblyAIOptions = {}): SttOpener {
53
37
  const speechModel = resolveSpeechModel(opts.model ?? "u3pro-rt");
54
38
  const transcriber = client.streaming.transcriber({
55
39
  sampleRate: openOpts.sampleRate,
56
- // The SDK types `speechModel` as a string-literal union; the adapter
57
- // accepts `string` as an escape hatch, so cast at the boundary.
40
+ // SDK types `speechModel` as a string-literal union; accept `string` here.
58
41
  speechModel: speechModel as never,
59
42
  ...(openOpts.sttPrompt ? { prompt: openOpts.sttPrompt } : {}),
60
43
  });
@@ -65,11 +48,8 @@ export function openAssemblyAI(opts: AssemblyAIOptions = {}): SttOpener {
65
48
  transcriber.on("turn", (event) => {
66
49
  if (closed) return;
67
50
  const text = event.transcript ?? "";
68
- if (event.end_of_turn) {
69
- if (text.length > 0) emitter.emit("final", text);
70
- } else if (text.length > 0) {
71
- emitter.emit("partial", text);
72
- }
51
+ if (text.length === 0) return;
52
+ emitter.emit(event.end_of_turn ? "final" : "partial", text);
73
53
  });
74
54
 
75
55
  transcriber.on("error", (err) => {
@@ -78,11 +58,8 @@ export function openAssemblyAI(opts: AssemblyAIOptions = {}): SttOpener {
78
58
  });
79
59
 
80
60
  transcriber.on("close", (code) => {
81
- if (closed) return;
82
- // 1000 = normal closure.
83
- if (code !== 1000) {
84
- emitter.emit("error", makeSttError("stt_stream_error", `socket closed ${code}`));
85
- }
61
+ if (closed || code === 1000) return;
62
+ emitter.emit("error", makeSttError("stt_stream_error", `socket closed ${code}`));
86
63
  });
87
64
 
88
65
  try {
@@ -100,23 +77,23 @@ export function openAssemblyAI(opts: AssemblyAIOptions = {}): SttOpener {
100
77
  try {
101
78
  await transcriber.close();
102
79
  } catch {
103
- // Swallow: the caller has already decided to tear down.
80
+ // Caller is tearing down; nothing to do on close failure.
104
81
  }
105
82
  };
106
83
 
107
84
  if (openOpts.signal.aborted) {
108
85
  void close();
109
86
  } else {
110
- openOpts.signal.addEventListener("abort", () => void close(), {
111
- once: true,
112
- });
87
+ openOpts.signal.addEventListener("abort", () => void close(), { once: true });
113
88
  }
114
89
 
115
90
  const session: AssemblyAISession = {
116
91
  sendAudio(pcm: Int16Array) {
117
92
  if (closed) return;
118
- const copy = new Uint8Array(pcm.byteLength);
119
- copy.set(new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength));
93
+ // Copy: caller may reuse `pcm`'s backing buffer for the next chunk.
94
+ const copy = new Uint8Array(
95
+ pcm.buffer.slice(pcm.byteOffset, pcm.byteOffset + pcm.byteLength),
96
+ );
120
97
  transcriber.sendAudio(copy.buffer);
121
98
  },
122
99
  on(event, fn) {
@@ -1,20 +1,9 @@
1
1
  // Copyright 2026 the AAI authors. MIT license.
2
- /** Unit test for the Deepgram STT adapter (mocked SDK). */
3
2
 
4
3
  import { describe, expect, test, vi } from "vitest";
5
4
  import { flush } from "../../_test-utils.ts";
6
5
  import { type DeepgramSession, openDeepgram } from "./deepgram.ts";
7
6
 
8
- // ---------------------------------------------------------------------------
9
- // Mock the `@deepgram/sdk` so no real sockets are opened.
10
- //
11
- // Each fake `V1Socket` keeps one listener per event (matching the real SDK's
12
- // `on()` which replaces rather than appends) and exposes `_fire(event, data)`
13
- // for tests to inject events. The adapter's `open()` returns a
14
- // `DeepgramSession` with a `_connection` pointer (which in tests is the fake)
15
- // giving the test a handle to `_fire`.
16
- // ---------------------------------------------------------------------------
17
-
18
7
  interface FakeSocket {
19
8
  on(ev: string, fn: (...args: unknown[]) => void): void;
20
9
  connect(): FakeSocket;
@@ -26,17 +15,17 @@ interface FakeSocket {
26
15
 
27
16
  vi.mock("@deepgram/sdk", () => {
28
17
  const makeFakeSocket = (): FakeSocket => {
18
+ // V1Socket replaces — not appends — the listener per event.
29
19
  const listeners = new Map<string, (...args: unknown[]) => void>();
30
20
  const fake: FakeSocket = {
31
21
  on(ev, fn) {
32
- // V1Socket replaces — not appends — the listener per event.
33
22
  listeners.set(ev, fn);
34
23
  },
35
24
  connect() {
36
25
  return fake;
37
26
  },
38
27
  async waitForOpen() {
39
- // Immediately resolves in tests.
28
+ /* no-op */
40
29
  },
41
30
  close() {
42
31
  /* no-op */
@@ -45,8 +34,7 @@ vi.mock("@deepgram/sdk", () => {
45
34
  /* no-op */
46
35
  },
47
36
  _fire(ev, ...args) {
48
- const fn = listeners.get(ev);
49
- if (fn) fn(...args);
37
+ listeners.get(ev)?.(...args);
50
38
  },
51
39
  };
52
40
  return fake;
@@ -63,10 +51,6 @@ vi.mock("@deepgram/sdk", () => {
63
51
  };
64
52
  });
65
53
 
66
- // ---------------------------------------------------------------------------
67
- // Helpers
68
- // ---------------------------------------------------------------------------
69
-
70
54
  function makeResult(transcript: string, isFinal: boolean) {
71
55
  return {
72
56
  type: "Results" as const,
@@ -79,44 +63,40 @@ function makeResult(transcript: string, isFinal: boolean) {
79
63
  };
80
64
  }
81
65
 
82
- // ---------------------------------------------------------------------------
83
- // Tests
84
- // ---------------------------------------------------------------------------
66
+ async function openSession(
67
+ args: Parameters<typeof openDeepgram>[0] = {},
68
+ ): Promise<{ session: DeepgramSession; fake: FakeSocket }> {
69
+ const opener = openDeepgram(args);
70
+ const session = (await opener.open({
71
+ sampleRate: 16_000,
72
+ apiKey: "test-key",
73
+ signal: new AbortController().signal,
74
+ })) as DeepgramSession;
75
+ return { session, fake: session._connection as unknown as FakeSocket };
76
+ }
85
77
 
86
78
  describe("Deepgram STT adapter", () => {
87
79
  test("openDeepgram({}) returns an opener with name 'deepgram'", () => {
88
- const opener = openDeepgram({});
89
- expect(opener.name).toBe("deepgram");
80
+ expect(openDeepgram({}).name).toBe("deepgram");
90
81
  });
91
82
 
92
83
  test("throws stt_auth_failed when API key is missing", async () => {
93
- // Clear env var for this test.
94
84
  const saved = process.env.DEEPGRAM_API_KEY;
95
85
  delete process.env.DEEPGRAM_API_KEY;
96
86
 
97
87
  const opener = openDeepgram({});
98
- const controller = new AbortController();
99
-
100
88
  await expect(
101
- opener.open({ sampleRate: 16_000, apiKey: "", signal: controller.signal }),
89
+ opener.open({ sampleRate: 16_000, apiKey: "", signal: new AbortController().signal }),
102
90
  ).rejects.toMatchObject({ code: "stt_auth_failed" });
103
91
 
104
92
  process.env.DEEPGRAM_API_KEY = saved;
105
93
  });
106
94
 
107
95
  test("final transcript fires 'final' event with text", async () => {
108
- const opener = openDeepgram({ model: "nova-3" });
109
- const controller = new AbortController();
110
- const session = (await opener.open({
111
- sampleRate: 16_000,
112
- apiKey: "test-key",
113
- signal: controller.signal,
114
- })) as DeepgramSession;
115
-
96
+ const { session, fake } = await openSession({ model: "nova-3" });
116
97
  const finals: string[] = [];
117
98
  session.on("final", (t) => finals.push(t));
118
99
 
119
- const fake = session._connection as unknown as FakeSocket;
120
100
  fake._fire("message", makeResult("hello world", true));
121
101
 
122
102
  await flush();
@@ -126,18 +106,10 @@ describe("Deepgram STT adapter", () => {
126
106
  });
127
107
 
128
108
  test("interim transcript fires 'partial' event with text", async () => {
129
- const opener = openDeepgram({ model: "nova-3" });
130
- const controller = new AbortController();
131
- const session = (await opener.open({
132
- sampleRate: 16_000,
133
- apiKey: "test-key",
134
- signal: controller.signal,
135
- })) as DeepgramSession;
136
-
109
+ const { session, fake } = await openSession({ model: "nova-3" });
137
110
  const partials: string[] = [];
138
111
  session.on("partial", (t) => partials.push(t));
139
112
 
140
- const fake = session._connection as unknown as FakeSocket;
141
113
  fake._fire("message", makeResult("hel", false));
142
114
  fake._fire("message", makeResult("hello", false));
143
115
 
@@ -148,21 +120,12 @@ describe("Deepgram STT adapter", () => {
148
120
  });
149
121
 
150
122
  test("empty transcript is NOT emitted (neither partial nor final)", async () => {
151
- const opener = openDeepgram({});
152
- const controller = new AbortController();
153
- const session = (await opener.open({
154
- sampleRate: 16_000,
155
- apiKey: "test-key",
156
- signal: controller.signal,
157
- })) as DeepgramSession;
158
-
123
+ const { session, fake } = await openSession();
159
124
  const partials: string[] = [];
160
125
  const finals: string[] = [];
161
126
  session.on("partial", (t) => partials.push(t));
162
127
  session.on("final", (t) => finals.push(t));
163
128
 
164
- const fake = session._connection as unknown as FakeSocket;
165
- // Fire results with empty transcript — neither should be emitted.
166
129
  fake._fire("message", makeResult("", false));
167
130
  fake._fire("message", makeResult("", true));
168
131
 
@@ -174,24 +137,13 @@ describe("Deepgram STT adapter", () => {
174
137
  });
175
138
 
176
139
  test("close fires close() and subsequent events are ignored (no double-close crash)", async () => {
177
- const opener = openDeepgram({});
178
- const controller = new AbortController();
179
- const session = (await opener.open({
180
- sampleRate: 16_000,
181
- apiKey: "test-key",
182
- signal: controller.signal,
183
- })) as DeepgramSession;
184
-
140
+ const { session, fake } = await openSession();
185
141
  const finals: string[] = [];
186
142
  session.on("final", (t) => finals.push(t));
187
143
 
188
144
  await session.close();
189
-
190
- // Subsequent close should not throw.
191
145
  await session.close();
192
146
 
193
- // Events after close should be dropped.
194
- const fake = session._connection as unknown as FakeSocket;
195
147
  fake._fire("message", makeResult("should be ignored", true));
196
148
 
197
149
  await flush();
@@ -199,15 +151,7 @@ describe("Deepgram STT adapter", () => {
199
151
  });
200
152
 
201
153
  test("sendAudio(Int16Array) forwards PCM bytes to the connection", async () => {
202
- const opener = openDeepgram({});
203
- const controller = new AbortController();
204
- const session = (await opener.open({
205
- sampleRate: 16_000,
206
- apiKey: "test-key",
207
- signal: controller.signal,
208
- })) as DeepgramSession;
209
-
210
- const fake = session._connection as unknown as FakeSocket;
154
+ const { session, fake } = await openSession();
211
155
  const sent: ArrayBufferView[] = [];
212
156
  fake.sendMedia = (data: ArrayBufferView) => sent.push(data);
213
157
 
@@ -215,12 +159,8 @@ describe("Deepgram STT adapter", () => {
215
159
  session.sendAudio(pcm);
216
160
 
217
161
  expect(sent).toHaveLength(1);
218
- // The sent buffer should contain the same bytes as the Int16Array.
219
- const sentBytes = new Uint8Array(
220
- (sent[0] as Uint8Array).buffer,
221
- (sent[0] as Uint8Array).byteOffset,
222
- (sent[0] as Uint8Array).byteLength,
223
- );
162
+ const sentView = sent[0] as Uint8Array;
163
+ const sentBytes = new Uint8Array(sentView.buffer, sentView.byteOffset, sentView.byteLength);
224
164
  const expectedBytes = new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength);
225
165
  expect(sentBytes).toEqual(expectedBytes);
226
166
 
@@ -2,18 +2,8 @@
2
2
  /**
3
3
  * Deepgram Nova streaming STT opener (host-only).
4
4
  *
5
- * The user-facing descriptor factory (`deepgram(...)`) lives in
6
- * `sdk/providers/stt/deepgram.ts`. This module is the host-side
7
- * counterpart: it takes the descriptor options + an API key and
8
- * returns an {@link SttOpener} that the pipeline session drives.
9
- *
10
- * Default model: `"nova-3"`. Any string is forwarded verbatim to the SDK.
11
- *
12
- * This adapter targets the Deepgram SDK v5 (`@deepgram/sdk@^5`). The v5
13
- * streaming API is:
14
- * `client.listen.v1.connect(args)` → `Promise<V1Socket>`
15
- * followed by:
16
- * `socket.connect()` + `socket.waitForOpen()` to establish the connection.
5
+ * Targets Deepgram SDK v5: `client.listen.v1.connect(args)` returns a
6
+ * socket; `socket.connect()` + `socket.waitForOpen()` establish it.
17
7
  */
18
8
 
19
9
  import { DeepgramClient, type listen } from "@deepgram/sdk";
@@ -27,10 +17,8 @@ import {
27
17
  type SttSession,
28
18
  } from "../../../sdk/providers.ts";
29
19
 
30
- // V1Socket type from the Deepgram SDK (accessed through the listen namespace).
31
20
  type V1Socket = Awaited<ReturnType<InstanceType<typeof DeepgramClient>["listen"]["v1"]["connect"]>>;
32
21
 
33
- /** Internal: SttSession with a test-only handle to the raw SDK socket. */
34
22
  export interface DeepgramSession extends SttSession {
35
23
  /** @internal Test-only: exposes the underlying SDK socket for fixture replay. */
36
24
  readonly _connection: V1Socket;
@@ -42,23 +30,17 @@ type MessagePayload =
42
30
  | listen.ListenV1UtteranceEnd
43
31
  | listen.ListenV1SpeechStarted;
44
32
 
45
- /**
46
- * Handle an incoming Deepgram transcript message, emitting `partial` or
47
- * `final` events on the emitter. Empty transcripts are silently dropped.
48
- */
33
+ function errMsg(cause: unknown): string {
34
+ return cause instanceof Error ? cause.message : String(cause);
35
+ }
36
+
49
37
  function handleMessage(data: MessagePayload, closed: boolean, emitter: Emitter<SttEvents>): void {
50
- if (closed) return;
51
- if (data.type !== "Results") return;
52
- const result = data as listen.ListenV1Results;
53
- const text = result.channel?.alternatives?.[0]?.transcript ?? "";
54
- if (result.is_final) {
55
- if (text.length > 0) emitter.emit("final", text);
56
- } else if (text.length > 0) {
57
- emitter.emit("partial", text);
58
- }
38
+ if (closed || data.type !== "Results") return;
39
+ const text = data.channel?.alternatives?.[0]?.transcript ?? "";
40
+ if (text.length === 0) return;
41
+ emitter.emit(data.is_final ? "final" : "partial", text);
59
42
  }
60
43
 
61
- /** Wire Deepgram socket events onto the nanoevents emitter. */
62
44
  function wireSocketEvents(
63
45
  connection: V1Socket,
64
46
  emitter: Emitter<SttEvents>,
@@ -79,16 +61,14 @@ function wireSocketEvents(
79
61
  });
80
62
  }
81
63
 
82
- /** Wire the AbortSignal to the close function. */
83
64
  function wireAbortSignal(signal: AbortSignal, close: () => Promise<void>): void {
84
65
  if (signal.aborted) {
85
66
  void close();
86
- } else {
87
- signal.addEventListener("abort", () => void close(), { once: true });
67
+ return;
88
68
  }
69
+ signal.addEventListener("abort", () => void close(), { once: true });
89
70
  }
90
71
 
91
- /** Build an {@link SttOpener} from resolved Deepgram descriptor options. */
92
72
  export function openDeepgram(opts: DeepgramOptions = {}): SttOpener {
93
73
  return {
94
74
  name: "deepgram",
@@ -119,10 +99,7 @@ export function openDeepgram(opts: DeepgramOptions = {}): SttOpener {
119
99
  Authorization: apiKey,
120
100
  });
121
101
  } catch (cause) {
122
- throw makeSttError(
123
- "stt_connect_failed",
124
- `Deepgram STT: connect failed: ${cause instanceof Error ? cause.message : String(cause)}`,
125
- );
102
+ throw makeSttError("stt_connect_failed", `Deepgram STT: connect failed: ${errMsg(cause)}`);
126
103
  }
127
104
 
128
105
  const emitter: Emitter<SttEvents> = createNanoEvents<SttEvents>();
@@ -130,15 +107,13 @@ export function openDeepgram(opts: DeepgramOptions = {}): SttOpener {
130
107
 
131
108
  wireSocketEvents(connection, emitter, () => closed);
132
109
 
133
- // Actually open the WebSocket connection (registers internal handlers
134
- // and initiates the TCP/TLS handshake).
135
110
  connection.connect();
136
111
  try {
137
112
  await connection.waitForOpen();
138
113
  } catch (cause) {
139
114
  throw makeSttError(
140
115
  "stt_connect_failed",
141
- `Deepgram STT: WebSocket open failed: ${cause instanceof Error ? cause.message : String(cause)}`,
116
+ `Deepgram STT: WebSocket open failed: ${errMsg(cause)}`,
142
117
  );
143
118
  }
144
119
 
@@ -148,7 +123,7 @@ export function openDeepgram(opts: DeepgramOptions = {}): SttOpener {
148
123
  try {
149
124
  connection.close();
150
125
  } catch {
151
- // Swallow: the caller has already decided to tear down.
126
+ // Caller already decided to tear down.
152
127
  }
153
128
  };
154
129
 
@@ -5,14 +5,6 @@ import { describe, expect, test, vi } from "vitest";
5
5
  import { flush } from "../../_test-utils.ts";
6
6
  import { openElevenLabs } from "./elevenlabs.ts";
7
7
 
8
- // ---------------------------------------------------------------------------
9
- // Mock the @elevenlabs/elevenlabs-js realtime client so no real sockets open.
10
- //
11
- // The fake connection keeps one listener per RealtimeEvents value (the SDK's
12
- // EventEmitter API allows multiple, but for simple unit tests one is enough)
13
- // and exposes `_fire` for tests to inject events.
14
- // ---------------------------------------------------------------------------
15
-
16
8
  interface FakeConnection {
17
9
  on(ev: string, fn: (data: unknown) => void): void;
18
10
  send(_: { audioBase64: string }): void;
@@ -22,36 +14,33 @@ interface FakeConnection {
22
14
 
23
15
  const captured: { connections: FakeConnection[] } = { connections: [] };
24
16
 
25
- vi.mock("@elevenlabs/elevenlabs-js", () => {
26
- return {
27
- ElevenLabsClient: class {
28
- speechToText = {
29
- realtime: {
30
- connect: async (_opts: unknown): Promise<FakeConnection> => {
31
- const listeners = new Map<string, (data: unknown) => void>();
32
- const conn: FakeConnection = {
33
- on(ev, fn) {
34
- listeners.set(ev, fn);
35
- },
36
- send() {
37
- /* no-op */
38
- },
39
- close() {
40
- /* no-op */
41
- },
42
- _fire(ev, data) {
43
- const fn = listeners.get(ev);
44
- if (fn) fn(data);
45
- },
46
- };
47
- captured.connections.push(conn);
48
- return conn;
49
- },
17
+ vi.mock("@elevenlabs/elevenlabs-js", () => ({
18
+ ElevenLabsClient: class {
19
+ speechToText = {
20
+ realtime: {
21
+ connect: async (_opts: unknown): Promise<FakeConnection> => {
22
+ const listeners = new Map<string, (data: unknown) => void>();
23
+ const conn: FakeConnection = {
24
+ on(ev, fn) {
25
+ listeners.set(ev, fn);
26
+ },
27
+ send() {
28
+ /* no-op */
29
+ },
30
+ close() {
31
+ /* no-op */
32
+ },
33
+ _fire(ev, data) {
34
+ listeners.get(ev)?.(data);
35
+ },
36
+ };
37
+ captured.connections.push(conn);
38
+ return conn;
50
39
  },
51
- };
52
- },
53
- };
54
- });
40
+ },
41
+ };
42
+ },
43
+ }));
55
44
 
56
45
  vi.mock("@elevenlabs/elevenlabs-js/wrapper/realtime", () => ({
57
46
  AudioFormat: {
@@ -72,7 +61,6 @@ vi.mock("@elevenlabs/elevenlabs-js/wrapper/realtime", () => ({
72
61
  },
73
62
  }));
74
63
 
75
- // Helper: open a session backed by a captured fake connection.
76
64
  async function openSession(sampleRate = 16_000) {
77
65
  captured.connections.length = 0;
78
66
  const opener = openElevenLabs({});