xo-harness 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.
- package/README.md +32 -0
- package/dist/harness.d.ts +1 -0
- package/dist/harness.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/internal/harness/event-stream.d.ts +7 -0
- package/dist/internal/harness/event-stream.js +42 -0
- package/dist/internal/harness/index.d.ts +7 -0
- package/dist/internal/harness/index.js +7 -0
- package/dist/internal/harness/message.d.ts +26 -0
- package/dist/internal/harness/message.js +192 -0
- package/dist/internal/harness/report.d.ts +51 -0
- package/dist/internal/harness/report.js +115 -0
- package/dist/internal/harness/shadow.d.ts +28 -0
- package/dist/internal/harness/shadow.js +118 -0
- package/dist/internal/harness/socket-bridge.d.ts +65 -0
- package/dist/internal/harness/socket-bridge.js +227 -0
- package/dist/internal/harness/task-supervisor.d.ts +20 -0
- package/dist/internal/harness/task-supervisor.js +180 -0
- package/dist/internal/harness/tool-runtime.d.ts +20 -0
- package/dist/internal/harness/tool-runtime.js +93 -0
- package/dist/internal/harness/tools.d.ts +32 -0
- package/dist/internal/harness/tools.js +30 -0
- package/dist/internal/harness/voice-session.d.ts +33 -0
- package/dist/internal/harness/voice-session.js +268 -0
- package/dist/internal/harness/xo.d.ts +25 -0
- package/dist/internal/harness/xo.js +24 -0
- package/dist/internal/protocol/async-queue.d.ts +8 -0
- package/dist/internal/protocol/async-queue.js +34 -0
- package/dist/internal/protocol/audio.d.ts +12 -0
- package/dist/internal/protocol/audio.js +24 -0
- package/dist/internal/protocol/events.d.ts +350 -0
- package/dist/internal/protocol/events.js +167 -0
- package/dist/internal/protocol/index.d.ts +6 -0
- package/dist/internal/protocol/index.js +6 -0
- package/dist/internal/protocol/parts.d.ts +219 -0
- package/dist/internal/protocol/parts.js +105 -0
- package/dist/internal/protocol/provider.d.ts +118 -0
- package/dist/internal/protocol/provider.js +77 -0
- package/dist/internal/protocol/tools.d.ts +62 -0
- package/dist/internal/protocol/tools.js +47 -0
- package/dist/internal/provider/contract.d.ts +24 -0
- package/dist/internal/provider/contract.js +1 -0
- package/dist/internal/provider/grok-voice.d.ts +45 -0
- package/dist/internal/provider/grok-voice.js +77 -0
- package/dist/internal/provider/index.d.ts +6 -0
- package/dist/internal/provider/index.js +9 -0
- package/dist/internal/provider/node-socket.d.ts +3 -0
- package/dist/internal/provider/node-socket.js +74 -0
- package/dist/internal/provider/openai-realtime.d.ts +43 -0
- package/dist/internal/provider/openai-realtime.js +76 -0
- package/dist/internal/provider/realtime-session.d.ts +30 -0
- package/dist/internal/provider/realtime-session.js +461 -0
- package/dist/internal/provider/realtime-socket.d.ts +42 -0
- package/dist/internal/provider/realtime-socket.js +30 -0
- package/dist/internal/provider/workers-socket.d.ts +11 -0
- package/dist/internal/provider/workers-socket.js +108 -0
- package/dist/internal/provider/workers.d.ts +6 -0
- package/dist/internal/provider/workers.js +11 -0
- package/dist/internal/provider-fake/index.d.ts +46 -0
- package/dist/internal/provider-fake/index.js +112 -0
- package/dist/internal/provider-fake/replay-voice-provider.d.ts +47 -0
- package/dist/internal/provider-fake/replay-voice-provider.js +133 -0
- package/dist/internal/provider-fake/scripted-voice-provider.d.ts +61 -0
- package/dist/internal/provider-fake/scripted-voice-provider.js +185 -0
- package/dist/internal/storage/append-tail.d.ts +12 -0
- package/dist/internal/storage/append-tail.js +19 -0
- package/dist/internal/storage/event-store.d.ts +19 -0
- package/dist/internal/storage/event-store.js +1 -0
- package/dist/internal/storage/index.d.ts +4 -0
- package/dist/internal/storage/index.js +4 -0
- package/dist/internal/storage/jsonl-event-store.d.ts +13 -0
- package/dist/internal/storage/jsonl-event-store.js +105 -0
- package/dist/internal/storage/memory-event-store.d.ts +8 -0
- package/dist/internal/storage/memory-event-store.js +22 -0
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +1 -0
- package/dist/provider-workers.d.ts +1 -0
- package/dist/provider-workers.js +1 -0
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +1 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +1 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CreateProviderSessionOptions, VoiceProvider, VoiceProviderSession } from "./contract.js";
|
|
2
|
+
import { type RealtimeSocketFactory } from "./realtime-socket.js";
|
|
3
|
+
export interface GrokVoiceProviderOptions {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** Transport override; defaults to the runtime's registered factory (Node ws or Workers). */
|
|
6
|
+
socketFactory?: RealtimeSocketFactory;
|
|
7
|
+
/** Voice model id; grok-voice-latest currently resolves to Grok Voice Think Fast 2.0. */
|
|
8
|
+
model?: string;
|
|
9
|
+
/** WebSocket endpoint, overridable for tests and proxies. */
|
|
10
|
+
url?: string;
|
|
11
|
+
/** Output voice: eve (default), ara, rex, sal, or leo. */
|
|
12
|
+
voice?: string;
|
|
13
|
+
/** Sample rate of the PCM16 mono input the harness will send. The API expects 24000. */
|
|
14
|
+
inputSampleRate?: number;
|
|
15
|
+
/** Server-side turn detection; xAI supports server_vad with threshold and padding tuning. */
|
|
16
|
+
turnDetection?: Record<string, unknown> | null;
|
|
17
|
+
/** Input transcription model recorded into the conversation. */
|
|
18
|
+
transcriptionModel?: string;
|
|
19
|
+
/** Think Fast reasoning effort: "high" reasons in parallel with speech, "none" disables it. */
|
|
20
|
+
reasoningEffort?: "high" | "none";
|
|
21
|
+
/** Request a spoken response when a background task settles. */
|
|
22
|
+
announceTaskSettlement?: boolean;
|
|
23
|
+
handshakeTimeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Adapts xAI's Grok voice realtime API (Think Fast models) to the XO duplex provider
|
|
27
|
+
* contract. The wire protocol is the same realtime dialect the OpenAI adapter speaks;
|
|
28
|
+
* the differences live entirely in the session.update payload: voice and turn_detection
|
|
29
|
+
* sit at the session top level, input transcription is configured explicitly, and
|
|
30
|
+
* reasoning effort is tunable. Think Fast listens and reasons while speaking, so the
|
|
31
|
+
* duplex capability flags here are literal, not bridged.
|
|
32
|
+
*/
|
|
33
|
+
export declare class GrokVoiceProvider implements VoiceProvider {
|
|
34
|
+
#private;
|
|
35
|
+
readonly id = "grok-voice";
|
|
36
|
+
readonly capabilities: {
|
|
37
|
+
readonly continuousInputDuringOutput: true;
|
|
38
|
+
readonly modelManagedFloor: true;
|
|
39
|
+
readonly supportsToolCalls: true;
|
|
40
|
+
readonly supportsAsyncContext: true;
|
|
41
|
+
readonly supportsPlayoutAcknowledgements: true;
|
|
42
|
+
};
|
|
43
|
+
constructor(options: GrokVoiceProviderOptions);
|
|
44
|
+
createSession(options: CreateProviderSessionOptions): Promise<VoiceProviderSession>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { RealtimeVoiceSession, toRealtimeFunctionTool } from "./realtime-session.js";
|
|
2
|
+
import { connectRealtimeSocket } from "./realtime-socket.js";
|
|
3
|
+
const DEFAULT_URL = "wss://api.x.ai/v1/realtime";
|
|
4
|
+
const DEFAULT_MODEL = "grok-voice-latest";
|
|
5
|
+
const DEFAULT_VOICE = "eve";
|
|
6
|
+
const DEFAULT_SAMPLE_RATE = 24_000;
|
|
7
|
+
const DEFAULT_TRANSCRIPTION_MODEL = "grok-transcribe";
|
|
8
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 15_000;
|
|
9
|
+
/**
|
|
10
|
+
* Adapts xAI's Grok voice realtime API (Think Fast models) to the XO duplex provider
|
|
11
|
+
* contract. The wire protocol is the same realtime dialect the OpenAI adapter speaks;
|
|
12
|
+
* the differences live entirely in the session.update payload: voice and turn_detection
|
|
13
|
+
* sit at the session top level, input transcription is configured explicitly, and
|
|
14
|
+
* reasoning effort is tunable. Think Fast listens and reasons while speaking, so the
|
|
15
|
+
* duplex capability flags here are literal, not bridged.
|
|
16
|
+
*/
|
|
17
|
+
export class GrokVoiceProvider {
|
|
18
|
+
id = "grok-voice";
|
|
19
|
+
capabilities = {
|
|
20
|
+
continuousInputDuringOutput: true,
|
|
21
|
+
modelManagedFloor: true,
|
|
22
|
+
supportsToolCalls: true,
|
|
23
|
+
supportsAsyncContext: true,
|
|
24
|
+
supportsPlayoutAcknowledgements: true,
|
|
25
|
+
};
|
|
26
|
+
#options;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
if (!options.apiKey)
|
|
29
|
+
throw new Error("GrokVoiceProvider requires an apiKey");
|
|
30
|
+
this.#options = options;
|
|
31
|
+
}
|
|
32
|
+
async createSession(options) {
|
|
33
|
+
const inputSampleRate = this.#options.inputSampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
34
|
+
const socket = await connectRealtimeSocket({
|
|
35
|
+
url: this.#options.url ?? DEFAULT_URL,
|
|
36
|
+
model: this.#options.model ?? DEFAULT_MODEL,
|
|
37
|
+
apiKey: this.#options.apiKey,
|
|
38
|
+
handshakeTimeoutMs: this.#options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS,
|
|
39
|
+
signal: options.signal,
|
|
40
|
+
...(this.#options.socketFactory === undefined ? {} : { factory: this.#options.socketFactory }),
|
|
41
|
+
});
|
|
42
|
+
return new RealtimeVoiceSession(socket, {
|
|
43
|
+
label: "Grok voice",
|
|
44
|
+
sessionUpdate: this.#sessionUpdate(options, inputSampleRate),
|
|
45
|
+
inputSampleRate,
|
|
46
|
+
outputSampleRate: DEFAULT_SAMPLE_RATE,
|
|
47
|
+
announceTaskSettlement: this.#options.announceTaskSettlement ?? true,
|
|
48
|
+
}, options);
|
|
49
|
+
}
|
|
50
|
+
#sessionUpdate(options, inputSampleRate) {
|
|
51
|
+
const turnDetection = this.#options.turnDetection === undefined ? { type: "server_vad" } : this.#options.turnDetection;
|
|
52
|
+
const session = {
|
|
53
|
+
voice: this.#options.voice ?? DEFAULT_VOICE,
|
|
54
|
+
output_modalities: [...(options.outputModalities ?? ["audio"])],
|
|
55
|
+
turn_detection: turnDetection,
|
|
56
|
+
audio: {
|
|
57
|
+
input: {
|
|
58
|
+
format: { type: "audio/pcm", rate: inputSampleRate },
|
|
59
|
+
transcription: { model: this.#options.transcriptionModel ?? DEFAULT_TRANSCRIPTION_MODEL },
|
|
60
|
+
},
|
|
61
|
+
output: {
|
|
62
|
+
format: { type: "audio/pcm", rate: DEFAULT_SAMPLE_RATE },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
if (options.instructions !== undefined)
|
|
67
|
+
session.instructions = options.instructions;
|
|
68
|
+
if (this.#options.reasoningEffort !== undefined) {
|
|
69
|
+
session.reasoning = { effort: this.#options.reasoningEffort };
|
|
70
|
+
}
|
|
71
|
+
if (options.tools.length > 0) {
|
|
72
|
+
session.tools = options.tools.map(toRealtimeFunctionTool);
|
|
73
|
+
session.tool_choice = "auto";
|
|
74
|
+
}
|
|
75
|
+
return session;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./contract.js";
|
|
2
|
+
export * from "./grok-voice.js";
|
|
3
|
+
export { nodeSocketFactory } from "./node-socket.js";
|
|
4
|
+
export * from "./openai-realtime.js";
|
|
5
|
+
export { RealtimeVoiceSession, type RealtimeWireOptions } from "./realtime-session.js";
|
|
6
|
+
export * from "./realtime-socket.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { nodeSocketFactory } from "./node-socket.js";
|
|
2
|
+
import { setDefaultRealtimeSocketFactory } from "./realtime-socket.js";
|
|
3
|
+
setDefaultRealtimeSocketFactory(nodeSocketFactory);
|
|
4
|
+
export * from "./contract.js";
|
|
5
|
+
export * from "./grok-voice.js";
|
|
6
|
+
export { nodeSocketFactory } from "./node-socket.js";
|
|
7
|
+
export * from "./openai-realtime.js";
|
|
8
|
+
export { RealtimeVoiceSession } from "./realtime-session.js";
|
|
9
|
+
export * from "./realtime-socket.js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import WebSocket, {} from "ws";
|
|
3
|
+
/** Node transport: the ws client, which supports Authorization headers directly. */
|
|
4
|
+
export const nodeSocketFactory = async (target) => {
|
|
5
|
+
const ws = new WebSocket(target.url, {
|
|
6
|
+
headers: { Authorization: `Bearer ${target.apiKey}` },
|
|
7
|
+
handshakeTimeout: target.handshakeTimeoutMs,
|
|
8
|
+
});
|
|
9
|
+
await openSocket(ws, target.signal);
|
|
10
|
+
const socket = {
|
|
11
|
+
get state() {
|
|
12
|
+
switch (ws.readyState) {
|
|
13
|
+
case WebSocket.CONNECTING:
|
|
14
|
+
return "connecting";
|
|
15
|
+
case WebSocket.OPEN:
|
|
16
|
+
return "open";
|
|
17
|
+
case WebSocket.CLOSING:
|
|
18
|
+
return "closing";
|
|
19
|
+
default:
|
|
20
|
+
return "closed";
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
send: (data) => {
|
|
24
|
+
ws.send(data, (error) => {
|
|
25
|
+
if (error)
|
|
26
|
+
socket.onError?.(errorMessage(error));
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
close: (code, reason) => ws.close(code, reason),
|
|
30
|
+
onMessage: undefined,
|
|
31
|
+
onError: undefined,
|
|
32
|
+
onClose: undefined,
|
|
33
|
+
};
|
|
34
|
+
ws.on("message", (data) => socket.onMessage?.(rawDataToString(data)));
|
|
35
|
+
ws.on("error", (error) => socket.onError?.(errorMessage(error)));
|
|
36
|
+
ws.on("close", (code) => socket.onClose?.(code));
|
|
37
|
+
return socket;
|
|
38
|
+
};
|
|
39
|
+
function openSocket(socket, signal) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const settle = (result) => {
|
|
42
|
+
socket.off("open", onOpen);
|
|
43
|
+
socket.off("error", onError);
|
|
44
|
+
socket.off("close", onClose);
|
|
45
|
+
signal.removeEventListener("abort", onAbort);
|
|
46
|
+
result();
|
|
47
|
+
};
|
|
48
|
+
const onOpen = () => settle(resolve);
|
|
49
|
+
const onError = (error) => settle(() => reject(error));
|
|
50
|
+
const onClose = (code) => settle(() => reject(new Error(`Socket closed during handshake with code ${code}`)));
|
|
51
|
+
const onAbort = () => settle(() => {
|
|
52
|
+
socket.terminate();
|
|
53
|
+
reject(new Error(`Connection aborted: ${String(signal.reason ?? "aborted")}`));
|
|
54
|
+
});
|
|
55
|
+
if (signal.aborted) {
|
|
56
|
+
onAbort();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
socket.once("open", onOpen);
|
|
60
|
+
socket.once("error", onError);
|
|
61
|
+
socket.once("close", onClose);
|
|
62
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function rawDataToString(data) {
|
|
66
|
+
if (Array.isArray(data))
|
|
67
|
+
return Buffer.concat(data).toString("utf8");
|
|
68
|
+
if (data instanceof ArrayBuffer)
|
|
69
|
+
return Buffer.from(data).toString("utf8");
|
|
70
|
+
return data.toString("utf8");
|
|
71
|
+
}
|
|
72
|
+
function errorMessage(error) {
|
|
73
|
+
return error instanceof Error ? error.message : String(error);
|
|
74
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CreateProviderSessionOptions, VoiceProvider, VoiceProviderSession } from "./contract.js";
|
|
2
|
+
import { type RealtimeSocketFactory } from "./realtime-socket.js";
|
|
3
|
+
export interface OpenAIRealtimeProviderOptions {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** Transport override; defaults to the runtime's registered factory (Node ws or Workers). */
|
|
6
|
+
socketFactory?: RealtimeSocketFactory;
|
|
7
|
+
/** Realtime-capable model id. Swap this for gpt-live once its API is public. */
|
|
8
|
+
model?: string;
|
|
9
|
+
/** WebSocket endpoint, overridable for tests and proxies. */
|
|
10
|
+
url?: string;
|
|
11
|
+
/** Output voice name. Omitted by default so the API default applies. */
|
|
12
|
+
voice?: string;
|
|
13
|
+
/** Sample rate of the PCM16 mono input the harness will send. */
|
|
14
|
+
inputSampleRate?: number;
|
|
15
|
+
/** Server-side turn detection. Defaults to semantic VAD; null disables it for push-to-talk. */
|
|
16
|
+
turnDetection?: Record<string, unknown> | null;
|
|
17
|
+
/** Input transcription model; final transcripts surface as transcript events. Null disables. */
|
|
18
|
+
transcriptionModel?: string | null;
|
|
19
|
+
/** Request a spoken response when a background task settles. */
|
|
20
|
+
announceTaskSettlement?: boolean;
|
|
21
|
+
handshakeTimeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Adapts the OpenAI Realtime API to the XO duplex provider contract.
|
|
25
|
+
*
|
|
26
|
+
* The Realtime API is turn-based under the hood: server-side VAD decides when the model
|
|
27
|
+
* takes the floor, and barge-in works by truncating the assistant item at the last
|
|
28
|
+
* acknowledged playout position. The shared realtime session absorbs that so the harness
|
|
29
|
+
* can treat the model as fully duplex.
|
|
30
|
+
*/
|
|
31
|
+
export declare class OpenAIRealtimeVoiceProvider implements VoiceProvider {
|
|
32
|
+
#private;
|
|
33
|
+
readonly id = "openai-realtime";
|
|
34
|
+
readonly capabilities: {
|
|
35
|
+
readonly continuousInputDuringOutput: true;
|
|
36
|
+
readonly modelManagedFloor: true;
|
|
37
|
+
readonly supportsToolCalls: true;
|
|
38
|
+
readonly supportsAsyncContext: true;
|
|
39
|
+
readonly supportsPlayoutAcknowledgements: true;
|
|
40
|
+
};
|
|
41
|
+
constructor(options: OpenAIRealtimeProviderOptions);
|
|
42
|
+
createSession(options: CreateProviderSessionOptions): Promise<VoiceProviderSession>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { RealtimeVoiceSession, toRealtimeFunctionTool } from "./realtime-session.js";
|
|
2
|
+
import { connectRealtimeSocket } from "./realtime-socket.js";
|
|
3
|
+
const DEFAULT_URL = "wss://api.openai.com/v1/realtime";
|
|
4
|
+
const DEFAULT_MODEL = "gpt-realtime-2";
|
|
5
|
+
const DEFAULT_SAMPLE_RATE = 24_000;
|
|
6
|
+
const DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe";
|
|
7
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 15_000;
|
|
8
|
+
/**
|
|
9
|
+
* Adapts the OpenAI Realtime API to the XO duplex provider contract.
|
|
10
|
+
*
|
|
11
|
+
* The Realtime API is turn-based under the hood: server-side VAD decides when the model
|
|
12
|
+
* takes the floor, and barge-in works by truncating the assistant item at the last
|
|
13
|
+
* acknowledged playout position. The shared realtime session absorbs that so the harness
|
|
14
|
+
* can treat the model as fully duplex.
|
|
15
|
+
*/
|
|
16
|
+
export class OpenAIRealtimeVoiceProvider {
|
|
17
|
+
id = "openai-realtime";
|
|
18
|
+
capabilities = {
|
|
19
|
+
continuousInputDuringOutput: true,
|
|
20
|
+
modelManagedFloor: true,
|
|
21
|
+
supportsToolCalls: true,
|
|
22
|
+
supportsAsyncContext: true,
|
|
23
|
+
supportsPlayoutAcknowledgements: true,
|
|
24
|
+
};
|
|
25
|
+
#options;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
if (!options.apiKey)
|
|
28
|
+
throw new Error("OpenAIRealtimeVoiceProvider requires an apiKey");
|
|
29
|
+
this.#options = options;
|
|
30
|
+
}
|
|
31
|
+
async createSession(options) {
|
|
32
|
+
const inputSampleRate = this.#options.inputSampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
33
|
+
const socket = await connectRealtimeSocket({
|
|
34
|
+
url: this.#options.url ?? DEFAULT_URL,
|
|
35
|
+
model: this.#options.model ?? DEFAULT_MODEL,
|
|
36
|
+
apiKey: this.#options.apiKey,
|
|
37
|
+
authProtocols: ["realtime", `openai-insecure-api-key.${this.#options.apiKey}`],
|
|
38
|
+
handshakeTimeoutMs: this.#options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS,
|
|
39
|
+
signal: options.signal,
|
|
40
|
+
...(this.#options.socketFactory === undefined ? {} : { factory: this.#options.socketFactory }),
|
|
41
|
+
});
|
|
42
|
+
return new RealtimeVoiceSession(socket, {
|
|
43
|
+
label: "OpenAI Realtime",
|
|
44
|
+
sessionUpdate: this.#sessionUpdate(options, inputSampleRate),
|
|
45
|
+
inputSampleRate,
|
|
46
|
+
outputSampleRate: DEFAULT_SAMPLE_RATE,
|
|
47
|
+
announceTaskSettlement: this.#options.announceTaskSettlement ?? true,
|
|
48
|
+
}, options);
|
|
49
|
+
}
|
|
50
|
+
#sessionUpdate(options, inputSampleRate) {
|
|
51
|
+
const turnDetection = this.#options.turnDetection === undefined ? { type: "semantic_vad" } : this.#options.turnDetection;
|
|
52
|
+
const transcriptionModel = this.#options.transcriptionModel === undefined
|
|
53
|
+
? DEFAULT_TRANSCRIPTION_MODEL
|
|
54
|
+
: this.#options.transcriptionModel;
|
|
55
|
+
const session = {
|
|
56
|
+
type: "realtime",
|
|
57
|
+
model: this.#options.model ?? DEFAULT_MODEL,
|
|
58
|
+
output_modalities: [...(options.outputModalities ?? ["audio"])],
|
|
59
|
+
audio: {
|
|
60
|
+
input: {
|
|
61
|
+
format: { type: "audio/pcm", rate: inputSampleRate },
|
|
62
|
+
turn_detection: turnDetection,
|
|
63
|
+
...(transcriptionModel === null ? {} : { transcription: { model: transcriptionModel } }),
|
|
64
|
+
},
|
|
65
|
+
output: {
|
|
66
|
+
format: { type: "audio/pcm", rate: DEFAULT_SAMPLE_RATE },
|
|
67
|
+
...(this.#options.voice === undefined ? {} : { voice: this.#options.voice }),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
tools: options.tools.map(toRealtimeFunctionTool),
|
|
71
|
+
};
|
|
72
|
+
if (options.instructions !== undefined)
|
|
73
|
+
session.instructions = options.instructions;
|
|
74
|
+
return session;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type AudioChunk, type PlayoutProgress, type ProviderContextEvent, type ProviderEvent, type ProviderToolDefinition, type ProviderToolResult } from "../protocol/index.js";
|
|
2
|
+
import type { CreateProviderSessionOptions, VoiceProviderSession } from "./contract.js";
|
|
3
|
+
import type { RealtimeSocket } from "./realtime-socket.js";
|
|
4
|
+
/**
|
|
5
|
+
* Wire-level configuration for one realtime session. Providers own the dialect —
|
|
6
|
+
* the exact session.update payload — while this session owns the shared duplex
|
|
7
|
+
* bridge: continuous input, sample-addressed output, tool calls, context items,
|
|
8
|
+
* and barge-in truncation at the last acknowledged playout sample.
|
|
9
|
+
*/
|
|
10
|
+
export interface RealtimeWireOptions {
|
|
11
|
+
/** Provider label used in error messages, e.g. "OpenAI Realtime". */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Complete session.update payload sent once the socket opens. */
|
|
14
|
+
sessionUpdate: Record<string, unknown>;
|
|
15
|
+
inputSampleRate: number;
|
|
16
|
+
outputSampleRate: number;
|
|
17
|
+
announceTaskSettlement: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class RealtimeVoiceSession implements VoiceProviderSession {
|
|
20
|
+
#private;
|
|
21
|
+
constructor(socket: RealtimeSocket, wire: RealtimeWireOptions, options: CreateProviderSessionOptions);
|
|
22
|
+
get events(): AsyncIterable<ProviderEvent>;
|
|
23
|
+
sendAudio(chunk: AudioChunk): Promise<void>;
|
|
24
|
+
sendText(text: string): Promise<void>;
|
|
25
|
+
reportPlayout(progress: PlayoutProgress): Promise<void>;
|
|
26
|
+
sendToolResult(result: ProviderToolResult): Promise<void>;
|
|
27
|
+
sendContext(event: ProviderContextEvent): Promise<void>;
|
|
28
|
+
close(reason?: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export declare function toRealtimeFunctionTool(definition: ProviderToolDefinition): Record<string, unknown>;
|