@vornrun/connector-openai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +173 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +1185 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/connector.ts
|
|
4
|
+
import {
|
|
5
|
+
defineConnector
|
|
6
|
+
} from "@vornrun/connector-sdk";
|
|
7
|
+
|
|
8
|
+
// src/client.ts
|
|
9
|
+
var API_ROOT = "https://api.openai.com/v1";
|
|
10
|
+
var RETRYABLE_429_CODES = /* @__PURE__ */ new Set(["rate_limit_error", "rate_limit_exceeded", "slow_down", "server_is_overloaded"]);
|
|
11
|
+
var RETRYABLE_SERVER_STATUS = /* @__PURE__ */ new Set([500, 502, 503, 504]);
|
|
12
|
+
var DEFAULT_RATE_LIMIT_WAIT_MS = 2e3;
|
|
13
|
+
var DEFAULT_SERVER_ERROR_WAIT_MS = 1e3;
|
|
14
|
+
var MAX_JITTER_MS = 500;
|
|
15
|
+
var MAX_PREEMPTIVE_WAIT_MS = 1e4;
|
|
16
|
+
var MAX_ERROR_BODY = 300;
|
|
17
|
+
var KEY_HINT = "Create one at https://platform.openai.com/api-keys (Create new secret key) and set OPENAI_API_KEY on the connection.";
|
|
18
|
+
function normalizeKey(raw) {
|
|
19
|
+
const key = String(raw ?? "").trim();
|
|
20
|
+
if (key === "") throw new Error(`OPENAI_API_KEY is required. ${KEY_HINT}`);
|
|
21
|
+
return key;
|
|
22
|
+
}
|
|
23
|
+
var DURATION_PART = /(\d+(?:\.\d+)?)(ms|h|m|s)/g;
|
|
24
|
+
var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
|
|
25
|
+
function durationMs(value) {
|
|
26
|
+
const text2 = String(value ?? "").trim();
|
|
27
|
+
if (text2 === "") return void 0;
|
|
28
|
+
let total = 0;
|
|
29
|
+
let matched = "";
|
|
30
|
+
for (const part of text2.matchAll(DURATION_PART)) {
|
|
31
|
+
total += Number(part[1]) * UNIT_MS[part[2]];
|
|
32
|
+
matched += part[0];
|
|
33
|
+
}
|
|
34
|
+
return matched === text2 ? total : void 0;
|
|
35
|
+
}
|
|
36
|
+
function seconds(value) {
|
|
37
|
+
if (value === null || value.trim() === "") return void 0;
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
40
|
+
}
|
|
41
|
+
var OpenAIApiError = class extends Error {
|
|
42
|
+
status;
|
|
43
|
+
code;
|
|
44
|
+
type;
|
|
45
|
+
param;
|
|
46
|
+
requestId;
|
|
47
|
+
constructor(status, body, requestId) {
|
|
48
|
+
super(describeFailure(status, body, requestId));
|
|
49
|
+
this.name = "OpenAIApiError";
|
|
50
|
+
const error = errorOf(body);
|
|
51
|
+
this.status = status;
|
|
52
|
+
this.code = error.code ?? void 0;
|
|
53
|
+
this.type = error.type;
|
|
54
|
+
this.param = error.param ?? void 0;
|
|
55
|
+
this.requestId = requestId;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function errorOf(body) {
|
|
59
|
+
const error = body?.error;
|
|
60
|
+
return typeof error === "object" && error !== null ? error : {};
|
|
61
|
+
}
|
|
62
|
+
function describeFailure(status, body, requestId) {
|
|
63
|
+
const error = errorOf(body);
|
|
64
|
+
const label = error.code ?? error.type;
|
|
65
|
+
let message = typeof error.message === "string" ? error.message.trim() : "";
|
|
66
|
+
if (message === "") {
|
|
67
|
+
const raw = typeof body === "string" ? body : body === void 0 ? "" : JSON.stringify(body);
|
|
68
|
+
message = raw.length > MAX_ERROR_BODY ? `${raw.slice(0, MAX_ERROR_BODY)}\u2026` : raw;
|
|
69
|
+
}
|
|
70
|
+
const head = label ? `${status} ${label}` : String(status);
|
|
71
|
+
const tail = requestId ? ` (request ${requestId})` : "";
|
|
72
|
+
return `${head}: ${message || "no body"}${tail}`;
|
|
73
|
+
}
|
|
74
|
+
var defaultSleep = (ms) => new Promise((resolve) => {
|
|
75
|
+
setTimeout(resolve, ms);
|
|
76
|
+
});
|
|
77
|
+
var defaultFetch = (input, init) => globalThis.fetch(input, init);
|
|
78
|
+
async function readBody(response) {
|
|
79
|
+
const text2 = await response.text();
|
|
80
|
+
if (text2 === "") return void 0;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(text2);
|
|
83
|
+
} catch {
|
|
84
|
+
return text2;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function createOpenAIClient(options) {
|
|
88
|
+
const fetchImpl = options.fetchImpl ?? defaultFetch;
|
|
89
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
90
|
+
const warn = options.warn ?? ((message) => console.warn(message));
|
|
91
|
+
const now = options.now ?? (() => Date.now());
|
|
92
|
+
const random = options.random ?? Math.random;
|
|
93
|
+
const headers = {
|
|
94
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
95
|
+
...options.organization && { "openai-organization": options.organization },
|
|
96
|
+
...options.project && { "openai-project": options.project }
|
|
97
|
+
};
|
|
98
|
+
let exhaustedUntil = 0;
|
|
99
|
+
async function pause(ms, why) {
|
|
100
|
+
warn(`OpenAI: ${why}; waiting ${(ms / 1e3).toFixed(1)}s.`);
|
|
101
|
+
await sleep(ms);
|
|
102
|
+
}
|
|
103
|
+
function jitter() {
|
|
104
|
+
return Math.floor(random() * MAX_JITTER_MS);
|
|
105
|
+
}
|
|
106
|
+
function remember(response) {
|
|
107
|
+
const remaining = seconds(response.headers.get("x-ratelimit-remaining-requests"));
|
|
108
|
+
const reset = durationMs(response.headers.get("x-ratelimit-reset-requests"));
|
|
109
|
+
if (remaining === 0 && reset !== void 0) exhaustedUntil = now() + reset;
|
|
110
|
+
}
|
|
111
|
+
async function waitForBudget() {
|
|
112
|
+
const left = Math.min(MAX_PREEMPTIVE_WAIT_MS, exhaustedUntil - now());
|
|
113
|
+
if (left > 0) await pause(left, "the request budget is spent");
|
|
114
|
+
}
|
|
115
|
+
async function request(method, path, opts = {}) {
|
|
116
|
+
const url = new URL(`${API_ROOT}/${path.replace(/^\//, "")}`);
|
|
117
|
+
for (const [key, value] of Object.entries(opts.query ?? {})) {
|
|
118
|
+
if (value !== void 0 && value !== "") url.searchParams.set(key, String(value));
|
|
119
|
+
}
|
|
120
|
+
const init = {
|
|
121
|
+
method: method.toUpperCase(),
|
|
122
|
+
headers: opts.body === void 0 ? headers : { ...headers, "content-type": "application/json" },
|
|
123
|
+
...opts.body !== void 0 && { body: JSON.stringify(opts.body) }
|
|
124
|
+
};
|
|
125
|
+
const route = `${method.toUpperCase()} /${path.replace(/^\//, "")}`;
|
|
126
|
+
let rateLimited = false;
|
|
127
|
+
let serverErrored = false;
|
|
128
|
+
for (; ; ) {
|
|
129
|
+
await waitForBudget();
|
|
130
|
+
const response = await fetchImpl(url.toString(), init);
|
|
131
|
+
remember(response);
|
|
132
|
+
const body = await readBody(response);
|
|
133
|
+
if (response.ok) return body;
|
|
134
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
135
|
+
const failure = new OpenAIApiError(response.status, body, requestId);
|
|
136
|
+
const retryAfter = seconds(response.headers.get("retry-after"));
|
|
137
|
+
if (response.status === 429) {
|
|
138
|
+
const retryable = failure.code === void 0 || RETRYABLE_429_CODES.has(failure.code);
|
|
139
|
+
if (!retryable || rateLimited) throw failure;
|
|
140
|
+
rateLimited = true;
|
|
141
|
+
const asked = retryAfter !== void 0 ? retryAfter * 1e3 : durationMs(response.headers.get("x-ratelimit-reset-requests")) ?? DEFAULT_RATE_LIMIT_WAIT_MS;
|
|
142
|
+
await pause(asked + jitter(), `${route} was rate limited`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (RETRYABLE_SERVER_STATUS.has(response.status) && !serverErrored) {
|
|
146
|
+
serverErrored = true;
|
|
147
|
+
const asked = retryAfter !== void 0 ? retryAfter * 1e3 : DEFAULT_SERVER_ERROR_WAIT_MS;
|
|
148
|
+
await pause(asked + jitter(), `${route} answered ${response.status}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
throw failure;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
request,
|
|
156
|
+
get: (path, query) => request("GET", path, { query })
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/items.ts
|
|
161
|
+
var PLATFORM_URL = "https://platform.openai.com";
|
|
162
|
+
var BATCH_TERMINAL_STATUSES = ["completed", "failed", "expired", "cancelled"];
|
|
163
|
+
var FINE_TUNING_TERMINAL_STATUSES = ["succeeded", "failed", "cancelled"];
|
|
164
|
+
function isoOf(seconds2) {
|
|
165
|
+
return typeof seconds2 === "number" && Number.isFinite(seconds2) ? new Date(seconds2 * 1e3).toISOString() : null;
|
|
166
|
+
}
|
|
167
|
+
function secondsOf(iso) {
|
|
168
|
+
return Math.floor(Date.parse(iso) / 1e3);
|
|
169
|
+
}
|
|
170
|
+
function isBatchTerminal(status) {
|
|
171
|
+
return BATCH_TERMINAL_STATUSES.includes(status ?? "");
|
|
172
|
+
}
|
|
173
|
+
function isFineTuningTerminal(status) {
|
|
174
|
+
return FINE_TUNING_TERMINAL_STATUSES.includes(status ?? "");
|
|
175
|
+
}
|
|
176
|
+
function batchFinishedAt(batch) {
|
|
177
|
+
return batch.completed_at ?? batch.failed_at ?? batch.expired_at ?? batch.cancelled_at ?? batch.created_at ?? void 0;
|
|
178
|
+
}
|
|
179
|
+
function jobFinishedAt(job) {
|
|
180
|
+
return job.finished_at ?? job.created_at ?? void 0;
|
|
181
|
+
}
|
|
182
|
+
function requestCountsText(batch) {
|
|
183
|
+
const counts = batch.request_counts ?? {};
|
|
184
|
+
const total = counts.total ?? 0;
|
|
185
|
+
const completed = counts.completed ?? 0;
|
|
186
|
+
const failed = counts.failed ?? 0;
|
|
187
|
+
return `${completed} of ${total} requests, ${failed} failed`;
|
|
188
|
+
}
|
|
189
|
+
function batchToItem(batch) {
|
|
190
|
+
const status = batch.status ?? "unknown";
|
|
191
|
+
return {
|
|
192
|
+
externalId: `${batch.id}:${status}`,
|
|
193
|
+
title: `Batch ${batch.id} ${status}: ${requestCountsText(batch)}`,
|
|
194
|
+
url: `${PLATFORM_URL}/batches/${encodeURIComponent(batch.id)}`,
|
|
195
|
+
status,
|
|
196
|
+
updatedAt: isoOf(batchFinishedAt(batch)) ?? void 0,
|
|
197
|
+
data: { ...batch }
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function fileToItem(file) {
|
|
201
|
+
const name = file.filename ?? file.id;
|
|
202
|
+
const purpose = file.purpose ?? "unknown purpose";
|
|
203
|
+
return {
|
|
204
|
+
externalId: file.id,
|
|
205
|
+
title: `${name} (${purpose}, ${file.bytes ?? 0} bytes)`,
|
|
206
|
+
url: `${PLATFORM_URL}/storage/files/${encodeURIComponent(file.id)}`,
|
|
207
|
+
updatedAt: isoOf(file.created_at) ?? void 0,
|
|
208
|
+
data: { ...file }
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function fineTuningJobToItem(job) {
|
|
212
|
+
const status = job.status ?? "unknown";
|
|
213
|
+
const detail = status === "failed" && job.error?.message ? job.error.message : job.fine_tuned_model ?? job.model ?? job.id;
|
|
214
|
+
return {
|
|
215
|
+
externalId: `${job.id}:${status}`,
|
|
216
|
+
title: `Fine-tuning job ${job.id} ${status}: ${detail}`,
|
|
217
|
+
url: `${PLATFORM_URL}/finetune/${encodeURIComponent(job.id)}`,
|
|
218
|
+
status,
|
|
219
|
+
updatedAt: isoOf(jobFinishedAt(job)) ?? void 0,
|
|
220
|
+
data: { ...job }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function batchOutput(batch) {
|
|
224
|
+
const counts = batch.request_counts ?? {};
|
|
225
|
+
return {
|
|
226
|
+
id: batch.id,
|
|
227
|
+
status: batch.status ?? null,
|
|
228
|
+
endpoint: batch.endpoint ?? null,
|
|
229
|
+
inputFileId: batch.input_file_id ?? null,
|
|
230
|
+
outputFileId: batch.output_file_id ?? null,
|
|
231
|
+
errorFileId: batch.error_file_id ?? null,
|
|
232
|
+
requestCounts: { total: counts.total ?? 0, completed: counts.completed ?? 0, failed: counts.failed ?? 0 },
|
|
233
|
+
createdAt: isoOf(batch.created_at),
|
|
234
|
+
completedAt: isoOf(batch.completed_at),
|
|
235
|
+
failedAt: isoOf(batch.failed_at),
|
|
236
|
+
expiredAt: isoOf(batch.expired_at),
|
|
237
|
+
cancelledAt: isoOf(batch.cancelled_at),
|
|
238
|
+
errors: batch.errors ?? null,
|
|
239
|
+
metadata: batch.metadata ?? null,
|
|
240
|
+
batch
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function fileSummary(file) {
|
|
244
|
+
return {
|
|
245
|
+
id: file.id,
|
|
246
|
+
filename: file.filename ?? null,
|
|
247
|
+
bytes: file.bytes ?? null,
|
|
248
|
+
purpose: file.purpose ?? null,
|
|
249
|
+
createdAt: isoOf(file.created_at),
|
|
250
|
+
expiresAt: isoOf(file.expires_at)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function modelSummary(model) {
|
|
254
|
+
return {
|
|
255
|
+
id: model.id,
|
|
256
|
+
created: isoOf(model.created),
|
|
257
|
+
ownedBy: model.owned_by ?? null,
|
|
258
|
+
shutdownDate: model.shutdown_date ?? null
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
var SAMPLE_BATCH = {
|
|
262
|
+
id: "batch_abc123",
|
|
263
|
+
object: "batch",
|
|
264
|
+
endpoint: "/v1/chat/completions",
|
|
265
|
+
errors: null,
|
|
266
|
+
input_file_id: "file-abc123",
|
|
267
|
+
completion_window: "24h",
|
|
268
|
+
status: "completed",
|
|
269
|
+
output_file_id: "file-cvaTdG",
|
|
270
|
+
error_file_id: "file-HOWS94",
|
|
271
|
+
created_at: 1711471533,
|
|
272
|
+
in_progress_at: 1711471538,
|
|
273
|
+
expires_at: 1711557933,
|
|
274
|
+
finalizing_at: 1711493133,
|
|
275
|
+
completed_at: 1711493163,
|
|
276
|
+
failed_at: null,
|
|
277
|
+
expired_at: null,
|
|
278
|
+
cancelling_at: null,
|
|
279
|
+
cancelled_at: null,
|
|
280
|
+
request_counts: { total: 100, completed: 95, failed: 5 },
|
|
281
|
+
metadata: { customer_id: "user_123456789", batch_description: "Nightly job" }
|
|
282
|
+
};
|
|
283
|
+
var SAMPLE_FILE = {
|
|
284
|
+
id: "file-abc123",
|
|
285
|
+
object: "file",
|
|
286
|
+
bytes: 175,
|
|
287
|
+
created_at: 1613677385,
|
|
288
|
+
expires_at: 1677614202,
|
|
289
|
+
filename: "salesOverview.pdf",
|
|
290
|
+
purpose: "assistants"
|
|
291
|
+
};
|
|
292
|
+
var SAMPLE_FINE_TUNING_JOB = {
|
|
293
|
+
object: "fine_tuning.job",
|
|
294
|
+
id: "ftjob-abc123",
|
|
295
|
+
model: "gpt-4o-mini-2024-07-18",
|
|
296
|
+
created_at: 1721764800,
|
|
297
|
+
finished_at: 1721851200,
|
|
298
|
+
fine_tuned_model: "ft:gpt-4o-mini-2024-07-18:org::abc123",
|
|
299
|
+
organization_id: "org-123",
|
|
300
|
+
result_files: ["file-results123"],
|
|
301
|
+
status: "succeeded",
|
|
302
|
+
validation_file: null,
|
|
303
|
+
training_file: "file-abc123",
|
|
304
|
+
trained_tokens: 5768,
|
|
305
|
+
error: null,
|
|
306
|
+
metadata: { key: "value" }
|
|
307
|
+
};
|
|
308
|
+
var SAMPLE_BATCH_ITEM = batchToItem(SAMPLE_BATCH);
|
|
309
|
+
var SAMPLE_FILE_ITEM = fileToItem(SAMPLE_FILE);
|
|
310
|
+
var SAMPLE_FINE_TUNING_JOB_ITEM = fineTuningJobToItem(SAMPLE_FINE_TUNING_JOB);
|
|
311
|
+
|
|
312
|
+
// package.json
|
|
313
|
+
var package_default = {
|
|
314
|
+
name: "@vornrun/connector-openai",
|
|
315
|
+
version: "0.1.0",
|
|
316
|
+
description: "Trigger workflows when an OpenAI batch or fine-tuning job finishes or a file is uploaded, and create responses, chat completions, embeddings, moderations and batches from a step.",
|
|
317
|
+
type: "module",
|
|
318
|
+
license: "MIT",
|
|
319
|
+
author: "Javier Canizalez <javier-canizalez@outlook.com>",
|
|
320
|
+
repository: {
|
|
321
|
+
type: "git",
|
|
322
|
+
url: "git+https://github.com/vorn-run/connectors.git",
|
|
323
|
+
directory: "packages/openai"
|
|
324
|
+
},
|
|
325
|
+
keywords: [
|
|
326
|
+
"vorn",
|
|
327
|
+
"connector",
|
|
328
|
+
"openai",
|
|
329
|
+
"mcp"
|
|
330
|
+
],
|
|
331
|
+
bin: {
|
|
332
|
+
"vorn-connector-openai": "dist/index.js"
|
|
333
|
+
},
|
|
334
|
+
main: "./dist/index.js",
|
|
335
|
+
types: "./dist/index.d.ts",
|
|
336
|
+
exports: {
|
|
337
|
+
".": {
|
|
338
|
+
types: "./dist/index.d.ts",
|
|
339
|
+
default: "./dist/index.js"
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
files: [
|
|
343
|
+
"dist",
|
|
344
|
+
"README.md",
|
|
345
|
+
"CHANGELOG.md"
|
|
346
|
+
],
|
|
347
|
+
scripts: {
|
|
348
|
+
build: "tsup",
|
|
349
|
+
typecheck: "tsc --noEmit",
|
|
350
|
+
test: "vitest run"
|
|
351
|
+
},
|
|
352
|
+
dependencies: {
|
|
353
|
+
"@vornrun/connector-sdk": "^0.7.0-beta.14"
|
|
354
|
+
},
|
|
355
|
+
devDependencies: {
|
|
356
|
+
"@types/node": "^22.10.2",
|
|
357
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
358
|
+
tsup: "^8.5.1",
|
|
359
|
+
typescript: "^6.0.3",
|
|
360
|
+
vitest: "^4.1.10"
|
|
361
|
+
},
|
|
362
|
+
vorn: {
|
|
363
|
+
category: "AI",
|
|
364
|
+
keywords: [
|
|
365
|
+
"openai",
|
|
366
|
+
"gpt",
|
|
367
|
+
"chatgpt",
|
|
368
|
+
"llm",
|
|
369
|
+
"responses",
|
|
370
|
+
"chat completions",
|
|
371
|
+
"embeddings",
|
|
372
|
+
"moderation",
|
|
373
|
+
"batches",
|
|
374
|
+
"fine-tuning"
|
|
375
|
+
],
|
|
376
|
+
auth: "Paste an API key from https://platform.openai.com/api-keys. There is no OpenAI CLI to sign in with.",
|
|
377
|
+
packs: true
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/connector.ts
|
|
382
|
+
var PAGE_SIZE = 100;
|
|
383
|
+
var MAX_PAGES = 5;
|
|
384
|
+
var DEFAULT_BATCH_LOOKBACK_HOURS = 48;
|
|
385
|
+
var DEFAULT_FINE_TUNING_LOOKBACK_HOURS = 168;
|
|
386
|
+
var FIRST_POLL_BATCH_HOURS = 24 * 7;
|
|
387
|
+
var FIRST_POLL_FILE_HOURS = 1;
|
|
388
|
+
var DEFAULT_LIST_FILES_LIMIT = 100;
|
|
389
|
+
var DEFAULT_SCHEMA_NAME = "output";
|
|
390
|
+
var BATCH_ENDPOINTS = [
|
|
391
|
+
"/v1/responses",
|
|
392
|
+
"/v1/chat/completions",
|
|
393
|
+
"/v1/embeddings",
|
|
394
|
+
"/v1/completions",
|
|
395
|
+
"/v1/moderations",
|
|
396
|
+
"/v1/images/generations",
|
|
397
|
+
"/v1/images/edits",
|
|
398
|
+
"/v1/videos"
|
|
399
|
+
];
|
|
400
|
+
var FILE_PURPOSES = [
|
|
401
|
+
"assistants",
|
|
402
|
+
"assistants_output",
|
|
403
|
+
"batch",
|
|
404
|
+
"batch_output",
|
|
405
|
+
"fine-tune",
|
|
406
|
+
"fine-tune-results",
|
|
407
|
+
"vision",
|
|
408
|
+
"user_data"
|
|
409
|
+
];
|
|
410
|
+
function text(value) {
|
|
411
|
+
const trimmed = String(value ?? "").trim();
|
|
412
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
413
|
+
}
|
|
414
|
+
function hours(value, env, fallback) {
|
|
415
|
+
const raw = text(value);
|
|
416
|
+
if (raw === void 0) return fallback;
|
|
417
|
+
const parsed = Number(raw);
|
|
418
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
419
|
+
throw new Error(`${env} must be a number of hours of at least 0, got "${raw}"`);
|
|
420
|
+
}
|
|
421
|
+
return parsed;
|
|
422
|
+
}
|
|
423
|
+
function readSettings(config) {
|
|
424
|
+
const organization = text(config.organization);
|
|
425
|
+
const project = text(config.project);
|
|
426
|
+
const batchEndpoint = text(config.batchEndpoint);
|
|
427
|
+
const filePurpose = text(config.filePurpose);
|
|
428
|
+
return {
|
|
429
|
+
apiKey: normalizeKey(config.apiKey),
|
|
430
|
+
...organization && { organization },
|
|
431
|
+
...project && { project },
|
|
432
|
+
...batchEndpoint && { batchEndpoint },
|
|
433
|
+
batchLookbackHours: hours(config.batchLookbackHours, "OPENAI_BATCH_LOOKBACK_HOURS", DEFAULT_BATCH_LOOKBACK_HOURS),
|
|
434
|
+
...filePurpose && { filePurpose },
|
|
435
|
+
fineTuningLookbackHours: hours(
|
|
436
|
+
config.fineTuningLookbackHours,
|
|
437
|
+
"OPENAI_FINE_TUNING_LOOKBACK_HOURS",
|
|
438
|
+
DEFAULT_FINE_TUNING_LOOKBACK_HOURS
|
|
439
|
+
)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function textOrJsonArray(value) {
|
|
443
|
+
const raw = String(value ?? "");
|
|
444
|
+
if (raw.trim().startsWith("[")) {
|
|
445
|
+
try {
|
|
446
|
+
const parsed = JSON.parse(raw);
|
|
447
|
+
if (Array.isArray(parsed)) return parsed;
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return raw;
|
|
452
|
+
}
|
|
453
|
+
function messageList(value, key = "messages") {
|
|
454
|
+
const list = Array.isArray(value) ? value : [value];
|
|
455
|
+
if (list.length === 0) throw new Error(`${key} must hold at least one message`);
|
|
456
|
+
for (const message of list) {
|
|
457
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
458
|
+
throw new Error(`${key} must be a JSON array of { role, content } objects`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return list;
|
|
462
|
+
}
|
|
463
|
+
function jsonObject(value, key) {
|
|
464
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
465
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${key} must be a JSON object`);
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
function number(value) {
|
|
469
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
470
|
+
const parsed = Number(value);
|
|
471
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
472
|
+
}
|
|
473
|
+
function flag(value) {
|
|
474
|
+
return /^(true|1|yes)$/i.test(String(value ?? "").trim());
|
|
475
|
+
}
|
|
476
|
+
function responseText(response) {
|
|
477
|
+
const parts = [];
|
|
478
|
+
for (const item of response.output ?? []) {
|
|
479
|
+
if (item?.type !== "message") continue;
|
|
480
|
+
for (const part of item.content ?? []) {
|
|
481
|
+
if (part?.type === "output_text" && typeof part.text === "string") parts.push(part.text);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return parts.join("");
|
|
485
|
+
}
|
|
486
|
+
function parsedJson(textValue) {
|
|
487
|
+
try {
|
|
488
|
+
return JSON.parse(textValue);
|
|
489
|
+
} catch {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function createOpenAIConnector(options = {}) {
|
|
494
|
+
const version = options.version ?? "0.0.0";
|
|
495
|
+
const warn = options.warn ?? ((message) => console.warn(message));
|
|
496
|
+
const env = options.env ?? process.env;
|
|
497
|
+
function fromEnv(value) {
|
|
498
|
+
const raw = text(value);
|
|
499
|
+
if (raw === void 0) return void 0;
|
|
500
|
+
const match = /^\$([A-Z][A-Z0-9_]*)$/.exec(raw);
|
|
501
|
+
return match ? text(env[match[1]]) : raw;
|
|
502
|
+
}
|
|
503
|
+
function clientFor(config, fetchImpl) {
|
|
504
|
+
const settings = readSettings(config);
|
|
505
|
+
return createOpenAIClient({
|
|
506
|
+
apiKey: settings.apiKey,
|
|
507
|
+
...settings.organization && { organization: settings.organization },
|
|
508
|
+
...settings.project && { project: settings.project },
|
|
509
|
+
fetchImpl: options.fetchImpl ?? fetchImpl,
|
|
510
|
+
...options.sleep && { sleep: options.sleep },
|
|
511
|
+
warn
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async function walk(client, path, query, floor) {
|
|
515
|
+
const collected = /* @__PURE__ */ new Map();
|
|
516
|
+
let after;
|
|
517
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
518
|
+
const body = await client.get(path, { ...query, limit: PAGE_SIZE, after });
|
|
519
|
+
const data = Array.isArray(body?.data) ? body.data : [];
|
|
520
|
+
for (const entry of data) if (entry?.id) collected.set(entry.id, entry);
|
|
521
|
+
if (data.length === 0 || body.has_more !== true) break;
|
|
522
|
+
const oldest = Math.min(...data.map((entry) => entry.created_at ?? Number.POSITIVE_INFINITY));
|
|
523
|
+
if (oldest < floor) break;
|
|
524
|
+
after = body.last_id ?? data[data.length - 1].id;
|
|
525
|
+
}
|
|
526
|
+
return [...collected.values()];
|
|
527
|
+
}
|
|
528
|
+
function sinceSeconds(context) {
|
|
529
|
+
return context.since ? secondsOf(context.since) : void 0;
|
|
530
|
+
}
|
|
531
|
+
async function fetchBatches(context) {
|
|
532
|
+
const settings = readSettings(context.config);
|
|
533
|
+
const client = clientFor(context.config, context.fetch);
|
|
534
|
+
const since = sinceSeconds(context);
|
|
535
|
+
const firstPollFloor = secondsOf(context.now()) - FIRST_POLL_BATCH_HOURS * 3600;
|
|
536
|
+
const pagingFloor = since === void 0 ? firstPollFloor : since - settings.batchLookbackHours * 3600;
|
|
537
|
+
const batches = await walk(client, "batches", {}, pagingFloor);
|
|
538
|
+
return batches.filter((batch) => isBatchTerminal(batch.status)).filter((batch) => settings.batchEndpoint === void 0 || batch.endpoint === settings.batchEndpoint).filter((batch) => since !== void 0 || (batchFinishedAt(batch) ?? 0) >= firstPollFloor).sort((left, right) => (batchFinishedAt(left) ?? 0) - (batchFinishedAt(right) ?? 0)).map(batchToItem);
|
|
539
|
+
}
|
|
540
|
+
async function fetchFiles(context) {
|
|
541
|
+
const settings = readSettings(context.config);
|
|
542
|
+
const client = clientFor(context.config, context.fetch);
|
|
543
|
+
const floor = sinceSeconds(context) ?? secondsOf(context.now()) - FIRST_POLL_FILE_HOURS * 3600;
|
|
544
|
+
const files = await walk(client, "files", { order: "desc", purpose: settings.filePurpose }, floor);
|
|
545
|
+
return files.filter((file) => (file.created_at ?? 0) >= floor).sort((left, right) => (left.created_at ?? 0) - (right.created_at ?? 0)).map(fileToItem);
|
|
546
|
+
}
|
|
547
|
+
async function fetchFineTuningJobs(context) {
|
|
548
|
+
const settings = readSettings(context.config);
|
|
549
|
+
const client = clientFor(context.config, context.fetch);
|
|
550
|
+
const lookback = settings.fineTuningLookbackHours * 3600;
|
|
551
|
+
const since = sinceSeconds(context) ?? secondsOf(context.now()) - lookback;
|
|
552
|
+
const jobs = await walk(client, "fine_tuning/jobs", {}, since - lookback);
|
|
553
|
+
return jobs.filter((job) => isFineTuningTerminal(job.status)).filter((job) => (jobFinishedAt(job) ?? 0) >= since).sort((left, right) => (jobFinishedAt(left) ?? 0) - (jobFinishedAt(right) ?? 0)).map(fineTuningJobToItem);
|
|
554
|
+
}
|
|
555
|
+
function requiredArg(args, key) {
|
|
556
|
+
const value = fromEnv(args[key]);
|
|
557
|
+
if (!value) throw new Error(`${key} is required`);
|
|
558
|
+
return value;
|
|
559
|
+
}
|
|
560
|
+
const MODEL_INPUT = {
|
|
561
|
+
key: "model",
|
|
562
|
+
label: "Model",
|
|
563
|
+
required: true,
|
|
564
|
+
description: "Model id, such as gpt-4o-mini.",
|
|
565
|
+
builderHint: "Any id listModels returns. A fine-tuned model is its ft:\u2026 id."
|
|
566
|
+
};
|
|
567
|
+
const TEMPERATURE_INPUT = {
|
|
568
|
+
key: "temperature",
|
|
569
|
+
label: "Temperature",
|
|
570
|
+
type: "number",
|
|
571
|
+
description: "Sampling temperature, 0 to 2. Lower is more deterministic.",
|
|
572
|
+
builderHint: "Alter this or top_p, not both; leave empty for the model default of 1."
|
|
573
|
+
};
|
|
574
|
+
return defineConnector({
|
|
575
|
+
id: "openai",
|
|
576
|
+
name: "OpenAI",
|
|
577
|
+
version,
|
|
578
|
+
description: "Trigger workflows when an OpenAI batch or fine-tuning job finishes or a file is uploaded, and create responses, chat completions, embeddings, moderations and batches from a step.",
|
|
579
|
+
auth: { rung: "key", keys: ["apiKey"] },
|
|
580
|
+
// The hexagonal knot: six interlaced loops as one path, the counters punched through by the evenodd rule.
|
|
581
|
+
icon: {
|
|
582
|
+
viewBox: "0 0 24 24",
|
|
583
|
+
paths: [
|
|
584
|
+
"M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
|
|
585
|
+
]
|
|
586
|
+
},
|
|
587
|
+
config: [
|
|
588
|
+
{
|
|
589
|
+
key: "apiKey",
|
|
590
|
+
env: "OPENAI_API_KEY",
|
|
591
|
+
label: "API key",
|
|
592
|
+
secret: true,
|
|
593
|
+
required: true,
|
|
594
|
+
description: `Sent as \`Authorization: Bearer <key>\`. ${KEY_HINT}`,
|
|
595
|
+
builderHint: "A standard or project key (sk-\u2026 or sk-proj-\u2026), not an Admin key: Admin keys serve only the Administration API. Permissions are set per key at creation; a key that lacks an endpoint answers 401 insufficient permissions. Pasted whitespace is trimmed."
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
key: "organization",
|
|
599
|
+
env: "OPENAI_ORGANIZATION",
|
|
600
|
+
label: "Organization",
|
|
601
|
+
description: "Organization id (org-\u2026), sent as OpenAI-Organization. Only needed when the key belongs to more than one organization.",
|
|
602
|
+
builderHint: "Settings \u2192 Organization \u2192 General. Leave empty for a project key, which is already scoped."
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
key: "project",
|
|
606
|
+
env: "OPENAI_PROJECT",
|
|
607
|
+
label: "Project",
|
|
608
|
+
description: "Project id (proj_\u2026), sent as OpenAI-Project. Only needed when a legacy user key should bill one project.",
|
|
609
|
+
builderHint: "Settings \u2192 Project \u2192 General. Leave empty for a project key."
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
key: "batchEndpoint",
|
|
613
|
+
env: "OPENAI_BATCH_ENDPOINT",
|
|
614
|
+
label: "Batch endpoint filter",
|
|
615
|
+
description: "Only batches for this endpoint, such as /v1/chat/completions. Empty delivers every batch.",
|
|
616
|
+
builderHint: "The list has no server-side filter; the connector compares the batch object\u2019s endpoint field."
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
key: "batchLookbackHours",
|
|
620
|
+
env: "OPENAI_BATCH_LOOKBACK_HOURS",
|
|
621
|
+
label: "Batch look-back (hours)",
|
|
622
|
+
default: String(DEFAULT_BATCH_LOOKBACK_HOURS),
|
|
623
|
+
description: "How far before the watermark the batch poll reads, because a batch finishes up to a day after it is created.",
|
|
624
|
+
builderHint: "Raise it if batches take longer than two days to finish or expire; lower it for a busy account."
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
key: "filePurpose",
|
|
628
|
+
env: "OPENAI_FILE_PURPOSE",
|
|
629
|
+
label: "File purpose filter",
|
|
630
|
+
description: `Only files uploaded with this purpose (${FILE_PURPOSES.join(", ")}). Empty delivers every file.`,
|
|
631
|
+
builderHint: "Passed through as the purpose query parameter, so the API does the filtering."
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
key: "fineTuningLookbackHours",
|
|
635
|
+
env: "OPENAI_FINE_TUNING_LOOKBACK_HOURS",
|
|
636
|
+
label: "Fine-tuning look-back (hours)",
|
|
637
|
+
default: String(DEFAULT_FINE_TUNING_LOOKBACK_HOURS),
|
|
638
|
+
description: "How far before the watermark the fine-tuning poll reads, because a job is created long before it finishes.",
|
|
639
|
+
builderHint: "A week by default. Jobs that queue longer than this before finishing are missed."
|
|
640
|
+
}
|
|
641
|
+
],
|
|
642
|
+
async preflight() {
|
|
643
|
+
const key = text(env.OPENAI_API_KEY);
|
|
644
|
+
if (!key) return { ok: false, message: `Set OPENAI_API_KEY. ${KEY_HINT}` };
|
|
645
|
+
const client = createOpenAIClient({
|
|
646
|
+
apiKey: normalizeKey(key),
|
|
647
|
+
...text(env.OPENAI_ORGANIZATION) && { organization: text(env.OPENAI_ORGANIZATION) },
|
|
648
|
+
...text(env.OPENAI_PROJECT) && { project: text(env.OPENAI_PROJECT) },
|
|
649
|
+
...options.fetchImpl && { fetchImpl: options.fetchImpl },
|
|
650
|
+
...options.sleep && { sleep: options.sleep },
|
|
651
|
+
warn
|
|
652
|
+
});
|
|
653
|
+
const models = await client.get("models");
|
|
654
|
+
const count = Array.isArray(models?.data) ? models.data.length : 0;
|
|
655
|
+
return { ok: true, message: `Signed in; ${count} models available` };
|
|
656
|
+
},
|
|
657
|
+
triggers: [
|
|
658
|
+
{
|
|
659
|
+
type: "batchFinished",
|
|
660
|
+
label: "A batch finishes",
|
|
661
|
+
description: "Fires once when a batch reaches completed, failed, expired or cancelled. The list has no status filter, so every batch created inside the look-back is read and the terminal ones kept.",
|
|
662
|
+
defaultWorkflow: { name: "OpenAI: finished batches", defaultCronFromMinutes: 5 },
|
|
663
|
+
statusMapping: [
|
|
664
|
+
{ upstream: "completed", suggestedLocal: "done" },
|
|
665
|
+
{ upstream: "failed", suggestedLocal: "todo" },
|
|
666
|
+
{ upstream: "expired", suggestedLocal: "cancelled" },
|
|
667
|
+
{ upstream: "cancelled", suggestedLocal: "cancelled" }
|
|
668
|
+
],
|
|
669
|
+
dedupe: "timestamp",
|
|
670
|
+
sample: [SAMPLE_BATCH_ITEM],
|
|
671
|
+
fetch: fetchBatches
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
type: "fileUploaded",
|
|
675
|
+
label: "A file is uploaded",
|
|
676
|
+
description: "Fires once per file uploaded since the last poll, newest first from GET /files.",
|
|
677
|
+
defaultWorkflow: { name: "OpenAI: uploaded files", defaultCronFromMinutes: 5 },
|
|
678
|
+
dedupe: "timestamp",
|
|
679
|
+
sample: [SAMPLE_FILE_ITEM],
|
|
680
|
+
fetch: fetchFiles
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
type: "fineTuningJobFinished",
|
|
684
|
+
label: "A fine-tuning job finishes",
|
|
685
|
+
description: "Fires once when a fine-tuning job reaches succeeded, failed or cancelled.",
|
|
686
|
+
defaultWorkflow: { name: "OpenAI: finished fine-tuning jobs", defaultCronFromMinutes: 15 },
|
|
687
|
+
statusMapping: [
|
|
688
|
+
{ upstream: "succeeded", suggestedLocal: "done" },
|
|
689
|
+
{ upstream: "failed", suggestedLocal: "todo" },
|
|
690
|
+
{ upstream: "cancelled", suggestedLocal: "cancelled" }
|
|
691
|
+
],
|
|
692
|
+
dedupe: "timestamp",
|
|
693
|
+
sample: [SAMPLE_FINE_TUNING_JOB_ITEM],
|
|
694
|
+
fetch: fetchFineTuningJobs
|
|
695
|
+
}
|
|
696
|
+
],
|
|
697
|
+
actions: [
|
|
698
|
+
{
|
|
699
|
+
type: "createResponse",
|
|
700
|
+
label: "Create a response",
|
|
701
|
+
description: "Ask a model for a response with the Responses API, optionally constrained to a JSON schema. Spends tokens; not stored unless asked.",
|
|
702
|
+
idempotent: false,
|
|
703
|
+
inputs: [
|
|
704
|
+
MODEL_INPUT,
|
|
705
|
+
{
|
|
706
|
+
key: "input",
|
|
707
|
+
label: "Input",
|
|
708
|
+
required: true,
|
|
709
|
+
description: "Plain text, or a JSON array of message items { role, content } with role user, assistant, system or developer.",
|
|
710
|
+
builderHint: "A value that parses as a JSON array is sent as the array; anything else is sent as text."
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
key: "instructions",
|
|
714
|
+
label: "Instructions",
|
|
715
|
+
description: "A system or developer message inserted into the model\u2019s context.",
|
|
716
|
+
builderHint: "Prefer this over a system message in input; it is not carried into a later turn."
|
|
717
|
+
},
|
|
718
|
+
TEMPERATURE_INPUT,
|
|
719
|
+
{
|
|
720
|
+
key: "maxOutputTokens",
|
|
721
|
+
label: "Max output tokens",
|
|
722
|
+
type: "number",
|
|
723
|
+
description: "Upper bound on generated tokens, including reasoning tokens. Sent as max_output_tokens.",
|
|
724
|
+
builderHint: "The API refuses values under 16. A response cut here reports incompleteReason max_output_tokens."
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
key: "schema",
|
|
728
|
+
label: "JSON schema",
|
|
729
|
+
type: "json",
|
|
730
|
+
description: "A JSON Schema object the output must match, sent as text.format json_schema with strict true.",
|
|
731
|
+
builderHint: "Strict mode wants every property in required and additionalProperties false. The parsed result is the json output."
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
key: "schemaName",
|
|
735
|
+
label: "Schema name",
|
|
736
|
+
description: `Name of the format, letters, digits, underscores and dashes up to 64 characters. Default ${DEFAULT_SCHEMA_NAME}.`,
|
|
737
|
+
builderHint: "Only read when schema is given."
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
key: "store",
|
|
741
|
+
label: "Store the response",
|
|
742
|
+
type: "boolean",
|
|
743
|
+
description: "Keep the response retrievable for 30 days. Off here by default, unlike the API.",
|
|
744
|
+
builderHint: "true when a later step needs previous_response_id."
|
|
745
|
+
}
|
|
746
|
+
],
|
|
747
|
+
outputs: [
|
|
748
|
+
{ key: "id", description: "Response id" },
|
|
749
|
+
{ key: "status", description: "completed, incomplete, failed or in_progress" },
|
|
750
|
+
{ key: "text", description: "Every output_text part of every message, joined" },
|
|
751
|
+
{ key: "json", description: "The text parsed as JSON when a schema was given, else null" },
|
|
752
|
+
{ key: "model", description: "The model that answered" },
|
|
753
|
+
{ key: "usage", description: "{ input_tokens, output_tokens, total_tokens, reasoning_tokens }" },
|
|
754
|
+
{ key: "incompleteReason", description: "Why the response stopped early, or null" },
|
|
755
|
+
{ key: "response", description: "The raw response object" }
|
|
756
|
+
],
|
|
757
|
+
async run(args, context) {
|
|
758
|
+
const schema = jsonObject(args.schema, "schema");
|
|
759
|
+
const schemaName = text(args.schemaName) ?? DEFAULT_SCHEMA_NAME;
|
|
760
|
+
const temperature = number(args.temperature);
|
|
761
|
+
const maxOutputTokens = number(args.maxOutputTokens);
|
|
762
|
+
const instructions = text(args.instructions);
|
|
763
|
+
const client = clientFor(context.config, context.fetch);
|
|
764
|
+
const response = await client.request("POST", "responses", {
|
|
765
|
+
body: {
|
|
766
|
+
model: requiredArg(args, "model"),
|
|
767
|
+
input: textOrJsonArray(args.input),
|
|
768
|
+
store: flag(args.store),
|
|
769
|
+
...instructions && { instructions },
|
|
770
|
+
...temperature !== void 0 && { temperature },
|
|
771
|
+
...maxOutputTokens !== void 0 && { max_output_tokens: maxOutputTokens },
|
|
772
|
+
...schema && { text: { format: { type: "json_schema", name: schemaName, schema, strict: true } } }
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
const body = response ?? {};
|
|
776
|
+
const output = responseText(body);
|
|
777
|
+
return {
|
|
778
|
+
id: body.id ?? null,
|
|
779
|
+
status: body.status ?? null,
|
|
780
|
+
text: output,
|
|
781
|
+
json: schema ? parsedJson(output) : null,
|
|
782
|
+
model: body.model ?? null,
|
|
783
|
+
usage: {
|
|
784
|
+
input_tokens: body.usage?.input_tokens ?? 0,
|
|
785
|
+
output_tokens: body.usage?.output_tokens ?? 0,
|
|
786
|
+
total_tokens: body.usage?.total_tokens ?? 0,
|
|
787
|
+
reasoning_tokens: body.usage?.output_tokens_details?.reasoning_tokens ?? 0
|
|
788
|
+
},
|
|
789
|
+
incompleteReason: body.incomplete_details?.reason ?? null,
|
|
790
|
+
response: body
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
type: "createChatCompletion",
|
|
796
|
+
label: "Create a chat completion",
|
|
797
|
+
description: "Ask a model for the next message in a conversation with the Chat Completions API. Spends tokens.",
|
|
798
|
+
idempotent: false,
|
|
799
|
+
inputs: [
|
|
800
|
+
MODEL_INPUT,
|
|
801
|
+
{
|
|
802
|
+
key: "messages",
|
|
803
|
+
label: "Messages",
|
|
804
|
+
type: "json",
|
|
805
|
+
required: true,
|
|
806
|
+
description: "A JSON array of { role, content } with role system, developer, user, assistant or tool.",
|
|
807
|
+
builderHint: "One object is taken as a single message."
|
|
808
|
+
},
|
|
809
|
+
TEMPERATURE_INPUT,
|
|
810
|
+
{
|
|
811
|
+
key: "maxTokens",
|
|
812
|
+
label: "Max completion tokens",
|
|
813
|
+
type: "number",
|
|
814
|
+
description: "Upper bound on generated tokens, including reasoning tokens. Sent as max_completion_tokens.",
|
|
815
|
+
builderHint: "max_tokens is deprecated and refused by o-series models, so this never sends it."
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
key: "responseFormat",
|
|
819
|
+
label: "Response format",
|
|
820
|
+
type: "json",
|
|
821
|
+
description: 'JSON: { "type": "text" }, { "type": "json_object" } or { "type": "json_schema", "json_schema": { name, schema, strict } }.',
|
|
822
|
+
builderHint: "json_object needs the word JSON somewhere in the messages or the API refuses."
|
|
823
|
+
}
|
|
824
|
+
],
|
|
825
|
+
outputs: [
|
|
826
|
+
{ key: "id", description: "Completion id" },
|
|
827
|
+
{ key: "text", description: "Content of the first choice" },
|
|
828
|
+
{ key: "finishReason", description: "stop, length, tool_calls, content_filter or function_call" },
|
|
829
|
+
{ key: "refusal", description: "The refusal message of the first choice, or null" },
|
|
830
|
+
{ key: "model", description: "The model that answered" },
|
|
831
|
+
{ key: "usage", description: "{ prompt_tokens, completion_tokens, total_tokens }" },
|
|
832
|
+
{ key: "completion", description: "The raw completion object" }
|
|
833
|
+
],
|
|
834
|
+
async run(args, context) {
|
|
835
|
+
const temperature = number(args.temperature);
|
|
836
|
+
const maxTokens = number(args.maxTokens);
|
|
837
|
+
const responseFormat = jsonObject(args.responseFormat, "responseFormat");
|
|
838
|
+
const client = clientFor(context.config, context.fetch);
|
|
839
|
+
const completion = await client.request("POST", "chat/completions", {
|
|
840
|
+
body: {
|
|
841
|
+
model: requiredArg(args, "model"),
|
|
842
|
+
messages: messageList(args.messages),
|
|
843
|
+
...temperature !== void 0 && { temperature },
|
|
844
|
+
...maxTokens !== void 0 && { max_completion_tokens: maxTokens },
|
|
845
|
+
...responseFormat && { response_format: responseFormat }
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
const body = completion ?? {};
|
|
849
|
+
const first = body.choices?.[0];
|
|
850
|
+
return {
|
|
851
|
+
id: body.id ?? null,
|
|
852
|
+
text: first?.message?.content ?? "",
|
|
853
|
+
finishReason: first?.finish_reason ?? null,
|
|
854
|
+
refusal: first?.message?.refusal ?? null,
|
|
855
|
+
model: body.model ?? null,
|
|
856
|
+
usage: {
|
|
857
|
+
prompt_tokens: body.usage?.prompt_tokens ?? 0,
|
|
858
|
+
completion_tokens: body.usage?.completion_tokens ?? 0,
|
|
859
|
+
total_tokens: body.usage?.total_tokens ?? 0
|
|
860
|
+
},
|
|
861
|
+
completion: body
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
type: "createEmbeddings",
|
|
867
|
+
label: "Create embeddings",
|
|
868
|
+
description: "Turn text into embedding vectors. The same input yields the same vectors and nothing is stored.",
|
|
869
|
+
idempotent: true,
|
|
870
|
+
sample: { model: "text-embedding-3-small", input: "hello" },
|
|
871
|
+
inputs: [
|
|
872
|
+
{
|
|
873
|
+
...MODEL_INPUT,
|
|
874
|
+
description: "text-embedding-3-small, text-embedding-3-large or text-embedding-ada-002.",
|
|
875
|
+
builderHint: "text-embedding-3-small is the cheapest; dimensions only works on the 3 series."
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
key: "input",
|
|
879
|
+
label: "Input",
|
|
880
|
+
required: true,
|
|
881
|
+
description: "Plain text, or a JSON array of strings to embed in one call.",
|
|
882
|
+
builderHint: "Each string at most 8192 tokens, an array at most 2048 entries."
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
key: "dimensions",
|
|
886
|
+
label: "Dimensions",
|
|
887
|
+
type: "number",
|
|
888
|
+
description: "How many dimensions each vector should have. text-embedding-3 models only.",
|
|
889
|
+
builderHint: "Leave empty for the model default (1536 for small, 3072 for large)."
|
|
890
|
+
}
|
|
891
|
+
],
|
|
892
|
+
outputs: [
|
|
893
|
+
{ key: "embeddings", description: "Array of number arrays, in input order" },
|
|
894
|
+
{ key: "dimensions", type: "number", description: "Length of the first vector" },
|
|
895
|
+
{ key: "model", description: "The model used" },
|
|
896
|
+
{ key: "usage", description: "{ prompt_tokens, total_tokens }" }
|
|
897
|
+
],
|
|
898
|
+
async run(args, context) {
|
|
899
|
+
const dimensions = number(args.dimensions);
|
|
900
|
+
const client = clientFor(context.config, context.fetch);
|
|
901
|
+
const body = await client.request("POST", "embeddings", {
|
|
902
|
+
body: {
|
|
903
|
+
model: requiredArg(args, "model"),
|
|
904
|
+
input: textOrJsonArray(args.input),
|
|
905
|
+
...dimensions !== void 0 && { dimensions }
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
const embeddings = [...body?.data ?? []].sort((left, right) => (left.index ?? 0) - (right.index ?? 0)).map((entry) => entry.embedding ?? []);
|
|
909
|
+
return {
|
|
910
|
+
embeddings,
|
|
911
|
+
dimensions: embeddings[0]?.length ?? 0,
|
|
912
|
+
model: body?.model ?? null,
|
|
913
|
+
usage: { prompt_tokens: body?.usage?.prompt_tokens ?? 0, total_tokens: body?.usage?.total_tokens ?? 0 }
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
type: "moderateText",
|
|
919
|
+
label: "Moderate text",
|
|
920
|
+
description: "Classify text against OpenAI\u2019s usage policies. Free, and nothing is stored.",
|
|
921
|
+
idempotent: true,
|
|
922
|
+
sample: { input: "hello" },
|
|
923
|
+
inputs: [
|
|
924
|
+
{
|
|
925
|
+
key: "input",
|
|
926
|
+
label: "Input",
|
|
927
|
+
required: true,
|
|
928
|
+
description: "Plain text, or a JSON array of strings to classify separately.",
|
|
929
|
+
builderHint: "One result per string, in order."
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
key: "model",
|
|
933
|
+
label: "Model",
|
|
934
|
+
description: "omni-moderation-latest (default), omni-moderation-2024-09-26, text-moderation-latest or text-moderation-stable.",
|
|
935
|
+
builderHint: "Leave empty for omni-moderation-latest."
|
|
936
|
+
}
|
|
937
|
+
],
|
|
938
|
+
outputs: [
|
|
939
|
+
{ key: "flagged", type: "boolean", description: "True when any result is flagged" },
|
|
940
|
+
{ key: "results", description: "Array of { flagged, categories, category_scores, category_applied_input_types }" },
|
|
941
|
+
{ key: "model", description: "The moderation model used" },
|
|
942
|
+
{ key: "id", description: "Moderation id" }
|
|
943
|
+
],
|
|
944
|
+
async run(args, context) {
|
|
945
|
+
const model = text(args.model);
|
|
946
|
+
const client = clientFor(context.config, context.fetch);
|
|
947
|
+
const body = await client.request("POST", "moderations", {
|
|
948
|
+
body: { input: textOrJsonArray(args.input), ...model && { model } }
|
|
949
|
+
});
|
|
950
|
+
const results = Array.isArray(body?.results) ? body.results : [];
|
|
951
|
+
return {
|
|
952
|
+
flagged: results.some((result) => result?.flagged === true),
|
|
953
|
+
results,
|
|
954
|
+
model: body?.model ?? null,
|
|
955
|
+
id: body?.id ?? null
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
type: "listModels",
|
|
961
|
+
label: "List models",
|
|
962
|
+
description: "List the models the key can use, with their owner.",
|
|
963
|
+
idempotent: true,
|
|
964
|
+
sample: {},
|
|
965
|
+
outputs: [
|
|
966
|
+
{ key: "models", description: "Array of { id, created, ownedBy, shutdownDate }" },
|
|
967
|
+
{ key: "count", type: "number", description: "How many models" }
|
|
968
|
+
],
|
|
969
|
+
async run(_args, context) {
|
|
970
|
+
const client = clientFor(context.config, context.fetch);
|
|
971
|
+
const body = await client.get("models");
|
|
972
|
+
const models = (Array.isArray(body?.data) ? body.data : []).map(modelSummary);
|
|
973
|
+
return { models, count: models.length };
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
type: "getModel",
|
|
978
|
+
label: "Get a model",
|
|
979
|
+
description: "Read one model by id. An unknown id answers 404 model_not_found.",
|
|
980
|
+
idempotent: true,
|
|
981
|
+
sample: { model: "gpt-4o-mini" },
|
|
982
|
+
inputs: [MODEL_INPUT],
|
|
983
|
+
outputs: [
|
|
984
|
+
{ key: "id", description: "Model id" },
|
|
985
|
+
{ key: "created", description: "When it was created, ISO 8601" },
|
|
986
|
+
{ key: "ownedBy", description: "The organization that owns it" },
|
|
987
|
+
{ key: "shutdownDate", description: "Planned retirement date, or null" }
|
|
988
|
+
],
|
|
989
|
+
async run(args, context) {
|
|
990
|
+
const client = clientFor(context.config, context.fetch);
|
|
991
|
+
const model = await client.get(`models/${encodeURIComponent(requiredArg(args, "model"))}`);
|
|
992
|
+
return modelSummary(model ?? { id: requiredArg(args, "model") });
|
|
993
|
+
}
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
type: "listFiles",
|
|
997
|
+
label: "List files",
|
|
998
|
+
description: "List uploaded files, newest first, optionally by purpose.",
|
|
999
|
+
idempotent: true,
|
|
1000
|
+
sample: { limit: "5" },
|
|
1001
|
+
inputs: [
|
|
1002
|
+
{
|
|
1003
|
+
key: "purpose",
|
|
1004
|
+
label: "Purpose",
|
|
1005
|
+
type: "select",
|
|
1006
|
+
options: FILE_PURPOSES.map((value) => ({ value })),
|
|
1007
|
+
description: "Only files uploaded with this purpose.",
|
|
1008
|
+
builderHint: "Leave empty for every purpose."
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
key: "limit",
|
|
1012
|
+
label: "Limit",
|
|
1013
|
+
type: "number",
|
|
1014
|
+
description: `1 to 10,000 files per page; default ${DEFAULT_LIST_FILES_LIMIT}.`,
|
|
1015
|
+
builderHint: "The API default is 10,000, which is a lot to hand a workflow step."
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
key: "order",
|
|
1019
|
+
label: "Order",
|
|
1020
|
+
type: "select",
|
|
1021
|
+
options: [{ value: "desc", label: "Newest first" }, { value: "asc", label: "Oldest first" }],
|
|
1022
|
+
description: "Sort by created_at. Default desc.",
|
|
1023
|
+
builderHint: "asc with after walks a backlog oldest first."
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
key: "after",
|
|
1027
|
+
label: "After",
|
|
1028
|
+
description: "A file id from an earlier page; the list continues after it.",
|
|
1029
|
+
builderHint: "Feed lastId from the previous call."
|
|
1030
|
+
}
|
|
1031
|
+
],
|
|
1032
|
+
outputs: [
|
|
1033
|
+
{ key: "files", description: "Array of { id, filename, bytes, purpose, createdAt, expiresAt }" },
|
|
1034
|
+
{ key: "hasMore", type: "boolean", description: "Whether another page follows" },
|
|
1035
|
+
{ key: "lastId", description: "Id of the last file listed, for after" }
|
|
1036
|
+
],
|
|
1037
|
+
async run(args, context) {
|
|
1038
|
+
const client = clientFor(context.config, context.fetch);
|
|
1039
|
+
const body = await client.get("files", {
|
|
1040
|
+
purpose: text(args.purpose),
|
|
1041
|
+
limit: number(args.limit) ?? DEFAULT_LIST_FILES_LIMIT,
|
|
1042
|
+
order: text(args.order),
|
|
1043
|
+
after: text(args.after)
|
|
1044
|
+
});
|
|
1045
|
+
const files = Array.isArray(body?.data) ? body.data : [];
|
|
1046
|
+
return {
|
|
1047
|
+
files: files.map(fileSummary),
|
|
1048
|
+
hasMore: body?.has_more === true,
|
|
1049
|
+
lastId: body?.last_id ?? files[files.length - 1]?.id ?? null
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
type: "getBatch",
|
|
1055
|
+
label: "Get a batch",
|
|
1056
|
+
description: "Read one batch by id, with its status, request counts and output files.",
|
|
1057
|
+
idempotent: true,
|
|
1058
|
+
sample: { batch: "$OPENAI_BATCH_ID" },
|
|
1059
|
+
inputs: [
|
|
1060
|
+
{
|
|
1061
|
+
key: "batch",
|
|
1062
|
+
label: "Batch",
|
|
1063
|
+
required: true,
|
|
1064
|
+
description: "Batch id, such as batch_abc123.",
|
|
1065
|
+
builderHint: "Often {{trigger.item.id}} from the batch trigger. $NAME reads the environment for live checks."
|
|
1066
|
+
}
|
|
1067
|
+
],
|
|
1068
|
+
outputs: batchOutputs(),
|
|
1069
|
+
async run(args, context) {
|
|
1070
|
+
const client = clientFor(context.config, context.fetch);
|
|
1071
|
+
const id = requiredArg(args, "batch");
|
|
1072
|
+
const batch = await client.get(`batches/${encodeURIComponent(id)}`);
|
|
1073
|
+
return batchOutput(batch ?? { id });
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
type: "createBatch",
|
|
1078
|
+
label: "Create a batch",
|
|
1079
|
+
description: "Queue a batch of requests from an uploaded JSONL file. Each call queues a new batch that runs and bills.",
|
|
1080
|
+
idempotent: false,
|
|
1081
|
+
inputs: [
|
|
1082
|
+
{
|
|
1083
|
+
key: "inputFileId",
|
|
1084
|
+
label: "Input file",
|
|
1085
|
+
required: true,
|
|
1086
|
+
description: "Id of an uploaded JSONL file with purpose batch, one request per line.",
|
|
1087
|
+
builderHint: "Upload with the Files API first; this connector does not upload."
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
key: "endpoint",
|
|
1091
|
+
label: "Endpoint",
|
|
1092
|
+
type: "select",
|
|
1093
|
+
required: true,
|
|
1094
|
+
options: BATCH_ENDPOINTS.map((value) => ({ value })),
|
|
1095
|
+
description: "The endpoint every request in the file targets.",
|
|
1096
|
+
builderHint: "Must match the url field of each line in the file."
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
key: "completionWindow",
|
|
1100
|
+
label: "Completion window",
|
|
1101
|
+
type: "select",
|
|
1102
|
+
options: [{ value: "24h" }],
|
|
1103
|
+
description: "How long the batch has to finish. Only 24h is supported; default 24h.",
|
|
1104
|
+
builderHint: "Left as a select so a future window can be added without a code change."
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
key: "metadata",
|
|
1108
|
+
label: "Metadata",
|
|
1109
|
+
type: "json",
|
|
1110
|
+
description: "A JSON object of up to 16 string pairs, keys up to 64 characters and values up to 512.",
|
|
1111
|
+
builderHint: "Comes back on the batch object and in the trigger item\u2019s data.metadata."
|
|
1112
|
+
}
|
|
1113
|
+
],
|
|
1114
|
+
outputs: batchOutputs(),
|
|
1115
|
+
async run(args, context) {
|
|
1116
|
+
const metadata = jsonObject(args.metadata, "metadata");
|
|
1117
|
+
const client = clientFor(context.config, context.fetch);
|
|
1118
|
+
const batch = await client.request("POST", "batches", {
|
|
1119
|
+
body: {
|
|
1120
|
+
input_file_id: requiredArg(args, "inputFileId"),
|
|
1121
|
+
endpoint: requiredArg(args, "endpoint"),
|
|
1122
|
+
completion_window: text(args.completionWindow) ?? "24h",
|
|
1123
|
+
...metadata && { metadata }
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
return batchOutput(batch ?? { id: "" });
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
]
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function batchOutputs() {
|
|
1133
|
+
return [
|
|
1134
|
+
{ key: "id", description: "Batch id" },
|
|
1135
|
+
{ key: "status", description: "validating, failed, in_progress, finalizing, completed, expired, cancelling or cancelled" },
|
|
1136
|
+
{ key: "endpoint", description: "The endpoint the batch calls" },
|
|
1137
|
+
{ key: "inputFileId", description: "The JSONL file of requests" },
|
|
1138
|
+
{ key: "outputFileId", description: "The file of results, once completed" },
|
|
1139
|
+
{ key: "errorFileId", description: "The file of failed requests, or null" },
|
|
1140
|
+
{ key: "requestCounts", description: "{ total, completed, failed }" },
|
|
1141
|
+
{ key: "createdAt", description: "When it was created, ISO 8601" },
|
|
1142
|
+
{ key: "completedAt", description: "When it completed, or null" },
|
|
1143
|
+
{ key: "failedAt", description: "When it failed, or null" },
|
|
1144
|
+
{ key: "expiredAt", description: "When it expired, or null" },
|
|
1145
|
+
{ key: "cancelledAt", description: "When it was cancelled, or null" },
|
|
1146
|
+
{ key: "errors", description: "Validation errors, or null" },
|
|
1147
|
+
{ key: "metadata", description: "The metadata it was created with" },
|
|
1148
|
+
{ key: "batch", description: "The raw batch object" }
|
|
1149
|
+
];
|
|
1150
|
+
}
|
|
1151
|
+
var connector = createOpenAIConnector({ version: package_default.version });
|
|
1152
|
+
|
|
1153
|
+
// src/entry.ts
|
|
1154
|
+
import { realpathSync } from "fs";
|
|
1155
|
+
import { fileURLToPath } from "url";
|
|
1156
|
+
import { serveConnector } from "@vornrun/connector-sdk";
|
|
1157
|
+
function isEntryPoint(moduleUrl, argv = process.argv) {
|
|
1158
|
+
const invoked = argv[1];
|
|
1159
|
+
if (invoked === void 0) return false;
|
|
1160
|
+
try {
|
|
1161
|
+
return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(invoked);
|
|
1162
|
+
} catch {
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
async function serveIfEntryPoint(moduleUrl, serve = serveConnector) {
|
|
1167
|
+
if (!isEntryPoint(moduleUrl)) return false;
|
|
1168
|
+
await serve(connector);
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/index.ts
|
|
1173
|
+
var index_default = connector;
|
|
1174
|
+
await serveIfEntryPoint(import.meta.url);
|
|
1175
|
+
export {
|
|
1176
|
+
API_ROOT,
|
|
1177
|
+
OpenAIApiError,
|
|
1178
|
+
connector,
|
|
1179
|
+
createOpenAIClient,
|
|
1180
|
+
createOpenAIConnector,
|
|
1181
|
+
index_default as default,
|
|
1182
|
+
durationMs,
|
|
1183
|
+
normalizeKey,
|
|
1184
|
+
readSettings
|
|
1185
|
+
};
|