@livekit/agents-plugin-openai 1.0.49 → 1.0.51

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 (75) hide show
  1. package/dist/index.cjs +5 -2
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/llm.test.cjs +31 -16
  9. package/dist/llm.test.cjs.map +1 -1
  10. package/dist/llm.test.js +32 -17
  11. package/dist/llm.test.js.map +1 -1
  12. package/dist/responses/llm.cjs +71 -16
  13. package/dist/responses/llm.cjs.map +1 -1
  14. package/dist/responses/llm.d.cts +10 -25
  15. package/dist/responses/llm.d.ts +10 -25
  16. package/dist/responses/llm.d.ts.map +1 -1
  17. package/dist/responses/llm.js +71 -14
  18. package/dist/responses/llm.js.map +1 -1
  19. package/dist/responses/llm.test.cjs +32 -17
  20. package/dist/responses/llm.test.cjs.map +1 -1
  21. package/dist/responses/llm.test.js +33 -18
  22. package/dist/responses/llm.test.js.map +1 -1
  23. package/dist/stt.cjs +7 -3
  24. package/dist/stt.cjs.map +1 -1
  25. package/dist/stt.d.ts.map +1 -1
  26. package/dist/stt.js +8 -4
  27. package/dist/stt.js.map +1 -1
  28. package/dist/stt.test.cjs +11 -3
  29. package/dist/stt.test.cjs.map +1 -1
  30. package/dist/stt.test.js +12 -4
  31. package/dist/stt.test.js.map +1 -1
  32. package/dist/tts.test.cjs +11 -3
  33. package/dist/tts.test.cjs.map +1 -1
  34. package/dist/tts.test.js +12 -4
  35. package/dist/tts.test.js.map +1 -1
  36. package/dist/ws/index.cjs +29 -0
  37. package/dist/ws/index.cjs.map +1 -0
  38. package/dist/ws/index.d.cts +3 -0
  39. package/dist/ws/index.d.ts +3 -0
  40. package/dist/ws/index.d.ts.map +1 -0
  41. package/dist/ws/index.js +5 -0
  42. package/dist/ws/index.js.map +1 -0
  43. package/dist/ws/llm.cjs +502 -0
  44. package/dist/ws/llm.cjs.map +1 -0
  45. package/dist/ws/llm.d.cts +74 -0
  46. package/dist/ws/llm.d.ts +74 -0
  47. package/dist/ws/llm.d.ts.map +1 -0
  48. package/dist/ws/llm.js +485 -0
  49. package/dist/ws/llm.js.map +1 -0
  50. package/dist/ws/llm.test.cjs +26 -0
  51. package/dist/ws/llm.test.cjs.map +1 -0
  52. package/dist/ws/llm.test.d.cts +2 -0
  53. package/dist/ws/llm.test.d.ts +2 -0
  54. package/dist/ws/llm.test.d.ts.map +1 -0
  55. package/dist/ws/llm.test.js +25 -0
  56. package/dist/ws/llm.test.js.map +1 -0
  57. package/dist/ws/types.cjs +128 -0
  58. package/dist/ws/types.cjs.map +1 -0
  59. package/dist/ws/types.d.cts +167 -0
  60. package/dist/ws/types.d.ts +167 -0
  61. package/dist/ws/types.d.ts.map +1 -0
  62. package/dist/ws/types.js +95 -0
  63. package/dist/ws/types.js.map +1 -0
  64. package/package.json +6 -5
  65. package/src/index.ts +1 -0
  66. package/src/llm.test.ts +31 -17
  67. package/src/responses/llm.test.ts +32 -18
  68. package/src/responses/llm.ts +105 -19
  69. package/src/stt.test.ts +12 -4
  70. package/src/stt.ts +8 -4
  71. package/src/tts.test.ts +12 -4
  72. package/src/ws/index.ts +17 -0
  73. package/src/ws/llm.test.ts +30 -0
  74. package/src/ws/llm.ts +665 -0
  75. package/src/ws/types.ts +131 -0
