@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.
- package/.turbo/turbo-build.log +11 -9
- package/CHANGELOG.md +10 -0
- package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
- package/dist/constants-y68COEGj.js +29 -0
- package/dist/host/_base64.d.ts +2 -0
- package/dist/host/_mock-ws.d.ts +0 -61
- package/dist/host/_pipeline-test-fakes.d.ts +7 -4
- package/dist/host/_run-code.d.ts +0 -25
- package/dist/host/_runtime-conformance.d.ts +3 -34
- package/dist/host/memory-vector.d.ts +0 -11
- package/dist/host/providers/resolve-kv.d.ts +0 -7
- package/dist/host/providers/resolve-vector.d.ts +0 -8
- package/dist/host/providers/stt/assemblyai.d.ts +0 -14
- package/dist/host/providers/stt/deepgram.d.ts +2 -14
- package/dist/host/providers/stt/soniox.d.ts +0 -22
- package/dist/host/providers/tts/rime.d.ts +10 -31
- package/dist/host/runtime-barrel.js +619 -630
- package/dist/host/runtime-config.d.ts +9 -6
- package/dist/host/runtime.d.ts +3 -0
- package/dist/host/to-vercel-tools.d.ts +3 -33
- package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
- package/dist/host/unstorage-kv.d.ts +0 -26
- package/dist/index.js +3 -3
- package/dist/openai-realtime-cjPAHMMx.js +10 -0
- package/dist/sdk/_internal-types.d.ts +6 -55
- package/dist/sdk/allowed-hosts.d.ts +4 -3
- package/dist/sdk/constants.d.ts +4 -29
- package/dist/sdk/define.d.ts +7 -4
- package/dist/sdk/kv.d.ts +13 -37
- package/dist/sdk/manifest-barrel.js +1 -1
- package/dist/sdk/manifest.d.ts +8 -2
- package/dist/sdk/protocol.js +1 -1
- package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
- package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
- package/dist/sdk/providers/s2s-barrel.js +2 -0
- package/dist/sdk/providers/tts/rime.d.ts +1 -1
- package/dist/sdk/providers.d.ts +6 -2
- package/dist/sdk/types.d.ts +7 -1
- package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
- package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
- package/host/_base64.ts +9 -0
- package/host/_mock-ws.ts +0 -65
- package/host/_pipeline-test-fakes.ts +19 -31
- package/host/_run-code.ts +10 -53
- package/host/_runtime-conformance.ts +3 -44
- package/host/_test-utils.ts +20 -42
- package/host/builtin-tools.test.ts +127 -222
- package/host/builtin-tools.ts +6 -10
- package/host/cleanup.test.ts +30 -73
- package/host/integration/pipeline-reference.integration.test.ts +12 -17
- package/host/integration.test.ts +0 -7
- package/host/memory-vector.test.ts +3 -1
- package/host/memory-vector.ts +16 -21
- package/host/pinecone-vector.test.ts +14 -17
- package/host/pinecone-vector.ts +10 -19
- package/host/providers/providers.test-d.ts +5 -3
- package/host/providers/resolve-kv.ts +23 -41
- package/host/providers/resolve-vector.ts +3 -12
- package/host/providers/resolve.test.ts +15 -28
- package/host/providers/resolve.ts +24 -24
- package/host/providers/stt/assemblyai.test.ts +2 -14
- package/host/providers/stt/assemblyai.ts +12 -35
- package/host/providers/stt/deepgram.test.ts +23 -83
- package/host/providers/stt/deepgram.ts +15 -40
- package/host/providers/stt/elevenlabs.test.ts +26 -38
- package/host/providers/stt/elevenlabs.ts +10 -9
- package/host/providers/stt/soniox.test.ts +35 -85
- package/host/providers/stt/soniox.ts +8 -53
- package/host/providers/tts/cartesia.test.ts +19 -58
- package/host/providers/tts/cartesia.ts +36 -66
- package/host/providers/tts/rime.test.ts +12 -38
- package/host/providers/tts/rime.ts +23 -86
- package/host/runtime-config.test.ts +9 -9
- package/host/runtime-config.ts +16 -22
- package/host/runtime.test.ts +111 -73
- package/host/runtime.ts +138 -86
- package/host/s2s.test.ts +92 -191
- package/host/s2s.ts +55 -49
- package/host/server-shutdown.test.ts +9 -30
- package/host/server.test.ts +2 -13
- package/host/server.ts +85 -100
- package/host/session-core.test.ts +15 -30
- package/host/session-core.ts +10 -13
- package/host/session-prompt.test.ts +1 -5
- package/host/to-vercel-tools.test.ts +53 -72
- package/host/to-vercel-tools.ts +9 -39
- package/host/tool-executor.test.ts +25 -51
- package/host/tool-executor.ts +18 -12
- package/host/transports/openai-realtime-transport.test.ts +371 -0
- package/host/transports/openai-realtime-transport.ts +319 -0
- package/host/transports/pipeline-transport.test.ts +125 -298
- package/host/transports/pipeline-transport.ts +20 -68
- package/host/transports/s2s-transport-fixtures.test.ts +31 -92
- package/host/transports/s2s-transport.test.ts +65 -134
- package/host/transports/s2s-transport.ts +15 -43
- package/host/transports/types.test.ts +4 -8
- package/host/unstorage-kv.test.ts +3 -2
- package/host/unstorage-kv.ts +5 -35
- package/host/ws-handler.test.ts +72 -176
- package/host/ws-handler.ts +6 -12
- package/package.json +6 -1
- package/sdk/__snapshots__/exports.test.ts.snap +7 -0
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
- package/sdk/_internal-types.test.ts +6 -9
- package/sdk/_internal-types.ts +16 -57
- package/sdk/_test-matchers.ts +25 -15
- package/sdk/allowed-hosts.test.ts +50 -114
- package/sdk/allowed-hosts.ts +8 -14
- package/sdk/constants.ts +5 -52
- package/sdk/define.test.ts +7 -6
- package/sdk/define.ts +7 -3
- package/sdk/exports.test.ts +6 -1
- package/sdk/kv.ts +13 -37
- package/sdk/manifest.test-d.ts +5 -0
- package/sdk/manifest.test.ts +61 -9
- package/sdk/manifest.ts +11 -11
- package/sdk/protocol-compat.test.ts +66 -98
- package/sdk/protocol-snapshot.test.ts +2 -16
- package/sdk/protocol.test.ts +13 -22
- package/sdk/providers/s2s/openai-realtime.ts +36 -0
- package/sdk/providers/s2s-barrel.ts +12 -0
- package/sdk/providers/tts/rime.ts +1 -1
- package/sdk/providers.ts +24 -5
- package/sdk/schema-alignment.test.ts +25 -73
- package/sdk/schema-shapes.test.ts +1 -29
- package/sdk/system-prompt.test.ts +0 -1
- package/sdk/system-prompt.ts +17 -19
- package/sdk/types-inference.test.ts +10 -36
- package/sdk/types.ts +7 -0
- package/sdk/ws-upgrade.test.ts +24 -23
- package/sdk/ws-upgrade.ts +2 -3
- package/tsdown.config.ts +8 -11
- 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(
|
|
67
|
+
return openAssemblyAI(options<AssemblyAIOptions>(descriptor));
|
|
64
68
|
case DEEPGRAM_KIND:
|
|
65
|
-
return openDeepgram(
|
|
69
|
+
return openDeepgram(options<DeepgramOptions>(descriptor));
|
|
66
70
|
case ELEVENLABS_KIND:
|
|
67
|
-
return openElevenLabs(
|
|
71
|
+
return openElevenLabs(options<ElevenLabsOptions>(descriptor));
|
|
68
72
|
case SONIOX_KIND:
|
|
69
|
-
return openSoniox(
|
|
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(
|
|
86
|
+
return openCartesia(options<CartesiaOptions>(descriptor));
|
|
83
87
|
case RIME_KIND:
|
|
84
|
-
return openRime(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
156
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
157
|
+
const isMissing =
|
|
155
158
|
err instanceof Error &&
|
|
156
|
-
(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 (
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
119
|
-
copy
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
6
|
-
* `
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
//
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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({});
|