@nodetool-ai/llm-nodes 0.8.1
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 +52 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/agent-control-tools.d.ts +22 -0
- package/dist/nodes/agent-control-tools.d.ts.map +1 -0
- package/dist/nodes/agent-control-tools.js +123 -0
- package/dist/nodes/agent-control-tools.js.map +1 -0
- package/dist/nodes/agent-loop.d.ts +73 -0
- package/dist/nodes/agent-loop.d.ts.map +1 -0
- package/dist/nodes/agent-loop.js +168 -0
- package/dist/nodes/agent-loop.js.map +1 -0
- package/dist/nodes/agent-recommended-models.d.ts +152 -0
- package/dist/nodes/agent-recommended-models.d.ts.map +1 -0
- package/dist/nodes/agent-recommended-models.js +656 -0
- package/dist/nodes/agent-recommended-models.js.map +1 -0
- package/dist/nodes/agent-thinking.d.ts +29 -0
- package/dist/nodes/agent-thinking.d.ts.map +1 -0
- package/dist/nodes/agent-thinking.js +139 -0
- package/dist/nodes/agent-thinking.js.map +1 -0
- package/dist/nodes/agent-threads.d.ts +17 -0
- package/dist/nodes/agent-threads.d.ts.map +1 -0
- package/dist/nodes/agent-threads.js +93 -0
- package/dist/nodes/agent-threads.js.map +1 -0
- package/dist/nodes/agent-tool-hydration.d.ts +50 -0
- package/dist/nodes/agent-tool-hydration.d.ts.map +1 -0
- package/dist/nodes/agent-tool-hydration.js +106 -0
- package/dist/nodes/agent-tool-hydration.js.map +1 -0
- package/dist/nodes/agent-utils.d.ts +174 -0
- package/dist/nodes/agent-utils.d.ts.map +1 -0
- package/dist/nodes/agent-utils.js +553 -0
- package/dist/nodes/agent-utils.js.map +1 -0
- package/dist/nodes/agents.d.ts +402 -0
- package/dist/nodes/agents.d.ts.map +1 -0
- package/dist/nodes/agents.js +1491 -0
- package/dist/nodes/agents.js.map +1 -0
- package/dist/nodes/director.d.ts +126 -0
- package/dist/nodes/director.d.ts.map +1 -0
- package/dist/nodes/director.js +337 -0
- package/dist/nodes/director.js.map +1 -0
- package/dist/nodes/gemini.d.ts +171 -0
- package/dist/nodes/gemini.d.ts.map +1 -0
- package/dist/nodes/gemini.js +985 -0
- package/dist/nodes/gemini.js.map +1 -0
- package/dist/nodes/generators.d.ts +162 -0
- package/dist/nodes/generators.d.ts.map +1 -0
- package/dist/nodes/generators.js +966 -0
- package/dist/nodes/generators.js.map +1 -0
- package/dist/nodes/mistral.d.ts +109 -0
- package/dist/nodes/mistral.d.ts.map +1 -0
- package/dist/nodes/mistral.js +447 -0
- package/dist/nodes/mistral.js.map +1 -0
- package/dist/nodes/openai.d.ts +292 -0
- package/dist/nodes/openai.d.ts.map +1 -0
- package/dist/nodes/openai.js +1806 -0
- package/dist/nodes/openai.js.map +1 -0
- package/dist/nodes/provider-spend.d.ts +45 -0
- package/dist/nodes/provider-spend.d.ts.map +1 -0
- package/dist/nodes/provider-spend.js +69 -0
- package/dist/nodes/provider-spend.js.map +1 -0
- package/dist/nodes/shots.d.ts +108 -0
- package/dist/nodes/shots.d.ts.map +1 -0
- package/dist/nodes/shots.js +378 -0
- package/dist/nodes/shots.js.map +1 -0
- package/dist/nodes/xai.d.ts +99 -0
- package/dist/nodes/xai.d.ts.map +1 -0
- package/dist/nodes/xai.js +360 -0
- package/dist/nodes/xai.js.map +1 -0
- package/package.json +111 -0
|
@@ -0,0 +1,1806 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { BaseNode, prop } from "@nodetool-ai/node-sdk";
|
|
8
|
+
import { fetchExternalMedia } from "@nodetool-ai/runtime";
|
|
9
|
+
import { tagAsServer } from "@nodetool-ai/nodes-utils";
|
|
10
|
+
import { createLogger } from "@nodetool-ai/config";
|
|
11
|
+
import { encodeWav } from "./gemini.js";
|
|
12
|
+
import { isCallable, isString } from "@nodetool-ai/node-sdk";
|
|
13
|
+
const OPENAI_API_BASE = "https://api.openai.com/v1";
|
|
14
|
+
const log = createLogger("nodetool.llm-nodes.openai");
|
|
15
|
+
function getApiKey(secrets) {
|
|
16
|
+
const key = secrets.OPENAI_API_KEY || process.env.OPENAI_API_KEY || "";
|
|
17
|
+
if (!key)
|
|
18
|
+
throw new Error("OPENAI_API_KEY is not configured");
|
|
19
|
+
return key;
|
|
20
|
+
}
|
|
21
|
+
function authHeaders(apiKey) {
|
|
22
|
+
return {
|
|
23
|
+
Authorization: `Bearer ${apiKey}`,
|
|
24
|
+
"Content-Type": "application/json"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// 1. Embedding
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export class EmbeddingNode extends BaseNode {
|
|
31
|
+
static nodeType = "openai.text.Embedding";
|
|
32
|
+
static title = "Embedding";
|
|
33
|
+
static inlineFields = ["input"];
|
|
34
|
+
static inputFields = [];
|
|
35
|
+
static description = "Generate vector representations of text for semantic analysis.\n embeddings, similarity, search, clustering, classification\n\n Uses OpenAI's embedding models to create dense vector representations of text.\n These vectors capture semantic meaning, enabling:\n - Semantic search\n - Text clustering\n - Document classification\n - Recommendation systems\n - Anomaly detection\n - Measuring text similarity and diversity";
|
|
36
|
+
static metadataOutputTypes = {
|
|
37
|
+
output: "list"
|
|
38
|
+
};
|
|
39
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
40
|
+
async process() {
|
|
41
|
+
const apiKey = getApiKey(this._secrets);
|
|
42
|
+
const text = this.input;
|
|
43
|
+
const model = this.model;
|
|
44
|
+
const chunkSize = this.chunk_size;
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
47
|
+
chunks.push(text.slice(i, i + chunkSize));
|
|
48
|
+
}
|
|
49
|
+
if (chunks.length === 0)
|
|
50
|
+
chunks.push("");
|
|
51
|
+
const res = await fetch(`${OPENAI_API_BASE}/embeddings`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: authHeaders(apiKey),
|
|
54
|
+
body: JSON.stringify({ input: chunks, model })
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const err = await res.text();
|
|
58
|
+
throw new Error(`OpenAI Embedding API error ${res.status}: ${err}`);
|
|
59
|
+
}
|
|
60
|
+
const data = (await res.json());
|
|
61
|
+
const allEmbeddings = data.data.map((d) => d.embedding);
|
|
62
|
+
const dim = allEmbeddings[0]?.length ?? 0;
|
|
63
|
+
const avg = new Array(dim).fill(0);
|
|
64
|
+
for (const emb of allEmbeddings) {
|
|
65
|
+
for (let i = 0; i < dim; i++) {
|
|
66
|
+
avg[i] += emb[i] / allEmbeddings.length;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { output: avg };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
__decorate([
|
|
73
|
+
prop({ type: "str", default: "", title: "Input" })
|
|
74
|
+
], EmbeddingNode.prototype, "input", void 0);
|
|
75
|
+
__decorate([
|
|
76
|
+
prop({
|
|
77
|
+
type: "enum",
|
|
78
|
+
default: "text-embedding-3-small",
|
|
79
|
+
title: "Model",
|
|
80
|
+
values: ["text-embedding-3-large", "text-embedding-3-small"]
|
|
81
|
+
})
|
|
82
|
+
], EmbeddingNode.prototype, "model", void 0);
|
|
83
|
+
__decorate([
|
|
84
|
+
prop({ type: "int", default: 4096, title: "Chunk Size" })
|
|
85
|
+
], EmbeddingNode.prototype, "chunk_size", void 0);
|
|
86
|
+
export class WebSearchNode extends BaseNode {
|
|
87
|
+
static nodeType = "openai.text.WebSearch";
|
|
88
|
+
static title = "Web Search";
|
|
89
|
+
static inlineFields = ["query"];
|
|
90
|
+
static inputFields = [];
|
|
91
|
+
static description = "Search the web with OpenAI's web-search models to answer queries from current information.\n openai, web, search, internet, browsing, current, realtime";
|
|
92
|
+
static metadataOutputTypes = {
|
|
93
|
+
output: "str"
|
|
94
|
+
};
|
|
95
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
96
|
+
async process() {
|
|
97
|
+
const apiKey = getApiKey(this._secrets);
|
|
98
|
+
const query = this.query;
|
|
99
|
+
if (!query)
|
|
100
|
+
throw new Error("Search query cannot be empty");
|
|
101
|
+
const res = await fetch(`${OPENAI_API_BASE}/chat/completions`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: authHeaders(apiKey),
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
model: "gpt-4o-search-preview",
|
|
106
|
+
web_search_options: {},
|
|
107
|
+
messages: [{ role: "user", content: query }]
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const err = await res.text();
|
|
112
|
+
throw new Error(`OpenAI WebSearch API error ${res.status}: ${err}`);
|
|
113
|
+
}
|
|
114
|
+
const data = (await res.json());
|
|
115
|
+
let content;
|
|
116
|
+
if (data.choices && Array.isArray(data.choices)) {
|
|
117
|
+
const first = data.choices[0];
|
|
118
|
+
const msg = first?.message;
|
|
119
|
+
content = String(msg?.content ?? JSON.stringify(data));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
content = JSON.stringify(data);
|
|
123
|
+
}
|
|
124
|
+
return { output: content };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
__decorate([
|
|
128
|
+
prop({
|
|
129
|
+
type: "str",
|
|
130
|
+
default: "",
|
|
131
|
+
title: "Query",
|
|
132
|
+
description: "The search query to execute."
|
|
133
|
+
})
|
|
134
|
+
], WebSearchNode.prototype, "query", void 0);
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// 3. Moderation
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
export class ModerationNode extends BaseNode {
|
|
139
|
+
static nodeType = "openai.text.Moderation";
|
|
140
|
+
static title = "Moderation";
|
|
141
|
+
static description = "Check text content for potential policy violations using OpenAI's moderation API.\n moderation, safety, content, filter, policy, harmful, toxic\n\n Uses OpenAI's moderation models to detect potentially harmful content including:\n - Hate speech\n - Harassment\n - Self-harm content\n - Sexual content\n - Violence\n - Graphic violence\n\n Returns flagged status and category scores for comprehensive content analysis.";
|
|
142
|
+
static metadataOutputTypes = {
|
|
143
|
+
flagged: "bool",
|
|
144
|
+
categories: "dict[str, bool]",
|
|
145
|
+
category_scores: "dict[str, float]"
|
|
146
|
+
};
|
|
147
|
+
static inlineFields = ["input"];
|
|
148
|
+
static inputFields = [];
|
|
149
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
150
|
+
async process() {
|
|
151
|
+
const apiKey = getApiKey(this._secrets);
|
|
152
|
+
const text = this.input;
|
|
153
|
+
const model = this.model;
|
|
154
|
+
if (!text)
|
|
155
|
+
throw new Error("Input text cannot be empty");
|
|
156
|
+
const res = await fetch(`${OPENAI_API_BASE}/moderations`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: authHeaders(apiKey),
|
|
159
|
+
body: JSON.stringify({ input: text, model })
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const err = await res.text();
|
|
163
|
+
throw new Error(`OpenAI Moderation API error ${res.status}: ${err}`);
|
|
164
|
+
}
|
|
165
|
+
const data = (await res.json());
|
|
166
|
+
const results = data.results;
|
|
167
|
+
if (results && results.length > 0) {
|
|
168
|
+
const result = results[0];
|
|
169
|
+
return {
|
|
170
|
+
flagged: result.flagged ?? false,
|
|
171
|
+
categories: result.categories ?? {},
|
|
172
|
+
category_scores: result.category_scores ?? {}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return { flagged: false, categories: {}, category_scores: {} };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
__decorate([
|
|
179
|
+
prop({
|
|
180
|
+
type: "str",
|
|
181
|
+
default: "",
|
|
182
|
+
title: "Input",
|
|
183
|
+
description: "The text content to check for policy violations."
|
|
184
|
+
})
|
|
185
|
+
], ModerationNode.prototype, "input", void 0);
|
|
186
|
+
__decorate([
|
|
187
|
+
prop({
|
|
188
|
+
type: "enum",
|
|
189
|
+
default: "omni-moderation-latest",
|
|
190
|
+
title: "Model",
|
|
191
|
+
description: "The moderation model to use.",
|
|
192
|
+
values: [
|
|
193
|
+
"omni-moderation-latest",
|
|
194
|
+
"omni-moderation-2024-09-26",
|
|
195
|
+
"text-moderation-latest",
|
|
196
|
+
"text-moderation-stable"
|
|
197
|
+
]
|
|
198
|
+
})
|
|
199
|
+
], ModerationNode.prototype, "model", void 0);
|
|
200
|
+
export class CreateImageNode extends BaseNode {
|
|
201
|
+
static nodeType = "openai.image.CreateImage";
|
|
202
|
+
static body = "content_card";
|
|
203
|
+
static title = "Create Image";
|
|
204
|
+
static inlineFields = [];
|
|
205
|
+
static inputFields = ["prompt"];
|
|
206
|
+
static description = "Generates images from textual descriptions.\n image, t2i, tti, text-to-image, create, generate, picture, photo, art, drawing, illustration";
|
|
207
|
+
static metadataOutputTypes = {
|
|
208
|
+
output: "image"
|
|
209
|
+
};
|
|
210
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
211
|
+
static autoSaveAsset = true;
|
|
212
|
+
async process() {
|
|
213
|
+
const apiKey = getApiKey(this._secrets);
|
|
214
|
+
const prompt = this.prompt;
|
|
215
|
+
if (!prompt)
|
|
216
|
+
throw new Error("Prompt cannot be empty");
|
|
217
|
+
const model = this.model;
|
|
218
|
+
const size = this.size;
|
|
219
|
+
const quality = this.quality;
|
|
220
|
+
const background = this.background;
|
|
221
|
+
const res = await fetch(`${OPENAI_API_BASE}/images/generations`, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: authHeaders(apiKey),
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
prompt,
|
|
226
|
+
model,
|
|
227
|
+
n: 1,
|
|
228
|
+
size,
|
|
229
|
+
quality,
|
|
230
|
+
background
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok) {
|
|
234
|
+
const err = await res.text();
|
|
235
|
+
throw new Error(`OpenAI CreateImage API error ${res.status}: ${err}`);
|
|
236
|
+
}
|
|
237
|
+
const data = (await res.json());
|
|
238
|
+
const item = data.data?.[0];
|
|
239
|
+
if (!item)
|
|
240
|
+
throw new Error("No image data in response");
|
|
241
|
+
if (item.b64_json) {
|
|
242
|
+
return { output: imageRefFromB64(item.b64_json) };
|
|
243
|
+
}
|
|
244
|
+
else if (item.url) {
|
|
245
|
+
return { output: { type: "image", uri: item.url } };
|
|
246
|
+
}
|
|
247
|
+
throw new Error("No image data in response");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
__decorate([
|
|
251
|
+
prop({
|
|
252
|
+
type: "str",
|
|
253
|
+
default: "",
|
|
254
|
+
title: "Prompt",
|
|
255
|
+
description: "The prompt to use."
|
|
256
|
+
})
|
|
257
|
+
], CreateImageNode.prototype, "prompt", void 0);
|
|
258
|
+
__decorate([
|
|
259
|
+
prop({
|
|
260
|
+
type: "enum",
|
|
261
|
+
default: "gpt-image-1",
|
|
262
|
+
title: "Model",
|
|
263
|
+
description: "The model to use for image generation.",
|
|
264
|
+
values: ["gpt-image-1"]
|
|
265
|
+
})
|
|
266
|
+
], CreateImageNode.prototype, "model", void 0);
|
|
267
|
+
__decorate([
|
|
268
|
+
prop({
|
|
269
|
+
type: "enum",
|
|
270
|
+
default: "1024x1024",
|
|
271
|
+
title: "Size",
|
|
272
|
+
description: "The size of the image to generate.",
|
|
273
|
+
values: ["1024x1024", "1536x1024", "1024x1536"]
|
|
274
|
+
})
|
|
275
|
+
], CreateImageNode.prototype, "size", void 0);
|
|
276
|
+
__decorate([
|
|
277
|
+
prop({
|
|
278
|
+
type: "enum",
|
|
279
|
+
default: "auto",
|
|
280
|
+
title: "Background",
|
|
281
|
+
description: "The background of the image to generate.",
|
|
282
|
+
values: ["transparent", "opaque", "auto"]
|
|
283
|
+
})
|
|
284
|
+
], CreateImageNode.prototype, "background", void 0);
|
|
285
|
+
__decorate([
|
|
286
|
+
prop({
|
|
287
|
+
type: "enum",
|
|
288
|
+
default: "high",
|
|
289
|
+
title: "Quality",
|
|
290
|
+
description: "The quality of the image to generate.",
|
|
291
|
+
values: ["high", "medium", "low"]
|
|
292
|
+
})
|
|
293
|
+
], CreateImageNode.prototype, "quality", void 0);
|
|
294
|
+
export class EditImageNode extends BaseNode {
|
|
295
|
+
static nodeType = "openai.image.EditImage";
|
|
296
|
+
static body = "content_card";
|
|
297
|
+
static title = "Edit Image";
|
|
298
|
+
static inlineFields = [];
|
|
299
|
+
static inputFields = ["prompt", "image", "mask"];
|
|
300
|
+
static description = "Edit images using OpenAI's gpt-image-1 model.\n image, edit, modify, transform, inpaint, outpaint, variation\n\n Takes an input image and a text prompt to generate a modified version.\n Can be used for inpainting, outpainting, style transfer, and image modification.\n Optionally accepts a mask to specify which areas to edit.";
|
|
301
|
+
static metadataOutputTypes = {
|
|
302
|
+
output: "image"
|
|
303
|
+
};
|
|
304
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
305
|
+
static autoSaveAsset = true;
|
|
306
|
+
async process() {
|
|
307
|
+
const apiKey = getApiKey(this._secrets);
|
|
308
|
+
const prompt = this.prompt;
|
|
309
|
+
if (!prompt)
|
|
310
|
+
throw new Error("Edit prompt cannot be empty");
|
|
311
|
+
const image = this.image;
|
|
312
|
+
if (!image || (!image.data && !image.uri)) {
|
|
313
|
+
throw new Error("Input image is required");
|
|
314
|
+
}
|
|
315
|
+
const model = this.model;
|
|
316
|
+
const size = this.size;
|
|
317
|
+
const quality = this.quality;
|
|
318
|
+
// gpt-image-1 always returns base64 and rejects `response_format`, so it
|
|
319
|
+
// must not be sent (unlike the legacy dall-e-2 edits endpoint).
|
|
320
|
+
const formData = new FormData();
|
|
321
|
+
formData.append("prompt", prompt);
|
|
322
|
+
formData.append("model", model);
|
|
323
|
+
formData.append("size", size);
|
|
324
|
+
formData.append("quality", quality);
|
|
325
|
+
// Convert image ref to blob
|
|
326
|
+
const imageBlob = await refToBlob(image);
|
|
327
|
+
formData.append("image", imageBlob, "image.png");
|
|
328
|
+
// Optional mask
|
|
329
|
+
const mask = this.mask;
|
|
330
|
+
if (mask && (mask.data || mask.uri)) {
|
|
331
|
+
const maskBlob = await refToBlob(mask);
|
|
332
|
+
formData.append("mask", maskBlob, "mask.png");
|
|
333
|
+
}
|
|
334
|
+
const res = await fetch(`${OPENAI_API_BASE}/images/edits`, {
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
337
|
+
body: formData
|
|
338
|
+
});
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
const err = await res.text();
|
|
341
|
+
throw new Error(`OpenAI EditImage API error ${res.status}: ${err}`);
|
|
342
|
+
}
|
|
343
|
+
const data = (await res.json());
|
|
344
|
+
const item = data.data?.[0];
|
|
345
|
+
if (!item)
|
|
346
|
+
throw new Error("No image data in response");
|
|
347
|
+
if (item.b64_json) {
|
|
348
|
+
return { output: imageRefFromB64(item.b64_json) };
|
|
349
|
+
}
|
|
350
|
+
else if (item.url) {
|
|
351
|
+
return { output: { type: "image", uri: item.url } };
|
|
352
|
+
}
|
|
353
|
+
throw new Error("No image data in response");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
__decorate([
|
|
357
|
+
prop({
|
|
358
|
+
type: "image",
|
|
359
|
+
default: {
|
|
360
|
+
type: "image",
|
|
361
|
+
uri: "",
|
|
362
|
+
asset_id: null,
|
|
363
|
+
data: null,
|
|
364
|
+
metadata: null
|
|
365
|
+
},
|
|
366
|
+
title: "Image",
|
|
367
|
+
description: "The image to edit."
|
|
368
|
+
})
|
|
369
|
+
], EditImageNode.prototype, "image", void 0);
|
|
370
|
+
__decorate([
|
|
371
|
+
prop({
|
|
372
|
+
type: "image",
|
|
373
|
+
default: {
|
|
374
|
+
type: "image",
|
|
375
|
+
uri: "",
|
|
376
|
+
asset_id: null,
|
|
377
|
+
data: null,
|
|
378
|
+
metadata: null
|
|
379
|
+
},
|
|
380
|
+
title: "Mask",
|
|
381
|
+
description: "Optional mask image. White areas will be edited, black areas preserved."
|
|
382
|
+
})
|
|
383
|
+
], EditImageNode.prototype, "mask", void 0);
|
|
384
|
+
__decorate([
|
|
385
|
+
prop({
|
|
386
|
+
type: "str",
|
|
387
|
+
default: "",
|
|
388
|
+
title: "Prompt",
|
|
389
|
+
description: "The prompt describing the desired edit."
|
|
390
|
+
})
|
|
391
|
+
], EditImageNode.prototype, "prompt", void 0);
|
|
392
|
+
__decorate([
|
|
393
|
+
prop({
|
|
394
|
+
type: "enum",
|
|
395
|
+
default: "gpt-image-1",
|
|
396
|
+
title: "Model",
|
|
397
|
+
description: "The model to use for image editing.",
|
|
398
|
+
values: ["gpt-image-1"]
|
|
399
|
+
})
|
|
400
|
+
], EditImageNode.prototype, "model", void 0);
|
|
401
|
+
__decorate([
|
|
402
|
+
prop({
|
|
403
|
+
type: "enum",
|
|
404
|
+
default: "1024x1024",
|
|
405
|
+
title: "Size",
|
|
406
|
+
description: "The size of the output image.",
|
|
407
|
+
values: ["1024x1024", "1536x1024", "1024x1536"]
|
|
408
|
+
})
|
|
409
|
+
], EditImageNode.prototype, "size", void 0);
|
|
410
|
+
__decorate([
|
|
411
|
+
prop({
|
|
412
|
+
type: "enum",
|
|
413
|
+
default: "high",
|
|
414
|
+
title: "Quality",
|
|
415
|
+
description: "The quality of the generated image.",
|
|
416
|
+
values: ["high", "medium", "low"]
|
|
417
|
+
})
|
|
418
|
+
], EditImageNode.prototype, "quality", void 0);
|
|
419
|
+
export class ImageVariationNode extends BaseNode {
|
|
420
|
+
static nodeType = "openai.image.ImageVariation";
|
|
421
|
+
static body = "content_card";
|
|
422
|
+
static title = "Image Variation";
|
|
423
|
+
static inlineFields = [];
|
|
424
|
+
static inputFields = ["image"];
|
|
425
|
+
static description = "Generate variations of an input image using OpenAI's image variations API.\n image, variation, remix, generate, alternative, dall-e\n\n Takes a square PNG image and produces a new image that is a variation of it.\n No text prompt is used — the model riffs on the source image directly.";
|
|
426
|
+
static metadataOutputTypes = {
|
|
427
|
+
output: "image"
|
|
428
|
+
};
|
|
429
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
430
|
+
static autoSaveAsset = true;
|
|
431
|
+
async process() {
|
|
432
|
+
const apiKey = getApiKey(this._secrets);
|
|
433
|
+
const image = this.image;
|
|
434
|
+
if (!image || (!image.data && !image.uri)) {
|
|
435
|
+
throw new Error("Input image is required");
|
|
436
|
+
}
|
|
437
|
+
const size = this.size;
|
|
438
|
+
// The variations endpoint only supports dall-e-2, which requires the
|
|
439
|
+
// `response_format` to request base64 output.
|
|
440
|
+
const formData = new FormData();
|
|
441
|
+
formData.append("model", "dall-e-2");
|
|
442
|
+
formData.append("n", "1");
|
|
443
|
+
formData.append("size", size);
|
|
444
|
+
formData.append("response_format", "b64_json");
|
|
445
|
+
const imageBlob = await refToBlob(image);
|
|
446
|
+
formData.append("image", imageBlob, "image.png");
|
|
447
|
+
const res = await fetch(`${OPENAI_API_BASE}/images/variations`, {
|
|
448
|
+
method: "POST",
|
|
449
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
450
|
+
body: formData
|
|
451
|
+
});
|
|
452
|
+
if (!res.ok) {
|
|
453
|
+
const err = await res.text();
|
|
454
|
+
throw new Error(`OpenAI ImageVariation API error ${res.status}: ${err}`);
|
|
455
|
+
}
|
|
456
|
+
const data = (await res.json());
|
|
457
|
+
const item = data.data[0];
|
|
458
|
+
if (item.b64_json) {
|
|
459
|
+
return { output: imageRefFromB64(item.b64_json) };
|
|
460
|
+
}
|
|
461
|
+
else if (item.url) {
|
|
462
|
+
return { output: { type: "image", uri: item.url } };
|
|
463
|
+
}
|
|
464
|
+
throw new Error("No image data in response");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
__decorate([
|
|
468
|
+
prop({
|
|
469
|
+
type: "image",
|
|
470
|
+
default: {
|
|
471
|
+
type: "image",
|
|
472
|
+
uri: "",
|
|
473
|
+
asset_id: null,
|
|
474
|
+
data: null,
|
|
475
|
+
metadata: null
|
|
476
|
+
},
|
|
477
|
+
title: "Image",
|
|
478
|
+
description: "The source image to generate variations from (square PNG)."
|
|
479
|
+
})
|
|
480
|
+
], ImageVariationNode.prototype, "image", void 0);
|
|
481
|
+
__decorate([
|
|
482
|
+
prop({
|
|
483
|
+
type: "enum",
|
|
484
|
+
default: "1024x1024",
|
|
485
|
+
title: "Size",
|
|
486
|
+
description: "The size of the generated image.",
|
|
487
|
+
values: ["256x256", "512x512", "1024x1024"]
|
|
488
|
+
})
|
|
489
|
+
], ImageVariationNode.prototype, "size", void 0);
|
|
490
|
+
/**
|
|
491
|
+
* Build an image ref from raw base64. NodeTool's media refs carry **raw**
|
|
492
|
+
* base64 in `data` (matching `audioRefFromBytes` and the Gemini nodes); the
|
|
493
|
+
* asset-saving path (`decodeAssetBytes`) and downstream providers decode the
|
|
494
|
+
* field directly, so a `data:` prefix would corrupt the bytes.
|
|
495
|
+
*/
|
|
496
|
+
function imageRefFromB64(b64) {
|
|
497
|
+
return { type: "image", data: b64, content_type: "image/png" };
|
|
498
|
+
}
|
|
499
|
+
/** Build an audio ref from raw base64 with an explicit mime type. */
|
|
500
|
+
function audioRefFromB64(b64, contentType) {
|
|
501
|
+
return { type: "audio", data: b64, content_type: contentType };
|
|
502
|
+
}
|
|
503
|
+
async function refToBlob(ref) {
|
|
504
|
+
if (ref.data && isString(ref.data)) {
|
|
505
|
+
const dataStr = ref.data;
|
|
506
|
+
// Handle data: URI
|
|
507
|
+
if (dataStr.startsWith("data:")) {
|
|
508
|
+
const commaIdx = dataStr.indexOf(",");
|
|
509
|
+
const b64 = dataStr.slice(commaIdx + 1);
|
|
510
|
+
const buf = Buffer.from(b64, "base64");
|
|
511
|
+
return new Blob([buf]);
|
|
512
|
+
}
|
|
513
|
+
// Assume raw base64
|
|
514
|
+
const buf = Buffer.from(dataStr, "base64");
|
|
515
|
+
return new Blob([buf]);
|
|
516
|
+
}
|
|
517
|
+
if (ref.uri && isString(ref.uri)) {
|
|
518
|
+
// Workflow data — the media-ref egress policy decides.
|
|
519
|
+
const r = await fetchExternalMedia(ref.uri);
|
|
520
|
+
return await r.blob();
|
|
521
|
+
}
|
|
522
|
+
throw new Error("Cannot convert ref to blob: no data or uri");
|
|
523
|
+
}
|
|
524
|
+
export class TextToSpeechNode extends BaseNode {
|
|
525
|
+
static nodeType = "openai.audio.TextToSpeech";
|
|
526
|
+
static body = "content_card";
|
|
527
|
+
static title = "Text To Speech";
|
|
528
|
+
static description = "Converts text to speech using OpenAI TTS models.\n audio, tts, text-to-speech, voice, synthesis";
|
|
529
|
+
static metadataOutputTypes = {
|
|
530
|
+
output: "audio"
|
|
531
|
+
};
|
|
532
|
+
static inlineFields = ["input"];
|
|
533
|
+
static inputFields = [];
|
|
534
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
535
|
+
static autoSaveAsset = true;
|
|
536
|
+
async process() {
|
|
537
|
+
const apiKey = getApiKey(this._secrets);
|
|
538
|
+
const text = this.input;
|
|
539
|
+
if (!text)
|
|
540
|
+
throw new Error("Input text cannot be empty");
|
|
541
|
+
const model = this.model;
|
|
542
|
+
const voice = this.voice;
|
|
543
|
+
const speed = this.speed;
|
|
544
|
+
const instructions = this.instructions;
|
|
545
|
+
const isMiniTts = model === "gpt-4o-mini-tts";
|
|
546
|
+
const body = {
|
|
547
|
+
model,
|
|
548
|
+
input: text,
|
|
549
|
+
voice,
|
|
550
|
+
response_format: "mp3"
|
|
551
|
+
};
|
|
552
|
+
// `speed` is supported by tts-1 / tts-1-hd but not gpt-4o-mini-tts, while
|
|
553
|
+
// `instructions` is the inverse — only honored by gpt-4o-mini-tts. Sending
|
|
554
|
+
// the wrong one to a model returns a 400.
|
|
555
|
+
if (isMiniTts) {
|
|
556
|
+
if (instructions)
|
|
557
|
+
body.instructions = instructions;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
body.speed = speed;
|
|
561
|
+
}
|
|
562
|
+
const res = await fetch(`${OPENAI_API_BASE}/audio/speech`, {
|
|
563
|
+
method: "POST",
|
|
564
|
+
headers: authHeaders(apiKey),
|
|
565
|
+
body: JSON.stringify(body)
|
|
566
|
+
});
|
|
567
|
+
if (!res.ok) {
|
|
568
|
+
const err = await res.text();
|
|
569
|
+
throw new Error(`OpenAI TTS API error ${res.status}: ${err}`);
|
|
570
|
+
}
|
|
571
|
+
const arrayBuf = await res.arrayBuffer();
|
|
572
|
+
const b64 = Buffer.from(arrayBuf).toString("base64");
|
|
573
|
+
return { output: audioRefFromB64(b64, "audio/mpeg") };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
__decorate([
|
|
577
|
+
prop({
|
|
578
|
+
type: "enum",
|
|
579
|
+
default: "tts-1",
|
|
580
|
+
title: "Model",
|
|
581
|
+
values: ["tts-1", "tts-1-hd", "gpt-4o-mini-tts"]
|
|
582
|
+
})
|
|
583
|
+
], TextToSpeechNode.prototype, "model", void 0);
|
|
584
|
+
__decorate([
|
|
585
|
+
prop({
|
|
586
|
+
type: "enum",
|
|
587
|
+
default: "alloy",
|
|
588
|
+
title: "Voice",
|
|
589
|
+
values: [
|
|
590
|
+
"alloy",
|
|
591
|
+
"ash",
|
|
592
|
+
"ballad",
|
|
593
|
+
"coral",
|
|
594
|
+
"echo",
|
|
595
|
+
"fable",
|
|
596
|
+
"onyx",
|
|
597
|
+
"nova",
|
|
598
|
+
"sage",
|
|
599
|
+
"shimmer",
|
|
600
|
+
"verse"
|
|
601
|
+
]
|
|
602
|
+
})
|
|
603
|
+
], TextToSpeechNode.prototype, "voice", void 0);
|
|
604
|
+
__decorate([
|
|
605
|
+
prop({ type: "str", default: "", title: "Input" })
|
|
606
|
+
], TextToSpeechNode.prototype, "input", void 0);
|
|
607
|
+
__decorate([
|
|
608
|
+
prop({ type: "float", default: 1, title: "Speed", min: 0.25, max: 4 })
|
|
609
|
+
], TextToSpeechNode.prototype, "speed", void 0);
|
|
610
|
+
__decorate([
|
|
611
|
+
prop({
|
|
612
|
+
type: "str",
|
|
613
|
+
default: "",
|
|
614
|
+
title: "Instructions",
|
|
615
|
+
description: "Steer the voice's tone, emotion, and delivery (gpt-4o-mini-tts only)."
|
|
616
|
+
})
|
|
617
|
+
], TextToSpeechNode.prototype, "instructions", void 0);
|
|
618
|
+
export class TranslateNode extends BaseNode {
|
|
619
|
+
static nodeType = "openai.audio.Translate";
|
|
620
|
+
static body = "content_card";
|
|
621
|
+
static title = "Translate";
|
|
622
|
+
static inlineFields = [];
|
|
623
|
+
static inputFields = ["audio"];
|
|
624
|
+
static description = "Translates speech in audio to English text.\n audio, translation, speech-to-text, localization";
|
|
625
|
+
static metadataOutputTypes = {
|
|
626
|
+
output: "str"
|
|
627
|
+
};
|
|
628
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
629
|
+
async process() {
|
|
630
|
+
const apiKey = getApiKey(this._secrets);
|
|
631
|
+
const audio = this.audio;
|
|
632
|
+
if (!audio || (!audio.data && !audio.uri)) {
|
|
633
|
+
throw new Error("Audio input is required");
|
|
634
|
+
}
|
|
635
|
+
const temperature = this.temperature;
|
|
636
|
+
const audioBlob = await refToBlob(audio);
|
|
637
|
+
const formData = new FormData();
|
|
638
|
+
formData.append("file", audioBlob, "audio.mp3");
|
|
639
|
+
formData.append("model", "whisper-1");
|
|
640
|
+
formData.append("temperature", String(temperature));
|
|
641
|
+
const res = await fetch(`${OPENAI_API_BASE}/audio/translations`, {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
644
|
+
body: formData
|
|
645
|
+
});
|
|
646
|
+
if (!res.ok) {
|
|
647
|
+
const err = await res.text();
|
|
648
|
+
throw new Error(`OpenAI Translate API error ${res.status}: ${err}`);
|
|
649
|
+
}
|
|
650
|
+
const data = (await res.json());
|
|
651
|
+
return { output: data.text };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
__decorate([
|
|
655
|
+
prop({
|
|
656
|
+
type: "audio",
|
|
657
|
+
default: {
|
|
658
|
+
type: "audio",
|
|
659
|
+
uri: "",
|
|
660
|
+
asset_id: null,
|
|
661
|
+
data: null,
|
|
662
|
+
metadata: null
|
|
663
|
+
},
|
|
664
|
+
title: "Audio",
|
|
665
|
+
description: "The audio file to translate."
|
|
666
|
+
})
|
|
667
|
+
], TranslateNode.prototype, "audio", void 0);
|
|
668
|
+
__decorate([
|
|
669
|
+
prop({
|
|
670
|
+
type: "float",
|
|
671
|
+
default: 0,
|
|
672
|
+
title: "Temperature",
|
|
673
|
+
description: "The temperature to use for the translation."
|
|
674
|
+
})
|
|
675
|
+
], TranslateNode.prototype, "temperature", void 0);
|
|
676
|
+
export class TranscribeNode extends BaseNode {
|
|
677
|
+
static nodeType = "openai.audio.Transcribe";
|
|
678
|
+
static body = "content_card";
|
|
679
|
+
static title = "Transcribe";
|
|
680
|
+
static description = "Converts speech to text using OpenAI's speech-to-text API.\n audio, transcription, speech-to-text, stt, whisper";
|
|
681
|
+
static metadataOutputTypes = {
|
|
682
|
+
text: "str",
|
|
683
|
+
words: "list[audio_chunk]",
|
|
684
|
+
segments: "list[audio_chunk]"
|
|
685
|
+
};
|
|
686
|
+
static inlineFields = [];
|
|
687
|
+
static inputFields = ["audio", "prompt"];
|
|
688
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
689
|
+
async process() {
|
|
690
|
+
const apiKey = getApiKey(this._secrets);
|
|
691
|
+
const audio = this.audio;
|
|
692
|
+
if (!audio || (!audio.data && !audio.uri)) {
|
|
693
|
+
throw new Error("Audio input is required");
|
|
694
|
+
}
|
|
695
|
+
const model = this.model;
|
|
696
|
+
const language = this.language;
|
|
697
|
+
const timestamps = this.timestamps;
|
|
698
|
+
const promptText = this.prompt;
|
|
699
|
+
const temperature = this.temperature;
|
|
700
|
+
const isNewModel = model === "gpt-4o-transcribe" || model === "gpt-4o-mini-transcribe";
|
|
701
|
+
const audioBlob = await refToBlob(audio);
|
|
702
|
+
const formData = new FormData();
|
|
703
|
+
formData.append("file", audioBlob, "audio.mp3");
|
|
704
|
+
formData.append("model", model);
|
|
705
|
+
formData.append("temperature", String(temperature));
|
|
706
|
+
if (timestamps) {
|
|
707
|
+
if (isNewModel) {
|
|
708
|
+
throw new Error("New transcription models do not support verbose_json timestamps");
|
|
709
|
+
}
|
|
710
|
+
formData.append("response_format", "verbose_json");
|
|
711
|
+
formData.append("timestamp_granularities[]", "segment");
|
|
712
|
+
formData.append("timestamp_granularities[]", "word");
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
formData.append("response_format", "json");
|
|
716
|
+
}
|
|
717
|
+
if (language !== "auto_detect") {
|
|
718
|
+
formData.append("language", language);
|
|
719
|
+
}
|
|
720
|
+
if (promptText) {
|
|
721
|
+
formData.append("prompt", promptText);
|
|
722
|
+
}
|
|
723
|
+
const res = await fetch(`${OPENAI_API_BASE}/audio/transcriptions`, {
|
|
724
|
+
method: "POST",
|
|
725
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
726
|
+
body: formData
|
|
727
|
+
});
|
|
728
|
+
if (!res.ok) {
|
|
729
|
+
const err = await res.text();
|
|
730
|
+
throw new Error(`OpenAI Transcribe API error ${res.status}: ${err}`);
|
|
731
|
+
}
|
|
732
|
+
const data = (await res.json());
|
|
733
|
+
const text = String(data.text ?? "");
|
|
734
|
+
const words = [];
|
|
735
|
+
const segments = [];
|
|
736
|
+
if (timestamps && !isNewModel) {
|
|
737
|
+
const rawSegments = data.segments;
|
|
738
|
+
if (rawSegments) {
|
|
739
|
+
for (const seg of rawSegments) {
|
|
740
|
+
segments.push({
|
|
741
|
+
timestamp: [Number(seg.start), Number(seg.end)],
|
|
742
|
+
text: String(seg.text)
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const rawWords = data.words;
|
|
747
|
+
if (rawWords) {
|
|
748
|
+
for (const w of rawWords) {
|
|
749
|
+
words.push({
|
|
750
|
+
timestamp: [Number(w.start), Number(w.end)],
|
|
751
|
+
text: String(w.word)
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return { text, words, segments };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
__decorate([
|
|
760
|
+
prop({
|
|
761
|
+
type: "enum",
|
|
762
|
+
default: "whisper-1",
|
|
763
|
+
title: "Model",
|
|
764
|
+
description: "The model to use for transcription.",
|
|
765
|
+
values: ["whisper-1", "gpt-4o-transcribe", "gpt-4o-mini-transcribe"]
|
|
766
|
+
})
|
|
767
|
+
], TranscribeNode.prototype, "model", void 0);
|
|
768
|
+
__decorate([
|
|
769
|
+
prop({
|
|
770
|
+
type: "audio",
|
|
771
|
+
default: {
|
|
772
|
+
type: "audio",
|
|
773
|
+
uri: "",
|
|
774
|
+
asset_id: null,
|
|
775
|
+
data: null,
|
|
776
|
+
metadata: null
|
|
777
|
+
},
|
|
778
|
+
title: "Audio",
|
|
779
|
+
description: "The audio file to transcribe (max 25 MB)."
|
|
780
|
+
})
|
|
781
|
+
], TranscribeNode.prototype, "audio", void 0);
|
|
782
|
+
__decorate([
|
|
783
|
+
prop({
|
|
784
|
+
type: "enum",
|
|
785
|
+
default: "auto_detect",
|
|
786
|
+
title: "Language",
|
|
787
|
+
description: "The language of the input audio",
|
|
788
|
+
values: [
|
|
789
|
+
"auto_detect",
|
|
790
|
+
"af",
|
|
791
|
+
"ar",
|
|
792
|
+
"hy",
|
|
793
|
+
"az",
|
|
794
|
+
"be",
|
|
795
|
+
"bn",
|
|
796
|
+
"bs",
|
|
797
|
+
"bg",
|
|
798
|
+
"ca",
|
|
799
|
+
"hr",
|
|
800
|
+
"cs",
|
|
801
|
+
"da",
|
|
802
|
+
"nl",
|
|
803
|
+
"en",
|
|
804
|
+
"et",
|
|
805
|
+
"tl",
|
|
806
|
+
"fi",
|
|
807
|
+
"fr",
|
|
808
|
+
"gl",
|
|
809
|
+
"de",
|
|
810
|
+
"el",
|
|
811
|
+
"gu",
|
|
812
|
+
"he",
|
|
813
|
+
"hi",
|
|
814
|
+
"hu",
|
|
815
|
+
"is",
|
|
816
|
+
"id",
|
|
817
|
+
"it",
|
|
818
|
+
"ja",
|
|
819
|
+
"kn",
|
|
820
|
+
"kk",
|
|
821
|
+
"ko",
|
|
822
|
+
"lv",
|
|
823
|
+
"lt",
|
|
824
|
+
"mk",
|
|
825
|
+
"ms",
|
|
826
|
+
"zh",
|
|
827
|
+
"mi",
|
|
828
|
+
"mr",
|
|
829
|
+
"ne",
|
|
830
|
+
"no",
|
|
831
|
+
"fa",
|
|
832
|
+
"pl",
|
|
833
|
+
"pt",
|
|
834
|
+
"pa",
|
|
835
|
+
"ro",
|
|
836
|
+
"ru",
|
|
837
|
+
"sr",
|
|
838
|
+
"sk",
|
|
839
|
+
"sl",
|
|
840
|
+
"es",
|
|
841
|
+
"sw",
|
|
842
|
+
"sv",
|
|
843
|
+
"ta",
|
|
844
|
+
"te",
|
|
845
|
+
"th",
|
|
846
|
+
"tr",
|
|
847
|
+
"uk",
|
|
848
|
+
"ur",
|
|
849
|
+
"vi",
|
|
850
|
+
"cy"
|
|
851
|
+
]
|
|
852
|
+
})
|
|
853
|
+
], TranscribeNode.prototype, "language", void 0);
|
|
854
|
+
__decorate([
|
|
855
|
+
prop({
|
|
856
|
+
type: "bool",
|
|
857
|
+
default: false,
|
|
858
|
+
title: "Timestamps",
|
|
859
|
+
description: "Whether to return timestamps for the generated text."
|
|
860
|
+
})
|
|
861
|
+
], TranscribeNode.prototype, "timestamps", void 0);
|
|
862
|
+
__decorate([
|
|
863
|
+
prop({
|
|
864
|
+
type: "str",
|
|
865
|
+
default: "",
|
|
866
|
+
title: "Prompt",
|
|
867
|
+
description: "Optional text to guide the model's style or continue a previous audio segment."
|
|
868
|
+
})
|
|
869
|
+
], TranscribeNode.prototype, "prompt", void 0);
|
|
870
|
+
__decorate([
|
|
871
|
+
prop({
|
|
872
|
+
type: "float",
|
|
873
|
+
default: 0,
|
|
874
|
+
title: "Temperature",
|
|
875
|
+
description: "The sampling temperature between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.",
|
|
876
|
+
min: 0,
|
|
877
|
+
max: 1
|
|
878
|
+
})
|
|
879
|
+
], TranscribeNode.prototype, "temperature", void 0);
|
|
880
|
+
// ---------------------------------------------------------------------------
|
|
881
|
+
// 9. RealtimeAgent
|
|
882
|
+
// ---------------------------------------------------------------------------
|
|
883
|
+
export class RealtimeAgentNode extends BaseNode {
|
|
884
|
+
static nodeType = "openai.agents.RealtimeAgent";
|
|
885
|
+
static body = "content_card";
|
|
886
|
+
static title = "Realtime Agent";
|
|
887
|
+
static description = "Realtime conversational agent over OpenAI's WebSocket Realtime API, streaming audio/text in and text/audio responses out at low latency.\n" +
|
|
888
|
+
" realtime, streaming, openai, audio-input, text-output, websocket";
|
|
889
|
+
static metadataOutputTypes = {
|
|
890
|
+
chunk: "chunk",
|
|
891
|
+
audio: "audio",
|
|
892
|
+
text: "str"
|
|
893
|
+
};
|
|
894
|
+
static inlineFields = ["system"];
|
|
895
|
+
static inputFields = ["chunk"];
|
|
896
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
897
|
+
static supportsDynamicOutputs = true;
|
|
898
|
+
static isStreamingInput = true;
|
|
899
|
+
async process() {
|
|
900
|
+
return {};
|
|
901
|
+
}
|
|
902
|
+
async run(inputs, outputs, context) {
|
|
903
|
+
let apiKey = "";
|
|
904
|
+
if (context && isCallable(context.getSecret)) {
|
|
905
|
+
apiKey = (await context.getSecret("OPENAI_API_KEY")) ?? "";
|
|
906
|
+
}
|
|
907
|
+
if (!apiKey)
|
|
908
|
+
apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
909
|
+
if (!apiKey)
|
|
910
|
+
throw new Error("OPENAI_API_KEY is not configured");
|
|
911
|
+
const model = this.model;
|
|
912
|
+
const voice = this.voice;
|
|
913
|
+
const system = this.system;
|
|
914
|
+
const temperature = this.temperature;
|
|
915
|
+
const speed = this.speed;
|
|
916
|
+
const wantAudio = voice !== "none";
|
|
917
|
+
const { WebSocket } = await import("ws");
|
|
918
|
+
const wsUrl = `wss://api.openai.com/v1/realtime?model=${encodeURIComponent(model)}`;
|
|
919
|
+
const ws = new WebSocket(wsUrl, {
|
|
920
|
+
headers: {
|
|
921
|
+
Authorization: `Bearer ${apiKey}`,
|
|
922
|
+
"OpenAI-Beta": "realtime=v1"
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
await new Promise((resolve, reject) => {
|
|
926
|
+
ws.on("open", resolve);
|
|
927
|
+
ws.on("error", reject);
|
|
928
|
+
});
|
|
929
|
+
// Configure session
|
|
930
|
+
ws.send(JSON.stringify({
|
|
931
|
+
type: "session.update",
|
|
932
|
+
session: {
|
|
933
|
+
modalities: wantAudio ? ["text", "audio"] : ["text"],
|
|
934
|
+
instructions: system || undefined,
|
|
935
|
+
voice: wantAudio ? voice : undefined,
|
|
936
|
+
input_audio_format: "pcm16",
|
|
937
|
+
output_audio_format: "pcm16",
|
|
938
|
+
input_audio_transcription: { model: "gpt-4o-mini-transcribe" },
|
|
939
|
+
turn_detection: {
|
|
940
|
+
type: "server_vad",
|
|
941
|
+
silence_duration_ms: 200
|
|
942
|
+
},
|
|
943
|
+
temperature,
|
|
944
|
+
speed
|
|
945
|
+
}
|
|
946
|
+
}));
|
|
947
|
+
// Consumer: listen for response events
|
|
948
|
+
let fullText = "";
|
|
949
|
+
let consumerDone = false;
|
|
950
|
+
let resolveResponseDone;
|
|
951
|
+
const responseDonePromise = new Promise((resolve) => {
|
|
952
|
+
resolveResponseDone = resolve;
|
|
953
|
+
});
|
|
954
|
+
const consumerPromise = new Promise((resolve, reject) => {
|
|
955
|
+
ws.on("message", async (data) => {
|
|
956
|
+
try {
|
|
957
|
+
const msg = JSON.parse(data.toString());
|
|
958
|
+
const msgType = String(msg.type ?? "");
|
|
959
|
+
if (msgType === "response.text.delta") {
|
|
960
|
+
const delta = String(msg.delta ?? "");
|
|
961
|
+
if (delta) {
|
|
962
|
+
fullText += delta;
|
|
963
|
+
await outputs.emit("chunk", {
|
|
964
|
+
type: "chunk",
|
|
965
|
+
content: delta,
|
|
966
|
+
done: false,
|
|
967
|
+
content_type: "text"
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
else if (msgType === "response.audio.delta") {
|
|
972
|
+
const audioB64 = String(msg.delta ?? "");
|
|
973
|
+
if (audioB64) {
|
|
974
|
+
await outputs.emit("chunk", {
|
|
975
|
+
type: "chunk",
|
|
976
|
+
content: audioB64,
|
|
977
|
+
done: false,
|
|
978
|
+
content_type: "audio",
|
|
979
|
+
content_metadata: {
|
|
980
|
+
format: "pcm16",
|
|
981
|
+
encoding: "pcm16",
|
|
982
|
+
sample_rate: 24000,
|
|
983
|
+
channels: 1
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
// Also surface decoded PCM16 on the dedicated `audio` output so
|
|
987
|
+
// the declared handle isn't dead. PCM frames concatenate cleanly
|
|
988
|
+
// in arrival order downstream.
|
|
989
|
+
await outputs.emit("audio", {
|
|
990
|
+
data: new Uint8Array(Buffer.from(audioB64, "base64"))
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
else if (msgType === "response.audio_transcript.delta") {
|
|
995
|
+
const delta = String(msg.delta ?? "");
|
|
996
|
+
if (delta)
|
|
997
|
+
fullText += delta;
|
|
998
|
+
}
|
|
999
|
+
else if (msgType === "response.done") {
|
|
1000
|
+
resolveResponseDone();
|
|
1001
|
+
}
|
|
1002
|
+
else if (msgType === "error") {
|
|
1003
|
+
const errMsg = msg.error?.message ??
|
|
1004
|
+
"Unknown realtime error";
|
|
1005
|
+
reject(new Error(`OpenAI Realtime error: ${errMsg}`));
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
catch (err) {
|
|
1009
|
+
if (!consumerDone)
|
|
1010
|
+
reject(err);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
ws.on("error", (err) => {
|
|
1014
|
+
if (!consumerDone)
|
|
1015
|
+
reject(err);
|
|
1016
|
+
});
|
|
1017
|
+
ws.on("close", () => {
|
|
1018
|
+
consumerDone = true;
|
|
1019
|
+
resolve();
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
// Producer: read streaming input and forward to WebSocket
|
|
1023
|
+
try {
|
|
1024
|
+
for await (const [handle, item] of inputs.any()) {
|
|
1025
|
+
if (handle === "__control__")
|
|
1026
|
+
continue;
|
|
1027
|
+
const chunk = item;
|
|
1028
|
+
let done = false;
|
|
1029
|
+
if (isString(chunk)) {
|
|
1030
|
+
// Raw base64 audio
|
|
1031
|
+
if (chunk && ws.readyState === WebSocket.OPEN) {
|
|
1032
|
+
ws.send(JSON.stringify({
|
|
1033
|
+
type: "input_audio_buffer.append",
|
|
1034
|
+
audio: chunk
|
|
1035
|
+
}));
|
|
1036
|
+
}
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
done = Boolean(chunk.done ?? false);
|
|
1040
|
+
if (done)
|
|
1041
|
+
break;
|
|
1042
|
+
const contentType = String(chunk.content_type ?? "text");
|
|
1043
|
+
const content = String(chunk.content ?? "");
|
|
1044
|
+
if (!content)
|
|
1045
|
+
continue;
|
|
1046
|
+
if (contentType === "audio") {
|
|
1047
|
+
// Audio chunk → append to audio buffer
|
|
1048
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1049
|
+
ws.send(JSON.stringify({
|
|
1050
|
+
type: "input_audio_buffer.append",
|
|
1051
|
+
audio: content
|
|
1052
|
+
}));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
// Text chunk → create conversation item and trigger response
|
|
1057
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1058
|
+
ws.send(JSON.stringify({
|
|
1059
|
+
type: "conversation.item.create",
|
|
1060
|
+
item: {
|
|
1061
|
+
type: "message",
|
|
1062
|
+
role: "user",
|
|
1063
|
+
content: [{ type: "input_text", text: content }]
|
|
1064
|
+
}
|
|
1065
|
+
}));
|
|
1066
|
+
ws.send(JSON.stringify({ type: "response.create" }));
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// Commit any remaining audio buffer
|
|
1071
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1072
|
+
ws.send(JSON.stringify({ type: "input_audio_buffer.commit" }));
|
|
1073
|
+
}
|
|
1074
|
+
// Wait for response.done with timeout
|
|
1075
|
+
await Promise.race([
|
|
1076
|
+
responseDonePromise,
|
|
1077
|
+
consumerPromise,
|
|
1078
|
+
new Promise((resolve) => setTimeout(resolve, 10000))
|
|
1079
|
+
]);
|
|
1080
|
+
}
|
|
1081
|
+
finally {
|
|
1082
|
+
if (ws.readyState === WebSocket.OPEN ||
|
|
1083
|
+
ws.readyState === WebSocket.CONNECTING) {
|
|
1084
|
+
ws.close();
|
|
1085
|
+
}
|
|
1086
|
+
consumerDone = true;
|
|
1087
|
+
}
|
|
1088
|
+
await consumerPromise;
|
|
1089
|
+
// Emit final done chunks
|
|
1090
|
+
await outputs.emit("chunk", {
|
|
1091
|
+
type: "chunk",
|
|
1092
|
+
content: "",
|
|
1093
|
+
done: true,
|
|
1094
|
+
content_type: "text"
|
|
1095
|
+
});
|
|
1096
|
+
await outputs.emit("text", fullText);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
__decorate([
|
|
1100
|
+
prop({
|
|
1101
|
+
type: "enum",
|
|
1102
|
+
default: "gpt-4o-mini-realtime-preview",
|
|
1103
|
+
title: "Model",
|
|
1104
|
+
values: ["gpt-4o-realtime-preview", "gpt-4o-mini-realtime-preview"]
|
|
1105
|
+
})
|
|
1106
|
+
], RealtimeAgentNode.prototype, "model", void 0);
|
|
1107
|
+
__decorate([
|
|
1108
|
+
prop({
|
|
1109
|
+
type: "str",
|
|
1110
|
+
default: "You are an AI assistant interacting in real-time. " +
|
|
1111
|
+
"Be concise but clear. Prioritize key information first. " +
|
|
1112
|
+
"Ask clarifying questions when needed.",
|
|
1113
|
+
title: "System",
|
|
1114
|
+
description: "System instructions for the realtime session"
|
|
1115
|
+
})
|
|
1116
|
+
], RealtimeAgentNode.prototype, "system", void 0);
|
|
1117
|
+
__decorate([
|
|
1118
|
+
prop({
|
|
1119
|
+
type: "chunk",
|
|
1120
|
+
default: {
|
|
1121
|
+
type: "chunk",
|
|
1122
|
+
node_id: null,
|
|
1123
|
+
thread_id: null,
|
|
1124
|
+
workflow_id: null,
|
|
1125
|
+
content_type: "text",
|
|
1126
|
+
content: "",
|
|
1127
|
+
content_metadata: {},
|
|
1128
|
+
done: false,
|
|
1129
|
+
thinking: false
|
|
1130
|
+
},
|
|
1131
|
+
title: "Chunk",
|
|
1132
|
+
description: "The audio chunk to use as input."
|
|
1133
|
+
})
|
|
1134
|
+
], RealtimeAgentNode.prototype, "chunk", void 0);
|
|
1135
|
+
__decorate([
|
|
1136
|
+
prop({
|
|
1137
|
+
type: "enum",
|
|
1138
|
+
default: "alloy",
|
|
1139
|
+
title: "Voice",
|
|
1140
|
+
description: "The voice for the audio output",
|
|
1141
|
+
values: [
|
|
1142
|
+
"none",
|
|
1143
|
+
"ash",
|
|
1144
|
+
"alloy",
|
|
1145
|
+
"ballad",
|
|
1146
|
+
"coral",
|
|
1147
|
+
"echo",
|
|
1148
|
+
"fable",
|
|
1149
|
+
"onyx",
|
|
1150
|
+
"nova",
|
|
1151
|
+
"shimmer",
|
|
1152
|
+
"sage",
|
|
1153
|
+
"verse"
|
|
1154
|
+
]
|
|
1155
|
+
})
|
|
1156
|
+
], RealtimeAgentNode.prototype, "voice", void 0);
|
|
1157
|
+
__decorate([
|
|
1158
|
+
prop({
|
|
1159
|
+
type: "float",
|
|
1160
|
+
default: 1,
|
|
1161
|
+
title: "Speed",
|
|
1162
|
+
description: "The speed of the model’s spoken response",
|
|
1163
|
+
min: 0.25,
|
|
1164
|
+
max: 1.5
|
|
1165
|
+
})
|
|
1166
|
+
], RealtimeAgentNode.prototype, "speed", void 0);
|
|
1167
|
+
__decorate([
|
|
1168
|
+
prop({
|
|
1169
|
+
type: "float",
|
|
1170
|
+
default: 0.8,
|
|
1171
|
+
title: "Temperature",
|
|
1172
|
+
description: "The temperature for the response",
|
|
1173
|
+
min: 0.6,
|
|
1174
|
+
max: 1.2
|
|
1175
|
+
})
|
|
1176
|
+
], RealtimeAgentNode.prototype, "temperature", void 0);
|
|
1177
|
+
// ---------------------------------------------------------------------------
|
|
1178
|
+
// 10. RealtimeTranscription
|
|
1179
|
+
// ---------------------------------------------------------------------------
|
|
1180
|
+
export class RealtimeTranscriptionNode extends BaseNode {
|
|
1181
|
+
static nodeType = "openai.agents.RealtimeTranscription";
|
|
1182
|
+
static title = "Realtime Transcription";
|
|
1183
|
+
static inlineFields = [];
|
|
1184
|
+
static inputFields = ["chunk"];
|
|
1185
|
+
static description = "Realtime speech-to-text over OpenAI's WebSocket Realtime API, streaming audio chunks in and transcription results out as they arrive.\n" +
|
|
1186
|
+
" audio, transcription, stt, streaming, realtime, websocket, openai";
|
|
1187
|
+
static metadataOutputTypes = {
|
|
1188
|
+
text: "str",
|
|
1189
|
+
chunk: "chunk"
|
|
1190
|
+
};
|
|
1191
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
1192
|
+
static isStreamingInput = true;
|
|
1193
|
+
async process() {
|
|
1194
|
+
return {};
|
|
1195
|
+
}
|
|
1196
|
+
async run(inputs, outputs, context) {
|
|
1197
|
+
let apiKey = "";
|
|
1198
|
+
if (context && isCallable(context.getSecret)) {
|
|
1199
|
+
apiKey = (await context.getSecret("OPENAI_API_KEY")) ?? "";
|
|
1200
|
+
}
|
|
1201
|
+
if (!apiKey)
|
|
1202
|
+
apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
1203
|
+
if (!apiKey)
|
|
1204
|
+
throw new Error("OPENAI_API_KEY is not configured");
|
|
1205
|
+
const model = this.model;
|
|
1206
|
+
const temperature = this.temperature;
|
|
1207
|
+
const { WebSocket } = await import("ws");
|
|
1208
|
+
const wsUrl = `wss://api.openai.com/v1/realtime?model=${encodeURIComponent(model)}`;
|
|
1209
|
+
const ws = new WebSocket(wsUrl, {
|
|
1210
|
+
headers: {
|
|
1211
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1212
|
+
"OpenAI-Beta": "realtime=v1"
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
await new Promise((resolve, reject) => {
|
|
1216
|
+
ws.on("open", resolve);
|
|
1217
|
+
ws.on("error", reject);
|
|
1218
|
+
});
|
|
1219
|
+
// Configure session for transcription only (no audio output)
|
|
1220
|
+
ws.send(JSON.stringify({
|
|
1221
|
+
type: "session.update",
|
|
1222
|
+
session: {
|
|
1223
|
+
modalities: ["text"],
|
|
1224
|
+
instructions: this.system || undefined,
|
|
1225
|
+
input_audio_format: "pcm16",
|
|
1226
|
+
input_audio_transcription: { model: "gpt-4o-mini-transcribe" },
|
|
1227
|
+
turn_detection: {
|
|
1228
|
+
type: "server_vad",
|
|
1229
|
+
silence_duration_ms: 500
|
|
1230
|
+
},
|
|
1231
|
+
temperature
|
|
1232
|
+
}
|
|
1233
|
+
}));
|
|
1234
|
+
// Consumer: listen for transcription events
|
|
1235
|
+
let fullText = "";
|
|
1236
|
+
let consumerDone = false;
|
|
1237
|
+
let resolveLastTranscript;
|
|
1238
|
+
const lastTranscriptPromise = new Promise((resolve) => {
|
|
1239
|
+
resolveLastTranscript = resolve;
|
|
1240
|
+
});
|
|
1241
|
+
let finalizeRequested = false;
|
|
1242
|
+
const consumerPromise = new Promise((resolve, reject) => {
|
|
1243
|
+
ws.on("message", async (data) => {
|
|
1244
|
+
try {
|
|
1245
|
+
const msg = JSON.parse(data.toString());
|
|
1246
|
+
const msgType = String(msg.type ?? "");
|
|
1247
|
+
if (msgType ===
|
|
1248
|
+
"conversation.item.input_audio_transcription.completed") {
|
|
1249
|
+
const text = String(msg.transcript ?? "");
|
|
1250
|
+
if (text) {
|
|
1251
|
+
fullText += (fullText ? " " : "") + text;
|
|
1252
|
+
await outputs.emit("chunk", {
|
|
1253
|
+
type: "chunk",
|
|
1254
|
+
content: text,
|
|
1255
|
+
done: false,
|
|
1256
|
+
content_type: "text"
|
|
1257
|
+
});
|
|
1258
|
+
if (finalizeRequested) {
|
|
1259
|
+
resolveLastTranscript();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
else if (msgType === "response.done") {
|
|
1264
|
+
if (finalizeRequested) {
|
|
1265
|
+
resolveLastTranscript();
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
else if (msgType === "error") {
|
|
1269
|
+
const errMsg = msg.error?.message ??
|
|
1270
|
+
"Unknown realtime error";
|
|
1271
|
+
reject(new Error(`OpenAI Realtime error: ${errMsg}`));
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
catch (err) {
|
|
1275
|
+
if (!consumerDone)
|
|
1276
|
+
reject(err);
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
ws.on("error", (err) => {
|
|
1280
|
+
if (!consumerDone)
|
|
1281
|
+
reject(err);
|
|
1282
|
+
});
|
|
1283
|
+
ws.on("close", () => {
|
|
1284
|
+
consumerDone = true;
|
|
1285
|
+
resolve();
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
// Producer: read streaming audio input and forward to WebSocket
|
|
1289
|
+
try {
|
|
1290
|
+
for await (const [handle, item] of inputs.any()) {
|
|
1291
|
+
if (handle === "__control__")
|
|
1292
|
+
continue;
|
|
1293
|
+
const chunk = item;
|
|
1294
|
+
let audioB64;
|
|
1295
|
+
let done = false;
|
|
1296
|
+
if (isString(chunk)) {
|
|
1297
|
+
audioB64 = chunk;
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
if (chunk.content_type && chunk.content_type !== "audio")
|
|
1301
|
+
continue;
|
|
1302
|
+
audioB64 = String(chunk.content ?? "");
|
|
1303
|
+
done = Boolean(chunk.done ?? false);
|
|
1304
|
+
}
|
|
1305
|
+
if (done)
|
|
1306
|
+
break;
|
|
1307
|
+
if (!audioB64)
|
|
1308
|
+
continue;
|
|
1309
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1310
|
+
ws.send(JSON.stringify({
|
|
1311
|
+
type: "input_audio_buffer.append",
|
|
1312
|
+
audio: audioB64
|
|
1313
|
+
}));
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
// Commit remaining audio
|
|
1317
|
+
finalizeRequested = true;
|
|
1318
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1319
|
+
ws.send(JSON.stringify({ type: "input_audio_buffer.commit" }));
|
|
1320
|
+
}
|
|
1321
|
+
// Wait for final transcript
|
|
1322
|
+
await Promise.race([
|
|
1323
|
+
lastTranscriptPromise,
|
|
1324
|
+
consumerPromise,
|
|
1325
|
+
new Promise((resolve) => setTimeout(resolve, 5000))
|
|
1326
|
+
]);
|
|
1327
|
+
}
|
|
1328
|
+
finally {
|
|
1329
|
+
if (ws.readyState === WebSocket.OPEN ||
|
|
1330
|
+
ws.readyState === WebSocket.CONNECTING) {
|
|
1331
|
+
ws.close();
|
|
1332
|
+
}
|
|
1333
|
+
consumerDone = true;
|
|
1334
|
+
}
|
|
1335
|
+
await consumerPromise;
|
|
1336
|
+
// Emit final done chunk and full text
|
|
1337
|
+
await outputs.emit("chunk", {
|
|
1338
|
+
type: "chunk",
|
|
1339
|
+
content: "",
|
|
1340
|
+
done: true,
|
|
1341
|
+
content_type: "text"
|
|
1342
|
+
});
|
|
1343
|
+
await outputs.emit("text", fullText);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
__decorate([
|
|
1347
|
+
prop({
|
|
1348
|
+
type: "enum",
|
|
1349
|
+
default: "gpt-4o-mini-realtime-preview",
|
|
1350
|
+
title: "Model",
|
|
1351
|
+
description: "The realtime model to use.",
|
|
1352
|
+
values: ["gpt-4o-realtime-preview", "gpt-4o-mini-realtime-preview"]
|
|
1353
|
+
})
|
|
1354
|
+
], RealtimeTranscriptionNode.prototype, "model", void 0);
|
|
1355
|
+
__decorate([
|
|
1356
|
+
prop({
|
|
1357
|
+
type: "chunk",
|
|
1358
|
+
default: {
|
|
1359
|
+
type: "chunk",
|
|
1360
|
+
node_id: null,
|
|
1361
|
+
thread_id: null,
|
|
1362
|
+
workflow_id: null,
|
|
1363
|
+
content_type: "audio",
|
|
1364
|
+
content: "",
|
|
1365
|
+
content_metadata: {},
|
|
1366
|
+
done: false,
|
|
1367
|
+
thinking: false
|
|
1368
|
+
},
|
|
1369
|
+
title: "Chunk",
|
|
1370
|
+
description: "Audio chunk input stream (base64-encoded PCM16 audio)."
|
|
1371
|
+
})
|
|
1372
|
+
], RealtimeTranscriptionNode.prototype, "chunk", void 0);
|
|
1373
|
+
__decorate([
|
|
1374
|
+
prop({
|
|
1375
|
+
type: "str",
|
|
1376
|
+
default: "",
|
|
1377
|
+
title: "System",
|
|
1378
|
+
description: "System instructions (optional)"
|
|
1379
|
+
})
|
|
1380
|
+
], RealtimeTranscriptionNode.prototype, "system", void 0);
|
|
1381
|
+
__decorate([
|
|
1382
|
+
prop({
|
|
1383
|
+
type: "float",
|
|
1384
|
+
default: 0.8,
|
|
1385
|
+
title: "Temperature",
|
|
1386
|
+
description: "Decoding temperature"
|
|
1387
|
+
})
|
|
1388
|
+
], RealtimeTranscriptionNode.prototype, "temperature", void 0);
|
|
1389
|
+
// ---------------------------------------------------------------------------
|
|
1390
|
+
// 11. LiveAgent (GPT-Live)
|
|
1391
|
+
// ---------------------------------------------------------------------------
|
|
1392
|
+
export const LIVE_SESSIONS_URL = "wss://api.openai.com/v1/live/sessions";
|
|
1393
|
+
const LIVE_SAMPLE_RATE = 24000;
|
|
1394
|
+
/** 100 ms of PCM16 mono silence at 24 kHz. */
|
|
1395
|
+
const LIVE_SILENCE_FRAME = Buffer.alloc((LIVE_SAMPLE_RATE / 10) * 2).toString("base64");
|
|
1396
|
+
const LIVE_SILENCE_INTERVAL_MS = 100;
|
|
1397
|
+
/** Quiet time after input ends before the session is closed. */
|
|
1398
|
+
const LIVE_IDLE_CLOSE_MS = 3000;
|
|
1399
|
+
/** Upper bound on the drain phase after input ends. */
|
|
1400
|
+
const LIVE_MAX_DRAIN_MS = 120_000;
|
|
1401
|
+
/** The API docs recommend waiting up to 15 s for `session.closed`. */
|
|
1402
|
+
const LIVE_CLOSE_TIMEOUT_MS = 15_000;
|
|
1403
|
+
const LIVE_RESPONSE_TERMINAL_EVENTS = new Set([
|
|
1404
|
+
"response.completed",
|
|
1405
|
+
"response.failed",
|
|
1406
|
+
"response.incomplete",
|
|
1407
|
+
"response.cancelled"
|
|
1408
|
+
]);
|
|
1409
|
+
export class LiveAgentNode extends BaseNode {
|
|
1410
|
+
static nodeType = "openai.agents.LiveAgent";
|
|
1411
|
+
static body = "content_card";
|
|
1412
|
+
static title = "Live Agent";
|
|
1413
|
+
static description = "Full-duplex voice agent over OpenAI's GPT-Live WebSocket API. Streams microphone audio in and spoken audio plus transcripts out, delegating lookups and reasoning to a Responses backend model.\n" +
|
|
1414
|
+
" live, gpt-live, realtime, voice, streaming, openai, audio-input, audio-output, websocket";
|
|
1415
|
+
static metadataOutputTypes = {
|
|
1416
|
+
chunk: "chunk",
|
|
1417
|
+
audio: "audio",
|
|
1418
|
+
text: "str",
|
|
1419
|
+
input_transcript: "str"
|
|
1420
|
+
};
|
|
1421
|
+
static inlineFields = ["instructions"];
|
|
1422
|
+
static inputFields = ["chunk"];
|
|
1423
|
+
static requiredSettings = ["OPENAI_API_KEY"];
|
|
1424
|
+
static isStreamingInput = true;
|
|
1425
|
+
async process() {
|
|
1426
|
+
return {};
|
|
1427
|
+
}
|
|
1428
|
+
async run(inputs, outputs, context) {
|
|
1429
|
+
let apiKey = "";
|
|
1430
|
+
if (context && isCallable(context.getSecret)) {
|
|
1431
|
+
apiKey = (await context.getSecret("OPENAI_API_KEY")) ?? "";
|
|
1432
|
+
}
|
|
1433
|
+
if (!apiKey)
|
|
1434
|
+
apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
1435
|
+
if (!apiKey)
|
|
1436
|
+
throw new Error("OPENAI_API_KEY is not configured");
|
|
1437
|
+
const backendModel = this.backend_model.trim();
|
|
1438
|
+
if (!backendModel) {
|
|
1439
|
+
throw new Error("Backend Model is required for GPT-Live delegation");
|
|
1440
|
+
}
|
|
1441
|
+
const responses = { model: backendModel };
|
|
1442
|
+
if (this.backend_instructions.trim()) {
|
|
1443
|
+
responses.instructions = this.backend_instructions;
|
|
1444
|
+
}
|
|
1445
|
+
if (this.web_search) {
|
|
1446
|
+
responses.tools = [{ type: "web_search" }];
|
|
1447
|
+
responses.tool_choice = "auto";
|
|
1448
|
+
}
|
|
1449
|
+
const { WebSocket } = await import("ws");
|
|
1450
|
+
const ws = new WebSocket(LIVE_SESSIONS_URL, {
|
|
1451
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1452
|
+
});
|
|
1453
|
+
const send = (event) => {
|
|
1454
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
1455
|
+
ws.send(JSON.stringify(event));
|
|
1456
|
+
};
|
|
1457
|
+
let outputText = "";
|
|
1458
|
+
let inputText = "";
|
|
1459
|
+
const outputAudio = [];
|
|
1460
|
+
let lastActivity;
|
|
1461
|
+
const openDelegations = new Set();
|
|
1462
|
+
let started = false;
|
|
1463
|
+
let finalized = false;
|
|
1464
|
+
let failure = null;
|
|
1465
|
+
let resolveStarted;
|
|
1466
|
+
let resolveEnded;
|
|
1467
|
+
const startedPromise = new Promise((resolve) => {
|
|
1468
|
+
resolveStarted = resolve;
|
|
1469
|
+
});
|
|
1470
|
+
// Settles on session.closed, socket close, or a fatal error.
|
|
1471
|
+
const endedPromise = new Promise((resolve) => {
|
|
1472
|
+
resolveEnded = resolve;
|
|
1473
|
+
});
|
|
1474
|
+
const fail = (err) => {
|
|
1475
|
+
failure ??= err;
|
|
1476
|
+
resolveStarted();
|
|
1477
|
+
resolveEnded();
|
|
1478
|
+
};
|
|
1479
|
+
ws.on("open", () => {
|
|
1480
|
+
send({
|
|
1481
|
+
type: "session.start",
|
|
1482
|
+
event_id: "event_start",
|
|
1483
|
+
session: {
|
|
1484
|
+
model: this.model,
|
|
1485
|
+
instructions: this.instructions || undefined,
|
|
1486
|
+
audio: {
|
|
1487
|
+
format: { type: "audio/pcm", rate: LIVE_SAMPLE_RATE },
|
|
1488
|
+
output: { voice: this.voice }
|
|
1489
|
+
},
|
|
1490
|
+
delegation: { type: "responses", responses }
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
});
|
|
1494
|
+
// Emits run sequentially so chunk order matches arrival order.
|
|
1495
|
+
let emitQueue = Promise.resolve();
|
|
1496
|
+
const enqueueEmit = (fn) => {
|
|
1497
|
+
emitQueue = emitQueue.then(fn).catch((err) => {
|
|
1498
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
1499
|
+
});
|
|
1500
|
+
};
|
|
1501
|
+
ws.on("message", (data) => {
|
|
1502
|
+
let msg;
|
|
1503
|
+
try {
|
|
1504
|
+
msg = JSON.parse(data.toString());
|
|
1505
|
+
}
|
|
1506
|
+
catch {
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const type = String(msg.type ?? "");
|
|
1510
|
+
if (type === "session.started") {
|
|
1511
|
+
started = true;
|
|
1512
|
+
resolveStarted();
|
|
1513
|
+
}
|
|
1514
|
+
else if (type === "session.output_audio.delta") {
|
|
1515
|
+
const audioB64 = String(msg.delta ?? "");
|
|
1516
|
+
if (!audioB64)
|
|
1517
|
+
return;
|
|
1518
|
+
lastActivity = Date.now();
|
|
1519
|
+
outputAudio.push(Buffer.from(audioB64, "base64"));
|
|
1520
|
+
enqueueEmit(async () => {
|
|
1521
|
+
await outputs.emit("chunk", {
|
|
1522
|
+
type: "chunk",
|
|
1523
|
+
content: audioB64,
|
|
1524
|
+
done: false,
|
|
1525
|
+
content_type: "audio",
|
|
1526
|
+
content_metadata: {
|
|
1527
|
+
format: "pcm16le",
|
|
1528
|
+
encoding: "pcm16le",
|
|
1529
|
+
sample_rate: LIVE_SAMPLE_RATE,
|
|
1530
|
+
channels: 1
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
else if (type === "session.output_transcript.delta") {
|
|
1536
|
+
const delta = String(msg.delta ?? "");
|
|
1537
|
+
if (!delta)
|
|
1538
|
+
return;
|
|
1539
|
+
lastActivity = Date.now();
|
|
1540
|
+
outputText += delta;
|
|
1541
|
+
enqueueEmit(async () => {
|
|
1542
|
+
await outputs.emit("chunk", {
|
|
1543
|
+
type: "chunk",
|
|
1544
|
+
content: delta,
|
|
1545
|
+
done: false,
|
|
1546
|
+
content_type: "text"
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
else if (type === "session.input_transcript.delta") {
|
|
1551
|
+
inputText += String(msg.delta ?? "");
|
|
1552
|
+
lastActivity = Date.now();
|
|
1553
|
+
}
|
|
1554
|
+
else if (type === "session.delegation.created") {
|
|
1555
|
+
const delegation = msg.delegation;
|
|
1556
|
+
if (delegation?.id)
|
|
1557
|
+
openDelegations.add(String(delegation.id));
|
|
1558
|
+
lastActivity = Date.now();
|
|
1559
|
+
}
|
|
1560
|
+
else if (type === "response.event") {
|
|
1561
|
+
lastActivity = Date.now();
|
|
1562
|
+
const nested = msg.event;
|
|
1563
|
+
if (LIVE_RESPONSE_TERMINAL_EVENTS.has(String(nested?.type ?? ""))) {
|
|
1564
|
+
openDelegations.delete(String(msg.delegation_id ?? ""));
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
else if (type === "session.closed") {
|
|
1568
|
+
finalized = true;
|
|
1569
|
+
resolveEnded();
|
|
1570
|
+
}
|
|
1571
|
+
else if (type === "error") {
|
|
1572
|
+
const error = msg.error;
|
|
1573
|
+
const message = String(error?.message ?? "Unknown GPT-Live error");
|
|
1574
|
+
// Before startup every error is fatal. Afterwards, moderation and
|
|
1575
|
+
// rejected commands are reported without ending the session.
|
|
1576
|
+
if (!started) {
|
|
1577
|
+
fail(new Error(`OpenAI GPT-Live error: ${message}`));
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
log.warn(`OpenAI GPT-Live error: ${message}`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
ws.on("error", (err) => fail(err));
|
|
1585
|
+
ws.on("close", () => {
|
|
1586
|
+
if (!started) {
|
|
1587
|
+
fail(new Error("GPT-Live connection closed before session.started"));
|
|
1588
|
+
}
|
|
1589
|
+
resolveEnded();
|
|
1590
|
+
});
|
|
1591
|
+
let silenceTimer;
|
|
1592
|
+
try {
|
|
1593
|
+
await startedPromise;
|
|
1594
|
+
if (failure)
|
|
1595
|
+
throw failure;
|
|
1596
|
+
for await (const [handle, item] of inputs.any()) {
|
|
1597
|
+
if (handle === "__control__")
|
|
1598
|
+
continue;
|
|
1599
|
+
if (failure)
|
|
1600
|
+
break;
|
|
1601
|
+
if (isString(item)) {
|
|
1602
|
+
if (item)
|
|
1603
|
+
send({ type: "session.input_audio.append", audio: item });
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
const chunk = item;
|
|
1607
|
+
if (chunk.done)
|
|
1608
|
+
break;
|
|
1609
|
+
const content = String(chunk.content ?? "");
|
|
1610
|
+
if (!content)
|
|
1611
|
+
continue;
|
|
1612
|
+
if (String(chunk.content_type ?? "text") === "audio") {
|
|
1613
|
+
const meta = chunk.content_metadata;
|
|
1614
|
+
const rate = meta?.sample_rate;
|
|
1615
|
+
if (rate !== undefined && Number(rate) !== LIVE_SAMPLE_RATE) {
|
|
1616
|
+
throw new Error(`GPT-Live expects ${LIVE_SAMPLE_RATE} Hz PCM16 audio, got ${String(rate)} Hz`);
|
|
1617
|
+
}
|
|
1618
|
+
send({ type: "session.input_audio.append", audio: content });
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
send({
|
|
1622
|
+
type: "response.item.create",
|
|
1623
|
+
item: {
|
|
1624
|
+
type: "message",
|
|
1625
|
+
role: "user",
|
|
1626
|
+
content: [{ type: "input_text", text: content }]
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
send({ type: "response.create" });
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (failure)
|
|
1633
|
+
throw failure;
|
|
1634
|
+
// Input has ended, but the reply may still be in progress. The session
|
|
1635
|
+
// timeline advances with input audio, so keep sending silence until
|
|
1636
|
+
// the model and its delegations go quiet, then close gracefully.
|
|
1637
|
+
lastActivity = Date.now();
|
|
1638
|
+
silenceTimer = setInterval(() => {
|
|
1639
|
+
send({
|
|
1640
|
+
type: "session.input_audio.append",
|
|
1641
|
+
audio: LIVE_SILENCE_FRAME
|
|
1642
|
+
});
|
|
1643
|
+
}, LIVE_SILENCE_INTERVAL_MS);
|
|
1644
|
+
const drainStart = Date.now();
|
|
1645
|
+
while (!failure &&
|
|
1646
|
+
ws.readyState === WebSocket.OPEN &&
|
|
1647
|
+
Date.now() - drainStart < LIVE_MAX_DRAIN_MS &&
|
|
1648
|
+
(openDelegations.size > 0 ||
|
|
1649
|
+
Date.now() - lastActivity < LIVE_IDLE_CLOSE_MS)) {
|
|
1650
|
+
await new Promise((resolve) => setTimeout(resolve, LIVE_SILENCE_INTERVAL_MS));
|
|
1651
|
+
}
|
|
1652
|
+
clearInterval(silenceTimer);
|
|
1653
|
+
silenceTimer = undefined;
|
|
1654
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1655
|
+
send({ type: "session.close" });
|
|
1656
|
+
let closeTimer;
|
|
1657
|
+
await Promise.race([
|
|
1658
|
+
endedPromise,
|
|
1659
|
+
new Promise((resolve) => {
|
|
1660
|
+
closeTimer = setTimeout(resolve, LIVE_CLOSE_TIMEOUT_MS);
|
|
1661
|
+
})
|
|
1662
|
+
]);
|
|
1663
|
+
clearTimeout(closeTimer);
|
|
1664
|
+
if (!finalized) {
|
|
1665
|
+
log.warn("GPT-Live session.closed was not received; final usage is unconfirmed");
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
await emitQueue;
|
|
1669
|
+
if (failure)
|
|
1670
|
+
throw failure;
|
|
1671
|
+
}
|
|
1672
|
+
finally {
|
|
1673
|
+
if (silenceTimer)
|
|
1674
|
+
clearInterval(silenceTimer);
|
|
1675
|
+
if (ws.readyState === WebSocket.OPEN ||
|
|
1676
|
+
ws.readyState === WebSocket.CONNECTING) {
|
|
1677
|
+
ws.close();
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
await outputs.emit("chunk", {
|
|
1681
|
+
type: "chunk",
|
|
1682
|
+
content: "",
|
|
1683
|
+
done: true,
|
|
1684
|
+
content_type: "text"
|
|
1685
|
+
});
|
|
1686
|
+
if (outputAudio.length > 0) {
|
|
1687
|
+
const pcm = Buffer.concat(outputAudio);
|
|
1688
|
+
const wav = encodeWav(pcm, LIVE_SAMPLE_RATE, 1, 16);
|
|
1689
|
+
await outputs.emit("audio", {
|
|
1690
|
+
type: "audio",
|
|
1691
|
+
data: Buffer.from(wav).toString("base64"),
|
|
1692
|
+
content_type: "audio/wav"
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
await outputs.emit("text", outputText);
|
|
1696
|
+
await outputs.emit("input_transcript", inputText);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
__decorate([
|
|
1700
|
+
prop({
|
|
1701
|
+
type: "enum",
|
|
1702
|
+
default: "gpt-live-1",
|
|
1703
|
+
title: "Model",
|
|
1704
|
+
values: ["gpt-live-1"]
|
|
1705
|
+
})
|
|
1706
|
+
], LiveAgentNode.prototype, "model", void 0);
|
|
1707
|
+
__decorate([
|
|
1708
|
+
prop({
|
|
1709
|
+
type: "str",
|
|
1710
|
+
default: "Be concise. Delegate requests needing current information or lookups to the backend.",
|
|
1711
|
+
title: "Instructions",
|
|
1712
|
+
description: "Conversation instructions for the voice model: style, and when to delegate to the backend"
|
|
1713
|
+
})
|
|
1714
|
+
], LiveAgentNode.prototype, "instructions", void 0);
|
|
1715
|
+
__decorate([
|
|
1716
|
+
prop({
|
|
1717
|
+
type: "chunk",
|
|
1718
|
+
default: {
|
|
1719
|
+
type: "chunk",
|
|
1720
|
+
node_id: null,
|
|
1721
|
+
thread_id: null,
|
|
1722
|
+
workflow_id: null,
|
|
1723
|
+
content_type: "audio",
|
|
1724
|
+
content: "",
|
|
1725
|
+
content_metadata: {},
|
|
1726
|
+
done: false,
|
|
1727
|
+
thinking: false
|
|
1728
|
+
},
|
|
1729
|
+
title: "Chunk",
|
|
1730
|
+
description: "Streaming input. Audio chunks are base64 mono PCM16 at 24 kHz. Text chunks are queued for the backend as user messages."
|
|
1731
|
+
})
|
|
1732
|
+
], LiveAgentNode.prototype, "chunk", void 0);
|
|
1733
|
+
__decorate([
|
|
1734
|
+
prop({
|
|
1735
|
+
type: "enum",
|
|
1736
|
+
default: "marin",
|
|
1737
|
+
title: "Voice",
|
|
1738
|
+
description: "The voice for spoken output. Fixed for the session.",
|
|
1739
|
+
values: [
|
|
1740
|
+
"marin",
|
|
1741
|
+
"cedar",
|
|
1742
|
+
"alloy",
|
|
1743
|
+
"ash",
|
|
1744
|
+
"ballad",
|
|
1745
|
+
"coral",
|
|
1746
|
+
"echo",
|
|
1747
|
+
"sage",
|
|
1748
|
+
"shimmer",
|
|
1749
|
+
"verse",
|
|
1750
|
+
"quartz",
|
|
1751
|
+
"ripple",
|
|
1752
|
+
"vesper",
|
|
1753
|
+
"willow",
|
|
1754
|
+
"stone",
|
|
1755
|
+
"gleam",
|
|
1756
|
+
"meridian",
|
|
1757
|
+
"bossa",
|
|
1758
|
+
"tempo",
|
|
1759
|
+
"beacon",
|
|
1760
|
+
"delta",
|
|
1761
|
+
"cinder"
|
|
1762
|
+
]
|
|
1763
|
+
})
|
|
1764
|
+
], LiveAgentNode.prototype, "voice", void 0);
|
|
1765
|
+
__decorate([
|
|
1766
|
+
prop({
|
|
1767
|
+
type: "str",
|
|
1768
|
+
default: "gpt-5.6-luna",
|
|
1769
|
+
title: "Backend Model",
|
|
1770
|
+
description: "Responses model that handles delegated reasoning and tools"
|
|
1771
|
+
})
|
|
1772
|
+
], LiveAgentNode.prototype, "backend_model", void 0);
|
|
1773
|
+
__decorate([
|
|
1774
|
+
prop({
|
|
1775
|
+
type: "str",
|
|
1776
|
+
default: "",
|
|
1777
|
+
title: "Backend Instructions",
|
|
1778
|
+
description: "Prompt for the backend model: task rules and how to return results"
|
|
1779
|
+
})
|
|
1780
|
+
], LiveAgentNode.prototype, "backend_instructions", void 0);
|
|
1781
|
+
__decorate([
|
|
1782
|
+
prop({
|
|
1783
|
+
type: "bool",
|
|
1784
|
+
default: true,
|
|
1785
|
+
title: "Web Search",
|
|
1786
|
+
description: "Let the backend model search the web"
|
|
1787
|
+
})
|
|
1788
|
+
], LiveAgentNode.prototype, "web_search", void 0);
|
|
1789
|
+
// ---------------------------------------------------------------------------
|
|
1790
|
+
// Export
|
|
1791
|
+
// ---------------------------------------------------------------------------
|
|
1792
|
+
export const OPENAI_NODES = tagAsServer([
|
|
1793
|
+
EmbeddingNode,
|
|
1794
|
+
WebSearchNode,
|
|
1795
|
+
ModerationNode,
|
|
1796
|
+
CreateImageNode,
|
|
1797
|
+
EditImageNode,
|
|
1798
|
+
ImageVariationNode,
|
|
1799
|
+
TextToSpeechNode,
|
|
1800
|
+
TranslateNode,
|
|
1801
|
+
TranscribeNode,
|
|
1802
|
+
RealtimeAgentNode,
|
|
1803
|
+
RealtimeTranscriptionNode,
|
|
1804
|
+
LiveAgentNode
|
|
1805
|
+
]);
|
|
1806
|
+
//# sourceMappingURL=openai.js.map
|