@@ -0,0 +1,74 @@
1
+ import type { APIConnectOptions } from '@livekit/agents';
2
+ import { ConnectionPool, llm, stream } from '@livekit/agents';
3
+ import { WebSocket } from 'ws';
4
+ import type { ChatModels } from '../models.js';
5
+ import type { WsResponseCreateEvent, WsServerEvent } from './types.js';
6
+ export declare class ResponsesWebSocket {
7
+ #private;
8
+ constructor(ws: WebSocket);
9
+ /**
10
+ * Send a response.create event. Returns a typed `StreamChannel<WsServerEvent>`
11
+ * that yields validated server events until the response terminates.
12
+ */
13
+ sendRequest(payload: WsResponseCreateEvent): stream.StreamChannel<WsServerEvent>;
14
+ close(): void;
15
+ }
16
+ export interface WSLLMOptions {
17
+ model: string | ChatModels;
18
+ apiKey?: string;
19
+ baseURL?: string;
20
+ temperature?: number;
21
+ parallelToolCalls?: boolean;
22
+ toolChoice?: llm.ToolChoice;
23
+ store?: boolean;
24
+ metadata?: Record<string, string>;
25
+ strictToolSchema?: boolean;
26
+ }
27
+ export declare class WSLLM extends llm.LLM {
28
+ #private;
29
+ /**
30
+ * Create a new instance of the OpenAI Responses API WebSocket LLM.
31
+ *
32
+ * @remarks
33
+ * `apiKey` must be set to your OpenAI API key, either using the argument or
34
+ * by setting the `OPENAI_API_KEY` environment variable.
35
+ *
36
+ * A persistent WebSocket connection to `/v1/responses` is maintained and
37
+ * reused across turns, reducing per-turn continuation overhead for
38
+ * tool-call-heavy workflows.
39
+ */
40
+ constructor(opts?: Partial<WSLLMOptions>);
41
+ label(): string;
42
+ get model(): string;
43
+ prewarm(): void;
44
+ close(): Promise<void>;
45
+ aclose(): Promise<void>;
46
+ /** Called by LLMStream once response.created fires to atomically persist both the
47
+ * response ID and its corresponding chat context for the next turn's diff. */
48
+ _onResponseCreated(responseId: string, chatCtx: llm.ChatContext): void;
49
+ _setPendingToolCalls(callIds: Set<string>): void;
50
+ chat({ chatCtx, toolCtx, connOptions, parallelToolCalls, toolChoice, extraKwargs, }: {
51
+ chatCtx: llm.ChatContext;
52
+ toolCtx?: llm.ToolContext;
53
+ connOptions?: APIConnectOptions;
54
+ parallelToolCalls?: boolean;
55
+ toolChoice?: llm.ToolChoice;
56
+ extraKwargs?: Record<string, unknown>;
57
+ }): WSLLMStream;
58
+ }
59
+ export declare class WSLLMStream extends llm.LLMStream {
60
+ #private;
61
+ constructor(llm: WSLLM, { pool, model, chatCtx, fullChatCtx, toolCtx, connOptions, modelOptions, prevResponseId, strictToolSchema, }: {
62
+ pool: ConnectionPool<ResponsesWebSocket>;
63
+ model: string | ChatModels;
64
+ chatCtx: llm.ChatContext;
65
+ fullChatCtx: llm.ChatContext;
66
+ toolCtx?: llm.ToolContext;
67
+ connOptions: APIConnectOptions;
68
+ modelOptions: Record<string, unknown>;
69
+ prevResponseId?: string;
70
+ strictToolSchema: boolean;
71
+ });
72
+ protected run(): Promise<void>;
73
+ }
74
+ //# sourceMappingURL=llm.d.ts.map
@@ -0,0 +1,74 @@
1
+ import type { APIConnectOptions } from '@livekit/agents';
2
+ import { ConnectionPool, llm, stream } from '@livekit/agents';
3
+ import { WebSocket } from 'ws';
4
+ import type { ChatModels } from '../models.js';
5
+ import type { WsResponseCreateEvent, WsServerEvent } from './types.js';
6
+ export declare class ResponsesWebSocket {
7
+ #private;
8
+ constructor(ws: WebSocket);
9
+ /**
10
+ * Send a response.create event. Returns a typed `StreamChannel<WsServerEvent>`
11
+ * that yields validated server events until the response terminates.
12
+ */
13
+ sendRequest(payload: WsResponseCreateEvent): stream.StreamChannel<WsServerEvent>;
14
+ close(): void;
15
+ }
16
+ export interface WSLLMOptions {
17
+ model: string | ChatModels;
18
+ apiKey?: string;
19
+ baseURL?: string;
20
+ temperature?: number;
21
+ parallelToolCalls?: boolean;
22
+ toolChoice?: llm.ToolChoice;
23
+ store?: boolean;
24
+ metadata?: Record<string, string>;
25
+ strictToolSchema?: boolean;
26
+ }
27
+ export declare class WSLLM extends llm.LLM {
28
+ #private;
29
+ /**
30
+ * Create a new instance of the OpenAI Responses API WebSocket LLM.
31
+ *
32
+ * @remarks
33
+ * `apiKey` must be set to your OpenAI API key, either using the argument or
34
+ * by setting the `OPENAI_API_KEY` environment variable.
35
+ *
36
+ * A persistent WebSocket connection to `/v1/responses` is maintained and
37
+ * reused across turns, reducing per-turn continuation overhead for
38
+ * tool-call-heavy workflows.
39
+ */
40
+ constructor(opts?: Partial<WSLLMOptions>);
41
+ label(): string;
42
+ get model(): string;
43
+ prewarm(): void;
44
+ close(): Promise<void>;
45
+ aclose(): Promise<void>;
46
+ /** Called by LLMStream once response.created fires to atomically persist both the
47
+ * response ID and its corresponding chat context for the next turn's diff. */
48
+ _onResponseCreated(responseId: string, chatCtx: llm.ChatContext): void;
49
+ _setPendingToolCalls(callIds: Set<string>): void;
50
+ chat({ chatCtx, toolCtx, connOptions, parallelToolCalls, toolChoice, extraKwargs, }: {
51
+ chatCtx: llm.ChatContext;
52
+ toolCtx?: llm.ToolContext;
53
+ connOptions?: APIConnectOptions;
54
+ parallelToolCalls?: boolean;
55
+ toolChoice?: llm.ToolChoice;
56
+ extraKwargs?: Record<string, unknown>;
57
+ }): WSLLMStream;
58
+ }
59
+ export declare class WSLLMStream extends llm.LLMStream {
60
+ #private;
61
+ constructor(llm: WSLLM, { pool, model, chatCtx, fullChatCtx, toolCtx, connOptions, modelOptions, prevResponseId, strictToolSchema, }: {
62
+ pool: ConnectionPool<ResponsesWebSocket>;
63
+ model: string | ChatModels;
64
+ chatCtx: llm.ChatContext;
65
+ fullChatCtx: llm.ChatContext;
66
+ toolCtx?: llm.ToolContext;
67
+ connOptions: APIConnectOptions;
68
+ modelOptions: Record<string, unknown>;
69
+ prevResponseId?: string;
70
+ strictToolSchema: boolean;
71
+ });
72
+ protected run(): Promise<void>;
73
+ }
74
+ //# sourceMappingURL=llm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../src/ws/llm.ts"],"names":[],"mappings":"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"}
package/dist/ws/llm.js ADDED
@@ -0,0 +1,485 @@
1
+ import {
2
+ APIConnectionError,
3
+ APIStatusError,
4
+ APITimeoutError,
5
+ ConnectionPool,
6
+ DEFAULT_API_CONNECT_OPTIONS,
7
+ llm,
8
+ stream,
9
+ toError
10
+ } from "@livekit/agents";
11
+ import { WebSocket } from "ws";
12
+ import { wsServerEventSchema } from "./types.js";
13
+ const OPENAI_RESPONSES_WS_URL = "wss://api.openai.com/v1/responses";
14
+ const WS_MAX_SESSION_DURATION = 36e5;
15
+ class ResponsesWebSocket {
16
+ #ws;
17
+ // FIFO queue: the front entry receives validated WsServerEvents for the in-flight response.
18
+ #outputQueue = [];
19
+ constructor(ws) {
20
+ this.#ws = ws;
21
+ ws.on("message", (data) => {
22
+ const current = this.#outputQueue[0];
23
+ if (!current) return;
24
+ let raw;
25
+ try {
26
+ raw = JSON.parse(data.toString());
27
+ } catch {
28
+ return;
29
+ }
30
+ const parsed = wsServerEventSchema.safeParse(raw);
31
+ if (!parsed.success) return;
32
+ const event = parsed.data;
33
+ void current.write(event);
34
+ if (event.type === "response.completed" || event.type === "response.failed" || event.type === "error") {
35
+ void current.close();
36
+ this.#outputQueue.shift();
37
+ }
38
+ });
39
+ ws.on("close", () => {
40
+ for (const current of this.#outputQueue) {
41
+ if (!current.closed) {
42
+ const closeError = {
43
+ type: "error",
44
+ error: {
45
+ code: "websocket_closed",
46
+ message: "OpenAI Responses WebSocket closed unexpectedly"
47
+ }
48
+ };
49
+ void current.write(closeError).finally(() => current.close());
50
+ }
51
+ }
52
+ this.#outputQueue = [];
53
+ });
54
+ }
55
+ /**
56
+ * Send a response.create event. Returns a typed `StreamChannel<WsServerEvent>`
57
+ * that yields validated server events until the response terminates.
58
+ */
59
+ sendRequest(payload) {
60
+ if (this.#ws.readyState !== WebSocket.OPEN) {
61
+ throw new APIConnectionError({
62
+ message: `OpenAI Responses WebSocket is not open (state ${getWebSocketStateLabel(this.#ws.readyState)})`,
63
+ options: { retryable: true }
64
+ });
65
+ }
66
+ const channel = stream.createStreamChannel();
67
+ this.#outputQueue.push(channel);
68
+ this.#ws.send(JSON.stringify(payload));
69
+ return channel;
70
+ }
71
+ close() {
72
+ for (const ch of this.#outputQueue) {
73
+ void ch.close();
74
+ }
75
+ this.#outputQueue = [];
76
+ this.#ws.close();
77
+ }
78
+ }
79
+ const defaultLLMOptions = {
80
+ model: "gpt-4.1",
81
+ apiKey: process.env.OPENAI_API_KEY,
82
+ strictToolSchema: true
83
+ };
84
+ class WSLLM extends llm.LLM {
85
+ #opts;
86
+ #pool;
87
+ #prevResponseId = "";
88
+ #prevChatCtx = null;
89
+ #pendingToolCalls = /* @__PURE__ */ new Set();
90
+ /**
91
+ * Create a new instance of the OpenAI Responses API WebSocket LLM.
92
+ *
93
+ * @remarks
94
+ * `apiKey` must be set to your OpenAI API key, either using the argument or
95
+ * by setting the `OPENAI_API_KEY` environment variable.
96
+ *
97
+ * A persistent WebSocket connection to `/v1/responses` is maintained and
98
+ * reused across turns, reducing per-turn continuation overhead for
99
+ * tool-call-heavy workflows.
100
+ */
101
+ constructor(opts = defaultLLMOptions) {
102
+ super();
103
+ this.#opts = { ...defaultLLMOptions, ...opts };
104
+ if (!this.#opts.apiKey) {
105
+ throw new Error("OpenAI API key is required, whether as an argument or as $OPENAI_API_KEY");
106
+ }
107
+ this.#pool = new ConnectionPool({
108
+ maxSessionDuration: WS_MAX_SESSION_DURATION,
109
+ connectCb: async (timeoutMs) => {
110
+ const wsUrl = this.#opts.baseURL ? `${this.#opts.baseURL.replace(/^https?/, "wss").replace(/\/+$/, "")}/responses` : OPENAI_RESPONSES_WS_URL;
111
+ const ws = await connectWs(wsUrl, this.#opts.apiKey, timeoutMs);
112
+ return new ResponsesWebSocket(ws);
113
+ },
114
+ closeCb: async (conn) => {
115
+ conn.close();
116
+ }
117
+ });
118
+ }
119
+ label() {
120
+ return "openai.ws.LLM";
121
+ }
122
+ get model() {
123
+ return this.#opts.model;
124
+ }
125
+ prewarm() {
126
+ this.#pool.prewarm();
127
+ }
128
+ async close() {
129
+ await this.#pool.close();
130
+ }
131
+ async aclose() {
132
+ await this.close();
133
+ }
134
+ /** Called by LLMStream once response.created fires to atomically persist both the
135
+ * response ID and its corresponding chat context for the next turn's diff. */
136
+ _onResponseCreated(responseId, chatCtx) {
137
+ this.#prevResponseId = responseId;
138
+ this.#prevChatCtx = chatCtx;
139
+ }
140
+ _setPendingToolCalls(callIds) {
141
+ this.#pendingToolCalls = callIds;
142
+ }
143
+ chat({
144
+ chatCtx,
145
+ toolCtx,
146
+ connOptions = DEFAULT_API_CONNECT_OPTIONS,
147
+ parallelToolCalls,
148
+ toolChoice,
149
+ extraKwargs
150
+ }) {
151
+ var _a;
152
+ const modelOptions = { ...extraKwargs ?? {} };
153
+ parallelToolCalls = parallelToolCalls !== void 0 ? parallelToolCalls : this.#opts.parallelToolCalls;
154
+ if (toolCtx && Object.keys(toolCtx).length > 0 && parallelToolCalls !== void 0) {
155
+ modelOptions.parallel_tool_calls = parallelToolCalls;
156
+ }
157
+ toolChoice = toolChoice !== void 0 ? toolChoice : this.#opts.toolChoice;
158
+ if (toolChoice) {
159
+ modelOptions.tool_choice = toolChoice;
160
+ }
161
+ if (this.#opts.temperature !== void 0) {
162
+ modelOptions.temperature = this.#opts.temperature;
163
+ }
164
+ if (this.#opts.store !== void 0) {
165
+ modelOptions.store = this.#opts.store;
166
+ }
167
+ if (this.#opts.metadata) {
168
+ modelOptions.metadata = this.#opts.metadata;
169
+ }
170
+ let inputChatCtx = chatCtx;
171
+ let prevResponseId;
172
+ const canUseStoredResponse = modelOptions.store !== false;
173
+ if (canUseStoredResponse && this.#prevChatCtx && this.#prevResponseId) {
174
+ const diff = llm.computeChatCtxDiff(this.#prevChatCtx, chatCtx);
175
+ const lastPrevItemId = ((_a = this.#prevChatCtx.items.at(-1)) == null ? void 0 : _a.id) ?? null;
176
+ if (diff.toRemove.length === 0 && diff.toCreate.length > 0 && diff.toCreate[0][0] === lastPrevItemId) {
177
+ const newItemIds = new Set(diff.toCreate.map(([, id]) => id));
178
+ const newItems = chatCtx.items.filter((item) => newItemIds.has(item.id));
179
+ const pendingToolCallsCompleted = this.#pendingToolCallsCompleted(newItems);
180
+ if (pendingToolCallsCompleted) {
181
+ inputChatCtx = new llm.ChatContext(newItems);
182
+ prevResponseId = this.#prevResponseId;
183
+ }
184
+ }
185
+ }
186
+ return new WSLLMStream(this, {
187
+ pool: this.#pool,
188
+ model: this.#opts.model,
189
+ chatCtx: inputChatCtx,
190
+ fullChatCtx: chatCtx,
191
+ toolCtx,
192
+ connOptions,
193
+ modelOptions,
194
+ prevResponseId,
195
+ strictToolSchema: this.#opts.strictToolSchema ?? true
196
+ });
197
+ }
198
+ #pendingToolCallsCompleted(items) {
199
+ if (this.#pendingToolCalls.size === 0) return true;
200
+ const completedCallIds = new Set(
201
+ items.filter((item) => item.type === "function_call_output").map((item) => item.callId)
202
+ );
203
+ return [...this.#pendingToolCalls].every((callId) => completedCallIds.has(callId));
204
+ }
205
+ }
206
+ class WSLLMStream extends llm.LLMStream {
207
+ #llm;
208
+ #pool;
209
+ #model;
210
+ #modelOptions;
211
+ #strictToolSchema;
212
+ #prevResponseId;
213
+ /** Full chat context — used as fallback when previous_response_id is stale. */
214
+ #fullChatCtx;
215
+ #responseId = "";
216
+ #pendingToolCalls = /* @__PURE__ */ new Set();
217
+ constructor(llm2, {
218
+ pool,
219
+ model,
220
+ chatCtx,
221
+ fullChatCtx,
222
+ toolCtx,
223
+ connOptions,
224
+ modelOptions,
225
+ prevResponseId,
226
+ strictToolSchema
227
+ }) {
228
+ super(llm2, { chatCtx, toolCtx, connOptions });
229
+ this.#llm = llm2;
230
+ this.#pool = pool;
231
+ this.#model = model;
232
+ this.#modelOptions = modelOptions;
233
+ this.#strictToolSchema = strictToolSchema;
234
+ this.#prevResponseId = prevResponseId;
235
+ this.#fullChatCtx = fullChatCtx;
236
+ }
237
+ async run() {
238
+ let retryable = true;
239
+ try {
240
+ await this.#pool.withConnection(async (conn) => {
241
+ const needsRetry = await this.#runWithConn(conn, this.chatCtx, this.#prevResponseId);
242
+ if (needsRetry) {
243
+ retryable = true;
244
+ await this.#runWithConn(conn, this.#fullChatCtx, void 0);
245
+ }
246
+ });
247
+ } catch (error) {
248
+ if (error instanceof APIStatusError || error instanceof APITimeoutError || error instanceof APIConnectionError) {
249
+ throw error;
250
+ }
251
+ throw new APIConnectionError({
252
+ message: toError(error).message,
253
+ options: { retryable }
254
+ });
255
+ }
256
+ }
257
+ /**
258
+ * Execute a single response.create round-trip on the given connection.
259
+ * Returns `true` when the caller should retry with the full chat context
260
+ * (i.e. `previous_response_not_found`), `false` otherwise.
261
+ */
262
+ async #runWithConn(conn, chatCtx, prevResponseId) {
263
+ const messages = await chatCtx.toProviderFormat(
264
+ "openai.responses"
265
+ );
266
+ const tools = this.toolCtx ? Object.entries(this.toolCtx).map(([name, func]) => {
267
+ const oaiParams = {
268
+ type: "function",
269
+ name,
270
+ description: func.description,
271
+ parameters: llm.toJsonSchema(
272
+ func.parameters,
273
+ true,
274
+ this.#strictToolSchema
275
+ )
276
+ };
277
+ if (this.#strictToolSchema) {
278
+ oaiParams.strict = true;
279
+ }
280
+ return oaiParams;
281
+ }) : void 0;
282
+ const requestOptions = { ...this.#modelOptions };
283
+ if (!tools) {
284
+ delete requestOptions.tool_choice;
285
+ }
286
+ const payload = {
287
+ type: "response.create",
288
+ model: this.#model,
289
+ input: messages,
290
+ tools: tools ?? [],
291
+ ...prevResponseId ? { previous_response_id: prevResponseId } : {},
292
+ ...requestOptions
293
+ };
294
+ let channel;
295
+ try {
296
+ channel = conn.sendRequest(payload);
297
+ } catch (error) {
298
+ if (error instanceof APIConnectionError) {
299
+ conn.close();
300
+ this.#pool.invalidate();
301
+ }
302
+ throw error;
303
+ }
304
+ const reader = channel.stream().getReader();
305
+ try {
306
+ while (true) {
307
+ const { done, value: event } = await reader.read();
308
+ if (done) break;
309
+ let chunk;
310
+ switch (event.type) {
311
+ case "error": {
312
+ const retry = this.#handleError(event, conn);
313
+ if (retry) return true;
314
+ break;
315
+ }
316
+ case "response.created":
317
+ this.#handleResponseCreated(event);
318
+ break;
319
+ case "response.output_item.done":
320
+ chunk = this.#handleOutputItemDone(event);
321
+ break;
322
+ case "response.output_text.delta":
323
+ chunk = this.#handleOutputTextDelta(event);
324
+ break;
325
+ case "response.completed":
326
+ chunk = this.#handleResponseCompleted(event);
327
+ break;
328
+ case "response.failed":
329
+ this.#handleResponseFailed(event);
330
+ break;
331
+ default:
332
+ break;
333
+ }
334
+ if (chunk) {
335
+ this.queue.put(chunk);
336
+ }
337
+ }
338
+ } finally {
339
+ reader.releaseLock();
340
+ }
341
+ return false;
342
+ }
343
+ /**
344
+ * Returns `true` when the caller should retry with full context
345
+ * (`previous_response_not_found`), throws for all other errors.
346
+ */
347
+ #handleError(event, conn) {
348
+ var _a, _b, _c;
349
+ const code = (_a = event.error) == null ? void 0 : _a.code;
350
+ if (code === "previous_response_not_found") {
351
+ return true;
352
+ }
353
+ if (code === "websocket_connection_limit_reached" || code === "websocket_closed") {
354
+ conn.close();
355
+ this.#pool.invalidate();
356
+ throw new APIConnectionError({
357
+ message: ((_b = event.error) == null ? void 0 : _b.message) ?? `WebSocket closed (${code})`,
358
+ options: { retryable: true }
359
+ });
360
+ }
361
+ throw new APIStatusError({
362
+ message: ((_c = event.error) == null ? void 0 : _c.message) ?? event.message ?? "Unknown error from OpenAI Responses WS",
363
+ options: {
364
+ statusCode: event.status ?? -1,
365
+ retryable: false
366
+ }
367
+ });
368
+ }
369
+ #handleResponseCreated(event) {
370
+ this.#responseId = event.response.id;
371
+ this.#llm._onResponseCreated(event.response.id, this.#fullChatCtx);
372
+ }
373
+ #handleOutputItemDone(event) {
374
+ if (event.item.type === "function_call") {
375
+ this.#pendingToolCalls.add(event.item.call_id);
376
+ return {
377
+ id: this.#responseId,
378
+ delta: {
379
+ role: "assistant",
380
+ content: void 0,
381
+ toolCalls: [
382
+ llm.FunctionCall.create({
383
+ callId: event.item.call_id,
384
+ name: event.item.name,
385
+ args: event.item.arguments
386
+ })
387
+ ]
388
+ }
389
+ };
390
+ }
391
+ return void 0;
392
+ }
393
+ #handleOutputTextDelta(event) {
394
+ return {
395
+ id: this.#responseId,
396
+ delta: {
397
+ role: "assistant",
398
+ content: event.delta
399
+ }
400
+ };
401
+ }
402
+ #handleResponseCompleted(event) {
403
+ this.#llm._setPendingToolCalls(this.#pendingToolCalls);
404
+ if (event.response.usage) {
405
+ return {
406
+ id: this.#responseId,
407
+ usage: {
408
+ completionTokens: event.response.usage.output_tokens,
409
+ promptTokens: event.response.usage.input_tokens,
410
+ promptCachedTokens: event.response.usage.input_tokens_details.cached_tokens,
411
+ totalTokens: event.response.usage.total_tokens
412
+ }
413
+ };
414
+ }
415
+ return void 0;
416
+ }
417
+ #handleResponseFailed(event) {
418
+ var _a, _b;
419
+ throw new APIStatusError({
420
+ message: ((_b = (_a = event.response) == null ? void 0 : _a.error) == null ? void 0 : _b.message) ?? "Response failed",
421
+ options: { statusCode: -1, retryable: false }
422
+ });
423
+ }
424
+ }
425
+ async function connectWs(url, apiKey, timeoutMs) {
426
+ return new Promise((resolve, reject) => {
427
+ const ws = new WebSocket(url, {
428
+ headers: { Authorization: `Bearer ${apiKey}` }
429
+ });
430
+ let settled = false;
431
+ const timer = setTimeout(() => {
432
+ settled = true;
433
+ ws.close();
434
+ reject(
435
+ new APIConnectionError({ message: "Timeout connecting to OpenAI Responses WebSocket" })
436
+ );
437
+ }, timeoutMs);
438
+ ws.once("open", () => {
439
+ if (settled) return;
440
+ settled = true;
441
+ clearTimeout(timer);
442
+ resolve(ws);
443
+ });
444
+ ws.once("error", (err) => {
445
+ if (settled) return;
446
+ settled = true;
447
+ clearTimeout(timer);
448
+ reject(
449
+ new APIConnectionError({
450
+ message: `Error connecting to OpenAI Responses WebSocket: ${err.message}`
451
+ })
452
+ );
453
+ });
454
+ ws.once("close", (code) => {
455
+ if (settled) return;
456
+ settled = true;
457
+ clearTimeout(timer);
458
+ reject(
459
+ new APIConnectionError({
460
+ message: `OpenAI Responses WebSocket closed unexpectedly during connect (code ${code})`
461
+ })
462
+ );
463
+ });
464
+ });
465
+ }
466
+ function getWebSocketStateLabel(readyState) {
467
+ switch (readyState) {
468
+ case WebSocket.CONNECTING:
469
+ return "CONNECTING";
470
+ case WebSocket.OPEN:
471
+ return "OPEN";
472
+ case WebSocket.CLOSING:
473
+ return "CLOSING";
474
+ case WebSocket.CLOSED:
475
+ return "CLOSED";
476
+ default:
477
+ return `UNKNOWN:${readyState}`;
478
+ }
479
+ }
480
+ export {
481
+ ResponsesWebSocket,
482
+ WSLLM,
483
+ WSLLMStream
484
+ };
485
+ //# sourceMappingURL=llm.js.map