@kuralle-syrinx/realtime 4.2.0 → 4.3.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 +28 -0
- package/package.json +4 -4
- package/src/from-gemini-live.ts +147 -9
- package/src/from-openai-realtime.ts +29 -5
- package/src/index.ts +6 -1
- package/src/openai-compatible-realtime.ts +53 -3
- package/src/realtime-adapter.ts +11 -1
- package/src/realtime-bridge.ts +126 -7
- package/src/edge-safety.test.ts +0 -173
- package/src/from-gemini-live.test.ts +0 -241
- package/src/from-openai-realtime.test.ts +0 -676
- package/src/gemini-translate.test.ts +0 -82
- package/src/openai-compatible-realtime.test.ts +0 -188
- package/src/realtime-bridge.test.ts +0 -1358
- package/src/workers-seam.test.ts +0 -143
- package/tsconfig.json +0 -21
package/src/workers-seam.test.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
//
|
|
3
|
-
// Proves fromOpenAIRealtime composes with createWorkersSocket: fetch-upgrade uses
|
|
4
|
-
// https:// (not wss://), carries Authorization: Bearer, accept()s the socket,
|
|
5
|
-
// and round-trips provider audio into RealtimeEvent.
|
|
6
|
-
|
|
7
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
8
|
-
|
|
9
|
-
import { createWorkersSocket } from "@kuralle-syrinx/ws/workers";
|
|
10
|
-
import type { WebSocketEventLike, WebSocketLike } from "@kuralle-syrinx/ws/web";
|
|
11
|
-
|
|
12
|
-
import { bytesToBase64, fromOpenAIRealtime } from "./from-openai-realtime.js";
|
|
13
|
-
import type { RealtimeEvent } from "./realtime-adapter.js";
|
|
14
|
-
|
|
15
|
-
interface MockWorkerdHarness {
|
|
16
|
-
readonly fetchCalls: Array<{ url: string; headers: Record<string, string> }>;
|
|
17
|
-
readonly sent: string[];
|
|
18
|
-
readonly acceptCalled: boolean;
|
|
19
|
-
inject(msg: Record<string, unknown>): void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function installMockWorkerdFetch(): MockWorkerdHarness {
|
|
23
|
-
const fetchCalls: Array<{ url: string; headers: Record<string, string> }> = [];
|
|
24
|
-
const sent: string[] = [];
|
|
25
|
-
let acceptCalled = false;
|
|
26
|
-
const messageListeners = new Set<(event: WebSocketEventLike) => void>();
|
|
27
|
-
|
|
28
|
-
const mockWs: WebSocketLike & { accept(): void } = {
|
|
29
|
-
readyState: 1,
|
|
30
|
-
binaryType: "arraybuffer",
|
|
31
|
-
accept: () => {
|
|
32
|
-
acceptCalled = true;
|
|
33
|
-
},
|
|
34
|
-
send: (data) => {
|
|
35
|
-
if (typeof data === "string") sent.push(data);
|
|
36
|
-
},
|
|
37
|
-
close: () => {},
|
|
38
|
-
addEventListener: (type, listener) => {
|
|
39
|
-
if (type === "message") messageListeners.add(listener);
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const originalFetch = globalThis.fetch;
|
|
44
|
-
globalThis.fetch = async (input, init) => {
|
|
45
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : String(input);
|
|
46
|
-
const headers = (init?.headers ?? {}) as Record<string, string>;
|
|
47
|
-
fetchCalls.push({ url, headers });
|
|
48
|
-
return { status: 101, webSocket: mockWs } as unknown as Response;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
fetchCalls,
|
|
53
|
-
sent,
|
|
54
|
-
get acceptCalled() {
|
|
55
|
-
return acceptCalled;
|
|
56
|
-
},
|
|
57
|
-
inject: (msg) => {
|
|
58
|
-
const payload = JSON.stringify(msg);
|
|
59
|
-
for (const listener of messageListeners) listener({ data: payload });
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let restoreFetch: (() => void) | null = null;
|
|
65
|
-
|
|
66
|
-
afterEach(() => {
|
|
67
|
-
restoreFetch?.();
|
|
68
|
-
restoreFetch = null;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
async function collectEvents(
|
|
72
|
-
events: AsyncIterable<RealtimeEvent>,
|
|
73
|
-
max = 4,
|
|
74
|
-
): Promise<RealtimeEvent[]> {
|
|
75
|
-
const out: RealtimeEvent[] = [];
|
|
76
|
-
for await (const event of events) {
|
|
77
|
-
out.push(event);
|
|
78
|
-
if (out.length >= max) break;
|
|
79
|
-
}
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function waitFor(predicate: () => boolean, timeoutMs = 2000): Promise<void> {
|
|
84
|
-
const startedAt = Date.now();
|
|
85
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
86
|
-
if (predicate()) return;
|
|
87
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
88
|
-
}
|
|
89
|
-
throw new Error("Timed out waiting for condition");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
describe("Workers seam", () => {
|
|
93
|
-
it("opens via createWorkersSocket fetch-upgrade and round-trips audio events", async () => {
|
|
94
|
-
const harness = installMockWorkerdFetch();
|
|
95
|
-
const originalFetch = globalThis.fetch;
|
|
96
|
-
restoreFetch = () => {
|
|
97
|
-
globalThis.fetch = originalFetch;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const adapter = fromOpenAIRealtime({
|
|
101
|
-
apiKey: "workers-test-key",
|
|
102
|
-
socketFactory: createWorkersSocket,
|
|
103
|
-
url: () => "wss://api.openai.com/v1/realtime?model=gpt-realtime-2",
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const eventsTask = collectEvents(adapter.events, 2);
|
|
107
|
-
const openTask = adapter.open(new AbortController().signal);
|
|
108
|
-
|
|
109
|
-
await waitFor(() => harness.fetchCalls.length > 0);
|
|
110
|
-
await waitFor(() => harness.sent.length > 0);
|
|
111
|
-
|
|
112
|
-
expect(harness.fetchCalls[0]!.url).toBe(
|
|
113
|
-
"https://api.openai.com/v1/realtime?model=gpt-realtime-2",
|
|
114
|
-
);
|
|
115
|
-
expect(harness.fetchCalls[0]!.url).not.toMatch(/^wss:\/\//);
|
|
116
|
-
expect(harness.fetchCalls[0]!.headers).toMatchObject({
|
|
117
|
-
Authorization: "Bearer workers-test-key",
|
|
118
|
-
Upgrade: "websocket",
|
|
119
|
-
});
|
|
120
|
-
expect(harness.acceptCalled).toBe(true);
|
|
121
|
-
expect(JSON.parse(harness.sent[0]!)["type"]).toBe("session.update");
|
|
122
|
-
|
|
123
|
-
harness.inject({ type: "session.updated" });
|
|
124
|
-
await openTask;
|
|
125
|
-
|
|
126
|
-
const audioBytes = new Uint8Array([9, 10, 11, 12]);
|
|
127
|
-
harness.inject({ type: "response.created" });
|
|
128
|
-
harness.inject({
|
|
129
|
-
type: "response.output_audio.delta",
|
|
130
|
-
delta: bytesToBase64(audioBytes),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const events = await eventsTask;
|
|
134
|
-
const audioEvent = events.find((e) => e.type === "audio");
|
|
135
|
-
expect(audioEvent).toEqual({
|
|
136
|
-
type: "audio",
|
|
137
|
-
pcm16: audioBytes,
|
|
138
|
-
sampleRateHz: 24_000,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
await adapter.close();
|
|
142
|
-
});
|
|
143
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"noUncheckedIndexedAccess": true,
|
|
9
|
-
"noImplicitReturns": true,
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"declarationMap": true,
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"outDir": "./dist",
|
|
14
|
-
"rootDir": "./src",
|
|
15
|
-
"esModuleInterop": true,
|
|
16
|
-
"skipLibCheck": true,
|
|
17
|
-
"forceConsistentCasingInFileNames": true
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*.ts"],
|
|
20
|
-
"exclude": ["node_modules", "dist"]
|
|
21
|
-
}
|