@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/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
- // src/index.ts
1630
- init_market_time();
1631
-
1632
- // src/types/llm-types.ts
1633
- var OPENAI_MODELS = [
1634
- "gpt-5.6",
1635
- "gpt-5.6-sol",
1636
- "gpt-5.6-terra",
1637
- "gpt-5.6-luna",
1638
- "gpt-5",
1639
- "gpt-5-mini",
1640
- "gpt-5.4-mini",
1641
- "gpt-5.4-nano",
1642
- "gpt-5-nano",
1643
- "gpt-5.1",
1644
- "gpt-5.4",
1645
- "gpt-5.5",
1646
- "gpt-5.5-pro",
1647
- "gpt-5.2",
1648
- "gpt-5.2-pro",
1649
- "gpt-5.1-codex",
1650
- "gpt-5.1-codex-max"
1651
- ];
1652
- function isOpenAIModel(model) {
1653
- return OPENAI_MODELS.includes(model);
1654
- }
1655
- var OPENROUTER_MODELS = [
1656
- "openai/gpt-5",
1657
- "openai/gpt-5-mini",
1658
- "openai/gpt-5.4-mini",
1659
- "openai/gpt-5.4-nano",
1660
- "openai/gpt-5-nano",
1661
- "openai/gpt-5.1",
1662
- "openai/gpt-5.4",
1663
- "openai/gpt-5.4-pro",
1664
- "openai/gpt-5.5",
1665
- "openai/gpt-5.5-pro",
1666
- "openai/gpt-5.2",
1667
- "openai/gpt-5.2-pro",
1668
- "openai/gpt-5.1-codex",
1669
- "openai/gpt-5.1-codex-max",
1670
- "openai/gpt-oss-120b",
1671
- "z.ai/glm-4.5",
1672
- "z.ai/glm-4.5-air",
1673
- "google/gemini-2.5-flash",
1674
- "google/gemini-2.5-flash-lite",
1675
- "deepseek/deepseek-r1-0528",
1676
- "deepseek/deepseek-chat-v3-0324"
1677
- ];
1678
- function isOpenRouterModel(model) {
1679
- return OPENROUTER_MODELS.includes(model);
1680
- }
1681
-
1682
- // src/llm-openai.ts
1683
- import OpenAI2 from "openai";
1684
- import { zodTextFormat } from "openai/helpers/zod";
1685
-
1686
- // src/llm-config.ts
1687
- var DEFAULT_MODEL = "gpt-5.6-luna";
1688
- var GPT_5_HIGH_CONTEXT_THRESHOLD_TOKENS = 272e3;
1689
- var GPT_5_HIGH_CONTEXT_INPUT_MULTIPLIER = 2;
1690
- var GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER = 1.5;
1691
- var openAiModelCosts = {
1692
- "gpt-5.6": {
1693
- inputCost: 5 / 1e6,
1694
- cacheHitCost: 0.5 / 1e6,
1695
- cacheWriteCost: 6.25 / 1e6,
1696
- outputCost: 30 / 1e6
1697
- },
1698
- "gpt-5.6-sol": {
1699
- inputCost: 5 / 1e6,
1700
- cacheHitCost: 0.5 / 1e6,
1701
- cacheWriteCost: 6.25 / 1e6,
1702
- outputCost: 30 / 1e6
1703
- },
1704
- "gpt-5.6-terra": {
1705
- inputCost: 2.5 / 1e6,
1706
- cacheHitCost: 0.25 / 1e6,
1707
- cacheWriteCost: 3.125 / 1e6,
1708
- outputCost: 15 / 1e6
1709
- },
1710
- "gpt-5.6-luna": {
1711
- inputCost: 1 / 1e6,
1712
- cacheHitCost: 0.1 / 1e6,
1713
- cacheWriteCost: 1.25 / 1e6,
1714
- outputCost: 6 / 1e6
1715
- },
1716
- "gpt-5": {
1717
- inputCost: 1.25 / 1e6,
1718
- cacheHitCost: 0.125 / 1e6,
1719
- outputCost: 10 / 1e6
1720
- },
1721
- "gpt-5-mini": {
1722
- inputCost: 0.25 / 1e6,
1723
- cacheHitCost: 0.025 / 1e6,
1724
- outputCost: 2 / 1e6
1725
- },
1726
- "gpt-5.4-mini": {
1727
- inputCost: 0.75 / 1e6,
1728
- cacheHitCost: 0.075 / 1e6,
1729
- outputCost: 4.5 / 1e6
1730
- },
1731
- "gpt-5.4-nano": {
1732
- inputCost: 0.2 / 1e6,
1733
- cacheHitCost: 0.02 / 1e6,
1734
- outputCost: 1.25 / 1e6
1735
- },
1736
- "gpt-5-nano": {
1737
- inputCost: 0.05 / 1e6,
1738
- cacheHitCost: 5e-3 / 1e6,
1739
- outputCost: 0.4 / 1e6
1740
- },
1741
- "gpt-5.1": {
1742
- inputCost: 1.25 / 1e6,
1743
- cacheHitCost: 0.125 / 1e6,
1744
- outputCost: 10 / 1e6
1745
- },
1746
- "gpt-5.4": {
1747
- inputCost: 2.5 / 1e6,
1748
- cacheHitCost: 0.25 / 1e6,
1749
- outputCost: 15 / 1e6
1750
- },
1751
- "gpt-5.5": {
1752
- inputCost: 5 / 1e6,
1753
- cacheHitCost: 0.5 / 1e6,
1754
- outputCost: 30 / 1e6
1755
- },
1756
- "gpt-5.5-pro": {
1757
- inputCost: 30 / 1e6,
1758
- outputCost: 180 / 1e6
1759
- },
1760
- "gpt-5.2": {
1761
- inputCost: 1.75 / 1e6,
1762
- cacheHitCost: 0.175 / 1e6,
1763
- outputCost: 14 / 1e6
1764
- },
1765
- "gpt-5.2-pro": {
1766
- inputCost: 21 / 1e6,
1767
- outputCost: 168 / 1e6
1768
- },
1769
- "gpt-5.1-codex": {
1770
- inputCost: 1.25 / 1e6,
1771
- cacheHitCost: 0.125 / 1e6,
1772
- outputCost: 10 / 1e6
1773
- },
1774
- "gpt-5.1-codex-max": {
1775
- inputCost: 1.25 / 1e6,
1776
- cacheHitCost: 0.125 / 1e6,
1777
- outputCost: 10 / 1e6
1778
- }
1779
- };
1780
- var deepseekModelCosts = {
1781
- "deepseek-chat": {
1782
- inputCost: 0.27 / 1e6,
1783
- // $0.27 per 1M tokens (Cache miss price)
1784
- cacheHitCost: 0.07 / 1e6,
1785
- // $0.07 per 1M tokens (Cache hit price)
1786
- outputCost: 1.1 / 1e6
1787
- // $1.10 per 1M tokens
1788
- },
1789
- "deepseek-reasoner": {
1790
- inputCost: 0.55 / 1e6,
1791
- // $0.55 per 1M tokens (Cache miss price)
1792
- cacheHitCost: 0.14 / 1e6,
1793
- // $0.14 per 1M tokens (Cache hit price)
1794
- outputCost: 2.19 / 1e6
1795
- // $2.19 per 1M tokens
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
- high: {
1867
- "1024x1024": 0.167,
1868
- "1024x1536": 0.25,
1869
- "1536x1024": 0.25
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
- "gpt-image-1-mini": {
1873
- low: {
1874
- "1024x1024": 5e-3,
1875
- "1024x1536": 6e-3,
1876
- "1536x1024": 6e-3
1877
- },
1878
- medium: {
1879
- "1024x1024": 0.011,
1880
- "1024x1536": 0.015,
1881
- "1536x1024": 0.015
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
- var openAiImageCosts = {
1891
- "gpt-image-2": openAiImageCostByQualityAndSize["gpt-image-2"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
1892
- "gpt-image-2-2026-04-21": openAiImageCostByQualityAndSize["gpt-image-2-2026-04-21"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
1893
- "gpt-image-1.5": openAiImageCostByQualityAndSize["gpt-image-1.5"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
1894
- "gpt-image-1": openAiImageCostByQualityAndSize["gpt-image-1"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE],
1895
- "gpt-image-1-mini": openAiImageCostByQualityAndSize["gpt-image-1-mini"][DEFAULT_IMAGE_PRICING_QUALITY][DEFAULT_IMAGE_PRICING_SIZE]
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
- const modelCosts = provider === "deepseek" ? deepseekModelCosts[model] : openAiModelCosts[model];
1902
- if (!modelCosts) return 0;
1903
- const boundedCacheHitTokens = Math.min(Math.max(cacheHitTokens || 0, 0), inputTokens);
1904
- const boundedCacheWriteTokens = Math.min(Math.max(cacheWriteTokens || 0, 0), inputTokens - boundedCacheHitTokens);
1905
- const uncachedInputTokens = inputTokens - boundedCacheHitTokens - boundedCacheWriteTokens;
1906
- const inputCost = uncachedInputTokens * modelCosts.inputCost + boundedCacheHitTokens * (modelCosts.cacheHitCost || modelCosts.inputCost) + boundedCacheWriteTokens * (modelCosts.cacheWriteCost || modelCosts.inputCost);
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
- return inputCost + outputCost + reasoningCost;
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
- testOpenAIModelRegistryAndPricing();
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