@discomedia/utils 1.0.78 → 1.0.79
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/dist/index-frontend.cjs.map +1 -1
- package/dist/index-frontend.mjs.map +1 -1
- package/dist/index.cjs +117 -0
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +115 -0
- package/dist/index.mjs.map +3 -3
- package/dist/package.json +1 -1
- package/dist/test.mjs +4065 -228
- package/dist/test.mjs.map +4 -4
- package/dist/types/disco-mail.d.ts +24 -0
- package/dist/types/disco-mail.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types/disco-mail-types.d.ts +80 -0
- package/dist/types/types/disco-mail-types.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/test.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
1
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
3
|
var __esm = (fn, res, err) => function __init() {
|
|
3
4
|
if (err) throw err[0];
|
|
@@ -7,6 +8,10 @@ var __esm = (fn, res, err) => function __init() {
|
|
|
7
8
|
throw err = [e], e;
|
|
8
9
|
}
|
|
9
10
|
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
10
15
|
|
|
11
16
|
// src/logging.ts
|
|
12
17
|
function log(message, options = { source: "App", type: "info" }) {
|
|
@@ -174,6 +179,81 @@ function fromNYTime(date) {
|
|
|
174
179
|
const utcMillis = nyMillis - offset * 60 * 60 * 1e3;
|
|
175
180
|
return new Date(utcMillis);
|
|
176
181
|
}
|
|
182
|
+
function formatDate(date, outputFormat = "iso") {
|
|
183
|
+
switch (outputFormat) {
|
|
184
|
+
case "unix-seconds":
|
|
185
|
+
return Math.floor(date.getTime() / 1e3);
|
|
186
|
+
case "unix-ms":
|
|
187
|
+
return date.getTime();
|
|
188
|
+
case "iso":
|
|
189
|
+
default:
|
|
190
|
+
return date.toISOString();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function formatNYLocale(date) {
|
|
194
|
+
return date.toLocaleString("en-US", { timeZone: "America/New_York" });
|
|
195
|
+
}
|
|
196
|
+
function getMarketTimes(date) {
|
|
197
|
+
const calendar = new MarketCalendar();
|
|
198
|
+
const nyDate = toNYTime(date);
|
|
199
|
+
if (!calendar.isMarketDay(date)) {
|
|
200
|
+
return {
|
|
201
|
+
marketOpen: false,
|
|
202
|
+
open: null,
|
|
203
|
+
close: null,
|
|
204
|
+
openExt: null,
|
|
205
|
+
closeExt: null
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const year = nyDate.getUTCFullYear();
|
|
209
|
+
const month = nyDate.getUTCMonth();
|
|
210
|
+
const day = nyDate.getUTCDate();
|
|
211
|
+
function buildNYTime(hour, minute) {
|
|
212
|
+
const d = new Date(Date.UTC(year, month, day, hour, minute, 0, 0));
|
|
213
|
+
return fromNYTime(d);
|
|
214
|
+
}
|
|
215
|
+
let open = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute);
|
|
216
|
+
let close = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute);
|
|
217
|
+
let openExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute);
|
|
218
|
+
let closeExt = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute);
|
|
219
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
220
|
+
close = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute);
|
|
221
|
+
closeExt = buildNYTime(MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour, MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
marketOpen: true,
|
|
225
|
+
open,
|
|
226
|
+
close,
|
|
227
|
+
openExt,
|
|
228
|
+
closeExt
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function isWithinMarketHours(date, intradayReporting = "market_hours") {
|
|
232
|
+
const calendar = new MarketCalendar();
|
|
233
|
+
if (!calendar.isMarketDay(date)) return false;
|
|
234
|
+
const nyDate = toNYTime(date);
|
|
235
|
+
const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
|
|
236
|
+
switch (intradayReporting) {
|
|
237
|
+
case "extended_hours": {
|
|
238
|
+
let endMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
239
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
240
|
+
endMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
241
|
+
}
|
|
242
|
+
const startMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
243
|
+
return minutes >= startMinutes && minutes <= endMinutes;
|
|
244
|
+
}
|
|
245
|
+
case "continuous":
|
|
246
|
+
return true;
|
|
247
|
+
default: {
|
|
248
|
+
let endMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
249
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
250
|
+
endMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
251
|
+
}
|
|
252
|
+
const startMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
253
|
+
return minutes >= startMinutes && minutes <= endMinutes;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
177
257
|
function getLastFullTradingDateImpl(currentDate = /* @__PURE__ */ new Date()) {
|
|
178
258
|
const calendar = new MarketCalendar();
|
|
179
259
|
const nyDate = toNYTime(currentDate);
|
|
@@ -204,9 +284,357 @@ function getLastFullTradingDateImpl(currentDate = /* @__PURE__ */ new Date()) {
|
|
|
204
284
|
const closeMinute = marketCloseMinutes % 60;
|
|
205
285
|
return fromNYTime(new Date(Date.UTC(year, month, day, closeHour, closeMinute, 0, 0)));
|
|
206
286
|
}
|
|
287
|
+
function getDayBoundaries(date, intradayReporting = "market_hours") {
|
|
288
|
+
const calendar = new MarketCalendar();
|
|
289
|
+
const nyDate = toNYTime(date);
|
|
290
|
+
const year = nyDate.getUTCFullYear();
|
|
291
|
+
const month = nyDate.getUTCMonth();
|
|
292
|
+
const day = nyDate.getUTCDate();
|
|
293
|
+
function buildNYTime(hour, minute, sec = 0, ms = 0) {
|
|
294
|
+
const d = new Date(Date.UTC(year, month, day, hour, minute, sec, ms));
|
|
295
|
+
return fromNYTime(d);
|
|
296
|
+
}
|
|
297
|
+
let start;
|
|
298
|
+
let end;
|
|
299
|
+
switch (intradayReporting) {
|
|
300
|
+
case "extended_hours":
|
|
301
|
+
start = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_START.hour, MARKET_CONFIG.TIMES.EXTENDED_START.minute, 0, 0);
|
|
302
|
+
end = buildNYTime(MARKET_CONFIG.TIMES.EXTENDED_END.hour, MARKET_CONFIG.TIMES.EXTENDED_END.minute, 59, 999);
|
|
303
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
304
|
+
end = buildNYTime(
|
|
305
|
+
MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour,
|
|
306
|
+
MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute,
|
|
307
|
+
59,
|
|
308
|
+
999
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
case "continuous":
|
|
313
|
+
start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
|
|
314
|
+
end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999));
|
|
315
|
+
break;
|
|
316
|
+
default:
|
|
317
|
+
start = buildNYTime(MARKET_CONFIG.TIMES.MARKET_OPEN.hour, MARKET_CONFIG.TIMES.MARKET_OPEN.minute, 0, 0);
|
|
318
|
+
end = buildNYTime(MARKET_CONFIG.TIMES.MARKET_CLOSE.hour, MARKET_CONFIG.TIMES.MARKET_CLOSE.minute, 59, 999);
|
|
319
|
+
if (calendar.isEarlyCloseDay(date)) {
|
|
320
|
+
end = buildNYTime(MARKET_CONFIG.TIMES.EARLY_CLOSE.hour, MARKET_CONFIG.TIMES.EARLY_CLOSE.minute, 59, 999);
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
return { start, end };
|
|
325
|
+
}
|
|
326
|
+
function calculatePeriodStartDate(endDate, period) {
|
|
327
|
+
const calendar = new MarketCalendar();
|
|
328
|
+
let startDate;
|
|
329
|
+
switch (period) {
|
|
330
|
+
case "YTD":
|
|
331
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), 0, 1));
|
|
332
|
+
break;
|
|
333
|
+
case "1D":
|
|
334
|
+
startDate = calendar.getPreviousMarketDay(endDate);
|
|
335
|
+
break;
|
|
336
|
+
case "3D":
|
|
337
|
+
startDate = new Date(endDate.getTime() - 3 * 24 * 60 * 60 * 1e3);
|
|
338
|
+
break;
|
|
339
|
+
case "1W":
|
|
340
|
+
startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
341
|
+
break;
|
|
342
|
+
case "2W":
|
|
343
|
+
startDate = new Date(endDate.getTime() - 14 * 24 * 60 * 60 * 1e3);
|
|
344
|
+
break;
|
|
345
|
+
case "1M":
|
|
346
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 1, endDate.getUTCDate()));
|
|
347
|
+
break;
|
|
348
|
+
case "3M":
|
|
349
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 3, endDate.getUTCDate()));
|
|
350
|
+
break;
|
|
351
|
+
case "6M":
|
|
352
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth() - 6, endDate.getUTCDate()));
|
|
353
|
+
break;
|
|
354
|
+
case "1Y":
|
|
355
|
+
startDate = new Date(Date.UTC(endDate.getUTCFullYear() - 1, endDate.getUTCMonth(), endDate.getUTCDate()));
|
|
356
|
+
break;
|
|
357
|
+
default:
|
|
358
|
+
throw new Error(`Invalid period: ${period}`);
|
|
359
|
+
}
|
|
360
|
+
while (!calendar.isMarketDay(startDate)) {
|
|
361
|
+
startDate = calendar.getNextMarketDay(startDate);
|
|
362
|
+
}
|
|
363
|
+
return startDate;
|
|
364
|
+
}
|
|
365
|
+
function getMarketTimePeriod(params) {
|
|
366
|
+
const { period, end = /* @__PURE__ */ new Date(), intraday_reporting = "market_hours", outputFormat = "iso" } = params;
|
|
367
|
+
if (!period) throw new Error("Period is required");
|
|
368
|
+
const calendar = new MarketCalendar();
|
|
369
|
+
const nyEndDate = toNYTime(end);
|
|
370
|
+
let endDate;
|
|
371
|
+
const isCurrentMarketDay = calendar.isMarketDay(end);
|
|
372
|
+
const isWithinHours = isWithinMarketHours(end, intraday_reporting);
|
|
373
|
+
const minutes = nyEndDate.getUTCHours() * 60 + nyEndDate.getUTCMinutes();
|
|
374
|
+
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
375
|
+
if (isCurrentMarketDay) {
|
|
376
|
+
if (minutes < marketStartMinutes) {
|
|
377
|
+
const lastMarketDay = calendar.getPreviousMarketDay(end);
|
|
378
|
+
const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
379
|
+
endDate = dayEnd;
|
|
380
|
+
} else if (isWithinHours) {
|
|
381
|
+
endDate = end;
|
|
382
|
+
} else {
|
|
383
|
+
const { end: dayEnd } = getDayBoundaries(end, intraday_reporting);
|
|
384
|
+
endDate = dayEnd;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
const lastMarketDay = calendar.getPreviousMarketDay(end);
|
|
388
|
+
const { end: dayEnd } = getDayBoundaries(lastMarketDay, intraday_reporting);
|
|
389
|
+
endDate = dayEnd;
|
|
390
|
+
}
|
|
391
|
+
const periodStartDate = calculatePeriodStartDate(endDate, period);
|
|
392
|
+
const { start: dayStart } = getDayBoundaries(periodStartDate, intraday_reporting);
|
|
393
|
+
if (endDate.getTime() < dayStart.getTime()) {
|
|
394
|
+
throw new Error("Start date cannot be after end date");
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
start: formatDate(dayStart, outputFormat),
|
|
398
|
+
end: formatDate(endDate, outputFormat)
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function getMarketStatusImpl(date = /* @__PURE__ */ new Date()) {
|
|
402
|
+
const calendar = new MarketCalendar();
|
|
403
|
+
const nyDate = toNYTime(date);
|
|
404
|
+
const minutes = nyDate.getUTCHours() * 60 + nyDate.getUTCMinutes();
|
|
405
|
+
const isMarketDay2 = calendar.isMarketDay(date);
|
|
406
|
+
const isEarlyCloseDay = calendar.isEarlyCloseDay(date);
|
|
407
|
+
const extendedStartMinutes = MARKET_CONFIG.TIMES.EXTENDED_START.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_START.minute;
|
|
408
|
+
const marketStartMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
409
|
+
const earlyMarketEndMinutes = MARKET_CONFIG.TIMES.EARLY_MARKET_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_MARKET_END.minute;
|
|
410
|
+
let marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
411
|
+
let extendedEndMinutes = MARKET_CONFIG.TIMES.EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EXTENDED_END.minute;
|
|
412
|
+
if (isEarlyCloseDay) {
|
|
413
|
+
marketCloseMinutes = MARKET_CONFIG.TIMES.EARLY_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.EARLY_CLOSE.minute;
|
|
414
|
+
extendedEndMinutes = MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.hour * 60 + MARKET_CONFIG.TIMES.EARLY_EXTENDED_END.minute;
|
|
415
|
+
}
|
|
416
|
+
let status;
|
|
417
|
+
let nextStatus;
|
|
418
|
+
let nextStatusTime;
|
|
419
|
+
let marketPeriod;
|
|
420
|
+
if (!isMarketDay2) {
|
|
421
|
+
status = "closed";
|
|
422
|
+
nextStatus = "extended hours";
|
|
423
|
+
marketPeriod = "closed";
|
|
424
|
+
const nextMarketDay = calendar.getNextMarketDay(date);
|
|
425
|
+
nextStatusTime = getDayBoundaries(nextMarketDay, "extended_hours").start;
|
|
426
|
+
} else if (minutes < extendedStartMinutes) {
|
|
427
|
+
status = "closed";
|
|
428
|
+
nextStatus = "extended hours";
|
|
429
|
+
marketPeriod = "closed";
|
|
430
|
+
nextStatusTime = getDayBoundaries(date, "extended_hours").start;
|
|
431
|
+
} else if (minutes < marketStartMinutes) {
|
|
432
|
+
status = "extended hours";
|
|
433
|
+
nextStatus = "open";
|
|
434
|
+
marketPeriod = "preMarket";
|
|
435
|
+
nextStatusTime = getDayBoundaries(date, "market_hours").start;
|
|
436
|
+
} else if (minutes < marketCloseMinutes) {
|
|
437
|
+
status = "open";
|
|
438
|
+
nextStatus = "extended hours";
|
|
439
|
+
marketPeriod = minutes < earlyMarketEndMinutes ? "earlyMarket" : "regularMarket";
|
|
440
|
+
nextStatusTime = getDayBoundaries(date, "market_hours").end;
|
|
441
|
+
} else if (minutes < extendedEndMinutes) {
|
|
442
|
+
status = "extended hours";
|
|
443
|
+
nextStatus = "closed";
|
|
444
|
+
marketPeriod = "afterMarket";
|
|
445
|
+
nextStatusTime = getDayBoundaries(date, "extended_hours").end;
|
|
446
|
+
} else {
|
|
447
|
+
status = "closed";
|
|
448
|
+
nextStatus = "extended hours";
|
|
449
|
+
marketPeriod = "closed";
|
|
450
|
+
const nextMarketDay = calendar.getNextMarketDay(date);
|
|
451
|
+
nextStatusTime = getDayBoundaries(nextMarketDay, "extended_hours").start;
|
|
452
|
+
}
|
|
453
|
+
const nextStatusTimeDifference = nextStatusTime.getTime() - date.getTime();
|
|
454
|
+
return {
|
|
455
|
+
time: date,
|
|
456
|
+
timeString: formatNYLocale(nyDate),
|
|
457
|
+
status,
|
|
458
|
+
nextStatus,
|
|
459
|
+
marketPeriod,
|
|
460
|
+
nextStatusTime,
|
|
461
|
+
nextStatusTimeDifference,
|
|
462
|
+
nextStatusTimeString: formatNYLocale(nextStatusTime)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function getMarketOpenClose(options = {}) {
|
|
466
|
+
const { date = /* @__PURE__ */ new Date() } = options;
|
|
467
|
+
return getMarketTimes(date);
|
|
468
|
+
}
|
|
469
|
+
function getStartAndEndDates(params = {}) {
|
|
470
|
+
const { start, end } = getMarketTimePeriod(params);
|
|
471
|
+
return {
|
|
472
|
+
start: typeof start === "string" || typeof start === "number" ? new Date(start) : start,
|
|
473
|
+
end: typeof end === "string" || typeof end === "number" ? new Date(end) : end
|
|
474
|
+
};
|
|
475
|
+
}
|
|
207
476
|
function getLastFullTradingDate(currentDate = /* @__PURE__ */ new Date()) {
|
|
208
477
|
return getLastFullTradingDateImpl(currentDate);
|
|
209
478
|
}
|
|
479
|
+
function getNextMarketDay({ referenceDate } = {}) {
|
|
480
|
+
const calendar = new MarketCalendar();
|
|
481
|
+
const startDate = referenceDate ?? /* @__PURE__ */ new Date();
|
|
482
|
+
const nextDate = calendar.getNextMarketDay(startDate);
|
|
483
|
+
const nyNext = toNYTime(nextDate);
|
|
484
|
+
const yyyymmdd = `${nyNext.getUTCFullYear()}-${String(nyNext.getUTCMonth() + 1).padStart(2, "0")}-${String(
|
|
485
|
+
nyNext.getUTCDate()
|
|
486
|
+
).padStart(2, "0")}`;
|
|
487
|
+
return {
|
|
488
|
+
date: nextDate,
|
|
489
|
+
// raw Date, unchanged
|
|
490
|
+
yyyymmdd,
|
|
491
|
+
// correct trading date string
|
|
492
|
+
dateISOString: nextDate.toISOString()
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function getPreviousMarketDay({ referenceDate } = {}) {
|
|
496
|
+
const calendar = new MarketCalendar();
|
|
497
|
+
const startDate = referenceDate || /* @__PURE__ */ new Date();
|
|
498
|
+
const prevDate = calendar.getPreviousMarketDay(startDate);
|
|
499
|
+
const nyPrev = toNYTime(prevDate);
|
|
500
|
+
const yyyymmdd = `${nyPrev.getUTCFullYear()}-${String(nyPrev.getUTCMonth() + 1).padStart(2, "0")}-${String(
|
|
501
|
+
nyPrev.getUTCDate()
|
|
502
|
+
).padStart(2, "0")}`;
|
|
503
|
+
return {
|
|
504
|
+
date: prevDate,
|
|
505
|
+
yyyymmdd,
|
|
506
|
+
dateISOString: prevDate.toISOString()
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function getTradingDate(time) {
|
|
510
|
+
const date = typeof time === "number" ? new Date(time) : typeof time === "string" ? new Date(time) : time;
|
|
511
|
+
const nyDate = toNYTime(date);
|
|
512
|
+
return `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, "0")}-${String(
|
|
513
|
+
nyDate.getUTCDate()
|
|
514
|
+
).padStart(2, "0")}`;
|
|
515
|
+
}
|
|
516
|
+
function getNYTimeZone(date) {
|
|
517
|
+
const offset = getNYOffset(date || /* @__PURE__ */ new Date());
|
|
518
|
+
return offset === -4 ? "-04:00" : "-05:00";
|
|
519
|
+
}
|
|
520
|
+
function getOpenCloseForTradingDay(dateStr) {
|
|
521
|
+
const utcAnchor = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
|
|
522
|
+
const nyOffset = getNYTimeZone(utcAnchor);
|
|
523
|
+
const nyNoon = /* @__PURE__ */ new Date(`${dateStr}T12:00:00${nyOffset}`);
|
|
524
|
+
if (!isMarketDay(nyNoon)) {
|
|
525
|
+
throw new Error(`Not a market day in ET: ${dateStr}`);
|
|
526
|
+
}
|
|
527
|
+
const { open, close } = getMarketOpenClose({ date: nyNoon });
|
|
528
|
+
if (!open || !close) {
|
|
529
|
+
throw new Error(`No market times available for ${dateStr}`);
|
|
530
|
+
}
|
|
531
|
+
return { open, close };
|
|
532
|
+
}
|
|
533
|
+
function convertDateToMarketTimeZone(date) {
|
|
534
|
+
return toNYTime(date);
|
|
535
|
+
}
|
|
536
|
+
function getMarketStatus(options = {}) {
|
|
537
|
+
const { date = /* @__PURE__ */ new Date() } = options;
|
|
538
|
+
return getMarketStatusImpl(date);
|
|
539
|
+
}
|
|
540
|
+
function isMarketDay(date) {
|
|
541
|
+
const calendar = new MarketCalendar();
|
|
542
|
+
return calendar.isMarketDay(date);
|
|
543
|
+
}
|
|
544
|
+
function getTradingStartAndEndDates(options = {}) {
|
|
545
|
+
const { endDate = /* @__PURE__ */ new Date(), days = 1 } = options;
|
|
546
|
+
const calendar = new MarketCalendar();
|
|
547
|
+
let endMarketDay = endDate;
|
|
548
|
+
const nyEnd = toNYTime(endDate);
|
|
549
|
+
const marketOpenMinutes = MARKET_CONFIG.TIMES.MARKET_OPEN.hour * 60 + MARKET_CONFIG.TIMES.MARKET_OPEN.minute;
|
|
550
|
+
const marketCloseMinutes = MARKET_CONFIG.TIMES.MARKET_CLOSE.hour * 60 + MARKET_CONFIG.TIMES.MARKET_CLOSE.minute;
|
|
551
|
+
const minutes = nyEnd.getUTCHours() * 60 + nyEnd.getUTCMinutes();
|
|
552
|
+
if (!calendar.isMarketDay(endDate) || minutes < marketOpenMinutes || minutes >= marketOpenMinutes && minutes < marketCloseMinutes) {
|
|
553
|
+
endMarketDay = calendar.getPreviousMarketDay(endDate);
|
|
554
|
+
} else {
|
|
555
|
+
endMarketDay = endDate;
|
|
556
|
+
}
|
|
557
|
+
const endClose = getMarketOpenClose({ date: endMarketDay }).close;
|
|
558
|
+
let startMarketDay = endMarketDay;
|
|
559
|
+
let count = Math.max(1, days);
|
|
560
|
+
for (let i = 1; i < count; i++) {
|
|
561
|
+
startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
|
|
562
|
+
}
|
|
563
|
+
if (days > 1) {
|
|
564
|
+
startMarketDay = endMarketDay;
|
|
565
|
+
for (let i = 1; i < days; i++) {
|
|
566
|
+
startMarketDay = calendar.getPreviousMarketDay(startMarketDay);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const startOpen = getMarketOpenClose({ date: startMarketDay }).open;
|
|
570
|
+
return { startDate: startOpen, endDate: endClose };
|
|
571
|
+
}
|
|
572
|
+
function countTradingDays(startDate, endDate = /* @__PURE__ */ new Date()) {
|
|
573
|
+
const calendar = new MarketCalendar();
|
|
574
|
+
if (startDate.getTime() > endDate.getTime()) {
|
|
575
|
+
throw new Error("Start date must be before end date");
|
|
576
|
+
}
|
|
577
|
+
let totalMinutes = 0;
|
|
578
|
+
const startNY = toNYTime(startDate);
|
|
579
|
+
const endNY = toNYTime(endDate);
|
|
580
|
+
const currentNY = new Date(
|
|
581
|
+
Date.UTC(startNY.getUTCFullYear(), startNY.getUTCMonth(), startNY.getUTCDate(), 0, 0, 0, 0)
|
|
582
|
+
);
|
|
583
|
+
while (currentNY.getTime() <= endNY.getTime()) {
|
|
584
|
+
const currentUTC = fromNYTime(currentNY);
|
|
585
|
+
if (calendar.isMarketDay(currentUTC)) {
|
|
586
|
+
const marketTimes = getMarketTimes(currentUTC);
|
|
587
|
+
if (marketTimes.marketOpen && marketTimes.open && marketTimes.close) {
|
|
588
|
+
const dayStart = Math.max(startDate.getTime(), marketTimes.open.getTime());
|
|
589
|
+
const dayEnd = Math.min(endDate.getTime(), marketTimes.close.getTime());
|
|
590
|
+
if (dayStart < dayEnd) {
|
|
591
|
+
totalMinutes += (dayEnd - dayStart) / (1e3 * 60);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
currentNY.setUTCDate(currentNY.getUTCDate() + 1);
|
|
596
|
+
}
|
|
597
|
+
const MINUTES_PER_TRADING_DAY = 390;
|
|
598
|
+
const days = totalMinutes / MINUTES_PER_TRADING_DAY;
|
|
599
|
+
const hours = totalMinutes / 60;
|
|
600
|
+
const minutes = totalMinutes;
|
|
601
|
+
return {
|
|
602
|
+
days: Math.round(days * 1e3) / 1e3,
|
|
603
|
+
// Round to 3 decimal places
|
|
604
|
+
hours: Math.round(hours * 100) / 100,
|
|
605
|
+
// Round to 2 decimal places
|
|
606
|
+
minutes: Math.round(minutes)
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function getTradingDaysBack(options) {
|
|
610
|
+
const calendar = new MarketCalendar();
|
|
611
|
+
const { referenceDate, days, includeMostRecentFullDay = true } = options;
|
|
612
|
+
const refDate = referenceDate || /* @__PURE__ */ new Date();
|
|
613
|
+
const daysBack = days;
|
|
614
|
+
if (!Number.isInteger(daysBack) || daysBack < 1) {
|
|
615
|
+
throw new Error("days must be an integer >= 1");
|
|
616
|
+
}
|
|
617
|
+
let targetDate = getLastFullTradingDateImpl(refDate);
|
|
618
|
+
if (!includeMostRecentFullDay) {
|
|
619
|
+
targetDate = calendar.getPreviousMarketDay(targetDate);
|
|
620
|
+
}
|
|
621
|
+
for (let i = 1; i < daysBack; i++) {
|
|
622
|
+
targetDate = calendar.getPreviousMarketDay(targetDate);
|
|
623
|
+
}
|
|
624
|
+
const marketTimes = getMarketTimes(targetDate);
|
|
625
|
+
if (!marketTimes.open) {
|
|
626
|
+
throw new Error(`No market open time for target date`);
|
|
627
|
+
}
|
|
628
|
+
const nyDate = toNYTime(marketTimes.open);
|
|
629
|
+
const dateStr = `${nyDate.getUTCFullYear()}-${String(nyDate.getUTCMonth() + 1).padStart(2, "0")}-${String(nyDate.getUTCDate()).padStart(2, "0")}`;
|
|
630
|
+
const marketOpenISO = marketTimes.open.toISOString();
|
|
631
|
+
const unixTimestamp = Math.floor(marketTimes.open.getTime() / 1e3);
|
|
632
|
+
return {
|
|
633
|
+
date: dateStr,
|
|
634
|
+
marketOpenISO,
|
|
635
|
+
unixTimestamp
|
|
636
|
+
};
|
|
637
|
+
}
|
|
210
638
|
var MARKET_CONFIG, MarketCalendar, MARKET_TIMES;
|
|
211
639
|
var init_market_time = __esm({
|
|
212
640
|
"src/market-time.ts"() {
|
|
@@ -1617,189 +2045,2059 @@ var init_alpaca_market_data_api = __esm({
|
|
|
1617
2045
|
|
|
1618
2046
|
// src/alpaca-trading-api.ts
|
|
1619
2047
|
import WebSocket2 from "ws";
|
|
2048
|
+
var limitPriceSlippagePercent100, AlpacaRequestError, AlpacaTradingAPI;
|
|
1620
2049
|
var init_alpaca_trading_api = __esm({
|
|
1621
2050
|
"src/alpaca-trading-api.ts"() {
|
|
1622
2051
|
"use strict";
|
|
1623
2052
|
init_logging();
|
|
1624
2053
|
init_alpaca_market_data_api();
|
|
1625
2054
|
init_market_time();
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2055
|
+
limitPriceSlippagePercent100 = 0.1;
|
|
2056
|
+
AlpacaRequestError = class extends Error {
|
|
2057
|
+
method;
|
|
2058
|
+
status;
|
|
2059
|
+
url;
|
|
2060
|
+
responseCode;
|
|
2061
|
+
responseDetails;
|
|
2062
|
+
rawResponse;
|
|
2063
|
+
symbol;
|
|
2064
|
+
constructor(params) {
|
|
2065
|
+
super(params.message);
|
|
2066
|
+
this.name = "AlpacaRequestError";
|
|
2067
|
+
this.method = params.method;
|
|
2068
|
+
this.status = params.status ?? null;
|
|
2069
|
+
this.url = params.url;
|
|
2070
|
+
this.responseCode = params.responseCode ?? null;
|
|
2071
|
+
this.responseDetails = params.responseDetails ?? null;
|
|
2072
|
+
this.rawResponse = params.rawResponse ?? null;
|
|
2073
|
+
this.symbol = params.symbol;
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
AlpacaTradingAPI = class _AlpacaTradingAPI {
|
|
2077
|
+
static new(credentials) {
|
|
2078
|
+
return new _AlpacaTradingAPI(credentials);
|
|
2079
|
+
}
|
|
2080
|
+
static getInstance(credentials) {
|
|
2081
|
+
return new _AlpacaTradingAPI(credentials);
|
|
2082
|
+
}
|
|
2083
|
+
ws = null;
|
|
2084
|
+
headers;
|
|
2085
|
+
tradeUpdateCallback = null;
|
|
2086
|
+
credentials;
|
|
2087
|
+
apiBaseUrl;
|
|
2088
|
+
wsUrl;
|
|
2089
|
+
authenticated = false;
|
|
2090
|
+
connecting = false;
|
|
2091
|
+
reconnectDelay = 1e4;
|
|
2092
|
+
// 10 seconds between reconnection attempts
|
|
2093
|
+
reconnectTimeout = null;
|
|
2094
|
+
messageHandlers = /* @__PURE__ */ new Map();
|
|
2095
|
+
debugLogging = false;
|
|
2096
|
+
manualDisconnect = false;
|
|
2097
|
+
/**
|
|
2098
|
+
* Constructor for AlpacaTradingAPI
|
|
2099
|
+
* @param credentials - Alpaca credentials,
|
|
2100
|
+
* accountName: string; // The account identifier used inthis.logs and tracking
|
|
2101
|
+
* apiKey: string; // Alpaca API key
|
|
2102
|
+
* apiSecret: string; // Alpaca API secret
|
|
2103
|
+
* type: AlpacaAccountType;
|
|
2104
|
+
* orderType: AlpacaOrderType;
|
|
2105
|
+
* @param options - Optional options
|
|
2106
|
+
* debugLogging: boolean; // Whether to log messages of type 'debug'
|
|
2107
|
+
*/
|
|
2108
|
+
constructor(credentials, options) {
|
|
2109
|
+
this.credentials = credentials;
|
|
2110
|
+
this.apiBaseUrl = credentials.type === "PAPER" ? "https://paper-api.alpaca.markets/v2" : "https://api.alpaca.markets/v2";
|
|
2111
|
+
this.wsUrl = credentials.type === "PAPER" ? "wss://paper-api.alpaca.markets/stream" : "wss://api.alpaca.markets/stream";
|
|
2112
|
+
this.headers = {
|
|
2113
|
+
"APCA-API-KEY-ID": credentials.apiKey,
|
|
2114
|
+
"APCA-API-SECRET-KEY": credentials.apiSecret,
|
|
2115
|
+
"Content-Type": "application/json"
|
|
2116
|
+
};
|
|
2117
|
+
this.messageHandlers.set("authorization", this.handleAuthMessage.bind(this));
|
|
2118
|
+
this.messageHandlers.set("listening", this.handleListenMessage.bind(this));
|
|
2119
|
+
this.messageHandlers.set("trade_updates", this.handleTradeUpdate.bind(this));
|
|
2120
|
+
this.debugLogging = options?.debugLogging || false;
|
|
2121
|
+
}
|
|
2122
|
+
log(message, options = { type: "info" }) {
|
|
2123
|
+
if (this.debugLogging && options.type === "debug") {
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
log(message, { ...options, source: "AlpacaTradingAPI", account: this.credentials.accountName });
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Round a price to the nearest 2 decimal places for Alpaca, or 4 decimal places for prices less than $1
|
|
2130
|
+
* @param price - The price to round
|
|
2131
|
+
* @returns The rounded price
|
|
2132
|
+
*/
|
|
2133
|
+
roundPriceForAlpaca = (price) => {
|
|
2134
|
+
return price >= 1 ? Math.round(price * 100) / 100 : Math.round(price * 1e4) / 1e4;
|
|
2135
|
+
};
|
|
2136
|
+
isJsonObject(value) {
|
|
2137
|
+
return value !== null && !Array.isArray(value) && typeof value === "object";
|
|
2138
|
+
}
|
|
2139
|
+
parseAlpacaAPIErrorResponse(rawResponse) {
|
|
2140
|
+
if (rawResponse.trim() === "") {
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
try {
|
|
2144
|
+
const parsedValue = JSON.parse(rawResponse);
|
|
2145
|
+
if (!this.isJsonObject(parsedValue)) {
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
const code = parsedValue["code"];
|
|
2149
|
+
const message = parsedValue["message"];
|
|
2150
|
+
const symbol = parsedValue["symbol"];
|
|
2151
|
+
if (typeof code !== "number" || typeof message !== "string") {
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
if (symbol !== void 0 && typeof symbol !== "string") {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
return parsedValue;
|
|
2158
|
+
} catch {
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
formatJsonValueForLog(value) {
|
|
2163
|
+
if (value === void 0) {
|
|
2164
|
+
return "undefined";
|
|
2165
|
+
}
|
|
2166
|
+
if (Array.isArray(value)) {
|
|
2167
|
+
return value.map((entry) => this.formatJsonValueForLog(entry)).join(", ");
|
|
2168
|
+
}
|
|
2169
|
+
if (value === null) {
|
|
2170
|
+
return "null";
|
|
2171
|
+
}
|
|
2172
|
+
if (typeof value === "object") {
|
|
2173
|
+
return JSON.stringify(value);
|
|
2174
|
+
}
|
|
2175
|
+
return `${value}`;
|
|
2176
|
+
}
|
|
2177
|
+
formatAlpacaAPIErrorDetails(errorResponse) {
|
|
2178
|
+
return Object.entries(errorResponse).filter(([key]) => key !== "code" && key !== "message" && key !== "symbol").map(([key, value]) => `${key}=${this.formatJsonValueForLog(value)}`).join(", ");
|
|
2179
|
+
}
|
|
2180
|
+
formatRequestAction(requestContext) {
|
|
2181
|
+
return requestContext?.action ? `${requestContext.action} failed` : "Alpaca request failed";
|
|
2182
|
+
}
|
|
2183
|
+
buildAlpacaAPIErrorMessage(params) {
|
|
2184
|
+
const { requestContext, status, url, errorResponse, rawResponse } = params;
|
|
2185
|
+
const action = this.formatRequestAction(requestContext);
|
|
2186
|
+
const message = errorResponse?.message ?? (rawResponse || "No error body returned");
|
|
2187
|
+
const responseCode = errorResponse?.code ? ` (code ${errorResponse.code})` : "";
|
|
2188
|
+
const detailSummary = errorResponse ? this.formatAlpacaAPIErrorDetails(errorResponse) : "";
|
|
2189
|
+
const detailText = detailSummary !== "" ? ` Details: ${detailSummary}.` : rawResponse.trim() !== "" && errorResponse === null ? ` Response: ${rawResponse}.` : "";
|
|
2190
|
+
return `${action}: Alpaca API ${status}${responseCode}: ${message}.${detailText} Url: ${url}`;
|
|
2191
|
+
}
|
|
2192
|
+
buildRequestExecutionErrorMessage(params) {
|
|
2193
|
+
const { requestContext, url, errorMessage } = params;
|
|
2194
|
+
const action = this.formatRequestAction(requestContext);
|
|
2195
|
+
return `${action}: ${errorMessage}. Url: ${url}`;
|
|
2196
|
+
}
|
|
2197
|
+
handleAuthMessage(data) {
|
|
2198
|
+
if (data.status === "authorized") {
|
|
2199
|
+
this.authenticated = true;
|
|
2200
|
+
this.log("WebSocket authenticated");
|
|
2201
|
+
} else {
|
|
2202
|
+
this.log(`Authentication failed: ${data.message || "Unknown error"}`, {
|
|
2203
|
+
type: "error"
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
handleListenMessage(data) {
|
|
2208
|
+
if (data.streams?.includes("trade_updates")) {
|
|
2209
|
+
this.log("Successfully subscribed to trade updates");
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
handleTradeUpdate(data) {
|
|
2213
|
+
if (this.tradeUpdateCallback) {
|
|
2214
|
+
this.log(
|
|
2215
|
+
`Trade update: ${data.event} to ${data.order.side} ${data.order.qty} shares${data.event === "partial_fill" ? ` (filled shares: ${data.order.filled_qty})` : ""}, type ${data.order.type}`,
|
|
2216
|
+
{
|
|
2217
|
+
symbol: data.order.symbol,
|
|
2218
|
+
type: "debug"
|
|
2219
|
+
}
|
|
2220
|
+
);
|
|
2221
|
+
this.tradeUpdateCallback(data);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
handleMessage(message) {
|
|
2225
|
+
try {
|
|
2226
|
+
const data = JSON.parse(message);
|
|
2227
|
+
const handler = this.messageHandlers.get(data.stream);
|
|
2228
|
+
if (handler) {
|
|
2229
|
+
handler(data.data);
|
|
2230
|
+
} else {
|
|
2231
|
+
this.log(`Received message for unknown stream: ${data.stream}`, {
|
|
2232
|
+
type: "warn"
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
this.log("Failed to parse WebSocket message", {
|
|
2237
|
+
type: "error",
|
|
2238
|
+
metadata: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
connectWebsocket() {
|
|
2243
|
+
this.manualDisconnect = false;
|
|
2244
|
+
if (this.connecting) {
|
|
2245
|
+
this.log("Connection attempt skipped - already connecting");
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
if (this.ws?.readyState === WebSocket2.OPEN) {
|
|
2249
|
+
this.log("Connection attempt skipped - already connected");
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
this.connecting = true;
|
|
2253
|
+
if (this.ws) {
|
|
2254
|
+
this.ws.removeAllListeners();
|
|
2255
|
+
this.ws.terminate();
|
|
2256
|
+
this.ws = null;
|
|
2257
|
+
}
|
|
2258
|
+
this.log(`Connecting to WebSocket at ${this.wsUrl}...`);
|
|
2259
|
+
this.ws = new WebSocket2(this.wsUrl);
|
|
2260
|
+
this.ws.on("open", async () => {
|
|
2261
|
+
try {
|
|
2262
|
+
this.log("WebSocket connected");
|
|
2263
|
+
await this.authenticate();
|
|
2264
|
+
await this.subscribeToTradeUpdates();
|
|
2265
|
+
this.connecting = false;
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
this.log("Failed to setup WebSocket connection", {
|
|
2268
|
+
type: "error",
|
|
2269
|
+
metadata: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
2270
|
+
});
|
|
2271
|
+
this.ws?.close();
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
this.ws.on("message", (data) => {
|
|
2275
|
+
this.handleMessage(data.toString());
|
|
2276
|
+
});
|
|
2277
|
+
this.ws.on("error", (error) => {
|
|
2278
|
+
this.log("WebSocket error", {
|
|
2279
|
+
type: "error",
|
|
2280
|
+
metadata: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
2281
|
+
});
|
|
2282
|
+
this.connecting = false;
|
|
2283
|
+
});
|
|
2284
|
+
this.ws.on("close", () => {
|
|
2285
|
+
this.log("WebSocket connection closed");
|
|
2286
|
+
this.authenticated = false;
|
|
2287
|
+
this.connecting = false;
|
|
2288
|
+
if (this.reconnectTimeout) {
|
|
2289
|
+
clearTimeout(this.reconnectTimeout);
|
|
2290
|
+
this.reconnectTimeout = null;
|
|
2291
|
+
}
|
|
2292
|
+
if (!this.manualDisconnect) {
|
|
2293
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
2294
|
+
this.log("Attempting to reconnect...");
|
|
2295
|
+
this.connectWebsocket();
|
|
2296
|
+
}, this.reconnectDelay);
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Cleanly disconnect from the WebSocket and stop auto-reconnects
|
|
2302
|
+
*/
|
|
2303
|
+
disconnect() {
|
|
2304
|
+
this.manualDisconnect = true;
|
|
2305
|
+
if (this.reconnectTimeout) {
|
|
2306
|
+
clearTimeout(this.reconnectTimeout);
|
|
2307
|
+
this.reconnectTimeout = null;
|
|
2308
|
+
}
|
|
2309
|
+
if (this.ws) {
|
|
2310
|
+
this.log("Disconnecting WebSocket...");
|
|
2311
|
+
this.ws.removeAllListeners();
|
|
2312
|
+
try {
|
|
2313
|
+
if (this.ws.readyState === WebSocket2.OPEN || this.ws.readyState === WebSocket2.CONNECTING) {
|
|
2314
|
+
this.ws.close(1e3, "Client disconnect");
|
|
2315
|
+
} else {
|
|
2316
|
+
this.ws.terminate();
|
|
2317
|
+
}
|
|
2318
|
+
} catch {
|
|
2319
|
+
try {
|
|
2320
|
+
this.ws.terminate();
|
|
2321
|
+
} catch {
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
this.ws = null;
|
|
2325
|
+
}
|
|
2326
|
+
this.authenticated = false;
|
|
2327
|
+
this.connecting = false;
|
|
2328
|
+
this.log("WebSocket disconnected");
|
|
2329
|
+
}
|
|
2330
|
+
async authenticate() {
|
|
2331
|
+
if (!this.ws || this.ws.readyState !== WebSocket2.OPEN) {
|
|
2332
|
+
throw new Error("WebSocket not ready for authentication");
|
|
2333
|
+
}
|
|
2334
|
+
const authMessage = {
|
|
2335
|
+
action: "auth",
|
|
2336
|
+
key: this.credentials.apiKey,
|
|
2337
|
+
secret: this.credentials.apiSecret
|
|
2338
|
+
};
|
|
2339
|
+
this.ws.send(JSON.stringify(authMessage));
|
|
2340
|
+
return new Promise((resolve, reject) => {
|
|
2341
|
+
const authTimeout = setTimeout(() => {
|
|
2342
|
+
this.log("Authentication timeout", { type: "error" });
|
|
2343
|
+
reject(new Error("Authentication timed out"));
|
|
2344
|
+
}, 1e4);
|
|
2345
|
+
const handleAuthResponse = (data) => {
|
|
2346
|
+
try {
|
|
2347
|
+
const message = JSON.parse(data.toString());
|
|
2348
|
+
if (message.stream === "authorization") {
|
|
2349
|
+
this.ws?.removeListener("message", handleAuthResponse);
|
|
2350
|
+
clearTimeout(authTimeout);
|
|
2351
|
+
if (message.data?.status === "authorized") {
|
|
2352
|
+
this.authenticated = true;
|
|
2353
|
+
resolve();
|
|
2354
|
+
} else {
|
|
2355
|
+
const error = `Authentication failed: ${message.data?.message || "Unknown error"}`;
|
|
2356
|
+
this.log(error, { type: "error" });
|
|
2357
|
+
reject(new Error(error));
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
} catch (error) {
|
|
2361
|
+
this.log("Failed to parse auth response", {
|
|
2362
|
+
type: "error",
|
|
2363
|
+
metadata: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
this.ws?.on("message", handleAuthResponse);
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
async subscribeToTradeUpdates() {
|
|
2371
|
+
if (!this.ws || this.ws.readyState !== WebSocket2.OPEN || !this.authenticated) {
|
|
2372
|
+
throw new Error("WebSocket not ready for subscription");
|
|
2373
|
+
}
|
|
2374
|
+
const listenMessage = {
|
|
2375
|
+
action: "listen",
|
|
2376
|
+
data: {
|
|
2377
|
+
streams: ["trade_updates"]
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
this.ws.send(JSON.stringify(listenMessage));
|
|
2381
|
+
return new Promise((resolve, reject) => {
|
|
2382
|
+
const listenTimeout = setTimeout(() => {
|
|
2383
|
+
reject(new Error("Subscribe timeout"));
|
|
2384
|
+
}, 1e4);
|
|
2385
|
+
const handleListenResponse = (data) => {
|
|
2386
|
+
try {
|
|
2387
|
+
const message = JSON.parse(data.toString());
|
|
2388
|
+
if (message.stream === "listening") {
|
|
2389
|
+
this.ws?.removeListener("message", handleListenResponse);
|
|
2390
|
+
clearTimeout(listenTimeout);
|
|
2391
|
+
if (message.data?.streams?.includes("trade_updates")) {
|
|
2392
|
+
resolve();
|
|
2393
|
+
} else {
|
|
2394
|
+
reject(new Error("Failed to subscribe to trade updates"));
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
this.log("Failed to parse listen response", {
|
|
2399
|
+
type: "error",
|
|
2400
|
+
metadata: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
this.ws?.on("message", handleListenResponse);
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
async makeRequest(endpoint, method = "GET", body, queryString = "", requestContext) {
|
|
2408
|
+
const url = `${this.apiBaseUrl}${endpoint}${queryString}`;
|
|
2409
|
+
try {
|
|
2410
|
+
const response = await fetch(url, {
|
|
2411
|
+
method,
|
|
2412
|
+
headers: this.headers,
|
|
2413
|
+
body: body ? JSON.stringify(body) : void 0
|
|
2414
|
+
});
|
|
2415
|
+
if (!response.ok) {
|
|
2416
|
+
const rawResponse = await response.text();
|
|
2417
|
+
const errorResponse = this.parseAlpacaAPIErrorResponse(rawResponse);
|
|
2418
|
+
const symbol = requestContext?.symbol ?? errorResponse?.symbol;
|
|
2419
|
+
const error = new AlpacaRequestError({
|
|
2420
|
+
message: this.buildAlpacaAPIErrorMessage({
|
|
2421
|
+
requestContext,
|
|
2422
|
+
status: response.status,
|
|
2423
|
+
url,
|
|
2424
|
+
errorResponse,
|
|
2425
|
+
rawResponse
|
|
2426
|
+
}),
|
|
2427
|
+
method,
|
|
2428
|
+
status: response.status,
|
|
2429
|
+
url,
|
|
2430
|
+
responseCode: errorResponse?.code ?? null,
|
|
2431
|
+
responseDetails: errorResponse,
|
|
2432
|
+
rawResponse,
|
|
2433
|
+
symbol
|
|
2434
|
+
});
|
|
2435
|
+
this.log(error.message, { symbol, type: "error" });
|
|
2436
|
+
throw error;
|
|
2437
|
+
}
|
|
2438
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
2439
|
+
return null;
|
|
2440
|
+
}
|
|
2441
|
+
const contentType = response.headers.get("content-type");
|
|
2442
|
+
if (contentType && contentType.includes("application/json")) {
|
|
2443
|
+
return await response.json();
|
|
2444
|
+
}
|
|
2445
|
+
const textContent = await response.text();
|
|
2446
|
+
return textContent || null;
|
|
2447
|
+
} catch (error) {
|
|
2448
|
+
if (error instanceof AlpacaRequestError) {
|
|
2449
|
+
throw error;
|
|
2450
|
+
}
|
|
2451
|
+
const normalizedError = error instanceof Error ? error : new Error(`${error}`);
|
|
2452
|
+
const symbol = requestContext?.symbol;
|
|
2453
|
+
const requestError = new AlpacaRequestError({
|
|
2454
|
+
message: this.buildRequestExecutionErrorMessage({
|
|
2455
|
+
requestContext,
|
|
2456
|
+
url,
|
|
2457
|
+
errorMessage: normalizedError.message
|
|
2458
|
+
}),
|
|
2459
|
+
method,
|
|
2460
|
+
url,
|
|
2461
|
+
rawResponse: null,
|
|
2462
|
+
symbol
|
|
2463
|
+
});
|
|
2464
|
+
this.log(requestError.message, { symbol, type: "error" });
|
|
2465
|
+
throw requestError;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
async getPositions(assetClass) {
|
|
2469
|
+
const positions = await this.makeRequest("/positions", "GET", void 0, "", {
|
|
2470
|
+
action: "Get positions"
|
|
2471
|
+
});
|
|
2472
|
+
if (assetClass) {
|
|
2473
|
+
return positions.filter((position) => position.asset_class === assetClass);
|
|
2474
|
+
}
|
|
2475
|
+
return positions;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Get all orders
|
|
2479
|
+
* @param params (GetOrdersParams) - optional parameters to filter the orders
|
|
2480
|
+
* - status: 'open' | 'closed' | 'all'
|
|
2481
|
+
* - limit: number
|
|
2482
|
+
* - after: string
|
|
2483
|
+
* - until: string
|
|
2484
|
+
* - direction: 'asc' | 'desc'
|
|
2485
|
+
* - nested: boolean
|
|
2486
|
+
* - symbols: string[], an array of all the symbols
|
|
2487
|
+
* - side: 'buy' | 'sell'
|
|
2488
|
+
* @returns all orders
|
|
2489
|
+
*/
|
|
2490
|
+
async getOrders(params = {}) {
|
|
2491
|
+
const queryParams = new URLSearchParams();
|
|
2492
|
+
if (params.status) queryParams.append("status", params.status);
|
|
2493
|
+
if (params.limit) queryParams.append("limit", params.limit.toString());
|
|
2494
|
+
if (params.after) queryParams.append("after", params.after);
|
|
2495
|
+
if (params.until) queryParams.append("until", params.until);
|
|
2496
|
+
if (params.direction) queryParams.append("direction", params.direction);
|
|
2497
|
+
if (params.nested) queryParams.append("nested", params.nested.toString());
|
|
2498
|
+
if (params.symbols) queryParams.append("symbols", params.symbols.join(","));
|
|
2499
|
+
if (params.side) queryParams.append("side", params.side);
|
|
2500
|
+
const endpoint = `/orders${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
|
2501
|
+
return this.makeRequest(endpoint, "GET", void 0, "", {
|
|
2502
|
+
action: "Get orders",
|
|
2503
|
+
symbol: params.symbols?.join(",")
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
async getAccountDetails() {
|
|
2507
|
+
return this.makeRequest("/account", "GET", void 0, "", {
|
|
2508
|
+
action: "Get account details"
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Create a trailing stop order
|
|
2513
|
+
* @param symbol (string) - the symbol of the order
|
|
2514
|
+
* @param qty (number) - the quantity of the order
|
|
2515
|
+
* @param side (string) - the side of the order
|
|
2516
|
+
* @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
|
|
2517
|
+
* @param position_intent (string) - the position intent of the order
|
|
2518
|
+
* @param client_order_id (string) - optional client order id
|
|
2519
|
+
* @returns The created trailing stop order
|
|
2520
|
+
*/
|
|
2521
|
+
async createTrailingStop(symbol, qty, side, trailPercent100, position_intent, client_order_id) {
|
|
2522
|
+
this.log(
|
|
2523
|
+
`Creating trailing stop ${side.toUpperCase()} ${qty} shares for ${symbol} with trail percent ${trailPercent100}%`,
|
|
2524
|
+
{
|
|
2525
|
+
symbol
|
|
2526
|
+
}
|
|
2527
|
+
);
|
|
2528
|
+
const body = {
|
|
2529
|
+
symbol,
|
|
2530
|
+
qty: Math.abs(qty).toString(),
|
|
2531
|
+
side,
|
|
2532
|
+
position_intent,
|
|
2533
|
+
order_class: "simple",
|
|
2534
|
+
type: "trailing_stop",
|
|
2535
|
+
trail_percent: trailPercent100.toString(),
|
|
2536
|
+
time_in_force: "gtc"
|
|
2537
|
+
};
|
|
2538
|
+
if (client_order_id !== void 0) {
|
|
2539
|
+
body.client_order_id = client_order_id;
|
|
2540
|
+
}
|
|
2541
|
+
return this.makeRequest("/orders", "POST", body, "", {
|
|
2542
|
+
action: "Create trailing stop order",
|
|
2543
|
+
symbol
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Create a stop order (stop or stop-limit)
|
|
2548
|
+
* @param symbol (string) - the symbol of the order
|
|
2549
|
+
* @param qty (number) - the quantity of the order
|
|
2550
|
+
* @param side (string) - the side of the order
|
|
2551
|
+
* @param stopPrice (number) - the stop price that triggers the order
|
|
2552
|
+
* @param position_intent (string) - the position intent of the order
|
|
2553
|
+
* @param limitPrice (number) - optional limit price (if provided, creates a stop-limit order)
|
|
2554
|
+
* @param client_order_id (string) - optional client order id
|
|
2555
|
+
* @returns The created stop order
|
|
2556
|
+
*/
|
|
2557
|
+
async createStopOrder(symbol, qty, side, stopPrice, position_intent, limitPrice, client_order_id) {
|
|
2558
|
+
const isStopLimit = limitPrice !== void 0;
|
|
2559
|
+
const orderType = isStopLimit ? "stop-limit" : "stop";
|
|
2560
|
+
this.log(
|
|
2561
|
+
`Creating ${orderType} ${side.toUpperCase()} ${qty} shares for ${symbol} with stop price ${stopPrice}${isStopLimit ? ` and limit price ${limitPrice}` : ""}`,
|
|
2562
|
+
{
|
|
2563
|
+
symbol
|
|
2564
|
+
}
|
|
2565
|
+
);
|
|
2566
|
+
const body = {
|
|
2567
|
+
symbol,
|
|
2568
|
+
qty: Math.abs(qty).toString(),
|
|
2569
|
+
side,
|
|
2570
|
+
position_intent,
|
|
2571
|
+
order_class: "simple",
|
|
2572
|
+
type: isStopLimit ? "stop_limit" : "stop",
|
|
2573
|
+
stop_price: this.roundPriceForAlpaca(stopPrice).toString(),
|
|
2574
|
+
time_in_force: "gtc"
|
|
2575
|
+
};
|
|
2576
|
+
if (limitPrice !== void 0) {
|
|
2577
|
+
body.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
2578
|
+
}
|
|
2579
|
+
if (client_order_id !== void 0) {
|
|
2580
|
+
body.client_order_id = client_order_id;
|
|
2581
|
+
}
|
|
2582
|
+
return this.makeRequest("/orders", "POST", body, "", {
|
|
2583
|
+
action: `Create ${orderType} order`,
|
|
2584
|
+
symbol
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Create a market order
|
|
2589
|
+
* @param symbol (string) - the symbol of the order
|
|
2590
|
+
* @param qty (number) - the quantity of the order
|
|
2591
|
+
* @param side (string) - the side of the order
|
|
2592
|
+
* @param position_intent (string) - the position intent of the order. Important for knowing if a position needs a trailing stop.
|
|
2593
|
+
* @param client_order_id (string) - optional client order id
|
|
2594
|
+
*/
|
|
2595
|
+
async createMarketOrder(symbol, qty, side, position_intent, client_order_id) {
|
|
2596
|
+
this.log(`Creating market order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
|
|
2597
|
+
symbol
|
|
2598
|
+
});
|
|
2599
|
+
const body = {
|
|
2600
|
+
symbol,
|
|
2601
|
+
qty: Math.abs(qty).toString(),
|
|
2602
|
+
side,
|
|
2603
|
+
position_intent,
|
|
2604
|
+
type: "market",
|
|
2605
|
+
time_in_force: "day",
|
|
2606
|
+
order_class: "simple"
|
|
2607
|
+
};
|
|
2608
|
+
if (client_order_id !== void 0) {
|
|
2609
|
+
body.client_order_id = client_order_id;
|
|
2610
|
+
}
|
|
2611
|
+
return this.makeRequest("/orders", "POST", body, "", {
|
|
2612
|
+
action: "Create market order",
|
|
2613
|
+
symbol
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Create a Market on Open (MOO) order - executes in the opening auction
|
|
2618
|
+
*
|
|
2619
|
+
* IMPORTANT TIMING CONSTRAINTS:
|
|
2620
|
+
* - Valid submission window: After 7:00pm ET and before 9:28am ET
|
|
2621
|
+
* - Orders submitted between 9:28am and 7:00pm ET will be REJECTED
|
|
2622
|
+
* - Orders submitted after 7:00pm ET are queued for the next trading day's opening auction
|
|
2623
|
+
* - Example: An order at 8:00pm Monday will execute at Tuesday's market open (9:30am)
|
|
2624
|
+
*
|
|
2625
|
+
* @param symbol - The symbol of the order
|
|
2626
|
+
* @param qty - The quantity of shares
|
|
2627
|
+
* @param side - Buy or sell
|
|
2628
|
+
* @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
|
|
2629
|
+
* @param client_order_id - Optional client order id
|
|
2630
|
+
* @returns The created order
|
|
2631
|
+
*/
|
|
2632
|
+
async createMOOOrder(symbol, qty, side, position_intent, client_order_id) {
|
|
2633
|
+
this.log(`Creating Market on Open order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
|
|
2634
|
+
symbol
|
|
2635
|
+
});
|
|
2636
|
+
const body = {
|
|
2637
|
+
symbol,
|
|
2638
|
+
qty: Math.abs(qty).toString(),
|
|
2639
|
+
side,
|
|
2640
|
+
position_intent,
|
|
2641
|
+
type: "market",
|
|
2642
|
+
time_in_force: "opg",
|
|
2643
|
+
order_class: "simple"
|
|
2644
|
+
};
|
|
2645
|
+
if (client_order_id !== void 0) {
|
|
2646
|
+
body.client_order_id = client_order_id;
|
|
2647
|
+
}
|
|
2648
|
+
return this.makeRequest("/orders", "POST", body, "", {
|
|
2649
|
+
action: "Create market on open order",
|
|
2650
|
+
symbol
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Create a Market on Close (MOC) order - executes in the closing auction
|
|
2655
|
+
*
|
|
2656
|
+
* IMPORTANT TIMING CONSTRAINTS:
|
|
2657
|
+
* - Valid submission window: After 7:00pm ET (previous day) and before 3:50pm ET (same day)
|
|
2658
|
+
* - Orders submitted between 3:50pm and 7:00pm ET will be REJECTED
|
|
2659
|
+
* - Orders submitted after 7:00pm ET are queued for the next trading day's closing auction
|
|
2660
|
+
* - Example: An order at 8:00pm Monday will execute at Tuesday's market close (4:00pm)
|
|
2661
|
+
*
|
|
2662
|
+
* @param symbol - The symbol of the order
|
|
2663
|
+
* @param qty - The quantity of shares
|
|
2664
|
+
* @param side - Buy or sell
|
|
2665
|
+
* @param position_intent - The position intent (buy_to_open, sell_to_close, etc.)
|
|
2666
|
+
* @param client_order_id - Optional client order id
|
|
2667
|
+
* @returns The created order
|
|
2668
|
+
*/
|
|
2669
|
+
async createMOCOrder(symbol, qty, side, position_intent, client_order_id) {
|
|
2670
|
+
this.log(`Creating Market on Close order for ${symbol}: ${side} ${qty} shares (${position_intent})`, {
|
|
2671
|
+
symbol
|
|
2672
|
+
});
|
|
2673
|
+
const body = {
|
|
2674
|
+
symbol,
|
|
2675
|
+
qty: Math.abs(qty).toString(),
|
|
2676
|
+
side,
|
|
2677
|
+
position_intent,
|
|
2678
|
+
type: "market",
|
|
2679
|
+
time_in_force: "cls",
|
|
2680
|
+
order_class: "simple"
|
|
2681
|
+
};
|
|
2682
|
+
if (client_order_id !== void 0) {
|
|
2683
|
+
body.client_order_id = client_order_id;
|
|
2684
|
+
}
|
|
2685
|
+
return this.makeRequest("/orders", "POST", body, "", {
|
|
2686
|
+
action: "Create market on close order",
|
|
2687
|
+
symbol
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Create an OCO (One-Cancels-Other) order with take profit and stop loss
|
|
2692
|
+
* @param symbol (string) - the symbol of the order
|
|
2693
|
+
* @param qty (number) - the quantity of the order
|
|
2694
|
+
* @param side (string) - the side of the order (buy or sell)
|
|
2695
|
+
* @param position_intent (string) - the position intent of the order
|
|
2696
|
+
* @param limitPrice (number) - the limit price for the entry order (OCO orders must be limit orders)
|
|
2697
|
+
* @param takeProfitPrice (number) - the take profit price
|
|
2698
|
+
* @param stopLossPrice (number) - the stop loss price
|
|
2699
|
+
* @param stopLossLimitPrice (number) - optional limit price for stop loss (creates stop-limit instead of stop)
|
|
2700
|
+
* @param client_order_id (string) - optional client order id
|
|
2701
|
+
* @returns The created OCO order
|
|
2702
|
+
*/
|
|
2703
|
+
async createOCOOrder(symbol, qty, side, position_intent, limitPrice, takeProfitPrice, stopLossPrice, stopLossLimitPrice, client_order_id) {
|
|
2704
|
+
this.log(
|
|
2705
|
+
`Creating OCO order ${side.toUpperCase()} ${qty} shares for ${symbol} at limit ${limitPrice} with take profit ${takeProfitPrice} and stop loss ${stopLossPrice}`,
|
|
2706
|
+
{
|
|
2707
|
+
symbol
|
|
2708
|
+
}
|
|
2709
|
+
);
|
|
2710
|
+
const body = {
|
|
2711
|
+
symbol,
|
|
2712
|
+
qty: Math.abs(qty).toString(),
|
|
2713
|
+
side,
|
|
2714
|
+
position_intent,
|
|
2715
|
+
order_class: "oco",
|
|
2716
|
+
type: "limit",
|
|
2717
|
+
limit_price: this.roundPriceForAlpaca(limitPrice).toString(),
|
|
2718
|
+
time_in_force: "gtc",
|
|
2719
|
+
take_profit: {
|
|
2720
|
+
limit_price: this.roundPriceForAlpaca(takeProfitPrice).toString()
|
|
2721
|
+
},
|
|
2722
|
+
stop_loss: {
|
|
2723
|
+
stop_price: this.roundPriceForAlpaca(stopLossPrice).toString()
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
if (stopLossLimitPrice !== void 0) {
|
|
2727
|
+
body.stop_loss = {
|
|
2728
|
+
stop_price: this.roundPriceForAlpaca(stopLossPrice).toString(),
|
|
2729
|
+
limit_price: this.roundPriceForAlpaca(stopLossLimitPrice).toString()
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
if (client_order_id !== void 0) {
|
|
2733
|
+
body.client_order_id = client_order_id;
|
|
2734
|
+
}
|
|
2735
|
+
return this.makeRequest("/orders", "POST", body, "", {
|
|
2736
|
+
action: "Create OCO order",
|
|
2737
|
+
symbol
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Get the current trail percent for a symbol, assuming that it has an open position and a trailing stop order to close it. Because this relies on an orders request for one symbol, you can't do it too often.
|
|
2742
|
+
* @param symbol (string) - the symbol of the order
|
|
2743
|
+
* @returns the current trail percent
|
|
2744
|
+
*/
|
|
2745
|
+
async getCurrentTrailPercent(symbol) {
|
|
2746
|
+
const orders = await this.getOrders({
|
|
2747
|
+
status: "open",
|
|
2748
|
+
symbols: [symbol]
|
|
2749
|
+
});
|
|
2750
|
+
const trailingStopOrder = orders.find(
|
|
2751
|
+
(order) => order.type === "trailing_stop" && (order.position_intent === "sell_to_close" || order.position_intent === "buy_to_close")
|
|
2752
|
+
);
|
|
2753
|
+
if (!trailingStopOrder) {
|
|
2754
|
+
this.log(`No closing trailing stop order found for ${symbol}`, {
|
|
2755
|
+
symbol
|
|
2756
|
+
});
|
|
2757
|
+
return null;
|
|
2758
|
+
}
|
|
2759
|
+
if (!trailingStopOrder.trail_percent) {
|
|
2760
|
+
this.log(`Trailing stop order found for ${symbol} but no trail_percent value`, {
|
|
2761
|
+
symbol
|
|
2762
|
+
});
|
|
2763
|
+
return null;
|
|
2764
|
+
}
|
|
2765
|
+
const trailPercent = parseFloat(trailingStopOrder.trail_percent);
|
|
2766
|
+
return trailPercent;
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Update the trail percent for a trailing stop order
|
|
2770
|
+
* @param symbol (string) - the symbol of the order
|
|
2771
|
+
* @param trailPercent100 (number) - the trail percent of the order (scale 100, i.e. 0.5 = 0.5%)
|
|
2772
|
+
*/
|
|
2773
|
+
async updateTrailingStop(symbol, trailPercent100) {
|
|
2774
|
+
const orders = await this.getOrders({
|
|
2775
|
+
status: "open",
|
|
2776
|
+
symbols: [symbol]
|
|
2777
|
+
});
|
|
2778
|
+
const trailingStopOrder = orders.find((order) => order.type === "trailing_stop");
|
|
2779
|
+
if (!trailingStopOrder) {
|
|
2780
|
+
this.log(`No open trailing stop order found for ${symbol}`, { type: "error", symbol });
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
const currentTrailPercent = trailingStopOrder.trail_percent ? parseFloat(trailingStopOrder.trail_percent) : null;
|
|
2784
|
+
const epsilon = 1e-4;
|
|
2785
|
+
if (currentTrailPercent !== null && Math.abs(currentTrailPercent - trailPercent100) < epsilon) {
|
|
2786
|
+
this.log(
|
|
2787
|
+
`Trailing stop for ${symbol} already set to ${trailPercent100}% (current: ${currentTrailPercent}%), skipping update`,
|
|
2788
|
+
{
|
|
2789
|
+
symbol
|
|
2790
|
+
}
|
|
2791
|
+
);
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
this.log(`Updating trailing stop for ${symbol} from ${currentTrailPercent}% to ${trailPercent100}%`, {
|
|
2795
|
+
symbol
|
|
2796
|
+
});
|
|
2797
|
+
await this.makeRequest(
|
|
2798
|
+
`/orders/${trailingStopOrder.id}`,
|
|
2799
|
+
"PATCH",
|
|
2800
|
+
{ trail: trailPercent100.toString() },
|
|
2801
|
+
"",
|
|
2802
|
+
{
|
|
2803
|
+
action: "Update trailing stop order",
|
|
2804
|
+
symbol
|
|
2805
|
+
}
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Cancel all open orders
|
|
2810
|
+
*/
|
|
2811
|
+
async cancelAllOrders() {
|
|
2812
|
+
this.log(`Canceling all open orders`);
|
|
2813
|
+
try {
|
|
2814
|
+
await this.makeRequest("/orders", "DELETE", void 0, "", {
|
|
2815
|
+
action: "Cancel all open orders"
|
|
2816
|
+
});
|
|
2817
|
+
} catch (error) {
|
|
2818
|
+
if (!(error instanceof AlpacaRequestError)) {
|
|
2819
|
+
this.log(`Error canceling all orders: ${error instanceof Error ? error.message : `${error}`}`, {
|
|
2820
|
+
type: "error"
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
/**
|
|
2826
|
+
* Cancel a specific order by its ID
|
|
2827
|
+
* @param orderId The id of the order to cancel
|
|
2828
|
+
* @throws Error if the order is not cancelable (status 422) or if the order doesn't exist
|
|
2829
|
+
* @returns Promise that resolves when the order is successfully canceled
|
|
2830
|
+
*/
|
|
2831
|
+
async cancelOrder(orderId) {
|
|
2832
|
+
this.log(`Attempting to cancel order ${orderId}`);
|
|
2833
|
+
try {
|
|
2834
|
+
await this.makeRequest(`/orders/${orderId}`, "DELETE", void 0, "", {
|
|
2835
|
+
action: `Cancel order ${orderId}`
|
|
2836
|
+
});
|
|
2837
|
+
this.log(`Successfully canceled order ${orderId}`);
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
if (error instanceof AlpacaRequestError && error.status === 422) {
|
|
2840
|
+
throw new Error(`Order ${orderId} is not cancelable`);
|
|
2841
|
+
}
|
|
2842
|
+
throw error;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Create a limit order
|
|
2847
|
+
* @param symbol (string) - the symbol of the order
|
|
2848
|
+
* @param qty (number) - the quantity of the order
|
|
2849
|
+
* @param side (string) - the side of the order
|
|
2850
|
+
* @param limitPrice (number) - the limit price of the order
|
|
2851
|
+
* @param position_intent (string) - the position intent of the order
|
|
2852
|
+
* @param extended_hours (boolean) - whether the order is in extended hours
|
|
2853
|
+
* @param client_order_id (string) - the client order id of the order
|
|
2854
|
+
*/
|
|
2855
|
+
async createLimitOrder(symbol, qty, side, limitPrice, position_intent, extended_hours = false, client_order_id) {
|
|
2856
|
+
this.log(
|
|
2857
|
+
`Creating limit order for ${symbol}: ${side} ${qty} shares at $${limitPrice.toFixed(2)} (${position_intent})`,
|
|
2858
|
+
{
|
|
2859
|
+
symbol
|
|
2860
|
+
}
|
|
2861
|
+
);
|
|
2862
|
+
const body = {
|
|
2863
|
+
symbol,
|
|
2864
|
+
qty: Math.abs(qty).toString(),
|
|
2865
|
+
side,
|
|
2866
|
+
position_intent,
|
|
2867
|
+
type: "limit",
|
|
2868
|
+
limit_price: this.roundPriceForAlpaca(limitPrice).toString(),
|
|
2869
|
+
time_in_force: "day",
|
|
2870
|
+
order_class: "simple",
|
|
2871
|
+
extended_hours
|
|
2872
|
+
};
|
|
2873
|
+
if (client_order_id !== void 0) {
|
|
2874
|
+
body.client_order_id = client_order_id;
|
|
2875
|
+
}
|
|
2876
|
+
return this.makeRequest("/orders", "POST", body, "", {
|
|
2877
|
+
action: "Create limit order",
|
|
2878
|
+
symbol
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Close all equities positions
|
|
2883
|
+
* @param options (object) - the options for closing the positions
|
|
2884
|
+
* - cancel_orders (boolean) - whether to cancel related orders
|
|
2885
|
+
* - useLimitOrders (boolean) - whether to use limit orders to close the positions
|
|
2886
|
+
*/
|
|
2887
|
+
async closeAllPositions(options = { cancel_orders: true, useLimitOrders: false }) {
|
|
2888
|
+
this.log(
|
|
2889
|
+
`Closing all positions${options.useLimitOrders ? " using limit orders" : ""}${options.cancel_orders ? " and canceling open orders" : ""}`
|
|
2890
|
+
);
|
|
2891
|
+
if (options.useLimitOrders) {
|
|
2892
|
+
const positions = await this.getPositions("us_equity");
|
|
2893
|
+
if (positions.length === 0) {
|
|
2894
|
+
this.log("No positions to close");
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
this.log(`Found ${positions.length} positions to close`);
|
|
2898
|
+
const symbols = positions.map((position) => position.symbol);
|
|
2899
|
+
const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
|
|
2900
|
+
const lengthOfQuotes = Object.keys(quotesResponse.quotes).length;
|
|
2901
|
+
if (lengthOfQuotes === 0) {
|
|
2902
|
+
this.log("No quotes available for positions, received 0 quotes", {
|
|
2903
|
+
type: "error"
|
|
2904
|
+
});
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
if (lengthOfQuotes !== positions.length) {
|
|
2908
|
+
this.log(
|
|
2909
|
+
`Received ${lengthOfQuotes} quotes for ${positions.length} positions, expected ${positions.length} quotes`,
|
|
2910
|
+
{ type: "warn" }
|
|
2911
|
+
);
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
for (const position of positions) {
|
|
2915
|
+
const quote = quotesResponse.quotes[position.symbol];
|
|
2916
|
+
if (!quote) {
|
|
2917
|
+
this.log(`No quote available for ${position.symbol}, skipping limit order`, {
|
|
2918
|
+
symbol: position.symbol,
|
|
2919
|
+
type: "warn"
|
|
2920
|
+
});
|
|
2921
|
+
continue;
|
|
2922
|
+
}
|
|
2923
|
+
const qty = Math.abs(parseFloat(position.qty));
|
|
2924
|
+
const side = position.side === "long" ? "sell" : "buy";
|
|
2925
|
+
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
2926
|
+
const currentPrice = side === "sell" ? quote.bp : quote.ap;
|
|
2927
|
+
if (!currentPrice) {
|
|
2928
|
+
this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
|
|
2929
|
+
symbol: position.symbol,
|
|
2930
|
+
type: "warn"
|
|
2931
|
+
});
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2934
|
+
const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
|
|
2935
|
+
const limitPrice = side === "sell" ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1));
|
|
2936
|
+
this.log(
|
|
2937
|
+
`Creating limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(
|
|
2938
|
+
2
|
|
2939
|
+
)}`,
|
|
2940
|
+
{
|
|
2941
|
+
symbol: position.symbol
|
|
2942
|
+
}
|
|
2943
|
+
);
|
|
2944
|
+
await this.createLimitOrder(position.symbol, qty, side, limitPrice, positionIntent);
|
|
2945
|
+
}
|
|
2946
|
+
} else {
|
|
2947
|
+
await this.makeRequest("/positions", "DELETE", void 0, options.cancel_orders ? "?cancel_orders=true" : "", {
|
|
2948
|
+
action: "Close all positions"
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Close all equities positions using limit orders during extended hours trading
|
|
2954
|
+
* @param cancelOrders Whether to cancel related orders (default: true)
|
|
2955
|
+
* @returns Promise that resolves when all positions are closed
|
|
2956
|
+
*/
|
|
2957
|
+
async closeAllPositionsAfterHours() {
|
|
2958
|
+
this.log("Closing all positions using limit orders during extended hours trading");
|
|
2959
|
+
const positions = await this.getPositions();
|
|
2960
|
+
this.log(`Found ${positions.length} positions to close`);
|
|
2961
|
+
if (positions.length === 0) {
|
|
2962
|
+
this.log("No positions to close");
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
await this.cancelAllOrders();
|
|
2966
|
+
this.log(`Cancelled all open orders`);
|
|
2967
|
+
const symbols = positions.map((position) => position.symbol);
|
|
2968
|
+
const quotesResponse = await marketDataAPI.getLatestQuotes(symbols);
|
|
2969
|
+
for (const position of positions) {
|
|
2970
|
+
const quote = quotesResponse.quotes[position.symbol];
|
|
2971
|
+
if (!quote) {
|
|
2972
|
+
this.log(`No quote available for ${position.symbol}, skipping limit order`, {
|
|
2973
|
+
symbol: position.symbol,
|
|
2974
|
+
type: "warn"
|
|
2975
|
+
});
|
|
2976
|
+
continue;
|
|
2977
|
+
}
|
|
2978
|
+
const qty = Math.abs(parseFloat(position.qty));
|
|
2979
|
+
const side = position.side === "long" ? "sell" : "buy";
|
|
2980
|
+
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
2981
|
+
const currentPrice = side === "sell" ? quote.bp : quote.ap;
|
|
2982
|
+
if (!currentPrice) {
|
|
2983
|
+
this.log(`No valid price available for ${position.symbol}, skipping limit order`, {
|
|
2984
|
+
symbol: position.symbol,
|
|
2985
|
+
type: "warn"
|
|
2986
|
+
});
|
|
2987
|
+
continue;
|
|
2988
|
+
}
|
|
2989
|
+
const limitSlippagePercent1 = limitPriceSlippagePercent100 / 100;
|
|
2990
|
+
const limitPrice = side === "sell" ? this.roundPriceForAlpaca(currentPrice * (1 - limitSlippagePercent1)) : this.roundPriceForAlpaca(currentPrice * (1 + limitSlippagePercent1));
|
|
2991
|
+
this.log(
|
|
2992
|
+
`Creating extended hours limit order to close ${position.symbol} position: ${side} ${qty} shares at $${limitPrice.toFixed(2)}`,
|
|
2993
|
+
{
|
|
2994
|
+
symbol: position.symbol
|
|
2995
|
+
}
|
|
2996
|
+
);
|
|
2997
|
+
await this.createLimitOrder(
|
|
2998
|
+
position.symbol,
|
|
2999
|
+
qty,
|
|
3000
|
+
side,
|
|
3001
|
+
limitPrice,
|
|
3002
|
+
positionIntent,
|
|
3003
|
+
true
|
|
3004
|
+
// Enable extended hours trading
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
this.log(`All positions closed: ${positions.map((p) => p.symbol).join(", ")}`);
|
|
3008
|
+
}
|
|
3009
|
+
onTradeUpdate(callback) {
|
|
3010
|
+
this.tradeUpdateCallback = callback;
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* Get portfolio history for the account
|
|
3014
|
+
* @param params Parameters for the portfolio history request
|
|
3015
|
+
* @returns Portfolio history data
|
|
3016
|
+
*/
|
|
3017
|
+
async getPortfolioHistory(params) {
|
|
3018
|
+
const queryParams = new URLSearchParams();
|
|
3019
|
+
if (params.timeframe) queryParams.append("timeframe", params.timeframe);
|
|
3020
|
+
if (params.period) queryParams.append("period", params.period);
|
|
3021
|
+
if (params.extended_hours !== void 0) queryParams.append("extended_hours", params.extended_hours.toString());
|
|
3022
|
+
if (params.start) queryParams.append("start", params.start);
|
|
3023
|
+
if (params.end) queryParams.append("end", params.end);
|
|
3024
|
+
if (params.date_end) queryParams.append("date_end", params.date_end);
|
|
3025
|
+
const response = await this.makeRequest(
|
|
3026
|
+
`/account/portfolio/history?${queryParams.toString()}`,
|
|
3027
|
+
"GET",
|
|
3028
|
+
void 0,
|
|
3029
|
+
"",
|
|
3030
|
+
{
|
|
3031
|
+
action: "Get portfolio history"
|
|
3032
|
+
}
|
|
3033
|
+
);
|
|
3034
|
+
return response;
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Get portfolio daily history for the account, ensuring the most recent day is included
|
|
3038
|
+
* by combining daily and hourly history if needed.
|
|
3039
|
+
*
|
|
3040
|
+
* This function performs two API calls:
|
|
3041
|
+
* 1. Retrieves daily portfolio history
|
|
3042
|
+
* 2. Retrieves hourly portfolio history to check for more recent data
|
|
3043
|
+
*
|
|
3044
|
+
* If hourly history has timestamps more recent than the last timestamp in daily history,
|
|
3045
|
+
* it appends one additional day to the daily history using the most recent hourly values.
|
|
3046
|
+
*
|
|
3047
|
+
* @param params Parameters for the portfolio history request (same as getPortfolioHistory except timeframe is forced to '1D')
|
|
3048
|
+
* @returns Portfolio history data with daily timeframe, including the most recent day if available from hourly data
|
|
3049
|
+
*/
|
|
3050
|
+
async getPortfolioDailyHistory(params) {
|
|
3051
|
+
const dailyParams = { ...params, timeframe: "1D" };
|
|
3052
|
+
const hourlyParams = { timeframe: "1Min", period: "1D" };
|
|
3053
|
+
const [dailyHistory, hourlyHistory] = await Promise.all([
|
|
3054
|
+
this.getPortfolioHistory(dailyParams),
|
|
3055
|
+
this.getPortfolioHistory(hourlyParams)
|
|
3056
|
+
]);
|
|
3057
|
+
if (!hourlyHistory.timestamp || hourlyHistory.timestamp.length === 0) {
|
|
3058
|
+
return dailyHistory;
|
|
3059
|
+
}
|
|
3060
|
+
const lastDailyTimestamp = dailyHistory.timestamp[dailyHistory.timestamp.length - 1];
|
|
3061
|
+
const recentHourlyData = hourlyHistory.timestamp.map((timestamp, index) => ({ timestamp, index })).filter(({ timestamp }) => timestamp > lastDailyTimestamp);
|
|
3062
|
+
if (recentHourlyData.length === 0) {
|
|
3063
|
+
return dailyHistory;
|
|
3064
|
+
}
|
|
3065
|
+
const mostRecentHourly = recentHourlyData[recentHourlyData.length - 1];
|
|
3066
|
+
const mostRecentIndex = mostRecentHourly.index;
|
|
3067
|
+
const mostRecentMs = mostRecentHourly.timestamp * 1e3;
|
|
3068
|
+
const tradingDateStr = getTradingDate(new Date(mostRecentMs));
|
|
3069
|
+
const [yearStr, monthStr, dayStr] = tradingDateStr.split("-");
|
|
3070
|
+
const year = Number(yearStr);
|
|
3071
|
+
const month = Number(monthStr);
|
|
3072
|
+
const day = Number(dayStr);
|
|
3073
|
+
const newDailyTimestamp = Math.floor(Date.UTC(year, month - 1, day + 1, 0, 0, 0, 0) / 1e3);
|
|
3074
|
+
const updatedDailyHistory = {
|
|
3075
|
+
...dailyHistory,
|
|
3076
|
+
timestamp: [...dailyHistory.timestamp, newDailyTimestamp],
|
|
3077
|
+
equity: [...dailyHistory.equity, hourlyHistory.equity[mostRecentIndex]],
|
|
3078
|
+
profit_loss: [...dailyHistory.profit_loss, hourlyHistory.profit_loss[mostRecentIndex]],
|
|
3079
|
+
profit_loss_pct: [...dailyHistory.profit_loss_pct, hourlyHistory.profit_loss_pct[mostRecentIndex]]
|
|
3080
|
+
};
|
|
3081
|
+
return updatedDailyHistory;
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Get option contracts based on specified parameters
|
|
3085
|
+
* @param params Parameters to filter option contracts
|
|
3086
|
+
* @returns Option contracts matching the criteria
|
|
3087
|
+
*/
|
|
3088
|
+
async getOptionContracts(params) {
|
|
3089
|
+
const queryParams = new URLSearchParams();
|
|
3090
|
+
queryParams.append("underlying_symbols", params.underlying_symbols.join(","));
|
|
3091
|
+
if (params.expiration_date_gte) queryParams.append("expiration_date_gte", params.expiration_date_gte);
|
|
3092
|
+
if (params.expiration_date_lte) queryParams.append("expiration_date_lte", params.expiration_date_lte);
|
|
3093
|
+
if (params.strike_price_gte) queryParams.append("strike_price_gte", params.strike_price_gte);
|
|
3094
|
+
if (params.strike_price_lte) queryParams.append("strike_price_lte", params.strike_price_lte);
|
|
3095
|
+
if (params.type) queryParams.append("type", params.type);
|
|
3096
|
+
if (params.status) queryParams.append("status", params.status);
|
|
3097
|
+
if (params.limit) queryParams.append("limit", params.limit.toString());
|
|
3098
|
+
if (params.page_token) queryParams.append("page_token", params.page_token);
|
|
3099
|
+
this.log(`Fetching option contracts for ${params.underlying_symbols.join(", ")}`, {
|
|
3100
|
+
symbol: params.underlying_symbols.join(", ")
|
|
3101
|
+
});
|
|
3102
|
+
const response = await this.makeRequest(
|
|
3103
|
+
`/options/contracts?${queryParams.toString()}`,
|
|
3104
|
+
"GET",
|
|
3105
|
+
void 0,
|
|
3106
|
+
"",
|
|
3107
|
+
{
|
|
3108
|
+
action: "Get option contracts",
|
|
3109
|
+
symbol: params.underlying_symbols.join(", ")
|
|
3110
|
+
}
|
|
3111
|
+
);
|
|
3112
|
+
this.log(`Found ${response.option_contracts.length} option contracts`, {
|
|
3113
|
+
symbol: params.underlying_symbols.join(", ")
|
|
3114
|
+
});
|
|
3115
|
+
return response;
|
|
3116
|
+
}
|
|
3117
|
+
/**
|
|
3118
|
+
* Get a specific option contract by symbol or ID
|
|
3119
|
+
* @param symbolOrId The symbol or ID of the option contract
|
|
3120
|
+
* @returns The option contract details
|
|
3121
|
+
*/
|
|
3122
|
+
async getOptionContract(symbolOrId) {
|
|
3123
|
+
this.log(`Fetching option contract details for ${symbolOrId}`, {
|
|
3124
|
+
symbol: symbolOrId
|
|
3125
|
+
});
|
|
3126
|
+
const response = await this.makeRequest(`/options/contracts/${symbolOrId}`, "GET", void 0, "", {
|
|
3127
|
+
action: "Get option contract details",
|
|
3128
|
+
symbol: symbolOrId
|
|
3129
|
+
});
|
|
3130
|
+
this.log(`Found option contract details for ${symbolOrId}: ${response.name}`, {
|
|
3131
|
+
symbol: symbolOrId
|
|
3132
|
+
});
|
|
3133
|
+
return response;
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Create a simple option order (market or limit)
|
|
3137
|
+
* @param symbol Option contract symbol
|
|
3138
|
+
* @param qty Quantity of contracts (must be a whole number)
|
|
3139
|
+
* @param side Buy or sell
|
|
3140
|
+
* @param position_intent Position intent (buy_to_open, buy_to_close, sell_to_open, sell_to_close)
|
|
3141
|
+
* @param type Order type (market or limit)
|
|
3142
|
+
* @param limitPrice Limit price (required for limit orders)
|
|
3143
|
+
* @returns The created order
|
|
3144
|
+
*/
|
|
3145
|
+
async createOptionOrder(symbol, qty, side, position_intent, type, limitPrice) {
|
|
3146
|
+
if (!Number.isInteger(qty) || qty <= 0) {
|
|
3147
|
+
this.log("Quantity must be a positive whole number for option orders", { symbol, type: "error" });
|
|
3148
|
+
}
|
|
3149
|
+
if (type === "limit" && limitPrice === void 0) {
|
|
3150
|
+
this.log("Limit price is required for limit orders", { symbol, type: "error" });
|
|
3151
|
+
}
|
|
3152
|
+
this.log(
|
|
3153
|
+
`Creating ${type} option order for ${symbol}: ${side} ${qty} contracts (${position_intent})${type === "limit" ? ` at $${limitPrice?.toFixed(2)}` : ""}`,
|
|
3154
|
+
{
|
|
3155
|
+
symbol
|
|
3156
|
+
}
|
|
3157
|
+
);
|
|
3158
|
+
const orderData = {
|
|
3159
|
+
symbol,
|
|
3160
|
+
qty: qty.toString(),
|
|
3161
|
+
side,
|
|
3162
|
+
position_intent,
|
|
3163
|
+
type,
|
|
3164
|
+
time_in_force: "day",
|
|
3165
|
+
order_class: "simple",
|
|
3166
|
+
extended_hours: false
|
|
3167
|
+
};
|
|
3168
|
+
if (type === "limit" && limitPrice !== void 0) {
|
|
3169
|
+
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
3170
|
+
}
|
|
3171
|
+
return this.makeRequest("/orders", "POST", orderData, "", {
|
|
3172
|
+
action: "Create option order",
|
|
3173
|
+
symbol
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Create a multi-leg option order
|
|
3178
|
+
* @param legs Array of order legs
|
|
3179
|
+
* @param qty Quantity of the multi-leg order (must be a whole number)
|
|
3180
|
+
* @param type Order type (market or limit)
|
|
3181
|
+
* @param limitPrice Limit price (required for limit orders)
|
|
3182
|
+
* @returns The created multi-leg order
|
|
3183
|
+
*/
|
|
3184
|
+
async createMultiLegOptionOrder(legs, qty, type, limitPrice) {
|
|
3185
|
+
const legSymbols = legs.map((leg) => leg.symbol).join(", ");
|
|
3186
|
+
if (!Number.isInteger(qty) || qty <= 0) {
|
|
3187
|
+
this.log("Quantity must be a positive whole number for option orders", {
|
|
3188
|
+
symbol: legSymbols,
|
|
3189
|
+
type: "error"
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
if (type === "limit" && limitPrice === void 0) {
|
|
3193
|
+
this.log("Limit price is required for limit orders", { symbol: legSymbols, type: "error" });
|
|
3194
|
+
}
|
|
3195
|
+
if (legs.length < 2) {
|
|
3196
|
+
this.log("Multi-leg orders require at least 2 legs", { symbol: legSymbols, type: "error" });
|
|
3197
|
+
}
|
|
3198
|
+
this.log(
|
|
3199
|
+
`Creating multi-leg ${type} option order with ${legs.length} legs (${legSymbols})${type === "limit" ? ` at $${limitPrice?.toFixed(2)}` : ""}`,
|
|
3200
|
+
{
|
|
3201
|
+
symbol: legSymbols
|
|
3202
|
+
}
|
|
3203
|
+
);
|
|
3204
|
+
const orderData = {
|
|
3205
|
+
order_class: "mleg",
|
|
3206
|
+
qty: qty.toString(),
|
|
3207
|
+
type,
|
|
3208
|
+
time_in_force: "day",
|
|
3209
|
+
legs
|
|
3210
|
+
};
|
|
3211
|
+
if (type === "limit" && limitPrice !== void 0) {
|
|
3212
|
+
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
3213
|
+
}
|
|
3214
|
+
return this.makeRequest("/orders", "POST", orderData, "", {
|
|
3215
|
+
action: "Create multi-leg option order",
|
|
3216
|
+
symbol: legSymbols
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
/**
|
|
3220
|
+
* Exercise an option contract
|
|
3221
|
+
* @param symbolOrContractId The symbol or ID of the option contract to exercise
|
|
3222
|
+
* @returns Response from the exercise request
|
|
3223
|
+
*/
|
|
3224
|
+
async exerciseOption(symbolOrContractId) {
|
|
3225
|
+
this.log(`Exercising option contract ${symbolOrContractId}`, {
|
|
3226
|
+
symbol: symbolOrContractId
|
|
3227
|
+
});
|
|
3228
|
+
return this.makeRequest(`/positions/${symbolOrContractId}/exercise`, "POST", void 0, "", {
|
|
3229
|
+
action: "Exercise option contract",
|
|
3230
|
+
symbol: symbolOrContractId
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Get option positions
|
|
3235
|
+
* @returns Array of option positions
|
|
3236
|
+
*/
|
|
3237
|
+
async getOptionPositions() {
|
|
3238
|
+
this.log("Fetching option positions");
|
|
3239
|
+
const positions = await this.getPositions("us_option");
|
|
3240
|
+
return positions;
|
|
3241
|
+
}
|
|
3242
|
+
async getOptionsOpenSpreadTrades() {
|
|
3243
|
+
this.log("Fetching option open trades");
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Get option account activities (exercises, assignments, expirations)
|
|
3247
|
+
* @param activityType Type of option activity to filter by
|
|
3248
|
+
* @param date Date to filter activities (YYYY-MM-DD format)
|
|
3249
|
+
* @returns Array of option account activities
|
|
3250
|
+
*/
|
|
3251
|
+
async getOptionActivities(activityType, date) {
|
|
3252
|
+
const queryParams = new URLSearchParams();
|
|
3253
|
+
if (activityType) {
|
|
3254
|
+
queryParams.append("activity_types", activityType);
|
|
3255
|
+
} else {
|
|
3256
|
+
queryParams.append("activity_types", "OPEXC,OPASN,OPEXP");
|
|
3257
|
+
}
|
|
3258
|
+
if (date) {
|
|
3259
|
+
queryParams.append("date", date);
|
|
3260
|
+
}
|
|
3261
|
+
this.log(
|
|
3262
|
+
`Fetching option activities${activityType ? ` of type ${activityType}` : ""}${date ? ` for date ${date}` : ""}`
|
|
3263
|
+
);
|
|
3264
|
+
return this.makeRequest(`/account/activities?${queryParams.toString()}`, "GET", void 0, "", {
|
|
3265
|
+
action: "Get option activities"
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Create a long call spread (buy lower strike call, sell higher strike call)
|
|
3270
|
+
* @param lowerStrikeCallSymbol Symbol of the lower strike call option
|
|
3271
|
+
* @param higherStrikeCallSymbol Symbol of the higher strike call option
|
|
3272
|
+
* @param qty Quantity of spreads to create (must be a whole number)
|
|
3273
|
+
* @param limitPrice Limit price for the spread
|
|
3274
|
+
* @returns The created multi-leg order
|
|
3275
|
+
*/
|
|
3276
|
+
async createLongCallSpread(lowerStrikeCallSymbol, higherStrikeCallSymbol, qty, limitPrice) {
|
|
3277
|
+
this.log(
|
|
3278
|
+
`Creating long call spread: Buy ${lowerStrikeCallSymbol}, Sell ${higherStrikeCallSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(
|
|
3279
|
+
2
|
|
3280
|
+
)}`,
|
|
3281
|
+
{
|
|
3282
|
+
symbol: `${lowerStrikeCallSymbol},${higherStrikeCallSymbol}`
|
|
3283
|
+
}
|
|
3284
|
+
);
|
|
3285
|
+
const legs = [
|
|
3286
|
+
{
|
|
3287
|
+
symbol: lowerStrikeCallSymbol,
|
|
3288
|
+
ratio_qty: "1",
|
|
3289
|
+
side: "buy",
|
|
3290
|
+
position_intent: "buy_to_open"
|
|
3291
|
+
},
|
|
3292
|
+
{
|
|
3293
|
+
symbol: higherStrikeCallSymbol,
|
|
3294
|
+
ratio_qty: "1",
|
|
3295
|
+
side: "sell",
|
|
3296
|
+
position_intent: "sell_to_open"
|
|
3297
|
+
}
|
|
3298
|
+
];
|
|
3299
|
+
return this.createMultiLegOptionOrder(legs, qty, "limit", limitPrice);
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Create a long put spread (buy higher strike put, sell lower strike put)
|
|
3303
|
+
* @param higherStrikePutSymbol Symbol of the higher strike put option
|
|
3304
|
+
* @param lowerStrikePutSymbol Symbol of the lower strike put option
|
|
3305
|
+
* @param qty Quantity of spreads to create (must be a whole number)
|
|
3306
|
+
* @param limitPrice Limit price for the spread
|
|
3307
|
+
* @returns The created multi-leg order
|
|
3308
|
+
*/
|
|
3309
|
+
async createLongPutSpread(higherStrikePutSymbol, lowerStrikePutSymbol, qty, limitPrice) {
|
|
3310
|
+
this.log(
|
|
3311
|
+
`Creating long put spread: Buy ${higherStrikePutSymbol}, Sell ${lowerStrikePutSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(
|
|
3312
|
+
2
|
|
3313
|
+
)}`,
|
|
3314
|
+
{
|
|
3315
|
+
symbol: `${higherStrikePutSymbol},${lowerStrikePutSymbol}`
|
|
3316
|
+
}
|
|
3317
|
+
);
|
|
3318
|
+
const legs = [
|
|
3319
|
+
{
|
|
3320
|
+
symbol: higherStrikePutSymbol,
|
|
3321
|
+
ratio_qty: "1",
|
|
3322
|
+
side: "buy",
|
|
3323
|
+
position_intent: "buy_to_open"
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
symbol: lowerStrikePutSymbol,
|
|
3327
|
+
ratio_qty: "1",
|
|
3328
|
+
side: "sell",
|
|
3329
|
+
position_intent: "sell_to_open"
|
|
3330
|
+
}
|
|
3331
|
+
];
|
|
3332
|
+
return this.createMultiLegOptionOrder(legs, qty, "limit", limitPrice);
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Create an iron condor (sell call spread and put spread)
|
|
3336
|
+
* @param longPutSymbol Symbol of the lower strike put (long)
|
|
3337
|
+
* @param shortPutSymbol Symbol of the higher strike put (short)
|
|
3338
|
+
* @param shortCallSymbol Symbol of the lower strike call (short)
|
|
3339
|
+
* @param longCallSymbol Symbol of the higher strike call (long)
|
|
3340
|
+
* @param qty Quantity of iron condors to create (must be a whole number)
|
|
3341
|
+
* @param limitPrice Limit price for the iron condor (credit)
|
|
3342
|
+
* @returns The created multi-leg order
|
|
3343
|
+
*/
|
|
3344
|
+
async createIronCondor(longPutSymbol, shortPutSymbol, shortCallSymbol, longCallSymbol, qty, limitPrice) {
|
|
3345
|
+
this.log(`Creating iron condor with ${qty} contracts at $${limitPrice.toFixed(2)}`, {
|
|
3346
|
+
symbol: `${longPutSymbol},${shortPutSymbol},${shortCallSymbol},${longCallSymbol}`
|
|
3347
|
+
});
|
|
3348
|
+
const legs = [
|
|
3349
|
+
{
|
|
3350
|
+
symbol: longPutSymbol,
|
|
3351
|
+
ratio_qty: "1",
|
|
3352
|
+
side: "buy",
|
|
3353
|
+
position_intent: "buy_to_open"
|
|
3354
|
+
},
|
|
3355
|
+
{
|
|
3356
|
+
symbol: shortPutSymbol,
|
|
3357
|
+
ratio_qty: "1",
|
|
3358
|
+
side: "sell",
|
|
3359
|
+
position_intent: "sell_to_open"
|
|
3360
|
+
},
|
|
3361
|
+
{
|
|
3362
|
+
symbol: shortCallSymbol,
|
|
3363
|
+
ratio_qty: "1",
|
|
3364
|
+
side: "sell",
|
|
3365
|
+
position_intent: "sell_to_open"
|
|
3366
|
+
},
|
|
3367
|
+
{
|
|
3368
|
+
symbol: longCallSymbol,
|
|
3369
|
+
ratio_qty: "1",
|
|
3370
|
+
side: "buy",
|
|
3371
|
+
position_intent: "buy_to_open"
|
|
3372
|
+
}
|
|
3373
|
+
];
|
|
3374
|
+
return this.createMultiLegOptionOrder(legs, qty, "limit", limitPrice);
|
|
3375
|
+
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Create a covered call (sell call option against owned stock)
|
|
3378
|
+
* @param stockSymbol Symbol of the underlying stock
|
|
3379
|
+
* @param callOptionSymbol Symbol of the call option to sell
|
|
3380
|
+
* @param qty Quantity of covered calls to create (must be a whole number)
|
|
3381
|
+
* @param limitPrice Limit price for the call option
|
|
3382
|
+
* @returns The created order
|
|
3383
|
+
*/
|
|
3384
|
+
async createCoveredCall(stockSymbol, callOptionSymbol, qty, limitPrice) {
|
|
3385
|
+
this.log(
|
|
3386
|
+
`Creating covered call: Sell ${callOptionSymbol} against ${stockSymbol}, Qty: ${qty}, Price: $${limitPrice.toFixed(
|
|
3387
|
+
2
|
|
3388
|
+
)}`,
|
|
3389
|
+
{
|
|
3390
|
+
symbol: `${stockSymbol},${callOptionSymbol}`
|
|
3391
|
+
}
|
|
3392
|
+
);
|
|
3393
|
+
return this.createOptionOrder(callOptionSymbol, qty, "sell", "sell_to_open", "limit", limitPrice);
|
|
3394
|
+
}
|
|
3395
|
+
/**
|
|
3396
|
+
* Roll an option position to a new expiration or strike
|
|
3397
|
+
* @param currentOptionSymbol Symbol of the current option position
|
|
3398
|
+
* @param newOptionSymbol Symbol of the new option to roll to
|
|
3399
|
+
* @param qty Quantity of options to roll (must be a whole number)
|
|
3400
|
+
* @param currentPositionSide Side of the current position ('buy' or 'sell')
|
|
3401
|
+
* @param limitPrice Net limit price for the roll
|
|
3402
|
+
* @returns The created multi-leg order
|
|
3403
|
+
*/
|
|
3404
|
+
async rollOptionPosition(currentOptionSymbol, newOptionSymbol, qty, currentPositionSide, limitPrice) {
|
|
3405
|
+
this.log(`Rolling ${qty} ${currentOptionSymbol} to ${newOptionSymbol} at net price $${limitPrice.toFixed(2)}`, {
|
|
3406
|
+
symbol: `${currentOptionSymbol},${newOptionSymbol}`
|
|
3407
|
+
});
|
|
3408
|
+
const closePositionSide = currentPositionSide === "buy" ? "sell" : "buy";
|
|
3409
|
+
const openPositionSide = currentPositionSide;
|
|
3410
|
+
const closePositionIntent = closePositionSide === "buy" ? "buy_to_close" : "sell_to_close";
|
|
3411
|
+
const openPositionIntent = openPositionSide === "buy" ? "buy_to_open" : "sell_to_open";
|
|
3412
|
+
const legs = [
|
|
3413
|
+
{
|
|
3414
|
+
symbol: currentOptionSymbol,
|
|
3415
|
+
ratio_qty: "1",
|
|
3416
|
+
side: closePositionSide,
|
|
3417
|
+
position_intent: closePositionIntent
|
|
3418
|
+
},
|
|
3419
|
+
{
|
|
3420
|
+
symbol: newOptionSymbol,
|
|
3421
|
+
ratio_qty: "1",
|
|
3422
|
+
side: openPositionSide,
|
|
3423
|
+
position_intent: openPositionIntent
|
|
3424
|
+
}
|
|
3425
|
+
];
|
|
3426
|
+
return this.createMultiLegOptionOrder(legs, qty, "limit", limitPrice);
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* Get option chain for a specific underlying symbol and expiration date
|
|
3430
|
+
* @param underlyingSymbol The underlying stock symbol
|
|
3431
|
+
* @param expirationDate The expiration date (YYYY-MM-DD format)
|
|
3432
|
+
* @returns Option contracts for the specified symbol and expiration date
|
|
3433
|
+
*/
|
|
3434
|
+
async getOptionChain(underlyingSymbol, expirationDate) {
|
|
3435
|
+
this.log(`Fetching option chain for ${underlyingSymbol} with expiration date ${expirationDate}`, {
|
|
3436
|
+
symbol: underlyingSymbol
|
|
3437
|
+
});
|
|
3438
|
+
try {
|
|
3439
|
+
const params = {
|
|
3440
|
+
underlying_symbols: [underlyingSymbol],
|
|
3441
|
+
expiration_date_gte: expirationDate,
|
|
3442
|
+
expiration_date_lte: expirationDate,
|
|
3443
|
+
status: "active",
|
|
3444
|
+
limit: 500
|
|
3445
|
+
// Get a large number to ensure we get all strikes
|
|
3446
|
+
};
|
|
3447
|
+
const response = await this.getOptionContracts(params);
|
|
3448
|
+
return response.option_contracts || [];
|
|
3449
|
+
} catch (error) {
|
|
3450
|
+
if (!(error instanceof AlpacaRequestError)) {
|
|
3451
|
+
this.log(
|
|
3452
|
+
`Failed to fetch option chain for ${underlyingSymbol}: ${error instanceof Error ? error.message : `${error}`}`,
|
|
3453
|
+
{
|
|
3454
|
+
type: "error",
|
|
3455
|
+
symbol: underlyingSymbol
|
|
3456
|
+
}
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
return [];
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Get all available expiration dates for a specific underlying symbol
|
|
3464
|
+
* @param underlyingSymbol The underlying stock symbol
|
|
3465
|
+
* @returns Array of available expiration dates
|
|
3466
|
+
*/
|
|
3467
|
+
async getOptionExpirationDates(underlyingSymbol) {
|
|
3468
|
+
this.log(`Fetching available expiration dates for ${underlyingSymbol}`, {
|
|
3469
|
+
symbol: underlyingSymbol
|
|
3470
|
+
});
|
|
3471
|
+
try {
|
|
3472
|
+
const params = {
|
|
3473
|
+
underlying_symbols: [underlyingSymbol],
|
|
3474
|
+
status: "active",
|
|
3475
|
+
limit: 1e3
|
|
3476
|
+
// Get a large number to ensure we get contracts with all expiration dates
|
|
3477
|
+
};
|
|
3478
|
+
const response = await this.getOptionContracts(params);
|
|
3479
|
+
const expirationDates = /* @__PURE__ */ new Set();
|
|
3480
|
+
if (response.option_contracts) {
|
|
3481
|
+
response.option_contracts.forEach((contract) => {
|
|
3482
|
+
expirationDates.add(contract.expiration_date);
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
return Array.from(expirationDates).sort();
|
|
3486
|
+
} catch (error) {
|
|
3487
|
+
if (!(error instanceof AlpacaRequestError)) {
|
|
3488
|
+
this.log(
|
|
3489
|
+
`Failed to fetch expiration dates for ${underlyingSymbol}: ${error instanceof Error ? error.message : `${error}`}`,
|
|
3490
|
+
{
|
|
3491
|
+
type: "error",
|
|
3492
|
+
symbol: underlyingSymbol
|
|
3493
|
+
}
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
return [];
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
/**
|
|
3500
|
+
* Get the current options trading level for the account
|
|
3501
|
+
* @returns The options trading level (0-3)
|
|
3502
|
+
*/
|
|
3503
|
+
async getOptionsTradingLevel() {
|
|
3504
|
+
this.log("Fetching options trading level");
|
|
3505
|
+
const accountDetails = await this.getAccountDetails();
|
|
3506
|
+
return accountDetails.options_trading_level || 0;
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Check if the account has options trading enabled
|
|
3510
|
+
* @returns Boolean indicating if options trading is enabled
|
|
3511
|
+
*/
|
|
3512
|
+
async isOptionsEnabled() {
|
|
3513
|
+
this.log("Checking if options trading is enabled");
|
|
3514
|
+
const accountDetails = await this.getAccountDetails();
|
|
3515
|
+
const optionsLevel = accountDetails.options_trading_level || 0;
|
|
3516
|
+
const isEnabled = optionsLevel >= 2;
|
|
3517
|
+
this.log(`Options trading level: ${optionsLevel}, enabled: ${isEnabled}`);
|
|
3518
|
+
return isEnabled;
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Close all option positions
|
|
3522
|
+
* @param cancelOrders Whether to cancel related orders (default: true)
|
|
3523
|
+
* @returns Response from the close positions request
|
|
3524
|
+
*/
|
|
3525
|
+
async closeAllOptionPositions(cancelOrders = true) {
|
|
3526
|
+
this.log(`Closing all option positions${cancelOrders ? " and canceling related orders" : ""}`);
|
|
3527
|
+
const optionPositions = await this.getOptionPositions();
|
|
3528
|
+
if (optionPositions.length === 0) {
|
|
3529
|
+
this.log("No option positions to close");
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3532
|
+
for (const position of optionPositions) {
|
|
3533
|
+
const side = position.side === "long" ? "sell" : "buy";
|
|
3534
|
+
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
3535
|
+
this.log(`Closing ${position.side} position of ${position.qty} contracts for ${position.symbol}`, {
|
|
3536
|
+
symbol: position.symbol
|
|
3537
|
+
});
|
|
3538
|
+
await this.createOptionOrder(position.symbol, parseInt(position.qty), side, positionIntent, "market");
|
|
3539
|
+
}
|
|
3540
|
+
if (cancelOrders) {
|
|
3541
|
+
const orders = await this.getOrders({ status: "open" });
|
|
3542
|
+
const optionOrders = orders.filter((order) => order.asset_class === "us_option");
|
|
3543
|
+
for (const order of optionOrders) {
|
|
3544
|
+
this.log(`Canceling open order for ${order.symbol}`, {
|
|
3545
|
+
symbol: order.symbol
|
|
3546
|
+
});
|
|
3547
|
+
await this.makeRequest(`/orders/${order.id}`, "DELETE", void 0, "", {
|
|
3548
|
+
action: `Cancel option order ${order.id}`,
|
|
3549
|
+
symbol: order.symbol
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Close a specific option position
|
|
3556
|
+
* @param symbol The option contract symbol
|
|
3557
|
+
* @param qty Optional quantity to close (defaults to entire position)
|
|
3558
|
+
* @returns The created order
|
|
3559
|
+
*/
|
|
3560
|
+
async closeOptionPosition(symbol, qty) {
|
|
3561
|
+
this.log(`Closing option position for ${symbol}${qty ? ` (${qty} contracts)` : ""}`, {
|
|
3562
|
+
symbol
|
|
3563
|
+
});
|
|
3564
|
+
const positions = await this.getOptionPositions();
|
|
3565
|
+
const position = positions.find((p) => p.symbol === symbol);
|
|
3566
|
+
if (!position) {
|
|
3567
|
+
throw new Error(`No position found for option contract ${symbol}`);
|
|
3568
|
+
}
|
|
3569
|
+
const quantityToClose = qty || parseInt(position.qty);
|
|
3570
|
+
const side = position.side === "long" ? "sell" : "buy";
|
|
3571
|
+
const positionIntent = side === "sell" ? "sell_to_close" : "buy_to_close";
|
|
3572
|
+
return this.createOptionOrder(symbol, quantityToClose, side, positionIntent, "market");
|
|
3573
|
+
}
|
|
3574
|
+
/**
|
|
3575
|
+
* Create a complete equities trade with optional stop loss and take profit
|
|
3576
|
+
* @param params Trade parameters including symbol, qty, side, and optional referencePrice
|
|
3577
|
+
* @param options Trade options including order type, extended hours, stop loss, and take profit settings
|
|
3578
|
+
* @returns The created order
|
|
3579
|
+
*/
|
|
3580
|
+
async createEquitiesTrade(params, options) {
|
|
3581
|
+
const { symbol, qty, side, referencePrice } = params;
|
|
3582
|
+
const {
|
|
3583
|
+
type = "market",
|
|
3584
|
+
limitPrice,
|
|
3585
|
+
extendedHours = false,
|
|
3586
|
+
useStopLoss = false,
|
|
3587
|
+
stopPrice,
|
|
3588
|
+
stopPercent100,
|
|
3589
|
+
useTakeProfit = false,
|
|
3590
|
+
takeProfitPrice,
|
|
3591
|
+
takeProfitPercent100,
|
|
3592
|
+
clientOrderId
|
|
3593
|
+
} = options || {};
|
|
3594
|
+
if (extendedHours && type === "market") {
|
|
3595
|
+
this.log("Cannot create market order with extended hours enabled", {
|
|
3596
|
+
symbol,
|
|
3597
|
+
type: "error"
|
|
3598
|
+
});
|
|
3599
|
+
throw new Error("Cannot create market order with extended hours enabled");
|
|
3600
|
+
}
|
|
3601
|
+
if (type === "limit" && limitPrice === void 0) {
|
|
3602
|
+
this.log("Limit price is required for limit orders", {
|
|
3603
|
+
symbol,
|
|
3604
|
+
type: "error"
|
|
3605
|
+
});
|
|
3606
|
+
throw new Error("Limit price is required for limit orders");
|
|
3607
|
+
}
|
|
3608
|
+
let calculatedStopPrice;
|
|
3609
|
+
let calculatedTakeProfitPrice;
|
|
3610
|
+
if (useStopLoss) {
|
|
3611
|
+
if (stopPrice === void 0 && stopPercent100 === void 0) {
|
|
3612
|
+
this.log("Either stopPrice or stopPercent100 must be provided when useStopLoss is true", {
|
|
3613
|
+
symbol,
|
|
3614
|
+
type: "error"
|
|
3615
|
+
});
|
|
3616
|
+
throw new Error("Either stopPrice or stopPercent100 must be provided when useStopLoss is true");
|
|
3617
|
+
}
|
|
3618
|
+
if (stopPercent100 !== void 0) {
|
|
3619
|
+
if (referencePrice === void 0) {
|
|
3620
|
+
this.log("referencePrice is required when using stopPercent100", {
|
|
3621
|
+
symbol,
|
|
3622
|
+
type: "error"
|
|
3623
|
+
});
|
|
3624
|
+
throw new Error("referencePrice is required when using stopPercent100");
|
|
3625
|
+
}
|
|
3626
|
+
const stopPercentDecimal = stopPercent100 / 100;
|
|
3627
|
+
if (side === "buy") {
|
|
3628
|
+
calculatedStopPrice = referencePrice * (1 - stopPercentDecimal);
|
|
3629
|
+
} else {
|
|
3630
|
+
calculatedStopPrice = referencePrice * (1 + stopPercentDecimal);
|
|
3631
|
+
}
|
|
3632
|
+
} else {
|
|
3633
|
+
calculatedStopPrice = stopPrice;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
if (useTakeProfit) {
|
|
3637
|
+
if (takeProfitPrice === void 0 && takeProfitPercent100 === void 0) {
|
|
3638
|
+
this.log("Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true", {
|
|
3639
|
+
symbol,
|
|
3640
|
+
type: "error"
|
|
3641
|
+
});
|
|
3642
|
+
throw new Error("Either takeProfitPrice or takeProfitPercent100 must be provided when useTakeProfit is true");
|
|
3643
|
+
}
|
|
3644
|
+
if (takeProfitPercent100 !== void 0) {
|
|
3645
|
+
if (referencePrice === void 0) {
|
|
3646
|
+
this.log("referencePrice is required when using takeProfitPercent100", {
|
|
3647
|
+
symbol,
|
|
3648
|
+
type: "error"
|
|
3649
|
+
});
|
|
3650
|
+
throw new Error("referencePrice is required when using takeProfitPercent100");
|
|
3651
|
+
}
|
|
3652
|
+
const takeProfitPercentDecimal = takeProfitPercent100 / 100;
|
|
3653
|
+
if (side === "buy") {
|
|
3654
|
+
calculatedTakeProfitPrice = referencePrice * (1 + takeProfitPercentDecimal);
|
|
3655
|
+
} else {
|
|
3656
|
+
calculatedTakeProfitPrice = referencePrice * (1 - takeProfitPercentDecimal);
|
|
3657
|
+
}
|
|
3658
|
+
} else {
|
|
3659
|
+
calculatedTakeProfitPrice = takeProfitPrice;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
let orderClass = "simple";
|
|
3663
|
+
if (useStopLoss && useTakeProfit) {
|
|
3664
|
+
orderClass = "bracket";
|
|
3665
|
+
} else if (useStopLoss || useTakeProfit) {
|
|
3666
|
+
orderClass = "oto";
|
|
3667
|
+
}
|
|
3668
|
+
const orderData = {
|
|
3669
|
+
symbol,
|
|
3670
|
+
qty: Math.abs(qty).toString(),
|
|
3671
|
+
side,
|
|
3672
|
+
type,
|
|
3673
|
+
time_in_force: "day",
|
|
3674
|
+
order_class: orderClass,
|
|
3675
|
+
extended_hours: extendedHours,
|
|
3676
|
+
position_intent: side === "buy" ? "buy_to_open" : "sell_to_open"
|
|
3677
|
+
};
|
|
3678
|
+
if (clientOrderId) {
|
|
3679
|
+
orderData.client_order_id = clientOrderId;
|
|
3680
|
+
}
|
|
3681
|
+
if (type === "limit" && limitPrice !== void 0) {
|
|
3682
|
+
orderData.limit_price = this.roundPriceForAlpaca(limitPrice).toString();
|
|
3683
|
+
}
|
|
3684
|
+
if (useStopLoss && calculatedStopPrice !== void 0) {
|
|
3685
|
+
orderData.stop_loss = {
|
|
3686
|
+
stop_price: this.roundPriceForAlpaca(calculatedStopPrice).toString()
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
if (useTakeProfit && calculatedTakeProfitPrice !== void 0) {
|
|
3690
|
+
orderData.take_profit = {
|
|
3691
|
+
limit_price: this.roundPriceForAlpaca(calculatedTakeProfitPrice).toString()
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
3694
|
+
const logMessage = `Creating ${orderClass} ${type} ${side} order for ${symbol}: ${qty} shares${type === "limit" ? ` at $${limitPrice?.toFixed(2)}` : ""}${useStopLoss ? ` with stop loss at $${calculatedStopPrice?.toFixed(2)}` : ""}${useTakeProfit ? ` with take profit at $${calculatedTakeProfitPrice?.toFixed(2)}` : ""}${extendedHours ? " (extended hours)" : ""}`;
|
|
3695
|
+
this.log(logMessage, {
|
|
3696
|
+
symbol
|
|
3697
|
+
});
|
|
3698
|
+
return this.makeRequest("/orders", "POST", orderData, "", {
|
|
3699
|
+
action: "Create equities trade",
|
|
3700
|
+
symbol
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
});
|
|
3706
|
+
|
|
3707
|
+
// src/index.ts
|
|
3708
|
+
init_market_time();
|
|
3709
|
+
|
|
3710
|
+
// src/format-tools.ts
|
|
3711
|
+
function capFirstLetter(str) {
|
|
3712
|
+
if (!str || typeof str !== "string") return str;
|
|
3713
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3714
|
+
}
|
|
3715
|
+
function formatCurrency(value) {
|
|
3716
|
+
if (isNaN(value)) {
|
|
3717
|
+
return "$0.00";
|
|
3718
|
+
}
|
|
3719
|
+
return new Intl.NumberFormat("en-US", {
|
|
3720
|
+
style: "currency",
|
|
3721
|
+
currency: "USD"
|
|
3722
|
+
}).format(value);
|
|
3723
|
+
}
|
|
3724
|
+
function formatNumber(value) {
|
|
3725
|
+
if (isNaN(value)) {
|
|
3726
|
+
return "0";
|
|
3727
|
+
}
|
|
3728
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
3729
|
+
}
|
|
3730
|
+
function formatPercentage(value, decimalPlaces = 2) {
|
|
3731
|
+
if (isNaN(value)) {
|
|
3732
|
+
return "0%";
|
|
3733
|
+
}
|
|
3734
|
+
return new Intl.NumberFormat("en-US", {
|
|
3735
|
+
style: "percent",
|
|
3736
|
+
minimumFractionDigits: decimalPlaces
|
|
3737
|
+
}).format(value);
|
|
3738
|
+
}
|
|
3739
|
+
function dateTimeForGS(date) {
|
|
3740
|
+
return date.toLocaleString("en-AU", {
|
|
3741
|
+
day: "2-digit",
|
|
3742
|
+
month: "2-digit",
|
|
3743
|
+
year: "numeric",
|
|
3744
|
+
hour: "2-digit",
|
|
3745
|
+
minute: "2-digit",
|
|
3746
|
+
second: "2-digit",
|
|
3747
|
+
hour12: false
|
|
3748
|
+
}).replace(/\./g, "/");
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
// src/types/index.ts
|
|
3752
|
+
var types_exports = {};
|
|
3753
|
+
__export(types_exports, {
|
|
3754
|
+
OPENAI_IMAGE_MODELS: () => OPENAI_IMAGE_MODELS,
|
|
3755
|
+
OPENAI_MODELS: () => OPENAI_MODELS,
|
|
3756
|
+
OPENROUTER_MODELS: () => OPENROUTER_MODELS,
|
|
3757
|
+
isOpenAIImageModel: () => isOpenAIImageModel,
|
|
3758
|
+
isOpenAIModel: () => isOpenAIModel,
|
|
3759
|
+
isOpenRouterModel: () => isOpenRouterModel
|
|
3760
|
+
});
|
|
3761
|
+
|
|
3762
|
+
// src/types/llm-types.ts
|
|
3763
|
+
var OPENAI_MODELS = [
|
|
3764
|
+
"gpt-5.6",
|
|
3765
|
+
"gpt-5.6-sol",
|
|
3766
|
+
"gpt-5.6-terra",
|
|
3767
|
+
"gpt-5.6-luna",
|
|
3768
|
+
"gpt-5",
|
|
3769
|
+
"gpt-5-mini",
|
|
3770
|
+
"gpt-5.4-mini",
|
|
3771
|
+
"gpt-5.4-nano",
|
|
3772
|
+
"gpt-5-nano",
|
|
3773
|
+
"gpt-5.1",
|
|
3774
|
+
"gpt-5.4",
|
|
3775
|
+
"gpt-5.5",
|
|
3776
|
+
"gpt-5.5-pro",
|
|
3777
|
+
"gpt-5.2",
|
|
3778
|
+
"gpt-5.2-pro",
|
|
3779
|
+
"gpt-5.1-codex",
|
|
3780
|
+
"gpt-5.1-codex-max"
|
|
3781
|
+
];
|
|
3782
|
+
function isOpenAIModel(model) {
|
|
3783
|
+
return OPENAI_MODELS.includes(model);
|
|
3784
|
+
}
|
|
3785
|
+
var OPENAI_IMAGE_MODELS = [
|
|
3786
|
+
"gpt-image-2",
|
|
3787
|
+
"gpt-image-2-2026-04-21",
|
|
3788
|
+
"gpt-image-1.5",
|
|
3789
|
+
"gpt-image-1",
|
|
3790
|
+
"gpt-image-1-mini",
|
|
3791
|
+
"chatgpt-image-latest",
|
|
3792
|
+
"dall-e-2",
|
|
3793
|
+
"dall-e-3"
|
|
3794
|
+
];
|
|
3795
|
+
function isOpenAIImageModel(model) {
|
|
3796
|
+
return OPENAI_IMAGE_MODELS.includes(model);
|
|
3797
|
+
}
|
|
3798
|
+
var OPENROUTER_MODELS = [
|
|
3799
|
+
"openai/gpt-5",
|
|
3800
|
+
"openai/gpt-5-mini",
|
|
3801
|
+
"openai/gpt-5.4-mini",
|
|
3802
|
+
"openai/gpt-5.4-nano",
|
|
3803
|
+
"openai/gpt-5-nano",
|
|
3804
|
+
"openai/gpt-5.1",
|
|
3805
|
+
"openai/gpt-5.4",
|
|
3806
|
+
"openai/gpt-5.4-pro",
|
|
3807
|
+
"openai/gpt-5.5",
|
|
3808
|
+
"openai/gpt-5.5-pro",
|
|
3809
|
+
"openai/gpt-5.2",
|
|
3810
|
+
"openai/gpt-5.2-pro",
|
|
3811
|
+
"openai/gpt-5.1-codex",
|
|
3812
|
+
"openai/gpt-5.1-codex-max",
|
|
3813
|
+
"openai/gpt-oss-120b",
|
|
3814
|
+
"z.ai/glm-4.5",
|
|
3815
|
+
"z.ai/glm-4.5-air",
|
|
3816
|
+
"google/gemini-2.5-flash",
|
|
3817
|
+
"google/gemini-2.5-flash-lite",
|
|
3818
|
+
"deepseek/deepseek-r1-0528",
|
|
3819
|
+
"deepseek/deepseek-chat-v3-0324"
|
|
3820
|
+
];
|
|
3821
|
+
function isOpenRouterModel(model) {
|
|
3822
|
+
return OPENROUTER_MODELS.includes(model);
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
// src/misc-utils.ts
|
|
3826
|
+
var logIfDebug = (message, data, type = "info") => {
|
|
3827
|
+
const prefix = `[DEBUG][${type.toUpperCase()}]`;
|
|
3828
|
+
const formattedData = data !== void 0 ? JSON.stringify(data, null, 2) : "";
|
|
3829
|
+
switch (type) {
|
|
3830
|
+
case "error":
|
|
3831
|
+
console.error(prefix, message, formattedData);
|
|
3832
|
+
break;
|
|
3833
|
+
case "warn":
|
|
3834
|
+
console.warn(prefix, message, formattedData);
|
|
3835
|
+
break;
|
|
3836
|
+
case "debug":
|
|
3837
|
+
console.debug(prefix, message, formattedData);
|
|
3838
|
+
break;
|
|
3839
|
+
case "trace":
|
|
3840
|
+
console.trace(prefix, message, formattedData);
|
|
3841
|
+
break;
|
|
3842
|
+
case "info":
|
|
3843
|
+
default:
|
|
3844
|
+
console.info(prefix, message, formattedData);
|
|
3845
|
+
}
|
|
3846
|
+
};
|
|
3847
|
+
function maskApiKey(keyValue) {
|
|
3848
|
+
if (keyValue.length <= 4) {
|
|
3849
|
+
return keyValue;
|
|
3850
|
+
}
|
|
3851
|
+
const firstTwo = keyValue.slice(0, 2);
|
|
3852
|
+
const lastTwo = keyValue.slice(-2);
|
|
3853
|
+
return `${firstTwo}****${lastTwo}`;
|
|
3854
|
+
}
|
|
3855
|
+
function hideApiKeyFromurl(url) {
|
|
3856
|
+
try {
|
|
3857
|
+
const parsedUrl = new URL(url);
|
|
3858
|
+
for (const [key, value] of parsedUrl.searchParams.entries()) {
|
|
3859
|
+
if (key.toLowerCase() === "apikey") {
|
|
3860
|
+
const masked = maskApiKey(value);
|
|
3861
|
+
parsedUrl.searchParams.set(key, masked);
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
return parsedUrl.toString();
|
|
3865
|
+
} catch {
|
|
3866
|
+
return url;
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
function extractErrorDetails(error, response) {
|
|
3870
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
3871
|
+
const errName = error instanceof Error ? error.name : "Error";
|
|
3872
|
+
if (errName === "TypeError" && errMsg.includes("fetch")) {
|
|
3873
|
+
return { type: "NETWORK_ERROR", reason: "Network connectivity issue", status: null };
|
|
3874
|
+
}
|
|
3875
|
+
if (errMsg.includes("HTTP error: 429")) {
|
|
3876
|
+
const match = errMsg.match(/RATE_LIMIT: 429:(\d+)/);
|
|
3877
|
+
const retryAfter = match ? parseInt(match[1]) : void 0;
|
|
3878
|
+
return { type: "RATE_LIMIT", reason: "Rate limit exceeded", status: 429, retryAfter };
|
|
3879
|
+
}
|
|
3880
|
+
if (errMsg.includes("HTTP error: 401") || errMsg.includes("AUTH_ERROR: 401")) {
|
|
3881
|
+
return { type: "AUTH_ERROR", reason: "Authentication failed - invalid API key", status: 401 };
|
|
3882
|
+
}
|
|
3883
|
+
if (errMsg.includes("HTTP error: 403") || errMsg.includes("AUTH_ERROR: 403")) {
|
|
3884
|
+
return { type: "AUTH_ERROR", reason: "Access forbidden - insufficient permissions", status: 403 };
|
|
3885
|
+
}
|
|
3886
|
+
if (errMsg.includes("SERVER_ERROR:")) {
|
|
3887
|
+
const status = parseInt(errMsg.split("SERVER_ERROR: ")[1]) || 500;
|
|
3888
|
+
return { type: "SERVER_ERROR", reason: `Server error (${status})`, status };
|
|
3889
|
+
}
|
|
3890
|
+
if (errMsg.includes("CLIENT_ERROR:")) {
|
|
3891
|
+
const status = parseInt(errMsg.split("CLIENT_ERROR: ")[1]) || 400;
|
|
3892
|
+
return { type: "CLIENT_ERROR", reason: `Client error (${status})`, status };
|
|
3893
|
+
}
|
|
3894
|
+
return { type: "UNKNOWN", reason: errMsg || "Unknown error", status: null };
|
|
3895
|
+
}
|
|
3896
|
+
async function fetchWithRetry(url, options = {}, retries = 3, initialBackoff = 1e3) {
|
|
3897
|
+
let backoff = initialBackoff;
|
|
3898
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
3899
|
+
try {
|
|
3900
|
+
const response = await fetch(url, options);
|
|
3901
|
+
if (!response.ok) {
|
|
3902
|
+
if (response.status === 429) {
|
|
3903
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
3904
|
+
const retryDelay = retryAfter ? parseInt(retryAfter) * 1e3 : null;
|
|
3905
|
+
throw new Error(`RATE_LIMIT: ${response.status}${retryDelay ? `:${retryDelay}` : ""}`);
|
|
3906
|
+
}
|
|
3907
|
+
if ([500, 502, 503, 504].includes(response.status)) {
|
|
3908
|
+
throw new Error(`SERVER_ERROR: ${response.status}`);
|
|
3909
|
+
}
|
|
3910
|
+
if ([401, 403].includes(response.status)) {
|
|
3911
|
+
throw new Error(`AUTH_ERROR: ${response.status}`);
|
|
3912
|
+
}
|
|
3913
|
+
if (response.status >= 400 && response.status < 500) {
|
|
3914
|
+
throw new Error(`CLIENT_ERROR: ${response.status}`);
|
|
3915
|
+
}
|
|
3916
|
+
throw new Error(`HTTP_ERROR: ${response.status}`);
|
|
3917
|
+
}
|
|
3918
|
+
return response;
|
|
3919
|
+
} catch (error) {
|
|
3920
|
+
if (attempt === retries) {
|
|
3921
|
+
throw error;
|
|
3922
|
+
}
|
|
3923
|
+
const errorDetails = extractErrorDetails(error);
|
|
3924
|
+
let adaptiveBackoff = backoff;
|
|
3925
|
+
if (errorDetails.type === "RATE_LIMIT") {
|
|
3926
|
+
if (errorDetails.retryAfter) {
|
|
3927
|
+
adaptiveBackoff = errorDetails.retryAfter;
|
|
3928
|
+
} else {
|
|
3929
|
+
adaptiveBackoff = Math.max(backoff, 5e3);
|
|
3930
|
+
}
|
|
3931
|
+
} else if (errorDetails.type === "AUTH_ERROR") {
|
|
3932
|
+
console.error(`Authentication error for ${hideApiKeyFromurl(url)}: ${errorDetails.reason}`, {
|
|
3933
|
+
attemptNumber: attempt,
|
|
3934
|
+
errorType: errorDetails.type,
|
|
3935
|
+
httpStatus: errorDetails.status,
|
|
3936
|
+
url: hideApiKeyFromurl(url),
|
|
3937
|
+
source: "fetchWithRetry",
|
|
3938
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3939
|
+
});
|
|
3940
|
+
throw error;
|
|
3941
|
+
} else if (errorDetails.type === "CLIENT_ERROR") {
|
|
3942
|
+
console.error(`Client error for ${hideApiKeyFromurl(url)}: ${errorDetails.reason}`, {
|
|
3943
|
+
attemptNumber: attempt,
|
|
3944
|
+
errorType: errorDetails.type,
|
|
3945
|
+
httpStatus: errorDetails.status,
|
|
3946
|
+
url: hideApiKeyFromurl(url),
|
|
3947
|
+
source: "fetchWithRetry",
|
|
3948
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3949
|
+
});
|
|
3950
|
+
throw error;
|
|
3951
|
+
}
|
|
3952
|
+
console.warn(
|
|
3953
|
+
`Fetch attempt ${attempt} of ${retries} for ${hideApiKeyFromurl(url)} failed: ${errorDetails.reason}. Retrying in ${adaptiveBackoff}ms...`,
|
|
3954
|
+
{
|
|
3955
|
+
attemptNumber: attempt,
|
|
3956
|
+
totalRetries: retries,
|
|
3957
|
+
errorType: errorDetails.type,
|
|
3958
|
+
httpStatus: errorDetails.status,
|
|
3959
|
+
retryDelay: adaptiveBackoff,
|
|
3960
|
+
url: hideApiKeyFromurl(url),
|
|
3961
|
+
source: "fetchWithRetry",
|
|
3962
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3963
|
+
}
|
|
3964
|
+
);
|
|
3965
|
+
await new Promise((resolve) => setTimeout(resolve, adaptiveBackoff));
|
|
3966
|
+
backoff = Math.min(backoff * 2, 3e4);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
throw new Error("Failed to fetch after multiple attempts");
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
// src/llm-openai.ts
|
|
3973
|
+
import OpenAI2 from "openai";
|
|
3974
|
+
import { zodTextFormat } from "openai/helpers/zod";
|
|
3975
|
+
|
|
3976
|
+
// src/llm-config.ts
|
|
3977
|
+
var DEFAULT_MODEL = "gpt-5.6-luna";
|
|
3978
|
+
var GPT_5_HIGH_CONTEXT_THRESHOLD_TOKENS = 272e3;
|
|
3979
|
+
var GPT_5_HIGH_CONTEXT_INPUT_MULTIPLIER = 2;
|
|
3980
|
+
var GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER = 1.5;
|
|
3981
|
+
var openAiModelCosts = {
|
|
3982
|
+
"gpt-5.6": {
|
|
3983
|
+
inputCost: 5 / 1e6,
|
|
3984
|
+
cacheHitCost: 0.5 / 1e6,
|
|
3985
|
+
cacheWriteCost: 6.25 / 1e6,
|
|
3986
|
+
outputCost: 30 / 1e6
|
|
3987
|
+
},
|
|
3988
|
+
"gpt-5.6-sol": {
|
|
3989
|
+
inputCost: 5 / 1e6,
|
|
3990
|
+
cacheHitCost: 0.5 / 1e6,
|
|
3991
|
+
cacheWriteCost: 6.25 / 1e6,
|
|
3992
|
+
outputCost: 30 / 1e6
|
|
3993
|
+
},
|
|
3994
|
+
"gpt-5.6-terra": {
|
|
3995
|
+
inputCost: 2.5 / 1e6,
|
|
3996
|
+
cacheHitCost: 0.25 / 1e6,
|
|
3997
|
+
cacheWriteCost: 3.125 / 1e6,
|
|
3998
|
+
outputCost: 15 / 1e6
|
|
3999
|
+
},
|
|
4000
|
+
"gpt-5.6-luna": {
|
|
4001
|
+
inputCost: 1 / 1e6,
|
|
4002
|
+
cacheHitCost: 0.1 / 1e6,
|
|
4003
|
+
cacheWriteCost: 1.25 / 1e6,
|
|
4004
|
+
outputCost: 6 / 1e6
|
|
4005
|
+
},
|
|
4006
|
+
"gpt-5": {
|
|
4007
|
+
inputCost: 1.25 / 1e6,
|
|
4008
|
+
cacheHitCost: 0.125 / 1e6,
|
|
4009
|
+
outputCost: 10 / 1e6
|
|
4010
|
+
},
|
|
4011
|
+
"gpt-5-mini": {
|
|
4012
|
+
inputCost: 0.25 / 1e6,
|
|
4013
|
+
cacheHitCost: 0.025 / 1e6,
|
|
4014
|
+
outputCost: 2 / 1e6
|
|
4015
|
+
},
|
|
4016
|
+
"gpt-5.4-mini": {
|
|
4017
|
+
inputCost: 0.75 / 1e6,
|
|
4018
|
+
cacheHitCost: 0.075 / 1e6,
|
|
4019
|
+
outputCost: 4.5 / 1e6
|
|
4020
|
+
},
|
|
4021
|
+
"gpt-5.4-nano": {
|
|
4022
|
+
inputCost: 0.2 / 1e6,
|
|
4023
|
+
cacheHitCost: 0.02 / 1e6,
|
|
4024
|
+
outputCost: 1.25 / 1e6
|
|
4025
|
+
},
|
|
4026
|
+
"gpt-5-nano": {
|
|
4027
|
+
inputCost: 0.05 / 1e6,
|
|
4028
|
+
cacheHitCost: 5e-3 / 1e6,
|
|
4029
|
+
outputCost: 0.4 / 1e6
|
|
4030
|
+
},
|
|
4031
|
+
"gpt-5.1": {
|
|
4032
|
+
inputCost: 1.25 / 1e6,
|
|
4033
|
+
cacheHitCost: 0.125 / 1e6,
|
|
4034
|
+
outputCost: 10 / 1e6
|
|
4035
|
+
},
|
|
4036
|
+
"gpt-5.4": {
|
|
4037
|
+
inputCost: 2.5 / 1e6,
|
|
4038
|
+
cacheHitCost: 0.25 / 1e6,
|
|
4039
|
+
outputCost: 15 / 1e6
|
|
4040
|
+
},
|
|
4041
|
+
"gpt-5.5": {
|
|
4042
|
+
inputCost: 5 / 1e6,
|
|
4043
|
+
cacheHitCost: 0.5 / 1e6,
|
|
4044
|
+
outputCost: 30 / 1e6
|
|
4045
|
+
},
|
|
4046
|
+
"gpt-5.5-pro": {
|
|
4047
|
+
inputCost: 30 / 1e6,
|
|
4048
|
+
outputCost: 180 / 1e6
|
|
4049
|
+
},
|
|
4050
|
+
"gpt-5.2": {
|
|
4051
|
+
inputCost: 1.75 / 1e6,
|
|
4052
|
+
cacheHitCost: 0.175 / 1e6,
|
|
4053
|
+
outputCost: 14 / 1e6
|
|
4054
|
+
},
|
|
4055
|
+
"gpt-5.2-pro": {
|
|
4056
|
+
inputCost: 21 / 1e6,
|
|
4057
|
+
outputCost: 168 / 1e6
|
|
4058
|
+
},
|
|
4059
|
+
"gpt-5.1-codex": {
|
|
4060
|
+
inputCost: 1.25 / 1e6,
|
|
4061
|
+
cacheHitCost: 0.125 / 1e6,
|
|
4062
|
+
outputCost: 10 / 1e6
|
|
4063
|
+
},
|
|
4064
|
+
"gpt-5.1-codex-max": {
|
|
4065
|
+
inputCost: 1.25 / 1e6,
|
|
4066
|
+
cacheHitCost: 0.125 / 1e6,
|
|
4067
|
+
outputCost: 10 / 1e6
|
|
4068
|
+
}
|
|
4069
|
+
};
|
|
4070
|
+
var deepseekModelCosts = {
|
|
4071
|
+
"deepseek-chat": {
|
|
4072
|
+
inputCost: 0.27 / 1e6,
|
|
4073
|
+
// $0.27 per 1M tokens (Cache miss price)
|
|
4074
|
+
cacheHitCost: 0.07 / 1e6,
|
|
4075
|
+
// $0.07 per 1M tokens (Cache hit price)
|
|
4076
|
+
outputCost: 1.1 / 1e6
|
|
4077
|
+
// $1.10 per 1M tokens
|
|
4078
|
+
},
|
|
4079
|
+
"deepseek-reasoner": {
|
|
4080
|
+
inputCost: 0.55 / 1e6,
|
|
4081
|
+
// $0.55 per 1M tokens (Cache miss price)
|
|
4082
|
+
cacheHitCost: 0.14 / 1e6,
|
|
4083
|
+
// $0.14 per 1M tokens (Cache hit price)
|
|
4084
|
+
outputCost: 2.19 / 1e6
|
|
4085
|
+
// $2.19 per 1M tokens
|
|
4086
|
+
}
|
|
1797
4087
|
};
|
|
1798
4088
|
function shouldUseGPT5HighContextPricing(model, inputTokens) {
|
|
1799
4089
|
return (model === "gpt-5.4" || model === "gpt-5.5" || model === "gpt-5.6" || model === "gpt-5.6-sol" || model === "gpt-5.6-terra" || model === "gpt-5.6-luna") && inputTokens > GPT_5_HIGH_CONTEXT_THRESHOLD_TOKENS;
|
|
1800
4090
|
}
|
|
1801
4091
|
var DEFAULT_IMAGE_PRICING_QUALITY = "high";
|
|
1802
4092
|
var DEFAULT_IMAGE_PRICING_SIZE = "1024x1024";
|
|
4093
|
+
var PRICED_OPENAI_IMAGE_MODELS = [
|
|
4094
|
+
"gpt-image-2",
|
|
4095
|
+
"gpt-image-2-2026-04-21",
|
|
4096
|
+
"gpt-image-1.5",
|
|
4097
|
+
"gpt-image-1",
|
|
4098
|
+
"gpt-image-1-mini"
|
|
4099
|
+
];
|
|
4100
|
+
var OPENAI_IMAGE_PRICING_SIZES = ["1024x1024", "1024x1536", "1536x1024"];
|
|
1803
4101
|
var openAiImageCostByQualityAndSize = {
|
|
1804
4102
|
"gpt-image-2": {
|
|
1805
4103
|
low: {
|
|
@@ -1851,85 +4149,1482 @@ var openAiImageCostByQualityAndSize = {
|
|
|
1851
4149
|
"1024x1536": 0.2,
|
|
1852
4150
|
"1536x1024": 0.2
|
|
1853
4151
|
}
|
|
1854
|
-
},
|
|
1855
|
-
"gpt-image-1": {
|
|
1856
|
-
low: {
|
|
1857
|
-
"1024x1024": 0.011,
|
|
1858
|
-
"1024x1536": 0.016,
|
|
1859
|
-
"1536x1024": 0.016
|
|
1860
|
-
},
|
|
1861
|
-
medium: {
|
|
1862
|
-
"1024x1024": 0.042,
|
|
1863
|
-
"1024x1536": 0.063,
|
|
1864
|
-
"1536x1024": 0.063
|
|
4152
|
+
},
|
|
4153
|
+
"gpt-image-1": {
|
|
4154
|
+
low: {
|
|
4155
|
+
"1024x1024": 0.011,
|
|
4156
|
+
"1024x1536": 0.016,
|
|
4157
|
+
"1536x1024": 0.016
|
|
4158
|
+
},
|
|
4159
|
+
medium: {
|
|
4160
|
+
"1024x1024": 0.042,
|
|
4161
|
+
"1024x1536": 0.063,
|
|
4162
|
+
"1536x1024": 0.063
|
|
4163
|
+
},
|
|
4164
|
+
high: {
|
|
4165
|
+
"1024x1024": 0.167,
|
|
4166
|
+
"1024x1536": 0.25,
|
|
4167
|
+
"1536x1024": 0.25
|
|
4168
|
+
}
|
|
4169
|
+
},
|
|
4170
|
+
"gpt-image-1-mini": {
|
|
4171
|
+
low: {
|
|
4172
|
+
"1024x1024": 5e-3,
|
|
4173
|
+
"1024x1536": 6e-3,
|
|
4174
|
+
"1536x1024": 6e-3
|
|
4175
|
+
},
|
|
4176
|
+
medium: {
|
|
4177
|
+
"1024x1024": 0.011,
|
|
4178
|
+
"1024x1536": 0.015,
|
|
4179
|
+
"1536x1024": 0.015
|
|
4180
|
+
},
|
|
4181
|
+
high: {
|
|
4182
|
+
"1024x1024": 0.036,
|
|
4183
|
+
"1024x1536": 0.052,
|
|
4184
|
+
"1536x1024": 0.052
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
};
|
|
4188
|
+
var openAiImageCosts = {
|
|
4189
|
+
"gpt-image-2": openAiImageCostByQualityAndSize["gpt-image-2"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
|
|
4190
|
+
"gpt-image-2-2026-04-21": openAiImageCostByQualityAndSize["gpt-image-2-2026-04-21"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
|
|
4191
|
+
"gpt-image-1.5": openAiImageCostByQualityAndSize["gpt-image-1.5"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
|
|
4192
|
+
"gpt-image-1": openAiImageCostByQualityAndSize["gpt-image-1"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
|
|
4193
|
+
"gpt-image-1-mini": openAiImageCostByQualityAndSize["gpt-image-1-mini"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE]
|
|
4194
|
+
};
|
|
4195
|
+
function isPricedOpenAIImageModel(model) {
|
|
4196
|
+
return PRICED_OPENAI_IMAGE_MODELS.includes(model);
|
|
4197
|
+
}
|
|
4198
|
+
function isOpenAIImagePricingSize(size) {
|
|
4199
|
+
return OPENAI_IMAGE_PRICING_SIZES.includes(size);
|
|
4200
|
+
}
|
|
4201
|
+
function resolveImagePricingQuality(quality) {
|
|
4202
|
+
if (quality === "low" || quality === "medium" || quality === "high") {
|
|
4203
|
+
return quality;
|
|
4204
|
+
}
|
|
4205
|
+
return DEFAULT_IMAGE_PRICING_QUALITY;
|
|
4206
|
+
}
|
|
4207
|
+
function resolveImagePricingSize(size) {
|
|
4208
|
+
if (typeof size === "string" && isOpenAIImagePricingSize(size)) {
|
|
4209
|
+
return size;
|
|
4210
|
+
}
|
|
4211
|
+
return DEFAULT_IMAGE_PRICING_SIZE;
|
|
4212
|
+
}
|
|
4213
|
+
function calculateImageCost(model, imageCount, quality, size) {
|
|
4214
|
+
if (typeof model !== "string" || typeof imageCount !== "number" || imageCount <= 0) {
|
|
4215
|
+
return 0;
|
|
4216
|
+
}
|
|
4217
|
+
if (!isPricedOpenAIImageModel(model)) return 0;
|
|
4218
|
+
const pricingQuality = resolveImagePricingQuality(quality);
|
|
4219
|
+
const pricingSize = resolveImagePricingSize(size);
|
|
4220
|
+
const costPerImage = openAiImageCostByQualityAndSize[model][pricingQuality][pricingSize];
|
|
4221
|
+
return imageCount * costPerImage;
|
|
4222
|
+
}
|
|
4223
|
+
function calculateCost(provider, model, inputTokens, outputTokens, reasoningTokens, cacheHitTokens, cacheWriteTokens) {
|
|
4224
|
+
if (typeof provider !== "string" || typeof model !== "string" || typeof inputTokens !== "number" || typeof outputTokens !== "number" || reasoningTokens !== void 0 && typeof reasoningTokens !== "number" || cacheHitTokens !== void 0 && typeof cacheHitTokens !== "number" || cacheWriteTokens !== void 0 && typeof cacheWriteTokens !== "number") {
|
|
4225
|
+
return 0;
|
|
4226
|
+
}
|
|
4227
|
+
const modelCosts = provider === "deepseek" ? deepseekModelCosts[model] : openAiModelCosts[model];
|
|
4228
|
+
if (!modelCosts) return 0;
|
|
4229
|
+
const boundedCacheHitTokens = Math.min(Math.max(cacheHitTokens || 0, 0), inputTokens);
|
|
4230
|
+
const boundedCacheWriteTokens = Math.min(Math.max(cacheWriteTokens || 0, 0), inputTokens - boundedCacheHitTokens);
|
|
4231
|
+
const uncachedInputTokens = inputTokens - boundedCacheHitTokens - boundedCacheWriteTokens;
|
|
4232
|
+
const inputCost = uncachedInputTokens * modelCosts.inputCost + boundedCacheHitTokens * (modelCosts.cacheHitCost || modelCosts.inputCost) + boundedCacheWriteTokens * (modelCosts.cacheWriteCost || modelCosts.inputCost);
|
|
4233
|
+
let outputCost = outputTokens * modelCosts.outputCost;
|
|
4234
|
+
let reasoningCost = (reasoningTokens || 0) * modelCosts.outputCost;
|
|
4235
|
+
if (provider === "openai" && shouldUseGPT5HighContextPricing(model, inputTokens)) {
|
|
4236
|
+
return inputCost * GPT_5_HIGH_CONTEXT_INPUT_MULTIPLIER + outputCost * GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER + reasoningCost * GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER;
|
|
4237
|
+
}
|
|
4238
|
+
return inputCost + outputCost + reasoningCost;
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
// src/json-tools.ts
|
|
4242
|
+
import OpenAI from "openai";
|
|
4243
|
+
function fixBrokenJson(jsonStr) {
|
|
4244
|
+
jsonStr = jsonStr.replace(/\\\$/g, "$");
|
|
4245
|
+
let index = 0;
|
|
4246
|
+
function parse() {
|
|
4247
|
+
const results = [];
|
|
4248
|
+
while (index < jsonStr.length) {
|
|
4249
|
+
skipWhitespace();
|
|
4250
|
+
const value = parseValue();
|
|
4251
|
+
if (value !== void 0) {
|
|
4252
|
+
results.push(value);
|
|
4253
|
+
} else {
|
|
4254
|
+
index++;
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
return results.length === 1 ? results[0] : results;
|
|
4258
|
+
}
|
|
4259
|
+
function parseValue() {
|
|
4260
|
+
skipWhitespace();
|
|
4261
|
+
const char = getChar();
|
|
4262
|
+
if (!char) return void 0;
|
|
4263
|
+
if (char === "{") return parseObject();
|
|
4264
|
+
if (char === "[") return parseArray();
|
|
4265
|
+
if (char === '"' || char === "'") return parseString();
|
|
4266
|
+
if (char === "t" && jsonStr.slice(index, index + 4).toLowerCase() === "true") {
|
|
4267
|
+
index += 4;
|
|
4268
|
+
return true;
|
|
4269
|
+
}
|
|
4270
|
+
if (char === "f" && jsonStr.slice(index, index + 5).toLowerCase() === "false") {
|
|
4271
|
+
index += 5;
|
|
4272
|
+
return false;
|
|
4273
|
+
}
|
|
4274
|
+
if (char === "n" && jsonStr.slice(index, index + 4).toLowerCase() === "null") {
|
|
4275
|
+
index += 4;
|
|
4276
|
+
return null;
|
|
4277
|
+
}
|
|
4278
|
+
if (/[a-zA-Z]/.test(char)) return parseString();
|
|
4279
|
+
if (char === "-" || char === "." || /\d/.test(char)) return parseNumber();
|
|
4280
|
+
return void 0;
|
|
4281
|
+
}
|
|
4282
|
+
function parseObject() {
|
|
4283
|
+
const obj = {};
|
|
4284
|
+
index++;
|
|
4285
|
+
skipWhitespace();
|
|
4286
|
+
while (index < jsonStr.length && getChar() !== "}") {
|
|
4287
|
+
skipWhitespace();
|
|
4288
|
+
const key = parseString();
|
|
4289
|
+
if (key === void 0) {
|
|
4290
|
+
console.warn(`Expected key at position ${index}`);
|
|
4291
|
+
index++;
|
|
4292
|
+
continue;
|
|
4293
|
+
}
|
|
4294
|
+
skipWhitespace();
|
|
4295
|
+
if (getChar() === ":") {
|
|
4296
|
+
index++;
|
|
4297
|
+
} else {
|
|
4298
|
+
console.warn(`Missing colon after key "${key}" at position ${index}`);
|
|
4299
|
+
}
|
|
4300
|
+
skipWhitespace();
|
|
4301
|
+
const value = parseValue();
|
|
4302
|
+
if (value === void 0) {
|
|
4303
|
+
console.warn(`Expected value for key "${key}" at position ${index}`);
|
|
4304
|
+
index++;
|
|
4305
|
+
continue;
|
|
4306
|
+
}
|
|
4307
|
+
obj[key] = value;
|
|
4308
|
+
skipWhitespace();
|
|
4309
|
+
if (getChar() === ",") {
|
|
4310
|
+
index++;
|
|
4311
|
+
} else {
|
|
4312
|
+
break;
|
|
4313
|
+
}
|
|
4314
|
+
skipWhitespace();
|
|
4315
|
+
}
|
|
4316
|
+
if (getChar() === "}") {
|
|
4317
|
+
index++;
|
|
4318
|
+
} else {
|
|
4319
|
+
jsonStr += "}";
|
|
4320
|
+
}
|
|
4321
|
+
return obj;
|
|
4322
|
+
}
|
|
4323
|
+
function parseArray() {
|
|
4324
|
+
const arr = [];
|
|
4325
|
+
index++;
|
|
4326
|
+
skipWhitespace();
|
|
4327
|
+
while (index < jsonStr.length && getChar() !== "]") {
|
|
4328
|
+
const value = parseValue();
|
|
4329
|
+
if (value === void 0) {
|
|
4330
|
+
console.warn(`Expected value at position ${index}`);
|
|
4331
|
+
index++;
|
|
4332
|
+
} else {
|
|
4333
|
+
arr.push(value);
|
|
4334
|
+
}
|
|
4335
|
+
skipWhitespace();
|
|
4336
|
+
if (getChar() === ",") {
|
|
4337
|
+
index++;
|
|
4338
|
+
} else {
|
|
4339
|
+
break;
|
|
4340
|
+
}
|
|
4341
|
+
skipWhitespace();
|
|
4342
|
+
}
|
|
4343
|
+
if (getChar() === "]") {
|
|
4344
|
+
index++;
|
|
4345
|
+
} else {
|
|
4346
|
+
console.warn(`Missing closing bracket for array at position ${index}`);
|
|
4347
|
+
}
|
|
4348
|
+
return arr;
|
|
4349
|
+
}
|
|
4350
|
+
function parseString() {
|
|
4351
|
+
const startChar = getChar();
|
|
4352
|
+
let result = "";
|
|
4353
|
+
let isQuoted = false;
|
|
4354
|
+
let delimiter = "";
|
|
4355
|
+
if (startChar === '"' || startChar === "'") {
|
|
4356
|
+
isQuoted = true;
|
|
4357
|
+
delimiter = startChar;
|
|
4358
|
+
index++;
|
|
4359
|
+
}
|
|
4360
|
+
while (index < jsonStr.length) {
|
|
4361
|
+
const char = getChar();
|
|
4362
|
+
if (isQuoted) {
|
|
4363
|
+
if (char === delimiter) {
|
|
4364
|
+
index++;
|
|
4365
|
+
return result;
|
|
4366
|
+
} else if (char === "\\") {
|
|
4367
|
+
index++;
|
|
4368
|
+
const escapeChar = getChar();
|
|
4369
|
+
const escapeSequences = {
|
|
4370
|
+
n: "\n",
|
|
4371
|
+
r: "\r",
|
|
4372
|
+
t: " ",
|
|
4373
|
+
b: "\b",
|
|
4374
|
+
f: "\f",
|
|
4375
|
+
"\\": "\\",
|
|
4376
|
+
"/": "/",
|
|
4377
|
+
'"': '"',
|
|
4378
|
+
"'": "'",
|
|
4379
|
+
$: "$"
|
|
4380
|
+
};
|
|
4381
|
+
result += escapeSequences[escapeChar] || escapeChar;
|
|
4382
|
+
} else {
|
|
4383
|
+
result += char;
|
|
4384
|
+
}
|
|
4385
|
+
index++;
|
|
4386
|
+
} else {
|
|
4387
|
+
if (/\s|,|}|]|\:/.test(char)) {
|
|
4388
|
+
return result;
|
|
4389
|
+
} else {
|
|
4390
|
+
result += char;
|
|
4391
|
+
index++;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
if (isQuoted) {
|
|
4396
|
+
console.warn(`Missing closing quote for string starting at position ${index}`);
|
|
4397
|
+
}
|
|
4398
|
+
return result;
|
|
4399
|
+
}
|
|
4400
|
+
function parseNumber() {
|
|
4401
|
+
const numberRegex = /^-?\d+(\.\d+)?([eE][+-]?\d+)?/;
|
|
4402
|
+
const match = numberRegex.exec(jsonStr.slice(index));
|
|
4403
|
+
if (match) {
|
|
4404
|
+
index += match[0].length;
|
|
4405
|
+
return parseFloat(match[0]);
|
|
4406
|
+
} else {
|
|
4407
|
+
console.warn(`Invalid number at position ${index}`);
|
|
4408
|
+
index++;
|
|
4409
|
+
return void 0;
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
function getChar() {
|
|
4413
|
+
return jsonStr[index];
|
|
4414
|
+
}
|
|
4415
|
+
function skipWhitespace() {
|
|
4416
|
+
while (/\s/.test(getChar())) index++;
|
|
4417
|
+
}
|
|
4418
|
+
try {
|
|
4419
|
+
return parse();
|
|
4420
|
+
} catch (error) {
|
|
4421
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
4422
|
+
console.error(`Error parsing JSON at position ${index}: ${msg}`);
|
|
4423
|
+
return null;
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
function isValidJson(jsonString) {
|
|
4427
|
+
try {
|
|
4428
|
+
JSON.parse(jsonString);
|
|
4429
|
+
return true;
|
|
4430
|
+
} catch {
|
|
4431
|
+
return false;
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
var openai;
|
|
4435
|
+
function initializeOpenAI(apiKey) {
|
|
4436
|
+
const key = apiKey || process.env.OPENAI_API_KEY;
|
|
4437
|
+
if (!key) {
|
|
4438
|
+
throw new Error("OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set");
|
|
4439
|
+
}
|
|
4440
|
+
return new OpenAI({
|
|
4441
|
+
apiKey: key,
|
|
4442
|
+
defaultHeaders: { "User-Agent": "My Server-side Application (compatible; OpenAI API Client)" }
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
async function fixJsonWithAI(jsonStr, apiKey) {
|
|
4446
|
+
try {
|
|
4447
|
+
if (!openai) {
|
|
4448
|
+
openai = initializeOpenAI();
|
|
4449
|
+
}
|
|
4450
|
+
const completion = await openai.chat.completions.create({
|
|
4451
|
+
model: DEFAULT_MODEL,
|
|
4452
|
+
messages: [
|
|
4453
|
+
{
|
|
4454
|
+
role: "system",
|
|
4455
|
+
content: "You are a JSON fixer. Return only valid JSON without any additional text or explanation."
|
|
4456
|
+
},
|
|
4457
|
+
{
|
|
4458
|
+
role: "user",
|
|
4459
|
+
content: `Fix this broken JSON:
|
|
4460
|
+
${jsonStr}`
|
|
4461
|
+
}
|
|
4462
|
+
],
|
|
4463
|
+
response_format: { type: "json_object" }
|
|
4464
|
+
});
|
|
4465
|
+
const fixedJson = completion.choices[0]?.message?.content;
|
|
4466
|
+
if (fixedJson && isValidJson(fixedJson)) {
|
|
4467
|
+
return JSON.parse(fixedJson);
|
|
4468
|
+
}
|
|
4469
|
+
throw new Error("Failed to fix JSON with AI");
|
|
4470
|
+
} catch (err) {
|
|
4471
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error occurred";
|
|
4472
|
+
throw new Error(`Error fixing JSON with AI: ${errorMessage}`);
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
// src/llm-utils.ts
|
|
4477
|
+
function normalizeModelName(model) {
|
|
4478
|
+
return model.replace(/_/g, "-").toLowerCase();
|
|
4479
|
+
}
|
|
4480
|
+
var CODE_BLOCK_TYPES = ["javascript", "js", "graphql", "json", "typescript", "python", "markdown", "yaml"];
|
|
4481
|
+
async function parseResponse(content, responseFormat) {
|
|
4482
|
+
if (!content) return null;
|
|
4483
|
+
if (responseFormat === "json" || typeof responseFormat === "object" && responseFormat?.type === "json_schema") {
|
|
4484
|
+
let cleanedContent = content.trim();
|
|
4485
|
+
let detectedType = null;
|
|
4486
|
+
const startsWithCodeBlock = CODE_BLOCK_TYPES.some((type) => {
|
|
4487
|
+
if (cleanedContent.startsWith(`\`\`\`${type}`)) {
|
|
4488
|
+
detectedType = type;
|
|
4489
|
+
return true;
|
|
4490
|
+
}
|
|
4491
|
+
return false;
|
|
4492
|
+
});
|
|
4493
|
+
if (startsWithCodeBlock && cleanedContent.endsWith("```")) {
|
|
4494
|
+
const firstLineEndIndex = cleanedContent.indexOf("\n");
|
|
4495
|
+
if (firstLineEndIndex !== -1) {
|
|
4496
|
+
cleanedContent = cleanedContent.slice(firstLineEndIndex + 1, -3).trim();
|
|
4497
|
+
console.log(`Code block for type ${detectedType} detected and removed. Cleaned content: ${cleanedContent}`);
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
const jsonParsingStrategies = [
|
|
4501
|
+
// Strategy 1: Direct parsing
|
|
4502
|
+
() => {
|
|
4503
|
+
try {
|
|
4504
|
+
return JSON.parse(cleanedContent);
|
|
4505
|
+
} catch {
|
|
4506
|
+
return null;
|
|
4507
|
+
}
|
|
4508
|
+
},
|
|
4509
|
+
// Strategy 2: Extract JSON from between first {} or []
|
|
4510
|
+
() => {
|
|
4511
|
+
const jsonMatch = cleanedContent.match(/[\{\[].*[\}\]]/s);
|
|
4512
|
+
if (jsonMatch) {
|
|
4513
|
+
try {
|
|
4514
|
+
return JSON.parse(jsonMatch[0]);
|
|
4515
|
+
} catch {
|
|
4516
|
+
return null;
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
return null;
|
|
4520
|
+
},
|
|
4521
|
+
// Strategy 3: Remove leading/trailing text and try parsing
|
|
4522
|
+
() => {
|
|
4523
|
+
const jsonMatch = cleanedContent.replace(/^[^{[]*|[^}\]]*$/g, "");
|
|
4524
|
+
try {
|
|
4525
|
+
return JSON.parse(jsonMatch);
|
|
4526
|
+
} catch {
|
|
4527
|
+
return null;
|
|
4528
|
+
}
|
|
4529
|
+
},
|
|
4530
|
+
// Strategy 4: Use fixJsonWithAI
|
|
4531
|
+
() => {
|
|
4532
|
+
return fixJsonWithAI(cleanedContent);
|
|
4533
|
+
},
|
|
4534
|
+
// Strategy 5: Use fixBrokenJson
|
|
4535
|
+
() => {
|
|
4536
|
+
return fixBrokenJson(cleanedContent);
|
|
4537
|
+
}
|
|
4538
|
+
];
|
|
4539
|
+
for (const strategy of jsonParsingStrategies) {
|
|
4540
|
+
const result = await strategy();
|
|
4541
|
+
if (result !== null) {
|
|
4542
|
+
return result;
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
console.error(`Failed to parse JSON from content: ${cleanedContent}. Original JSON: ${content}`);
|
|
4546
|
+
throw new Error("Unable to parse JSON response");
|
|
4547
|
+
} else {
|
|
4548
|
+
return content;
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
// src/llm-openai.ts
|
|
4553
|
+
var DEFAULT_IMAGE_MIME_TYPE = "image/webp";
|
|
4554
|
+
function supportsTemperature(_model) {
|
|
4555
|
+
return false;
|
|
4556
|
+
}
|
|
4557
|
+
function isZodSchema(schema) {
|
|
4558
|
+
return typeof schema === "object" && schema !== null && "safeParse" in schema;
|
|
4559
|
+
}
|
|
4560
|
+
function buildJsonSchemaFormat(schema, schemaName, schemaDescription, schemaStrict) {
|
|
4561
|
+
if (isZodSchema(schema)) {
|
|
4562
|
+
const zodFormat = zodTextFormat(
|
|
4563
|
+
schema,
|
|
4564
|
+
schemaName,
|
|
4565
|
+
schemaDescription ? {
|
|
4566
|
+
description: schemaDescription
|
|
4567
|
+
} : void 0
|
|
4568
|
+
);
|
|
4569
|
+
return {
|
|
4570
|
+
type: "json_schema",
|
|
4571
|
+
name: zodFormat.name,
|
|
4572
|
+
schema: zodFormat.schema,
|
|
4573
|
+
...zodFormat.description ? { description: zodFormat.description } : {},
|
|
4574
|
+
...schemaStrict !== void 0 ? { strict: schemaStrict } : { strict: zodFormat.strict }
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4577
|
+
return {
|
|
4578
|
+
type: "json_schema",
|
|
4579
|
+
name: schemaName,
|
|
4580
|
+
schema,
|
|
4581
|
+
...schemaDescription ? { description: schemaDescription } : {},
|
|
4582
|
+
...schemaStrict !== void 0 ? { strict: schemaStrict } : {}
|
|
4583
|
+
};
|
|
4584
|
+
}
|
|
4585
|
+
function resolveOpenAIApiKey(apiKey) {
|
|
4586
|
+
const resolvedApiKey = apiKey || process.env.OPENAI_API_KEY;
|
|
4587
|
+
if (!resolvedApiKey) {
|
|
4588
|
+
throw new Error("OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set");
|
|
4589
|
+
}
|
|
4590
|
+
return resolvedApiKey;
|
|
4591
|
+
}
|
|
4592
|
+
function createOpenAIClient(apiKey) {
|
|
4593
|
+
return new OpenAI2({
|
|
4594
|
+
apiKey
|
|
4595
|
+
});
|
|
4596
|
+
}
|
|
4597
|
+
function normalizeImageDataUrl(imageData, mimeType = DEFAULT_IMAGE_MIME_TYPE) {
|
|
4598
|
+
return imageData.startsWith("data:") ? imageData : `data:${mimeType};base64,${imageData}`;
|
|
4599
|
+
}
|
|
4600
|
+
function getFilenameFromPath(filePath) {
|
|
4601
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
4602
|
+
const pathSegments = normalizedPath.split("/");
|
|
4603
|
+
const filename = pathSegments[pathSegments.length - 1];
|
|
4604
|
+
if (!filename) {
|
|
4605
|
+
throw new Error(`Could not determine a filename from file path: ${filePath}`);
|
|
4606
|
+
}
|
|
4607
|
+
return filename;
|
|
4608
|
+
}
|
|
4609
|
+
function inferImageMimeType(filePath) {
|
|
4610
|
+
const normalizedPath = filePath.toLowerCase();
|
|
4611
|
+
if (normalizedPath.endsWith(".png")) {
|
|
4612
|
+
return "image/png";
|
|
4613
|
+
}
|
|
4614
|
+
if (normalizedPath.endsWith(".jpg") || normalizedPath.endsWith(".jpeg")) {
|
|
4615
|
+
return "image/jpeg";
|
|
4616
|
+
}
|
|
4617
|
+
if (normalizedPath.endsWith(".webp")) {
|
|
4618
|
+
return "image/webp";
|
|
4619
|
+
}
|
|
4620
|
+
if (normalizedPath.endsWith(".gif")) {
|
|
4621
|
+
return "image/gif";
|
|
4622
|
+
}
|
|
4623
|
+
return void 0;
|
|
4624
|
+
}
|
|
4625
|
+
async function readLocalFileBytes(filePath) {
|
|
4626
|
+
try {
|
|
4627
|
+
const dynamicImport = new Function("specifier", "return import(specifier);");
|
|
4628
|
+
const { readFile } = await dynamicImport("node:fs/promises");
|
|
4629
|
+
return await readFile(filePath);
|
|
4630
|
+
} catch {
|
|
4631
|
+
throw new Error("Local image file uploads require a Node.js runtime with access to node:fs/promises");
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
async function uploadVisionFileFromPath(openai2, filePath, filename, mimeType) {
|
|
4635
|
+
const fileBytes = await readLocalFileBytes(filePath);
|
|
4636
|
+
const resolvedFilename = filename || getFilenameFromPath(filePath);
|
|
4637
|
+
const resolvedMimeType = mimeType || inferImageMimeType(filePath);
|
|
4638
|
+
const uploadableFile = await OpenAI2.toFile(
|
|
4639
|
+
fileBytes,
|
|
4640
|
+
resolvedFilename,
|
|
4641
|
+
resolvedMimeType ? { type: resolvedMimeType } : void 0
|
|
4642
|
+
);
|
|
4643
|
+
const uploadedFile = await openai2.files.create({
|
|
4644
|
+
file: uploadableFile,
|
|
4645
|
+
purpose: "vision"
|
|
4646
|
+
});
|
|
4647
|
+
return uploadedFile.id;
|
|
4648
|
+
}
|
|
4649
|
+
function collectImageInputs(image, images, imageBase64) {
|
|
4650
|
+
const collectedImages = [];
|
|
4651
|
+
if (image) {
|
|
4652
|
+
collectedImages.push(image);
|
|
4653
|
+
}
|
|
4654
|
+
if (images && images.length > 0) {
|
|
4655
|
+
collectedImages.push(...images);
|
|
4656
|
+
}
|
|
4657
|
+
if (imageBase64) {
|
|
4658
|
+
collectedImages.push({ base64: imageBase64 });
|
|
4659
|
+
}
|
|
4660
|
+
return collectedImages;
|
|
4661
|
+
}
|
|
4662
|
+
function hasLocalFileImage(images) {
|
|
4663
|
+
return images.some((image) => "filePath" in image);
|
|
4664
|
+
}
|
|
4665
|
+
async function buildImageContentPart(image, defaultDetail, openai2) {
|
|
4666
|
+
const detail = image.detail ?? defaultDetail;
|
|
4667
|
+
if ("url" in image) {
|
|
4668
|
+
return {
|
|
4669
|
+
type: "input_image",
|
|
4670
|
+
detail,
|
|
4671
|
+
image_url: image.url
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
if ("dataUrl" in image) {
|
|
4675
|
+
return {
|
|
4676
|
+
type: "input_image",
|
|
4677
|
+
detail,
|
|
4678
|
+
image_url: image.dataUrl
|
|
4679
|
+
};
|
|
4680
|
+
}
|
|
4681
|
+
if ("base64" in image) {
|
|
4682
|
+
return {
|
|
4683
|
+
type: "input_image",
|
|
4684
|
+
detail,
|
|
4685
|
+
image_url: normalizeImageDataUrl(image.base64, image.mimeType)
|
|
4686
|
+
};
|
|
4687
|
+
}
|
|
4688
|
+
if ("fileId" in image) {
|
|
4689
|
+
return {
|
|
4690
|
+
type: "input_image",
|
|
4691
|
+
detail,
|
|
4692
|
+
file_id: image.fileId
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
4695
|
+
if (!openai2) {
|
|
4696
|
+
throw new Error("A local image file path was provided, but no OpenAI client is available to upload it");
|
|
4697
|
+
}
|
|
4698
|
+
const fileId = await uploadVisionFileFromPath(openai2, image.filePath, image.filename, image.mimeType);
|
|
4699
|
+
return {
|
|
4700
|
+
type: "input_image",
|
|
4701
|
+
detail,
|
|
4702
|
+
file_id: fileId
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
async function buildPromptContent(input, imageInputs, imageDetail, openai2) {
|
|
4706
|
+
if (imageInputs.length === 0) {
|
|
4707
|
+
return input;
|
|
4708
|
+
}
|
|
4709
|
+
const imageContent = await Promise.all(imageInputs.map((image) => buildImageContentPart(image, imageDetail, openai2)));
|
|
4710
|
+
return [{ type: "input_text", text: input }, ...imageContent];
|
|
4711
|
+
}
|
|
4712
|
+
async function uploadVisionFile(filePath, options = {}) {
|
|
4713
|
+
const resolvedApiKey = resolveOpenAIApiKey(options.apiKey);
|
|
4714
|
+
const openai2 = createOpenAIClient(resolvedApiKey);
|
|
4715
|
+
return await uploadVisionFileFromPath(openai2, filePath, options.filename, options.mimeType);
|
|
4716
|
+
}
|
|
4717
|
+
var makeResponsesAPICall = async (input, options = {}) => {
|
|
4718
|
+
const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
|
|
4719
|
+
if (!isOpenAIModel(normalizedModel)) {
|
|
4720
|
+
throw new Error(`Unsupported OpenAI model: ${normalizedModel}. Please use a supported GPT-5 model.`);
|
|
4721
|
+
}
|
|
4722
|
+
const apiKey = resolveOpenAIApiKey(options.apiKey);
|
|
4723
|
+
const openai2 = createOpenAIClient(apiKey);
|
|
4724
|
+
const { apiKey: _, model: __, ...cleanOptions } = options;
|
|
4725
|
+
const requestBody = {
|
|
4726
|
+
model: normalizedModel,
|
|
4727
|
+
input,
|
|
4728
|
+
...cleanOptions
|
|
4729
|
+
};
|
|
4730
|
+
const response = await openai2.responses.create(requestBody);
|
|
4731
|
+
const cacheHitTokens = response.usage?.input_tokens_details?.cached_tokens || 0;
|
|
4732
|
+
const cacheWriteTokens = response.usage?.input_tokens_details?.cache_write_tokens || 0;
|
|
4733
|
+
const toolCalls = response.output?.filter((item) => item.type === "function_call").map((toolCall) => ({
|
|
4734
|
+
id: toolCall.call_id,
|
|
4735
|
+
type: "function",
|
|
4736
|
+
function: {
|
|
4737
|
+
name: toolCall.name,
|
|
4738
|
+
arguments: toolCall.arguments
|
|
4739
|
+
}
|
|
4740
|
+
}));
|
|
4741
|
+
const codeInterpreterCalls = response.output?.filter(
|
|
4742
|
+
(item) => item.type === "code_interpreter_call"
|
|
4743
|
+
);
|
|
4744
|
+
let codeInterpreterOutputs = void 0;
|
|
4745
|
+
if (codeInterpreterCalls && codeInterpreterCalls.length > 0) {
|
|
4746
|
+
codeInterpreterOutputs = codeInterpreterCalls.flatMap((ci) => {
|
|
4747
|
+
if (ci.outputs) return ci.outputs;
|
|
4748
|
+
return [];
|
|
4749
|
+
});
|
|
4750
|
+
}
|
|
4751
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
4752
|
+
return {
|
|
4753
|
+
response: {
|
|
4754
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
4755
|
+
id: tc.id,
|
|
4756
|
+
name: tc.function.name,
|
|
4757
|
+
arguments: JSON.parse(tc.function.arguments)
|
|
4758
|
+
}))
|
|
4759
|
+
},
|
|
4760
|
+
usage: {
|
|
4761
|
+
prompt_tokens: response.usage?.input_tokens || 0,
|
|
4762
|
+
completion_tokens: response.usage?.output_tokens || 0,
|
|
4763
|
+
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
4764
|
+
provider: "openai",
|
|
4765
|
+
model: normalizedModel,
|
|
4766
|
+
cache_hit_tokens: cacheHitTokens,
|
|
4767
|
+
cache_write_tokens: cacheWriteTokens,
|
|
4768
|
+
cost: calculateCost(
|
|
4769
|
+
"openai",
|
|
4770
|
+
normalizedModel,
|
|
4771
|
+
response.usage?.input_tokens || 0,
|
|
4772
|
+
response.usage?.output_tokens || 0,
|
|
4773
|
+
response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
4774
|
+
cacheHitTokens,
|
|
4775
|
+
cacheWriteTokens
|
|
4776
|
+
)
|
|
4777
|
+
},
|
|
4778
|
+
tool_calls: toolCalls,
|
|
4779
|
+
...codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
const textContent = response.output?.filter((item) => item.type === "message").map(
|
|
4783
|
+
(item) => item.content.filter((content) => content.type === "output_text").map((content) => content.text).join("")
|
|
4784
|
+
).join("") || "";
|
|
4785
|
+
let parsingFormat = "text";
|
|
4786
|
+
const requestedFormat = requestBody.text?.format;
|
|
4787
|
+
if (requestedFormat?.type === "json_object") {
|
|
4788
|
+
parsingFormat = "json";
|
|
4789
|
+
} else if (requestedFormat?.type === "json_schema") {
|
|
4790
|
+
parsingFormat = requestedFormat;
|
|
4791
|
+
}
|
|
4792
|
+
const parsedResponse = await parseResponse(textContent, parsingFormat);
|
|
4793
|
+
if (parsedResponse === null) {
|
|
4794
|
+
throw new Error("Failed to parse response from Responses API");
|
|
4795
|
+
}
|
|
4796
|
+
return {
|
|
4797
|
+
response: parsedResponse,
|
|
4798
|
+
usage: {
|
|
4799
|
+
prompt_tokens: response.usage?.input_tokens || 0,
|
|
4800
|
+
completion_tokens: response.usage?.output_tokens || 0,
|
|
4801
|
+
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
4802
|
+
provider: "openai",
|
|
4803
|
+
model: normalizedModel,
|
|
4804
|
+
cache_hit_tokens: cacheHitTokens,
|
|
4805
|
+
cache_write_tokens: cacheWriteTokens,
|
|
4806
|
+
cost: calculateCost(
|
|
4807
|
+
"openai",
|
|
4808
|
+
normalizedModel,
|
|
4809
|
+
response.usage?.input_tokens || 0,
|
|
4810
|
+
response.usage?.output_tokens || 0,
|
|
4811
|
+
response.usage?.output_tokens_details?.reasoning_tokens || 0,
|
|
4812
|
+
cacheHitTokens,
|
|
4813
|
+
cacheWriteTokens
|
|
4814
|
+
)
|
|
1865
4815
|
},
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
4816
|
+
tool_calls: toolCalls,
|
|
4817
|
+
...codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}
|
|
4818
|
+
};
|
|
4819
|
+
};
|
|
4820
|
+
async function makeLLMCall(input, options = {}) {
|
|
4821
|
+
const {
|
|
4822
|
+
apiKey,
|
|
4823
|
+
model = DEFAULT_MODEL,
|
|
4824
|
+
responseFormat = "text",
|
|
4825
|
+
schema,
|
|
4826
|
+
schemaName = "response",
|
|
4827
|
+
schemaDescription,
|
|
4828
|
+
schemaStrict,
|
|
4829
|
+
tools,
|
|
4830
|
+
useCodeInterpreter = false,
|
|
4831
|
+
useWebSearch = false,
|
|
4832
|
+
image,
|
|
4833
|
+
images,
|
|
4834
|
+
imageBase64,
|
|
4835
|
+
imageDetail = "high",
|
|
4836
|
+
context,
|
|
4837
|
+
reasoningEffort,
|
|
4838
|
+
reasoningMode,
|
|
4839
|
+
reasoningContext
|
|
4840
|
+
} = options;
|
|
4841
|
+
const normalizedModel = normalizeModelName(model);
|
|
4842
|
+
if (!isOpenAIModel(normalizedModel)) {
|
|
4843
|
+
throw new Error(`Unsupported OpenAI model: ${normalizedModel}. Please use a supported GPT-5 model.`);
|
|
4844
|
+
}
|
|
4845
|
+
const resolvedApiKey = resolveOpenAIApiKey(apiKey);
|
|
4846
|
+
const imageInputs = collectImageInputs(image, images, imageBase64);
|
|
4847
|
+
const openai2 = hasLocalFileImage(imageInputs) ? createOpenAIClient(resolvedApiKey) : void 0;
|
|
4848
|
+
const promptContent = await buildPromptContent(input, imageInputs, imageDetail, openai2);
|
|
4849
|
+
let processedInput;
|
|
4850
|
+
if (context && context.length > 0) {
|
|
4851
|
+
const conversationMessages = [];
|
|
4852
|
+
for (const contextMsg of context) {
|
|
4853
|
+
conversationMessages.push({
|
|
4854
|
+
role: contextMsg.role,
|
|
4855
|
+
content: contextMsg.content,
|
|
4856
|
+
type: "message"
|
|
4857
|
+
});
|
|
1870
4858
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
high: {
|
|
1884
|
-
"1024x1024": 0.036,
|
|
1885
|
-
"1024x1536": 0.052,
|
|
1886
|
-
"1536x1024": 0.052
|
|
4859
|
+
if (imageInputs.length > 0) {
|
|
4860
|
+
conversationMessages.push({
|
|
4861
|
+
role: "user",
|
|
4862
|
+
content: promptContent,
|
|
4863
|
+
type: "message"
|
|
4864
|
+
});
|
|
4865
|
+
} else {
|
|
4866
|
+
conversationMessages.push({
|
|
4867
|
+
role: "user",
|
|
4868
|
+
content: input,
|
|
4869
|
+
type: "message"
|
|
4870
|
+
});
|
|
1887
4871
|
}
|
|
4872
|
+
processedInput = conversationMessages;
|
|
4873
|
+
} else if (imageInputs.length > 0) {
|
|
4874
|
+
processedInput = [
|
|
4875
|
+
{
|
|
4876
|
+
role: "user",
|
|
4877
|
+
content: promptContent,
|
|
4878
|
+
type: "message"
|
|
4879
|
+
}
|
|
4880
|
+
];
|
|
4881
|
+
} else {
|
|
4882
|
+
processedInput = input;
|
|
1888
4883
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
function calculateCost(provider, model, inputTokens, outputTokens, reasoningTokens, cacheHitTokens, cacheWriteTokens) {
|
|
1898
|
-
if (typeof provider !== "string" || typeof model !== "string" || typeof inputTokens !== "number" || typeof outputTokens !== "number" || reasoningTokens !== void 0 && typeof reasoningTokens !== "number" || cacheHitTokens !== void 0 && typeof cacheHitTokens !== "number" || cacheWriteTokens !== void 0 && typeof cacheWriteTokens !== "number") {
|
|
1899
|
-
return 0;
|
|
4884
|
+
let responsesOptions = {
|
|
4885
|
+
apiKey: resolvedApiKey,
|
|
4886
|
+
model: normalizedModel,
|
|
4887
|
+
parallel_tool_calls: false,
|
|
4888
|
+
tools
|
|
4889
|
+
};
|
|
4890
|
+
if (supportsTemperature(normalizedModel)) {
|
|
4891
|
+
responsesOptions.temperature = 0.2;
|
|
1900
4892
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
let outputCost = outputTokens * modelCosts.outputCost;
|
|
1908
|
-
let reasoningCost = (reasoningTokens || 0) * modelCosts.outputCost;
|
|
1909
|
-
if (provider === "openai" && shouldUseGPT5HighContextPricing(model, inputTokens)) {
|
|
1910
|
-
return inputCost * GPT_5_HIGH_CONTEXT_INPUT_MULTIPLIER + outputCost * GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER + reasoningCost * GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER;
|
|
4893
|
+
if (reasoningEffort || reasoningMode || reasoningContext) {
|
|
4894
|
+
responsesOptions.reasoning = {
|
|
4895
|
+
...reasoningEffort ? { effort: reasoningEffort } : {},
|
|
4896
|
+
...reasoningMode ? { mode: reasoningMode } : {},
|
|
4897
|
+
...reasoningContext ? { context: reasoningContext } : {}
|
|
4898
|
+
};
|
|
1911
4899
|
}
|
|
1912
|
-
|
|
4900
|
+
if (schema) {
|
|
4901
|
+
responsesOptions.text = {
|
|
4902
|
+
format: buildJsonSchemaFormat(schema, schemaName, schemaDescription, schemaStrict)
|
|
4903
|
+
};
|
|
4904
|
+
} else if (responseFormat === "json") {
|
|
4905
|
+
responsesOptions.text = { format: { type: "json_object" } };
|
|
4906
|
+
} else if (typeof responseFormat === "object" && responseFormat.type === "json_schema") {
|
|
4907
|
+
responsesOptions.text = { format: responseFormat };
|
|
4908
|
+
}
|
|
4909
|
+
if (useCodeInterpreter) {
|
|
4910
|
+
responsesOptions.tools = [{ type: "code_interpreter", container: { type: "auto" } }];
|
|
4911
|
+
responsesOptions.include = ["code_interpreter_call.outputs"];
|
|
4912
|
+
}
|
|
4913
|
+
if (useWebSearch) {
|
|
4914
|
+
responsesOptions.tools = [{ type: "web_search_preview" }];
|
|
4915
|
+
}
|
|
4916
|
+
return await makeResponsesAPICall(processedInput, responsesOptions);
|
|
1913
4917
|
}
|
|
1914
4918
|
|
|
1915
|
-
// src/json-tools.ts
|
|
1916
|
-
import OpenAI from "openai";
|
|
1917
|
-
|
|
1918
4919
|
// src/llm-images.ts
|
|
1919
4920
|
import OpenAI3 from "openai";
|
|
4921
|
+
var DEFAULT_IMAGE_MODEL = "gpt-image-2";
|
|
4922
|
+
var resolveImageModel = (model) => model ?? DEFAULT_IMAGE_MODEL;
|
|
1920
4923
|
var MULTIMODAL_VISION_MODELS = new Set(OPENAI_MODELS);
|
|
4924
|
+
async function makeImagesCall(prompt, options = {}) {
|
|
4925
|
+
const {
|
|
4926
|
+
model,
|
|
4927
|
+
size = "auto",
|
|
4928
|
+
outputFormat = "webp",
|
|
4929
|
+
compression = 50,
|
|
4930
|
+
quality = "high",
|
|
4931
|
+
count = 1,
|
|
4932
|
+
background = "auto",
|
|
4933
|
+
moderation = "auto",
|
|
4934
|
+
apiKey,
|
|
4935
|
+
visionModel
|
|
4936
|
+
} = options;
|
|
4937
|
+
const imageModel = resolveImageModel(model);
|
|
4938
|
+
const supportedVisionModel = visionModel && MULTIMODAL_VISION_MODELS.has(visionModel) ? visionModel : void 0;
|
|
4939
|
+
if (visionModel && !supportedVisionModel) {
|
|
4940
|
+
console.warn(
|
|
4941
|
+
`Vision model ${visionModel} is not recognized as a multimodal OpenAI model. Ignoring for image usage metadata.`
|
|
4942
|
+
);
|
|
4943
|
+
}
|
|
4944
|
+
const effectiveApiKey = apiKey || process.env.OPENAI_API_KEY;
|
|
4945
|
+
if (!effectiveApiKey) {
|
|
4946
|
+
throw new Error("OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set");
|
|
4947
|
+
}
|
|
4948
|
+
if (!prompt || typeof prompt !== "string") {
|
|
4949
|
+
throw new Error("Prompt must be a non-empty string");
|
|
4950
|
+
}
|
|
4951
|
+
if (count && (count < 1 || count > 10)) {
|
|
4952
|
+
throw new Error("Count must be between 1 and 10");
|
|
4953
|
+
}
|
|
4954
|
+
if (compression && (compression < 0 || compression > 100)) {
|
|
4955
|
+
throw new Error("Compression must be between 0 and 100");
|
|
4956
|
+
}
|
|
4957
|
+
const openai2 = new OpenAI3({
|
|
4958
|
+
apiKey: effectiveApiKey
|
|
4959
|
+
});
|
|
4960
|
+
const requestParams = {
|
|
4961
|
+
model: imageModel,
|
|
4962
|
+
prompt,
|
|
4963
|
+
n: count || 1,
|
|
4964
|
+
size: size || "auto",
|
|
4965
|
+
quality: quality || "auto",
|
|
4966
|
+
background: background || "auto",
|
|
4967
|
+
moderation: moderation || "auto"
|
|
4968
|
+
};
|
|
4969
|
+
if (outputFormat) {
|
|
4970
|
+
requestParams.output_format = outputFormat;
|
|
4971
|
+
}
|
|
4972
|
+
if (compression !== void 0) {
|
|
4973
|
+
requestParams.output_compression = compression;
|
|
4974
|
+
}
|
|
4975
|
+
try {
|
|
4976
|
+
const response = await openai2.images.generate(requestParams);
|
|
4977
|
+
if (!response.data || response.data.length === 0) {
|
|
4978
|
+
throw new Error("No images returned from OpenAI Images API");
|
|
4979
|
+
}
|
|
4980
|
+
const cost = calculateImageCost(imageModel, count || 1, quality, size);
|
|
4981
|
+
const enhancedResponse = {
|
|
4982
|
+
...response,
|
|
4983
|
+
usage: {
|
|
4984
|
+
// OpenAI Images response may not include usage details per image; preserve if present
|
|
4985
|
+
...response.usage ?? {
|
|
4986
|
+
input_tokens: 0,
|
|
4987
|
+
input_tokens_details: { image_tokens: 0, text_tokens: 0 },
|
|
4988
|
+
output_tokens: 0,
|
|
4989
|
+
total_tokens: 0
|
|
4990
|
+
},
|
|
4991
|
+
provider: "openai",
|
|
4992
|
+
model: imageModel,
|
|
4993
|
+
cost,
|
|
4994
|
+
...supportedVisionModel ? { visionModel: supportedVisionModel } : {}
|
|
4995
|
+
}
|
|
4996
|
+
};
|
|
4997
|
+
return enhancedResponse;
|
|
4998
|
+
} catch (error) {
|
|
4999
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
5000
|
+
throw new Error(`OpenAI Images API call failed: ${message}`);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
1921
5003
|
|
|
1922
5004
|
// src/llm-deepseek.ts
|
|
1923
5005
|
import OpenAI4 from "openai";
|
|
5006
|
+
var DEFAULT_DEEPSEEK_OPTIONS = {
|
|
5007
|
+
defaultModel: "deepseek-chat"
|
|
5008
|
+
};
|
|
5009
|
+
var isSupportedDeepseekModel = (model) => {
|
|
5010
|
+
return ["deepseek-chat", "deepseek-reasoner"].includes(model);
|
|
5011
|
+
};
|
|
5012
|
+
var supportsJsonOutput = (model) => {
|
|
5013
|
+
return model === "deepseek-chat";
|
|
5014
|
+
};
|
|
5015
|
+
var supportsToolCalling = (model) => {
|
|
5016
|
+
return model === "deepseek-chat";
|
|
5017
|
+
};
|
|
5018
|
+
function getDeepseekResponseFormatOption(responseFormat, model) {
|
|
5019
|
+
if (responseFormat !== "text" && !supportsJsonOutput(model)) {
|
|
5020
|
+
console.warn(`Model ${model} does not support JSON output. Using text format instead.`);
|
|
5021
|
+
return { type: "text" };
|
|
5022
|
+
}
|
|
5023
|
+
if (responseFormat === "text") {
|
|
5024
|
+
return { type: "text" };
|
|
5025
|
+
}
|
|
5026
|
+
if (responseFormat === "json" || typeof responseFormat === "object" && responseFormat.type === "json_schema") {
|
|
5027
|
+
return { type: "json_object" };
|
|
5028
|
+
}
|
|
5029
|
+
return { type: "text" };
|
|
5030
|
+
}
|
|
5031
|
+
async function createDeepseekCompletion(content, responseFormat, options = {}) {
|
|
5032
|
+
const modelOption = options.model || DEFAULT_DEEPSEEK_OPTIONS.defaultModel;
|
|
5033
|
+
const normalizedModel = normalizeModelName(modelOption);
|
|
5034
|
+
if (!isSupportedDeepseekModel(normalizedModel)) {
|
|
5035
|
+
throw new Error(`Unsupported Deepseek model: ${normalizedModel}. Please use 'deepseek-chat' or 'deepseek-reasoner'.`);
|
|
5036
|
+
}
|
|
5037
|
+
if (options.tools && options.tools.length > 0 && !supportsToolCalling(normalizedModel)) {
|
|
5038
|
+
throw new Error(`Model ${normalizedModel} does not support tool calling.`);
|
|
5039
|
+
}
|
|
5040
|
+
const apiKey = options.apiKey || process.env.DEEPSEEK_API_KEY;
|
|
5041
|
+
if (!apiKey) {
|
|
5042
|
+
throw new Error("Deepseek API key is not provided and DEEPSEEK_API_KEY environment variable is not set");
|
|
5043
|
+
}
|
|
5044
|
+
const openai2 = new OpenAI4({
|
|
5045
|
+
apiKey,
|
|
5046
|
+
baseURL: "https://api.deepseek.com"
|
|
5047
|
+
});
|
|
5048
|
+
const messages = [];
|
|
5049
|
+
if (options.developerPrompt) {
|
|
5050
|
+
messages.push({
|
|
5051
|
+
role: "system",
|
|
5052
|
+
content: options.developerPrompt
|
|
5053
|
+
});
|
|
5054
|
+
}
|
|
5055
|
+
if (options.context) {
|
|
5056
|
+
messages.push(...options.context);
|
|
5057
|
+
}
|
|
5058
|
+
if (typeof content === "string") {
|
|
5059
|
+
messages.push({
|
|
5060
|
+
role: "user",
|
|
5061
|
+
content
|
|
5062
|
+
});
|
|
5063
|
+
} else {
|
|
5064
|
+
messages.push({
|
|
5065
|
+
role: "user",
|
|
5066
|
+
content
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
if ((responseFormat === "json" || typeof responseFormat === "object" && responseFormat.type === "json_schema") && supportsJsonOutput(normalizedModel)) {
|
|
5070
|
+
if (!messages.some((m) => m.role === "system")) {
|
|
5071
|
+
messages.unshift({
|
|
5072
|
+
role: "system",
|
|
5073
|
+
content: "Please respond in valid JSON format."
|
|
5074
|
+
});
|
|
5075
|
+
} else {
|
|
5076
|
+
const systemMsgIndex = messages.findIndex((m) => m.role === "system");
|
|
5077
|
+
const systemMsg = messages[systemMsgIndex];
|
|
5078
|
+
if (typeof systemMsg.content === "string") {
|
|
5079
|
+
messages[systemMsgIndex] = {
|
|
5080
|
+
...systemMsg,
|
|
5081
|
+
content: `${systemMsg.content} Please respond in valid JSON format.`
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
5086
|
+
const queryOptions = {
|
|
5087
|
+
model: normalizedModel,
|
|
5088
|
+
messages,
|
|
5089
|
+
response_format: getDeepseekResponseFormatOption(responseFormat, normalizedModel),
|
|
5090
|
+
temperature: options.temperature,
|
|
5091
|
+
top_p: options.top_p,
|
|
5092
|
+
frequency_penalty: options.frequency_penalty,
|
|
5093
|
+
presence_penalty: options.presence_penalty,
|
|
5094
|
+
max_tokens: options.max_completion_tokens
|
|
5095
|
+
};
|
|
5096
|
+
if (options.tools && supportsToolCalling(normalizedModel)) {
|
|
5097
|
+
queryOptions.tools = options.tools;
|
|
5098
|
+
}
|
|
5099
|
+
try {
|
|
5100
|
+
const completion = await openai2.chat.completions.create(queryOptions);
|
|
5101
|
+
return {
|
|
5102
|
+
id: completion.id,
|
|
5103
|
+
content: completion.choices[0]?.message?.content || "",
|
|
5104
|
+
tool_calls: completion.choices[0]?.message?.tool_calls,
|
|
5105
|
+
usage: completion.usage || {
|
|
5106
|
+
prompt_tokens: 0,
|
|
5107
|
+
completion_tokens: 0,
|
|
5108
|
+
total_tokens: 0
|
|
5109
|
+
},
|
|
5110
|
+
system_fingerprint: completion.system_fingerprint,
|
|
5111
|
+
provider: "deepseek",
|
|
5112
|
+
model: normalizedModel
|
|
5113
|
+
};
|
|
5114
|
+
} catch (error) {
|
|
5115
|
+
console.error("Error calling Deepseek API:", error);
|
|
5116
|
+
throw error;
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
var makeDeepseekCall = async (content, responseFormat = "json", options = {}) => {
|
|
5120
|
+
const mergedOptions = {
|
|
5121
|
+
...options
|
|
5122
|
+
};
|
|
5123
|
+
if (!mergedOptions.model) {
|
|
5124
|
+
mergedOptions.model = DEFAULT_DEEPSEEK_OPTIONS.defaultModel;
|
|
5125
|
+
}
|
|
5126
|
+
const modelName = normalizeModelName(mergedOptions.model);
|
|
5127
|
+
if (responseFormat !== "text" && !supportsJsonOutput(modelName)) {
|
|
5128
|
+
console.warn(`Model ${modelName} does not support JSON output. Will return error in the response.`);
|
|
5129
|
+
return {
|
|
5130
|
+
response: {
|
|
5131
|
+
error: `Model ${modelName} does not support JSON output format.`
|
|
5132
|
+
},
|
|
5133
|
+
usage: {
|
|
5134
|
+
prompt_tokens: 0,
|
|
5135
|
+
completion_tokens: 0,
|
|
5136
|
+
reasoning_tokens: 0,
|
|
5137
|
+
provider: "deepseek",
|
|
5138
|
+
model: modelName,
|
|
5139
|
+
cache_hit_tokens: 0,
|
|
5140
|
+
cost: 0
|
|
5141
|
+
},
|
|
5142
|
+
tool_calls: void 0
|
|
5143
|
+
};
|
|
5144
|
+
}
|
|
5145
|
+
if (mergedOptions.tools && mergedOptions.tools.length > 0 && !supportsToolCalling(modelName)) {
|
|
5146
|
+
console.warn(`Model ${modelName} does not support tool calling. Will return error in the response.`);
|
|
5147
|
+
return {
|
|
5148
|
+
response: {
|
|
5149
|
+
error: `Model ${modelName} does not support tool calling.`
|
|
5150
|
+
},
|
|
5151
|
+
usage: {
|
|
5152
|
+
prompt_tokens: 0,
|
|
5153
|
+
completion_tokens: 0,
|
|
5154
|
+
reasoning_tokens: 0,
|
|
5155
|
+
provider: "deepseek",
|
|
5156
|
+
model: modelName,
|
|
5157
|
+
cache_hit_tokens: 0,
|
|
5158
|
+
cost: 0
|
|
5159
|
+
},
|
|
5160
|
+
tool_calls: void 0
|
|
5161
|
+
};
|
|
5162
|
+
}
|
|
5163
|
+
try {
|
|
5164
|
+
const completion = await createDeepseekCompletion(content, responseFormat, mergedOptions);
|
|
5165
|
+
if (completion.tool_calls && completion.tool_calls.length > 0) {
|
|
5166
|
+
const fnCalls = completion.tool_calls.filter((tc) => tc.type === "function").map((tc) => ({
|
|
5167
|
+
id: tc.id,
|
|
5168
|
+
name: tc.function.name,
|
|
5169
|
+
arguments: JSON.parse(tc.function.arguments)
|
|
5170
|
+
}));
|
|
5171
|
+
return {
|
|
5172
|
+
response: { tool_calls: fnCalls },
|
|
5173
|
+
usage: {
|
|
5174
|
+
prompt_tokens: completion.usage.prompt_tokens,
|
|
5175
|
+
completion_tokens: completion.usage.completion_tokens,
|
|
5176
|
+
reasoning_tokens: 0,
|
|
5177
|
+
// Deepseek doesn't provide reasoning tokens separately
|
|
5178
|
+
provider: "deepseek",
|
|
5179
|
+
model: completion.model,
|
|
5180
|
+
cache_hit_tokens: 0,
|
|
5181
|
+
// Not provided directly in API response
|
|
5182
|
+
cost: calculateCost(
|
|
5183
|
+
"deepseek",
|
|
5184
|
+
completion.model,
|
|
5185
|
+
completion.usage.prompt_tokens,
|
|
5186
|
+
completion.usage.completion_tokens,
|
|
5187
|
+
0,
|
|
5188
|
+
0
|
|
5189
|
+
// Cache hit tokens (not provided in the response)
|
|
5190
|
+
)
|
|
5191
|
+
},
|
|
5192
|
+
tool_calls: completion.tool_calls
|
|
5193
|
+
};
|
|
5194
|
+
}
|
|
5195
|
+
const parsedResponse = await parseResponse(completion.content, responseFormat);
|
|
5196
|
+
if (parsedResponse === null) {
|
|
5197
|
+
throw new Error("Failed to parse Deepseek response");
|
|
5198
|
+
}
|
|
5199
|
+
return {
|
|
5200
|
+
response: parsedResponse,
|
|
5201
|
+
usage: {
|
|
5202
|
+
prompt_tokens: completion.usage.prompt_tokens,
|
|
5203
|
+
completion_tokens: completion.usage.completion_tokens,
|
|
5204
|
+
reasoning_tokens: 0,
|
|
5205
|
+
// Deepseek doesn't provide reasoning tokens separately
|
|
5206
|
+
provider: "deepseek",
|
|
5207
|
+
model: completion.model,
|
|
5208
|
+
cache_hit_tokens: 0,
|
|
5209
|
+
// Not provided directly in API response
|
|
5210
|
+
cost: calculateCost(
|
|
5211
|
+
"deepseek",
|
|
5212
|
+
completion.model,
|
|
5213
|
+
completion.usage.prompt_tokens,
|
|
5214
|
+
completion.usage.completion_tokens,
|
|
5215
|
+
0,
|
|
5216
|
+
0
|
|
5217
|
+
// Cache hit tokens (not provided in the response)
|
|
5218
|
+
)
|
|
5219
|
+
},
|
|
5220
|
+
tool_calls: completion.tool_calls
|
|
5221
|
+
};
|
|
5222
|
+
} catch (error) {
|
|
5223
|
+
console.error("Error in Deepseek API call:", error);
|
|
5224
|
+
return {
|
|
5225
|
+
response: {
|
|
5226
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
5227
|
+
},
|
|
5228
|
+
usage: {
|
|
5229
|
+
prompt_tokens: 0,
|
|
5230
|
+
completion_tokens: 0,
|
|
5231
|
+
reasoning_tokens: 0,
|
|
5232
|
+
provider: "deepseek",
|
|
5233
|
+
model: modelName,
|
|
5234
|
+
cache_hit_tokens: 0,
|
|
5235
|
+
cost: 0
|
|
5236
|
+
},
|
|
5237
|
+
tool_calls: void 0
|
|
5238
|
+
};
|
|
5239
|
+
}
|
|
5240
|
+
};
|
|
1924
5241
|
|
|
1925
5242
|
// src/llm-openrouter.ts
|
|
1926
5243
|
import OpenAI5 from "openai";
|
|
5244
|
+
function mapContextToMessages(context) {
|
|
5245
|
+
return context.map((msg) => {
|
|
5246
|
+
const role = msg.role === "developer" ? "system" : msg.role;
|
|
5247
|
+
return { role, content: msg.content };
|
|
5248
|
+
});
|
|
5249
|
+
}
|
|
5250
|
+
function toOpenRouterModel(model) {
|
|
5251
|
+
if (model && model.includes("/")) return model;
|
|
5252
|
+
const base = normalizeModelName(model || DEFAULT_MODEL);
|
|
5253
|
+
return `openai/${base}`;
|
|
5254
|
+
}
|
|
5255
|
+
function normalizeModelForPricing(model) {
|
|
5256
|
+
if (!model) return { provider: "openai", coreModel: normalizeModelName(DEFAULT_MODEL) };
|
|
5257
|
+
const [maybeProvider, maybeModel] = model.includes("/") ? model.split("/") : ["openai", model];
|
|
5258
|
+
const provider = maybeProvider === "deepseek" ? "deepseek" : "openai";
|
|
5259
|
+
const coreModel = normalizeModelName(maybeModel || model);
|
|
5260
|
+
return { provider, coreModel };
|
|
5261
|
+
}
|
|
5262
|
+
async function makeOpenRouterCall(input, options = {}) {
|
|
5263
|
+
const {
|
|
5264
|
+
apiKey = process.env.OPENROUTER_API_KEY,
|
|
5265
|
+
model,
|
|
5266
|
+
responseFormat = "text",
|
|
5267
|
+
tools,
|
|
5268
|
+
toolChoice,
|
|
5269
|
+
context,
|
|
5270
|
+
developerPrompt,
|
|
5271
|
+
temperature = 0.2,
|
|
5272
|
+
max_tokens,
|
|
5273
|
+
top_p,
|
|
5274
|
+
frequency_penalty,
|
|
5275
|
+
presence_penalty,
|
|
5276
|
+
stop,
|
|
5277
|
+
seed,
|
|
5278
|
+
referer = process.env.OPENROUTER_SITE_URL,
|
|
5279
|
+
title = process.env.OPENROUTER_SITE_NAME
|
|
5280
|
+
} = options;
|
|
5281
|
+
if (!apiKey) {
|
|
5282
|
+
throw new Error("OpenRouter API key is not provided and OPENROUTER_API_KEY is not set");
|
|
5283
|
+
}
|
|
5284
|
+
const client = new OpenAI5({
|
|
5285
|
+
apiKey,
|
|
5286
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
5287
|
+
defaultHeaders: {
|
|
5288
|
+
...referer ? { "HTTP-Referer": referer } : {},
|
|
5289
|
+
...title ? { "X-Title": title } : {}
|
|
5290
|
+
}
|
|
5291
|
+
});
|
|
5292
|
+
const messages = [];
|
|
5293
|
+
if (developerPrompt && developerPrompt.trim()) {
|
|
5294
|
+
messages.push({ role: "system", content: developerPrompt });
|
|
5295
|
+
}
|
|
5296
|
+
if (context && context.length > 0) {
|
|
5297
|
+
messages.push(...mapContextToMessages(context));
|
|
5298
|
+
}
|
|
5299
|
+
messages.push({ role: "user", content: input });
|
|
5300
|
+
let response_format;
|
|
5301
|
+
let parsingFormat = "text";
|
|
5302
|
+
if (responseFormat === "json") {
|
|
5303
|
+
response_format = { type: "json_object" };
|
|
5304
|
+
parsingFormat = "json";
|
|
5305
|
+
} else if (typeof responseFormat === "object") {
|
|
5306
|
+
response_format = { type: "json_object" };
|
|
5307
|
+
parsingFormat = responseFormat;
|
|
5308
|
+
}
|
|
5309
|
+
const modelId = toOpenRouterModel(model);
|
|
5310
|
+
const completion = await client.chat.completions.create({
|
|
5311
|
+
model: modelId,
|
|
5312
|
+
messages,
|
|
5313
|
+
response_format,
|
|
5314
|
+
tools,
|
|
5315
|
+
tool_choice: toolChoice,
|
|
5316
|
+
temperature,
|
|
5317
|
+
max_tokens,
|
|
5318
|
+
top_p,
|
|
5319
|
+
frequency_penalty,
|
|
5320
|
+
presence_penalty,
|
|
5321
|
+
stop,
|
|
5322
|
+
seed
|
|
5323
|
+
});
|
|
5324
|
+
const choice = completion.choices && completion.choices.length > 0 ? completion.choices[0] : void 0;
|
|
5325
|
+
const message = choice && "message" in choice ? choice.message : void 0;
|
|
5326
|
+
const { provider: pricingProvider, coreModel } = normalizeModelForPricing(modelId);
|
|
5327
|
+
const promptTokens = completion.usage?.prompt_tokens ?? 0;
|
|
5328
|
+
const completionTokens = completion.usage?.completion_tokens ?? 0;
|
|
5329
|
+
const cost = calculateCost(pricingProvider, coreModel, promptTokens, completionTokens);
|
|
5330
|
+
const hasToolCalls = Array.isArray(message?.tool_calls) && message.tool_calls.length > 0;
|
|
5331
|
+
if (hasToolCalls) {
|
|
5332
|
+
const usageModel2 = isOpenRouterModel(modelId) ? modelId : DEFAULT_MODEL;
|
|
5333
|
+
return {
|
|
5334
|
+
response: "",
|
|
5335
|
+
usage: {
|
|
5336
|
+
prompt_tokens: promptTokens,
|
|
5337
|
+
completion_tokens: completionTokens,
|
|
5338
|
+
provider: "openrouter",
|
|
5339
|
+
model: usageModel2,
|
|
5340
|
+
cost
|
|
5341
|
+
},
|
|
5342
|
+
tool_calls: message.tool_calls
|
|
5343
|
+
};
|
|
5344
|
+
}
|
|
5345
|
+
const rawText = typeof message?.content === "string" ? message.content : "";
|
|
5346
|
+
const parsed = await parseResponse(rawText, parsingFormat);
|
|
5347
|
+
if (parsed === null) {
|
|
5348
|
+
throw new Error("Failed to parse OpenRouter response");
|
|
5349
|
+
}
|
|
5350
|
+
const usageModel = isOpenRouterModel(modelId) ? modelId : DEFAULT_MODEL;
|
|
5351
|
+
return {
|
|
5352
|
+
response: parsed,
|
|
5353
|
+
usage: {
|
|
5354
|
+
prompt_tokens: promptTokens,
|
|
5355
|
+
completion_tokens: completionTokens,
|
|
5356
|
+
provider: "openrouter",
|
|
5357
|
+
model: usageModel,
|
|
5358
|
+
cost
|
|
5359
|
+
},
|
|
5360
|
+
...hasToolCalls ? { tool_calls: message.tool_calls } : {}
|
|
5361
|
+
};
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
// src/performance-timer.ts
|
|
5365
|
+
var PerformanceTimer = class {
|
|
5366
|
+
startTime;
|
|
5367
|
+
checkpoints;
|
|
5368
|
+
totalTime;
|
|
5369
|
+
constructor() {
|
|
5370
|
+
this.startTime = performance.now();
|
|
5371
|
+
this.checkpoints = /* @__PURE__ */ new Map();
|
|
5372
|
+
this.totalTime = null;
|
|
5373
|
+
}
|
|
5374
|
+
/**
|
|
5375
|
+
* Gets the current elapsed time in seconds.
|
|
5376
|
+
*
|
|
5377
|
+
* @returns The elapsed time in seconds with 3 decimal places of precision.
|
|
5378
|
+
*/
|
|
5379
|
+
getElapsedSeconds() {
|
|
5380
|
+
const elapsedMs = this.totalTime ?? performance.now() - this.startTime;
|
|
5381
|
+
return Number((elapsedMs / 1e3).toFixed(3));
|
|
5382
|
+
}
|
|
5383
|
+
/**
|
|
5384
|
+
* Records a checkpoint with the given label.
|
|
5385
|
+
*
|
|
5386
|
+
* @param label - A descriptive label for the checkpoint.
|
|
5387
|
+
*/
|
|
5388
|
+
checkpoint(label) {
|
|
5389
|
+
this.checkpoints.set(label, performance.now());
|
|
5390
|
+
}
|
|
5391
|
+
/**
|
|
5392
|
+
* Stops the timer and sets the total elapsed time.
|
|
5393
|
+
*/
|
|
5394
|
+
stop() {
|
|
5395
|
+
this.totalTime = performance.now() - this.startTime;
|
|
5396
|
+
}
|
|
5397
|
+
/**
|
|
5398
|
+
* Generates a performance report containing the total elapsed time and
|
|
5399
|
+
* breakdown of phases between checkpoints.
|
|
5400
|
+
*
|
|
5401
|
+
* @returns A performance report object.
|
|
5402
|
+
* @throws Error if the timer is not stopped before generating the report.
|
|
5403
|
+
*/
|
|
5404
|
+
analyseReportData() {
|
|
5405
|
+
if (this.totalTime === null) {
|
|
5406
|
+
throw new Error("Timer must be stopped before generating report");
|
|
5407
|
+
}
|
|
5408
|
+
const report = {
|
|
5409
|
+
totalTime: this.totalTime,
|
|
5410
|
+
phases: []
|
|
5411
|
+
};
|
|
5412
|
+
let lastTime = this.startTime;
|
|
5413
|
+
const sortedCheckpoints = Array.from(this.checkpoints.entries()).sort((a, b) => a[1] - b[1]);
|
|
5414
|
+
for (const [label, time] of sortedCheckpoints) {
|
|
5415
|
+
const duration = time - lastTime;
|
|
5416
|
+
if (duration < 0) {
|
|
5417
|
+
throw new Error(`Negative duration detected for checkpoint: ${label}`);
|
|
5418
|
+
}
|
|
5419
|
+
report.phases.push({
|
|
5420
|
+
label,
|
|
5421
|
+
duration
|
|
5422
|
+
});
|
|
5423
|
+
lastTime = time;
|
|
5424
|
+
}
|
|
5425
|
+
if (sortedCheckpoints.length > 0) {
|
|
5426
|
+
const finalDuration = this.totalTime - lastTime;
|
|
5427
|
+
report.phases.push({
|
|
5428
|
+
label: "Final Processing",
|
|
5429
|
+
duration: Math.max(finalDuration, 0)
|
|
5430
|
+
// Prevent negative duration
|
|
5431
|
+
});
|
|
5432
|
+
}
|
|
5433
|
+
return report;
|
|
5434
|
+
}
|
|
5435
|
+
/**
|
|
5436
|
+
* Returns a formatted string of the performance report.
|
|
5437
|
+
*
|
|
5438
|
+
* @returns A string detailing the total execution time and phase breakdown.
|
|
5439
|
+
*/
|
|
5440
|
+
generateReport() {
|
|
5441
|
+
const report = this.analyseReportData();
|
|
5442
|
+
const formatNumber2 = (num) => num.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
5443
|
+
const maxLabelLength = Math.max(...report.phases.map((p) => p.label.length), 30);
|
|
5444
|
+
const maxDurationLength = Math.max(...report.phases.map((p) => formatNumber2(p.duration).length), 10);
|
|
5445
|
+
let output = `\u2554${"\u2550".repeat(maxLabelLength + 2)}\u2566${"\u2550".repeat(maxDurationLength + 2)}\u2557
|
|
5446
|
+
`;
|
|
5447
|
+
output += `\u2551 ${"Phase".padEnd(maxLabelLength)} \u2551 ${"Time (ms)".padEnd(maxDurationLength)} \u2551
|
|
5448
|
+
`;
|
|
5449
|
+
output += `\u2560${"\u2550".repeat(maxLabelLength + 2)}\u256C${"\u2550".repeat(maxDurationLength + 2)}\u2563
|
|
5450
|
+
`;
|
|
5451
|
+
report.phases.forEach((phase) => {
|
|
5452
|
+
output += `\u2551 ${phase.label.padEnd(maxLabelLength)} \u2551 ${formatNumber2(phase.duration).padStart(maxDurationLength)} \u2551
|
|
5453
|
+
`;
|
|
5454
|
+
});
|
|
5455
|
+
output += `\u2560${"\u2550".repeat(maxLabelLength + 2)}\u256C${"\u2550".repeat(maxDurationLength + 2)}\u2563
|
|
5456
|
+
`;
|
|
5457
|
+
output += `\u2551 ${"Total".padEnd(maxLabelLength)} \u2551 ${formatNumber2(report.totalTime).padStart(maxDurationLength)} \u2551
|
|
5458
|
+
`;
|
|
5459
|
+
output += `\u255A${"\u2550".repeat(maxLabelLength + 2)}\u2569${"\u2550".repeat(maxDurationLength + 2)}\u255D
|
|
5460
|
+
`;
|
|
5461
|
+
return output;
|
|
5462
|
+
}
|
|
5463
|
+
};
|
|
5464
|
+
|
|
5465
|
+
// src/disco-mail.ts
|
|
5466
|
+
var DEFAULT_DISCO_MAIL_BASE_URL = "https://mail.discomedia.co/api/v1";
|
|
5467
|
+
var DiscoMailApiError = class extends Error {
|
|
5468
|
+
code;
|
|
5469
|
+
status;
|
|
5470
|
+
constructor(code, message, status) {
|
|
5471
|
+
super(`Disco Mail API error: ${code} ${message}`);
|
|
5472
|
+
this.name = "DiscoMailApiError";
|
|
5473
|
+
this.code = code;
|
|
5474
|
+
this.status = status;
|
|
5475
|
+
}
|
|
5476
|
+
};
|
|
5477
|
+
var DiscoMailAPI = class {
|
|
5478
|
+
constructor(clientOptions = {}) {
|
|
5479
|
+
this.clientOptions = clientOptions;
|
|
5480
|
+
}
|
|
5481
|
+
clientOptions;
|
|
5482
|
+
async send(payload, options = {}) {
|
|
5483
|
+
this.validateSendRequest(payload);
|
|
5484
|
+
const headers = new Headers();
|
|
5485
|
+
if (options.idempotencyKey) {
|
|
5486
|
+
headers.set("Idempotency-Key", options.idempotencyKey);
|
|
5487
|
+
}
|
|
5488
|
+
return this.request("/emails", "POST", options, payload, headers);
|
|
5489
|
+
}
|
|
5490
|
+
async listDomains(options = {}) {
|
|
5491
|
+
return this.request("/domains", "GET", options);
|
|
5492
|
+
}
|
|
5493
|
+
async getDomain(domainId, options = {}) {
|
|
5494
|
+
this.validateDomainId(domainId);
|
|
5495
|
+
return this.request(`/domains/${domainId}`, "GET", options);
|
|
5496
|
+
}
|
|
5497
|
+
async createDomain(payload, options = {}) {
|
|
5498
|
+
if (!payload.name.trim()) {
|
|
5499
|
+
throw new Error("Disco Mail domain name must be a non-empty string");
|
|
5500
|
+
}
|
|
5501
|
+
if (!payload.region.trim()) {
|
|
5502
|
+
throw new Error("Disco Mail domain region must be a non-empty string");
|
|
5503
|
+
}
|
|
5504
|
+
return this.request("/domains", "POST", options, payload);
|
|
5505
|
+
}
|
|
5506
|
+
async verifyDomain(domainId, options = {}) {
|
|
5507
|
+
this.validateDomainId(domainId);
|
|
5508
|
+
return this.request(`/domains/${domainId}/verify`, "PUT", options);
|
|
5509
|
+
}
|
|
5510
|
+
async request(path, method, options, body, additionalHeaders = new Headers()) {
|
|
5511
|
+
const apiKey = this.resolveApiKey(options);
|
|
5512
|
+
const headers = new Headers(additionalHeaders);
|
|
5513
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
5514
|
+
headers.set("Content-Type", "application/json");
|
|
5515
|
+
const response = await fetch(`${this.resolveBaseUrl(options)}${path}`, {
|
|
5516
|
+
method,
|
|
5517
|
+
headers,
|
|
5518
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
5519
|
+
});
|
|
5520
|
+
if (!response.ok) {
|
|
5521
|
+
throw await this.createApiError(response);
|
|
5522
|
+
}
|
|
5523
|
+
return await response.json();
|
|
5524
|
+
}
|
|
5525
|
+
resolveApiKey(options) {
|
|
5526
|
+
const apiKey = options.apiKey || this.clientOptions.apiKey || process.env.DISCO_MAIL_API_KEY || process.env.DISCOMAIL_API_KEY;
|
|
5527
|
+
if (!apiKey) {
|
|
5528
|
+
throw new Error(
|
|
5529
|
+
"Disco Mail API key is not provided. Pass options.apiKey or set DISCO_MAIL_API_KEY or DISCOMAIL_API_KEY."
|
|
5530
|
+
);
|
|
5531
|
+
}
|
|
5532
|
+
return apiKey;
|
|
5533
|
+
}
|
|
5534
|
+
resolveBaseUrl(options) {
|
|
5535
|
+
const baseUrl = options.baseUrl || this.clientOptions.baseUrl || DEFAULT_DISCO_MAIL_BASE_URL;
|
|
5536
|
+
return baseUrl.replace(/\/+$/, "");
|
|
5537
|
+
}
|
|
5538
|
+
validateSendRequest(payload) {
|
|
5539
|
+
const recipients = Array.isArray(payload.to) ? payload.to : [payload.to];
|
|
5540
|
+
if (recipients.length === 0 || recipients.some((recipient) => !recipient.trim())) {
|
|
5541
|
+
throw new Error("Disco Mail send requests require at least one non-empty recipient");
|
|
5542
|
+
}
|
|
5543
|
+
if (!payload.from.trim()) {
|
|
5544
|
+
throw new Error("Disco Mail send requests require a non-empty from address");
|
|
5545
|
+
}
|
|
5546
|
+
if (!payload.subject?.trim() && !payload.templateId?.trim()) {
|
|
5547
|
+
throw new Error("Disco Mail send requests require a non-empty subject or templateId");
|
|
5548
|
+
}
|
|
5549
|
+
if (!payload.html?.trim() && !payload.text?.trim()) {
|
|
5550
|
+
throw new Error("Disco Mail send requests require a non-empty html or text body");
|
|
5551
|
+
}
|
|
5552
|
+
if (payload.attachments && payload.attachments.length > 10) {
|
|
5553
|
+
throw new Error("Disco Mail send requests support a maximum of 10 attachments");
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5556
|
+
validateDomainId(domainId) {
|
|
5557
|
+
if (!Number.isInteger(domainId) || domainId <= 0) {
|
|
5558
|
+
throw new Error("Disco Mail domainId must be a positive integer");
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
async createApiError(response) {
|
|
5562
|
+
let body = null;
|
|
5563
|
+
try {
|
|
5564
|
+
body = await response.json();
|
|
5565
|
+
} catch {
|
|
5566
|
+
body = null;
|
|
5567
|
+
}
|
|
5568
|
+
const apiError = body?.error ?? body;
|
|
5569
|
+
return new DiscoMailApiError(
|
|
5570
|
+
apiError?.code || `HTTP_${response.status}`,
|
|
5571
|
+
apiError?.message || response.statusText || "Request failed",
|
|
5572
|
+
response.status
|
|
5573
|
+
);
|
|
5574
|
+
}
|
|
5575
|
+
};
|
|
1927
5576
|
|
|
1928
5577
|
// src/index.ts
|
|
1929
5578
|
init_alpaca_trading_api();
|
|
1930
5579
|
init_alpaca_market_data_api();
|
|
1931
5580
|
init_alpaca_trading_api();
|
|
1932
5581
|
init_alpaca_market_data_api();
|
|
5582
|
+
var disco = {
|
|
5583
|
+
types: types_exports,
|
|
5584
|
+
alpaca: {
|
|
5585
|
+
TradingAPI: AlpacaTradingAPI,
|
|
5586
|
+
MarketDataAPI: AlpacaMarketDataAPI
|
|
5587
|
+
},
|
|
5588
|
+
format: {
|
|
5589
|
+
capFirstLetter,
|
|
5590
|
+
currency: formatCurrency,
|
|
5591
|
+
number: formatNumber,
|
|
5592
|
+
pct: formatPercentage,
|
|
5593
|
+
dateTimeForGS
|
|
5594
|
+
},
|
|
5595
|
+
llm: {
|
|
5596
|
+
call: makeLLMCall,
|
|
5597
|
+
uploadVisionFile,
|
|
5598
|
+
seek: makeDeepseekCall,
|
|
5599
|
+
images: makeImagesCall,
|
|
5600
|
+
open: makeOpenRouterCall
|
|
5601
|
+
},
|
|
5602
|
+
mail: new DiscoMailAPI(),
|
|
5603
|
+
time: {
|
|
5604
|
+
convertDateToMarketTimeZone,
|
|
5605
|
+
getStartAndEndDates,
|
|
5606
|
+
getMarketOpenClose,
|
|
5607
|
+
getOpenCloseForTradingDay,
|
|
5608
|
+
getLastFullTradingDate,
|
|
5609
|
+
getNextMarketDay,
|
|
5610
|
+
getPreviousMarketDay,
|
|
5611
|
+
getMarketTimePeriod,
|
|
5612
|
+
getMarketStatus,
|
|
5613
|
+
getNYTimeZone,
|
|
5614
|
+
getTradingDate,
|
|
5615
|
+
getTradingStartAndEndDates,
|
|
5616
|
+
getTradingDaysBack,
|
|
5617
|
+
isMarketDay,
|
|
5618
|
+
isWithinMarketHours,
|
|
5619
|
+
countTradingDays,
|
|
5620
|
+
MARKET_TIMES
|
|
5621
|
+
},
|
|
5622
|
+
utils: {
|
|
5623
|
+
logIfDebug,
|
|
5624
|
+
fetchWithRetry,
|
|
5625
|
+
Timer: PerformanceTimer
|
|
5626
|
+
}
|
|
5627
|
+
};
|
|
1933
5628
|
|
|
1934
5629
|
// src/test.ts
|
|
1935
5630
|
init_logging();
|
|
@@ -1973,5 +5668,147 @@ function testOpenAIModelRegistryAndPricing() {
|
|
|
1973
5668
|
}
|
|
1974
5669
|
log3(`${directModel} registry and pricing test passed`);
|
|
1975
5670
|
}
|
|
1976
|
-
|
|
5671
|
+
function restoreEnvironmentVariable(name, value) {
|
|
5672
|
+
if (value === void 0) {
|
|
5673
|
+
delete process.env[name];
|
|
5674
|
+
return;
|
|
5675
|
+
}
|
|
5676
|
+
process.env[name] = value;
|
|
5677
|
+
}
|
|
5678
|
+
async function testDiscoMailAPI() {
|
|
5679
|
+
const originalFetch = globalThis.fetch;
|
|
5680
|
+
const originalDiscoMailApiKey = process.env.DISCO_MAIL_API_KEY;
|
|
5681
|
+
const originalDiscoMailLegacyApiKey = process.env.DISCOMAIL_API_KEY;
|
|
5682
|
+
const calls = [];
|
|
5683
|
+
const responses = [
|
|
5684
|
+
new Response(JSON.stringify({ emailId: "email_123" }), { status: 200 }),
|
|
5685
|
+
new Response(JSON.stringify([]), { status: 200 }),
|
|
5686
|
+
new Response(JSON.stringify([]), { status: 200 })
|
|
5687
|
+
];
|
|
5688
|
+
globalThis.fetch = async (input, init) => {
|
|
5689
|
+
calls.push({ input, init });
|
|
5690
|
+
const response = responses.shift();
|
|
5691
|
+
if (!response) {
|
|
5692
|
+
throw new Error("Unexpected Disco Mail fetch call");
|
|
5693
|
+
}
|
|
5694
|
+
return response;
|
|
5695
|
+
};
|
|
5696
|
+
try {
|
|
5697
|
+
process.env.DISCO_MAIL_API_KEY = "disco-primary-key";
|
|
5698
|
+
process.env.DISCOMAIL_API_KEY = "disco-legacy-key";
|
|
5699
|
+
const client = new DiscoMailAPI();
|
|
5700
|
+
const sendResult = await client.send(
|
|
5701
|
+
{
|
|
5702
|
+
to: "recipient@example.net",
|
|
5703
|
+
from: "Disco Media <hello@discomedia.co>",
|
|
5704
|
+
subject: "Test message",
|
|
5705
|
+
text: "Test body"
|
|
5706
|
+
},
|
|
5707
|
+
{
|
|
5708
|
+
apiKey: "disco-explicit-key",
|
|
5709
|
+
baseUrl: "https://mail-test.example/api/v1/",
|
|
5710
|
+
idempotencyKey: "mail-test-123"
|
|
5711
|
+
}
|
|
5712
|
+
);
|
|
5713
|
+
if (sendResult.emailId !== "email_123") {
|
|
5714
|
+
throw new Error(`Expected Disco Mail emailId email_123, received ${sendResult.emailId}`);
|
|
5715
|
+
}
|
|
5716
|
+
if (calls[0]?.input !== "https://mail-test.example/api/v1/emails") {
|
|
5717
|
+
throw new Error(`Unexpected Disco Mail send URL: ${String(calls[0]?.input)}`);
|
|
5718
|
+
}
|
|
5719
|
+
const sendHeaders = new Headers(calls[0]?.init?.headers);
|
|
5720
|
+
if (sendHeaders.get("Authorization") !== "Bearer disco-explicit-key") {
|
|
5721
|
+
throw new Error("Disco Mail explicit API key did not take precedence");
|
|
5722
|
+
}
|
|
5723
|
+
if (sendHeaders.get("Idempotency-Key") !== "mail-test-123") {
|
|
5724
|
+
throw new Error("Disco Mail idempotency key was not sent");
|
|
5725
|
+
}
|
|
5726
|
+
if (calls[0]?.init?.body !== JSON.stringify({
|
|
5727
|
+
to: "recipient@example.net",
|
|
5728
|
+
from: "Disco Media <hello@discomedia.co>",
|
|
5729
|
+
subject: "Test message",
|
|
5730
|
+
text: "Test body"
|
|
5731
|
+
})) {
|
|
5732
|
+
throw new Error("Disco Mail send payload was not serialized as expected");
|
|
5733
|
+
}
|
|
5734
|
+
await client.listDomains({ baseUrl: "https://mail-test.example/api/v1" });
|
|
5735
|
+
const primaryEnvironmentHeaders = new Headers(calls[1]?.init?.headers);
|
|
5736
|
+
if (primaryEnvironmentHeaders.get("Authorization") !== "Bearer disco-primary-key") {
|
|
5737
|
+
throw new Error("DISCO_MAIL_API_KEY was not used when no explicit key was passed");
|
|
5738
|
+
}
|
|
5739
|
+
delete process.env.DISCO_MAIL_API_KEY;
|
|
5740
|
+
await client.listDomains({ baseUrl: "https://mail-test.example/api/v1" });
|
|
5741
|
+
const legacyEnvironmentHeaders = new Headers(calls[2]?.init?.headers);
|
|
5742
|
+
if (legacyEnvironmentHeaders.get("Authorization") !== "Bearer disco-legacy-key") {
|
|
5743
|
+
throw new Error("DISCOMAIL_API_KEY was not used as the fallback key");
|
|
5744
|
+
}
|
|
5745
|
+
globalThis.fetch = async () => new Response(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Sender is invalid" } }), {
|
|
5746
|
+
status: 400,
|
|
5747
|
+
statusText: "Bad Request"
|
|
5748
|
+
});
|
|
5749
|
+
try {
|
|
5750
|
+
await client.listDomains({ apiKey: "disco-explicit-key" });
|
|
5751
|
+
throw new Error("Expected Disco Mail API error");
|
|
5752
|
+
} catch (error) {
|
|
5753
|
+
if (!(error instanceof DiscoMailApiError)) {
|
|
5754
|
+
throw error;
|
|
5755
|
+
}
|
|
5756
|
+
if (error.code !== "BAD_REQUEST" || error.status !== 400 || !error.message.includes("Sender is invalid")) {
|
|
5757
|
+
throw new Error(`Unexpected Disco Mail API error: ${error.message}`);
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
delete process.env.DISCOMAIL_API_KEY;
|
|
5761
|
+
try {
|
|
5762
|
+
await client.listDomains();
|
|
5763
|
+
throw new Error("Expected missing Disco Mail API key error");
|
|
5764
|
+
} catch (error) {
|
|
5765
|
+
if (!(error instanceof Error) || !error.message.includes("DISCO_MAIL_API_KEY")) {
|
|
5766
|
+
throw error;
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
try {
|
|
5770
|
+
await client.send(
|
|
5771
|
+
{
|
|
5772
|
+
to: "",
|
|
5773
|
+
from: "Disco Media <hello@discomedia.co>",
|
|
5774
|
+
subject: "Invalid message",
|
|
5775
|
+
text: "Test body"
|
|
5776
|
+
},
|
|
5777
|
+
{ apiKey: "disco-explicit-key" }
|
|
5778
|
+
);
|
|
5779
|
+
throw new Error("Expected invalid Disco Mail payload error");
|
|
5780
|
+
} catch (error) {
|
|
5781
|
+
if (!(error instanceof Error) || !error.message.includes("non-empty recipient")) {
|
|
5782
|
+
throw error;
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
log3("Disco Mail mocked API tests passed");
|
|
5786
|
+
} finally {
|
|
5787
|
+
globalThis.fetch = originalFetch;
|
|
5788
|
+
restoreEnvironmentVariable("DISCO_MAIL_API_KEY", originalDiscoMailApiKey);
|
|
5789
|
+
restoreEnvironmentVariable("DISCOMAIL_API_KEY", originalDiscoMailLegacyApiKey);
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
async function testLiveDiscoMailDomainLookup() {
|
|
5793
|
+
if (!process.env.DISCO_MAIL_API_KEY && !process.env.DISCOMAIL_API_KEY) {
|
|
5794
|
+
log3("Skipping live Disco Mail domain lookup: no Disco Mail API key is configured");
|
|
5795
|
+
return;
|
|
5796
|
+
}
|
|
5797
|
+
const domains = await disco.mail.listDomains();
|
|
5798
|
+
const discomediaDomain = domains.find((domain) => domain.name === "discomedia.co");
|
|
5799
|
+
if (!discomediaDomain) {
|
|
5800
|
+
throw new Error("Live Disco Mail API lookup did not return discomedia.co");
|
|
5801
|
+
}
|
|
5802
|
+
log3(`Live Disco Mail domain lookup passed: discomedia.co is ${discomediaDomain.status}`);
|
|
5803
|
+
}
|
|
5804
|
+
async function runActiveTests() {
|
|
5805
|
+
testOpenAIModelRegistryAndPricing();
|
|
5806
|
+
await testDiscoMailAPI();
|
|
5807
|
+
await testLiveDiscoMailDomainLookup();
|
|
5808
|
+
}
|
|
5809
|
+
void runActiveTests().catch((error) => {
|
|
5810
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5811
|
+
console.error(`Test run failed: ${message}`);
|
|
5812
|
+
process.exitCode = 1;
|
|
5813
|
+
});
|
|
1977
5814
|
//# sourceMappingURL=test.mjs.map
|