@adaptic/utils 0.0.1031 → 0.0.1033

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/index.cjs +631 -208
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.mjs +627 -209
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/types/__tests__/indicator-parity/generate.d.ts +64 -0
  6. package/dist/types/__tests__/indicator-parity/generate.d.ts.map +1 -0
  7. package/dist/types/__tests__/indicator-parity/record.d.ts +144 -0
  8. package/dist/types/__tests__/indicator-parity/record.d.ts.map +1 -0
  9. package/dist/types/__tests__/indicator-parity/reference.d.ts +92 -0
  10. package/dist/types/__tests__/indicator-parity/reference.d.ts.map +1 -0
  11. package/dist/types/__tests__/indicator-parity/series.d.ts +64 -0
  12. package/dist/types/__tests__/indicator-parity/series.d.ts.map +1 -0
  13. package/dist/types/__tests__/indicator-parity/subjects.d.ts +55 -0
  14. package/dist/types/__tests__/indicator-parity/subjects.d.ts.map +1 -0
  15. package/dist/types/__tests__/support/statistic.d.ts +18 -0
  16. package/dist/types/__tests__/support/statistic.d.ts.map +1 -0
  17. package/dist/types/alpaca/trading/order-utils.d.ts.map +1 -1
  18. package/dist/types/index.d.ts +1 -0
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/dist/types/llm/circuit-breaker.d.ts +18 -1
  21. package/dist/types/llm/circuit-breaker.d.ts.map +1 -1
  22. package/dist/types/llm/fallback-chain.d.ts.map +1 -1
  23. package/dist/types/llm/index.d.ts +3 -1
  24. package/dist/types/llm/index.d.ts.map +1 -1
  25. package/dist/types/llm/rate-guard.d.ts +82 -9
  26. package/dist/types/llm/rate-guard.d.ts.map +1 -1
  27. package/dist/types/llm/structured-content.d.ts +73 -0
  28. package/dist/types/llm/structured-content.d.ts.map +1 -0
  29. package/dist/types/llm/transports/gateway.d.ts.map +1 -1
  30. package/dist/types/metrics-calcs.d.ts +25 -0
  31. package/dist/types/metrics-calcs.d.ts.map +1 -1
  32. package/dist/types/performance-metrics.d.ts +16 -4
  33. package/dist/types/performance-metrics.d.ts.map +1 -1
  34. package/dist/types/sample-statistic.d.ts +123 -0
  35. package/dist/types/sample-statistic.d.ts.map +1 -0
  36. package/dist/types/schemas/alpaca-schemas.d.ts.map +1 -1
  37. package/dist/types/strategy-metrics.d.ts +38 -16
  38. package/dist/types/strategy-metrics.d.ts.map +1 -1
  39. package/dist/types/trading-policy/schemas/effective-policy.schema.d.ts +18 -18
  40. package/dist/types/trading-policy/schemas/model-prefs.schema.d.ts +24 -24
  41. package/dist/types/trading-policy/schemas/policy-mutation.schema.d.ts +36 -36
  42. package/dist/types/types/alpaca-types.d.ts +36 -5
  43. package/dist/types/types/alpaca-types.d.ts.map +1 -1
  44. package/package.json +3 -1
package/dist/index.cjs CHANGED
@@ -20243,6 +20243,93 @@ function getEquityValues(equityData, portfolioHistory, marketTimeUtil, period) {
20243
20243
  };
20244
20244
  }
20245
20245
 
20246
+ /**
20247
+ * A measured statistic and the cohort it was measured on, carried as one
20248
+ * inseparable value.
20249
+ *
20250
+ * A ratio is meaningless without the population it was taken over: the same
20251
+ * `0.42` is a strong result on 2,000 trades and noise on five, and a `0.0`
20252
+ * returned because nothing could be computed is indistinguishable from a `0.0`
20253
+ * that was genuinely measured. Both confusions are the same error — a number
20254
+ * read apart from its unit and its cohort — and both have produced wrong
20255
+ * conclusions from correct arithmetic.
20256
+ *
20257
+ * This type removes the option. Every statistic shaped by a population carries
20258
+ * `sampleCount` (how many observations actually entered the computation) and
20259
+ * `coverage` (what fraction of the observations the caller offered were usable),
20260
+ * on BOTH branches: an unavailable statistic still reports how much data it
20261
+ * saw, because "we had nothing" and "we had 900 rows and still could not
20262
+ * compute it" are different facts with different responses.
20263
+ *
20264
+ * Absence is a branch of the union rather than a sentinel value. There is no
20265
+ * number a caller can read without first proving the statistic exists, which is
20266
+ * what keeps an unknown from silently becoming a zero on its way to a decision.
20267
+ *
20268
+ * @module sample-statistic
20269
+ */
20270
+ /**
20271
+ * Build the cohort descriptor for a computation.
20272
+ *
20273
+ * `coverage` is derived here rather than supplied, so it cannot drift from the
20274
+ * counts it claims to summarise. A zero request yields zero coverage: no
20275
+ * observations were asked for, so none were covered, and the alternative (`1`)
20276
+ * would report a vacuous computation as fully covered.
20277
+ *
20278
+ * @param requestedCount - Observations offered, or the window width requested.
20279
+ * @param sampleCount - Observations that entered the computation.
20280
+ * @returns The cohort descriptor with `coverage` derived from the two counts.
20281
+ * @throws When either count is negative or non-finite, which is a programming
20282
+ * error rather than a data condition.
20283
+ */
20284
+ function sampleCohort(requestedCount, sampleCount) {
20285
+ if (!Number.isFinite(requestedCount) || requestedCount < 0) {
20286
+ throw new Error(`sampleCohort: requestedCount must be a non-negative finite number (got ${requestedCount})`);
20287
+ }
20288
+ if (!Number.isFinite(sampleCount) || sampleCount < 0) {
20289
+ throw new Error(`sampleCohort: sampleCount must be a non-negative finite number (got ${sampleCount})`);
20290
+ }
20291
+ const NOTHING_REQUESTED_COVERAGE = 0;
20292
+ const FULL_COVERAGE = 1;
20293
+ const coverage = requestedCount === 0
20294
+ ? NOTHING_REQUESTED_COVERAGE
20295
+ : Math.min(FULL_COVERAGE, sampleCount / requestedCount);
20296
+ return { sampleCount, requestedCount, coverage };
20297
+ }
20298
+ /**
20299
+ * Wrap a computed value with its cohort.
20300
+ *
20301
+ * @param value - The measured statistic.
20302
+ * @param cohort - The cohort it was measured on.
20303
+ * @returns The available branch of {@link SampleStatistic}.
20304
+ */
20305
+ function availableStatistic(value, cohort) {
20306
+ return { available: true, value, ...cohort };
20307
+ }
20308
+ /**
20309
+ * Record that a statistic could not be computed, and what was seen instead.
20310
+ *
20311
+ * @param reason - Which class of failure prevented the computation.
20312
+ * @param detail - Specifics for logs; never machine-parsed.
20313
+ * @param cohort - What data was available when the attempt was abandoned.
20314
+ * @returns The unavailable branch of {@link SampleStatistic}.
20315
+ */
20316
+ function unavailableStatistic(reason, detail, cohort) {
20317
+ return { available: false, reason, detail, ...cohort };
20318
+ }
20319
+ /**
20320
+ * Narrow a statistic to its available branch.
20321
+ *
20322
+ * Exists so consumers in other packages can discriminate without restating the
20323
+ * predicate, and so the discriminant stays a single named concept if the shape
20324
+ * ever grows a third branch.
20325
+ *
20326
+ * @param statistic - The statistic to test.
20327
+ * @returns Whether the statistic carries a value.
20328
+ */
20329
+ function isAvailable(statistic) {
20330
+ return statistic.available;
20331
+ }
20332
+
20246
20333
  // risk-free-rate.ts
20247
20334
  /**
20248
20335
  * Conservative fallback annual risk-free rate used when no live rate has been
@@ -20601,57 +20688,60 @@ function alignReturns(tradeBars, benchmarkBars) {
20601
20688
  });
20602
20689
  return { alignedTradeReturns, alignedBenchmarkReturns, alignedDates };
20603
20690
  }
20604
- /*
20605
- * Calculate Beta from Returns
20606
- * @param portfolioReturns - Array of portfolio returns
20607
- * @param benchmarkReturns - Array of benchmark returns
20608
- * @returns Object containing beta, covariance, variance, and average returns
20691
+ /**
20692
+ * Beta of a portfolio against a benchmark, from paired period returns.
20693
+ *
20694
+ * Non-finite rows are dropped pairwise — a return that is `NaN` on either leg
20695
+ * cannot contribute to a covariance — and the count that survives is reported
20696
+ * as the cohort rather than discarded. That reporting is the point: silently
20697
+ * computing a beta on the 12 rows that happened to be clean, and returning it
20698
+ * with the same shape as a beta over all 900, is how a statistic measured on
20699
+ * one population gets applied to another.
20700
+ *
20701
+ * When beta cannot be computed the result is the unavailable branch, never a
20702
+ * numeric stand-in. A beta of `0` asserts that the portfolio does not move with
20703
+ * the market, which is a strong and consequential claim; emitting it to mean
20704
+ * "we could not tell" makes every alpha derived from it wrong by the whole
20705
+ * benchmark term.
20706
+ *
20707
+ * @param portfolioReturns - Portfolio period returns.
20708
+ * @param benchmarkReturns - Benchmark period returns, index-aligned to the portfolio.
20709
+ * @returns The beta components with their cohort, or a typed unavailable result.
20609
20710
  * @example
20610
- * const portfolioReturns = [0.05, -0.02, 0.03];
20611
- * const benchmarkReturns = [0.03, -0.01, 0.02];
20612
- * const beta = calculateBetaFromReturns(portfolioReturns, benchmarkReturns);
20613
- * // beta = { beta: 1.5, covariance: 0.0005, variance: 0.0003, averagePortfolioReturn: 0.02, averageBenchmarkReturn: 0.02 }
20614
- * @throws Will log warnings if input data is invalid or insufficient
20615
- * @throws Will log warnings if benchmark variance is effectively zero
20616
- * @throws Will log warnings if beta calculation results in a non-finite value
20617
- * @throws Will log warnings if there are not enough valid data points for calculation
20618
- * @throws Will log warnings if benchmark variance is zero or non-finite
20711
+ * const result = calculateBetaFromReturns([0.05, -0.02, 0.03], [0.03, -0.01, 0.02]);
20712
+ * if (result.available) {
20713
+ * // result.value.beta, alongside result.sampleCount and result.coverage
20714
+ * }
20619
20715
  */
20620
20716
  function calculateBetaFromReturns$1(portfolioReturns, benchmarkReturns) {
20621
- // Input validation
20622
- if (!Array.isArray(portfolioReturns) ||
20623
- !Array.isArray(benchmarkReturns) ||
20624
- portfolioReturns.length !== benchmarkReturns.length ||
20625
- portfolioReturns.length < 2) {
20626
- getLogger().warn("Invalid or insufficient return data for beta calculation");
20627
- return {
20628
- beta: 0,
20629
- covariance: 0,
20630
- variance: 0,
20631
- averagePortfolioReturn: 0,
20632
- averageBenchmarkReturn: 0,
20633
- };
20634
- }
20635
- // Filter out any non-finite values before calculations
20636
- const validIndices = [...Array(portfolioReturns.length).keys()].filter((i) => isFinite(portfolioReturns[i]) && isFinite(benchmarkReturns[i]));
20637
- if (validIndices.length < 2) {
20638
- getLogger().warn("Not enough valid data points for beta calculation");
20639
- return {
20640
- beta: 0,
20641
- covariance: 0,
20642
- variance: 0,
20643
- averagePortfolioReturn: 0,
20644
- averageBenchmarkReturn: 0,
20645
- };
20717
+ // A covariance is defined over PAIRS, so the offered cohort is the number of
20718
+ // index positions both series can supply. Ragged input is a caller defect
20719
+ // rather than a data condition, and it is reported as such instead of being
20720
+ // silently truncated to the shorter series.
20721
+ if (!Array.isArray(portfolioReturns) || !Array.isArray(benchmarkReturns)) {
20722
+ return unavailableStatistic("invalid_input", "portfolioReturns and benchmarkReturns must both be arrays", sampleCohort(0, 0));
20723
+ }
20724
+ const requestedCount = portfolioReturns.length;
20725
+ if (portfolioReturns.length !== benchmarkReturns.length) {
20726
+ return unavailableStatistic("invalid_input", `series lengths differ: portfolio ${portfolioReturns.length}, benchmark ${benchmarkReturns.length}`, sampleCohort(requestedCount, 0));
20727
+ }
20728
+ // Pairwise finiteness filter. Both legs must be usable for the pair to
20729
+ // contribute; keeping a pair on the strength of one leg would mix a real
20730
+ // observation with a fabricated one.
20731
+ const validIndices = [...Array(requestedCount).keys()].filter((i) => isFinite(portfolioReturns[i]) && isFinite(benchmarkReturns[i]));
20732
+ const cohort = sampleCohort(requestedCount, validIndices.length);
20733
+ // Bessel-corrected estimators need at least one degree of freedom, so two
20734
+ // usable pairs is the floor below which no sample variance exists.
20735
+ const MIN_PAIRS_FOR_SAMPLE_VARIANCE = 2;
20736
+ if (validIndices.length < MIN_PAIRS_FOR_SAMPLE_VARIANCE) {
20737
+ getLogger().warn(`Beta unavailable: ${validIndices.length} usable pairs of ${requestedCount} offered.`);
20738
+ return unavailableStatistic(validIndices.length === 0 ? "no_usable_samples" : "insufficient_samples", `beta needs at least ${MIN_PAIRS_FOR_SAMPLE_VARIANCE} finite pairs; ${validIndices.length} of ${requestedCount} were usable`, cohort);
20646
20739
  }
20647
- // Use validated indices only
20648
20740
  const validPortfolioReturns = validIndices.map((i) => portfolioReturns[i]);
20649
20741
  const validBenchmarkReturns = validIndices.map((i) => benchmarkReturns[i]);
20650
- // Calculate means
20651
20742
  const n = validIndices.length;
20652
20743
  const averagePortfolioReturn = validPortfolioReturns.reduce((sum, ret) => sum + ret, 0) / n;
20653
20744
  const averageBenchmarkReturn = validBenchmarkReturns.reduce((sum, ret) => sum + ret, 0) / n;
20654
- // Calculate covariance and variance with Welford's online algorithm for numerical stability
20655
20745
  let covariance = 0;
20656
20746
  let variance = 0;
20657
20747
  for (let i = 0; i < n; i++) {
@@ -20660,30 +20750,26 @@ function calculateBetaFromReturns$1(portfolioReturns, benchmarkReturns) {
20660
20750
  covariance += portfolioDiff * benchmarkDiff;
20661
20751
  variance += benchmarkDiff * benchmarkDiff;
20662
20752
  }
20663
- // Finalize calculations using sample (Bessel-corrected) estimators —
20664
- // divide by (n - 1), not n. The guard above (validIndices.length < 2)
20665
- // already ensures n >= 2, so (n - 1) is always safe.
20753
+ // Sample (Bessel-corrected) estimators — divide by (n - 1), not n. The guard
20754
+ // above ensures n >= 2, so (n - 1) is always safe.
20666
20755
  covariance /= n - 1;
20667
20756
  variance /= n - 1;
20668
- // Handle zero variance case
20669
- if (Math.abs(variance) < 1e-10) {
20670
- getLogger().warn("Benchmark variance is effectively zero. Setting beta to 0.");
20671
- return {
20672
- beta: 0,
20673
- covariance,
20674
- variance,
20675
- averagePortfolioReturn,
20676
- averageBenchmarkReturn,
20677
- };
20678
- }
20679
- const beta = covariance / variance;
20680
- return {
20681
- beta,
20757
+ // A benchmark that never moved has no variance to regress against, so beta is
20758
+ // undefined rather than zero. VARIANCE_NOISE_FLOOR absorbs the case where a
20759
+ // constant series still produces a tiny positive variance because the computed
20760
+ // mean differs from the constant by a rounding unit.
20761
+ const VARIANCE_NOISE_FLOOR = 1e-10;
20762
+ if (Math.abs(variance) < VARIANCE_NOISE_FLOOR) {
20763
+ getLogger().warn("Beta unavailable: benchmark variance is effectively zero.");
20764
+ return unavailableStatistic("degenerate_population", `benchmark variance ${variance} is below the noise floor ${VARIANCE_NOISE_FLOOR}; beta is undefined`, cohort);
20765
+ }
20766
+ return availableStatistic({
20767
+ beta: covariance / variance,
20682
20768
  covariance,
20683
20769
  variance,
20684
20770
  averagePortfolioReturn,
20685
20771
  averageBenchmarkReturn,
20686
- };
20772
+ }, cohort);
20687
20773
  }
20688
20774
  /**
20689
20775
  * Calculates the total return for a position, respecting position direction
@@ -20767,7 +20853,18 @@ async function calculateAlphaAndBeta$1(tradeBars, benchmarkBars, isShort) {
20767
20853
  : rawTradeReturns;
20768
20854
  // Calculate beta with position-adjusted returns
20769
20855
  const beta = calculateBetaFromReturns$1(alignedTradeReturns, alignedBenchmarkReturns);
20770
- if (!isFinite(beta.beta)) {
20856
+ // Alpha is the return left over after the benchmark term, so an unknown beta
20857
+ // makes alpha unknown too. Substituting any number here — zero most of all —
20858
+ // would credit the whole benchmark move to the strategy.
20859
+ if (!beta.available) {
20860
+ getLogger().warn(`Alpha unavailable: beta could not be computed (${beta.reason}: ${beta.detail}).`);
20861
+ return {
20862
+ alpha: "N/A",
20863
+ alphaAnnualized: "N/A",
20864
+ beta: "N/A",
20865
+ };
20866
+ }
20867
+ if (!isFinite(beta.value.beta)) {
20771
20868
  getLogger().warn("Beta calculation resulted in a non-finite value.");
20772
20869
  return {
20773
20870
  alpha: "N/A",
@@ -20778,7 +20875,7 @@ async function calculateAlphaAndBeta$1(tradeBars, benchmarkBars, isShort) {
20778
20875
  // For short positions, the interpretation of beta changes
20779
20876
  // A positive beta on a short means the position moves with the market,
20780
20877
  // which is bad for a short. We invert it for consistency.
20781
- const positionAwareBeta = isShort ? -beta.beta : beta.beta;
20878
+ const positionAwareBeta = isShort ? -beta.value.beta : beta.value.beta;
20782
20879
  const avgTradeReturn = alignedTradeReturns.reduce((sum, ret) => sum + ret, 0) /
20783
20880
  alignedTradeReturns.length;
20784
20881
  const avgBenchmarkReturn = alignedBenchmarkReturns.reduce((sum, ret) => sum + ret, 0) /
@@ -21397,7 +21494,18 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
21397
21494
  const benchmarkAvgReturn = alignedBenchmarkReturns.reduce((sum, ret) => sum + ret, 0) / n;
21398
21495
  // **Calculate beta**
21399
21496
  const beta = calculateBetaFromReturns(alignedPortfolioReturns, alignedBenchmarkReturns);
21400
- if (!isFinite(beta.beta)) {
21497
+ // Alpha is what remains after subtracting the benchmark term, so an unknown
21498
+ // beta leaves alpha unknown. Any numeric stand-in — zero above all — would
21499
+ // attribute the entire benchmark move to the strategy.
21500
+ if (!beta.available) {
21501
+ getLogger().warn(`Alpha unavailable: beta could not be computed (${beta.reason}: ${beta.detail}).`);
21502
+ return {
21503
+ alpha: "N/A",
21504
+ alphaAnnualized: "N/A",
21505
+ beta: "N/A",
21506
+ };
21507
+ }
21508
+ if (!isFinite(beta.value.beta)) {
21401
21509
  getLogger().warn("Beta calculation resulted in a non-finite value.");
21402
21510
  return {
21403
21511
  alpha: "N/A",
@@ -21412,7 +21520,7 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
21412
21520
  const tradingDaysPerYear = 252;
21413
21521
  const riskFreeRateDaily = riskFreeRateAnnual / tradingDaysPerYear;
21414
21522
  const alpha = portfolioAvgReturn -
21415
- (riskFreeRateDaily + beta.beta * (benchmarkAvgReturn - riskFreeRateDaily));
21523
+ (riskFreeRateDaily + beta.value.beta * (benchmarkAvgReturn - riskFreeRateDaily));
21416
21524
  const alphaAnnualized = alpha * tradingDaysPerYear;
21417
21525
  if (!isFinite(alphaAnnualized)) {
21418
21526
  getLogger().warn("Alpha calculation resulted in a non-finite value.");
@@ -21425,7 +21533,7 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
21425
21533
  return {
21426
21534
  alpha: `${(alpha * 100).toFixed(2)}`,
21427
21535
  alphaAnnualized: `${(alphaAnnualized * 100).toFixed(2)}`,
21428
- beta: `${(beta.beta * 100).toFixed(2)}`,
21536
+ beta: `${(beta.value.beta * 100).toFixed(2)}`,
21429
21537
  };
21430
21538
  }
21431
21539
  // **Helper function to calculate daily returns with Unix millisecond timestamps**
@@ -21648,27 +21756,40 @@ function alignReturnsByDate(portfolioHistory, benchmarkBars) {
21648
21756
  return { alignedPortfolioReturns, alignedBenchmarkReturns };
21649
21757
  }
21650
21758
  /**
21651
- * Calculates the beta of the portfolio compared to a benchmark.
21652
- * @param portfolioReturns - Array of portfolio returns.
21653
- * @param benchmarkReturns - Array of benchmark returns.
21654
- * @returns An object containing beta and intermediate calculations.
21759
+ * Beta of a portfolio against a benchmark, from paired period returns.
21760
+ *
21761
+ * The two series are index-aligned pairs by contract: every mean, covariance
21762
+ * and variance below is taken over the SAME row set. A length mismatch is
21763
+ * therefore reported as invalid input rather than absorbed, because dividing
21764
+ * one series' sum by the other series' length produces a mean of a population
21765
+ * that does not exist — a number with no cohort, which is the failure this
21766
+ * return type exists to make impossible.
21767
+ *
21768
+ * An uncomputable beta is returned as the unavailable branch, never as `0`.
21769
+ * Zero beta is a claim of no market exposure, and downstream alpha attributes
21770
+ * the entire benchmark move to the strategy when it believes that claim.
21771
+ *
21772
+ * @param portfolioReturns - Portfolio period returns.
21773
+ * @param benchmarkReturns - Benchmark period returns, index-aligned to the portfolio.
21774
+ * @returns The beta components with their cohort, or a typed unavailable result.
21655
21775
  */
21656
21776
  function calculateBetaFromReturns(portfolioReturns, benchmarkReturns) {
21657
- const n = portfolioReturns.length;
21658
- if (n === 0) {
21659
- getLogger().warn("No returns to calculate beta.");
21660
- return {
21661
- beta: 0,
21662
- covariance: 0,
21663
- variance: 0,
21664
- averagePortfolioReturn: 0,
21665
- averageBenchmarkReturn: 0,
21666
- };
21667
- }
21668
- // Calculate average returns
21777
+ const requestedCount = portfolioReturns.length;
21778
+ if (portfolioReturns.length !== benchmarkReturns.length) {
21779
+ getLogger().warn(`Beta unavailable: series lengths differ (portfolio ${portfolioReturns.length}, benchmark ${benchmarkReturns.length}).`);
21780
+ return unavailableStatistic("invalid_input", `series lengths differ: portfolio ${portfolioReturns.length}, benchmark ${benchmarkReturns.length}`, sampleCohort(requestedCount, 0));
21781
+ }
21782
+ // Bessel-corrected estimators need one degree of freedom, so two paired
21783
+ // observations is the floor below which no sample variance exists.
21784
+ const MIN_PAIRS_FOR_SAMPLE_VARIANCE = 2;
21785
+ const n = requestedCount;
21786
+ if (n < MIN_PAIRS_FOR_SAMPLE_VARIANCE) {
21787
+ getLogger().warn(`Beta unavailable: ${n} paired returns offered.`);
21788
+ return unavailableStatistic(n === 0 ? "no_usable_samples" : "insufficient_samples", `beta needs at least ${MIN_PAIRS_FOR_SAMPLE_VARIANCE} paired returns; ${n} were offered`, sampleCohort(requestedCount, n));
21789
+ }
21790
+ const cohort = sampleCohort(requestedCount, n);
21669
21791
  const averagePortfolioReturn = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / n;
21670
21792
  const averageBenchmarkReturn = benchmarkReturns.reduce((sum, ret) => sum + ret, 0) / n;
21671
- // Calculate covariance and variance
21672
21793
  let covariance = 0;
21673
21794
  let variance = 0;
21674
21795
  for (let i = 0; i < n; i++) {
@@ -21678,37 +21799,26 @@ function calculateBetaFromReturns(portfolioReturns, benchmarkReturns) {
21678
21799
  variance += benchmarkDiff ** 2;
21679
21800
  }
21680
21801
  // Use sample (Bessel-corrected) estimators — divide by (n - 1), not n.
21681
- // For n === 1 there is no degrees-of-freedom left; treat as zero variance
21682
- // so beta falls through to the zero-variance guard below.
21683
- const denom = n > 1 ? n - 1 : 1;
21684
- covariance /= denom;
21685
- variance /= denom;
21686
- // Handle zero (or numerically-degenerate) variance. A constant benchmark
21687
- // series can still produce a tiny nonzero variance because the computed
21688
- // mean differs from the constant by an ulp; dividing covariance by that
21689
- // rounding noise yields a meaningless beta. Treat any variance at or
21690
- // below the summation noise floor — (n * eps * |mean|)^2, the square of
21691
- // the worst-case naive-summation error — as zero. When the mean is
21692
- // exactly 0 this reduces to the exact zero check.
21802
+ covariance /= n - 1;
21803
+ variance /= n - 1;
21804
+ // A constant benchmark series can still produce a tiny nonzero variance
21805
+ // because the computed mean differs from the constant by an ulp; dividing
21806
+ // covariance by that rounding noise yields a meaningless beta. Treat any
21807
+ // variance at or below the summation noise floor — (n * eps * |mean|)^2, the
21808
+ // square of the worst-case naive-summation error — as no variance at all.
21809
+ // When the mean is exactly 0 this reduces to the exact zero check.
21693
21810
  const varianceNoiseFloor = (n * Number.EPSILON * Math.abs(averageBenchmarkReturn)) ** 2;
21694
21811
  if (variance <= varianceNoiseFloor) {
21695
- getLogger().warn("Benchmark variance is zero or below the floating-point noise floor. Setting beta to 0.");
21696
- return {
21697
- beta: 0,
21698
- covariance,
21699
- variance,
21700
- averagePortfolioReturn,
21701
- averageBenchmarkReturn,
21702
- };
21812
+ getLogger().warn("Beta unavailable: benchmark variance is zero or below the floating-point noise floor.");
21813
+ return unavailableStatistic("degenerate_population", `benchmark variance ${variance} is at or below the summation noise floor ${varianceNoiseFloor}; beta is undefined`, cohort);
21703
21814
  }
21704
- const beta = covariance / variance;
21705
- return {
21706
- beta,
21815
+ return availableStatistic({
21816
+ beta: covariance / variance,
21707
21817
  covariance,
21708
21818
  variance,
21709
21819
  averagePortfolioReturn,
21710
21820
  averageBenchmarkReturn,
21711
- };
21821
+ }, cohort);
21712
21822
  }
21713
21823
  /**
21714
21824
  * Calculates the information ratio of the portfolio compared to a benchmark.
@@ -22187,7 +22297,14 @@ var riskNs = /*#__PURE__*/Object.freeze({
22187
22297
  * Conventions:
22188
22298
  * - tradePnls / tradeReturns is an array of per-trade realised P&L or return
22189
22299
  * (positive = win, negative = loss, zero = breakeven).
22190
- * - All "rolling*" functions return null when fewer than `windowSize` trades exist.
22300
+ * - Every statistic here is a ratio or a mean over a WINDOW, so every one is
22301
+ * returned as a {@link SampleStatistic}: the value cannot be read without the
22302
+ * `sampleCount` it was taken over and the `coverage` of the window that was
22303
+ * asked for. A hit-rate is a different claim on 5 trades than on 500, and a
22304
+ * window that could only be half-filled is a different cohort from a full
22305
+ * one — a caller holding a bare number can tell neither apart.
22306
+ * - A window that cannot support the statistic returns the unavailable branch
22307
+ * with a reason, never a numeric stand-in. Zero is a measurement.
22191
22308
  * - All public functions reject non-finite inputs (NaN, Infinity) by throwing.
22192
22309
  * Callers must pre-validate or filter their inputs.
22193
22310
  */
@@ -22203,21 +22320,38 @@ function assertFiniteArray(name, arr) {
22203
22320
  }
22204
22321
  }
22205
22322
  }
22323
+ /**
22324
+ * Report a window that holds fewer trades than it asked for.
22325
+ *
22326
+ * Shared so every rolling function describes a short window the same way — the
22327
+ * cohort is `(requested = windowSize, sampled = what exists)`, which is the
22328
+ * pair a caller needs to distinguish a warm-up from a data gap.
22329
+ *
22330
+ * @param name - The calling function, for the detail string.
22331
+ * @param available - Trades actually present.
22332
+ * @param windowSize - Trades the window asked for.
22333
+ * @returns The unavailable branch describing the short window.
22334
+ */
22335
+ function insufficientWindow(name, available, windowSize) {
22336
+ return unavailableStatistic("insufficient_samples", `${name}: window of ${windowSize} requested, only ${available} trades available`, sampleCohort(windowSize, available));
22337
+ }
22206
22338
  /**
22207
22339
  * Rolling expectancy: mean P&L over the most-recent `windowSize` trades.
22208
22340
  *
22209
22341
  * @param tradePnls - Array of per-trade realised P&L values.
22210
22342
  * @param windowSize - Number of most-recent trades to include. Must be a positive integer.
22211
- * @returns Mean P&L of the last `windowSize` trades, or null when fewer than `windowSize` exist.
22343
+ * @returns Mean P&L of the last `windowSize` trades with its cohort, or a typed
22344
+ * unavailable result when fewer than `windowSize` trades exist.
22212
22345
  * @throws When `windowSize` is not a positive integer or any input is non-finite.
22213
22346
  */
22214
22347
  function calculateRollingExpectancy(tradePnls, windowSize) {
22215
22348
  assertWindowSize("calculateRollingExpectancy", windowSize);
22216
- if (tradePnls.length < windowSize)
22217
- return null;
22349
+ if (tradePnls.length < windowSize) {
22350
+ return insufficientWindow("calculateRollingExpectancy", tradePnls.length, windowSize);
22351
+ }
22218
22352
  assertFiniteArray("calculateRollingExpectancy", tradePnls);
22219
22353
  const slice = tradePnls.slice(-windowSize);
22220
- return slice.reduce((a, b) => a + b, 0) / windowSize;
22354
+ return availableStatistic(slice.reduce((a, b) => a + b, 0) / windowSize, sampleCohort(windowSize, windowSize));
22221
22355
  }
22222
22356
  /**
22223
22357
  * Rolling hit-rate: fraction of strictly-positive P&L trades in the most-recent
@@ -22225,42 +22359,53 @@ function calculateRollingExpectancy(tradePnls, windowSize) {
22225
22359
  *
22226
22360
  * @param tradePnls - Array of per-trade realised P&L values.
22227
22361
  * @param windowSize - Number of most-recent trades to include. Must be a positive integer.
22228
- * @returns Fraction of winning trades in the window, or null when fewer than `windowSize` exist.
22362
+ * @returns Fraction of winning trades in the window with its cohort, or a typed
22363
+ * unavailable result when fewer than `windowSize` trades exist.
22229
22364
  * @throws When `windowSize` is not a positive integer or any input is non-finite.
22230
22365
  */
22231
22366
  function calculateRollingHitRate(tradePnls, windowSize) {
22232
22367
  assertWindowSize("calculateRollingHitRate", windowSize);
22233
- if (tradePnls.length < windowSize)
22234
- return null;
22368
+ if (tradePnls.length < windowSize) {
22369
+ return insufficientWindow("calculateRollingHitRate", tradePnls.length, windowSize);
22370
+ }
22235
22371
  assertFiniteArray("calculateRollingHitRate", tradePnls);
22236
22372
  const slice = tradePnls.slice(-windowSize);
22237
22373
  const wins = slice.filter((p) => p > 0).length;
22238
- return wins / windowSize;
22374
+ return availableStatistic(wins / windowSize, sampleCohort(windowSize, windowSize));
22239
22375
  }
22240
22376
  /**
22241
22377
  * Rolling profit factor: sum(wins) / |sum(losses)| over the most-recent `windowSize` trades.
22242
22378
  *
22243
22379
  * Edge cases:
22244
- * - no losses and at least one win → +Infinity
22245
- * - no wins and no losses (all zeros) → 0
22246
- * - fewer than windowSize trades → null
22380
+ * - no losses and at least one win → +Infinity (an unbounded but real ratio)
22381
+ * - no wins and no losses (all zeros) → unavailable: `0 / 0` is undefined, and a
22382
+ * window of breakeven trades has no profit factor rather than a profit factor
22383
+ * of zero
22384
+ * - fewer than windowSize trades → unavailable
22247
22385
  *
22248
22386
  * @param tradePnls - Array of per-trade realised P&L values.
22249
22387
  * @param windowSize - Number of most-recent trades to include. Must be a positive integer.
22250
- * @returns Profit factor for the rolling window, or null when fewer than `windowSize` exist.
22388
+ * @returns Profit factor for the rolling window with its cohort, or a typed
22389
+ * unavailable result.
22251
22390
  * @throws When `windowSize` is not a positive integer or any input is non-finite.
22252
22391
  */
22253
22392
  function calculateRollingProfitFactor(tradePnls, windowSize) {
22254
22393
  assertWindowSize("calculateRollingProfitFactor", windowSize);
22255
- if (tradePnls.length < windowSize)
22256
- return null;
22394
+ if (tradePnls.length < windowSize) {
22395
+ return insufficientWindow("calculateRollingProfitFactor", tradePnls.length, windowSize);
22396
+ }
22257
22397
  assertFiniteArray("calculateRollingProfitFactor", tradePnls);
22398
+ const cohort = sampleCohort(windowSize, windowSize);
22258
22399
  const slice = tradePnls.slice(-windowSize);
22259
22400
  const wins = slice.filter((p) => p > 0).reduce((a, b) => a + b, 0);
22260
22401
  const losses = slice.filter((p) => p < 0).reduce((a, b) => a + Math.abs(b), 0);
22261
- if (losses === 0)
22262
- return wins > 0 ? Number.POSITIVE_INFINITY : 0;
22263
- return wins / losses;
22402
+ if (losses === 0) {
22403
+ if (wins > 0) {
22404
+ return availableStatistic(Number.POSITIVE_INFINITY, cohort);
22405
+ }
22406
+ return unavailableStatistic("degenerate_population", `calculateRollingProfitFactor: window of ${windowSize} contains neither wins nor losses; the ratio is undefined`, cohort);
22407
+ }
22408
+ return availableStatistic(wins / losses, cohort);
22264
22409
  }
22265
22410
  /**
22266
22411
  * Rolling Sortino: delegate to `calculateSortino` over the most-recent `windowSize` returns.
@@ -22268,34 +22413,58 @@ function calculateRollingProfitFactor(tradePnls, windowSize) {
22268
22413
  * @param tradeReturns - Array of per-trade return values.
22269
22414
  * @param windowSize - Number of most-recent trades to include. Must be a positive integer.
22270
22415
  * @param riskFreeRate - Risk-free rate to subtract from returns (default 0).
22271
- * @returns Sortino ratio for the rolling window, or null when fewer than `windowSize` exist.
22416
+ * @returns Sortino ratio for the rolling window with its cohort, or a typed
22417
+ * unavailable result.
22272
22418
  * @throws When `windowSize` is not a positive integer or any input is non-finite.
22273
22419
  */
22274
22420
  function calculateRollingSortino(tradeReturns, windowSize, riskFreeRate = 0) {
22275
22421
  assertWindowSize("calculateRollingSortino", windowSize);
22276
- if (tradeReturns.length < windowSize)
22277
- return null;
22422
+ if (tradeReturns.length < windowSize) {
22423
+ return insufficientWindow("calculateRollingSortino", tradeReturns.length, windowSize);
22424
+ }
22278
22425
  assertFiniteArray("calculateRollingSortino", tradeReturns);
22279
- return calculateSortino(tradeReturns.slice(-windowSize), riskFreeRate);
22426
+ const cohort = sampleCohort(windowSize, windowSize);
22427
+ const sortino = calculateSortino(tradeReturns.slice(-windowSize), riskFreeRate);
22428
+ if (sortino === null) {
22429
+ // `calculateSortino` returns null only for a window it cannot form a
22430
+ // dispersion over — fewer than two samples. That is a property of the
22431
+ // window, so it is reported as one rather than as a ratio of zero.
22432
+ return unavailableStatistic("insufficient_samples", `calculateRollingSortino: window of ${windowSize} cannot support a dispersion estimate`, cohort);
22433
+ }
22434
+ return availableStatistic(sortino, cohort);
22280
22435
  }
22281
22436
  /**
22282
22437
  * Z-score of live-expectancy vs backtest-expectancy, scaled by the backtest stddev.
22283
22438
  * Positive Z = live outperforming; negative Z = live underperforming.
22284
22439
  *
22285
- * @param liveExpectancy - Mean P&L per trade in the live window.
22440
+ * The live expectancy is taken as a {@link SampleStatistic} rather than a bare
22441
+ * number so the z-score inherits the cohort it was actually derived from. A
22442
+ * z-score is a statement about how surprising a sample mean is, and how
22443
+ * surprising it is depends entirely on how many trades produced it — quoting
22444
+ * the z alone is the exact substitution this type exists to block. An
22445
+ * unavailable live expectancy yields an unavailable z, because there is no
22446
+ * mean to compare.
22447
+ *
22448
+ * @param liveExpectancy - Mean P&L per trade in the live window, with its cohort.
22286
22449
  * @param backtestExpectancy - Mean P&L per trade from the calibration backtest.
22287
22450
  * @param backtestStddev - Stddev of per-trade P&L in the backtest. Must be > 0.
22288
- * @returns Z-score measuring divergence between live and backtest performance.
22289
- * @throws When any input is non-finite or `backtestStddev` is not positive.
22451
+ * @returns Z-score measuring live-vs-backtest divergence, carrying the live cohort.
22452
+ * @throws When the backtest inputs are non-finite or `backtestStddev` is not positive.
22290
22453
  */
22291
22454
  function calculateBacktestDivergenceZ(liveExpectancy, backtestExpectancy, backtestStddev) {
22292
- if (!Number.isFinite(liveExpectancy) || !Number.isFinite(backtestExpectancy) || !Number.isFinite(backtestStddev)) {
22455
+ if (!Number.isFinite(backtestExpectancy) || !Number.isFinite(backtestStddev)) {
22293
22456
  throw new Error("calculateBacktestDivergenceZ: inputs must be finite numbers");
22294
22457
  }
22295
22458
  if (backtestStddev <= 0) {
22296
22459
  throw new Error("calculateBacktestDivergenceZ: stddev must be > 0");
22297
22460
  }
22298
- return (liveExpectancy - backtestExpectancy) / backtestStddev;
22461
+ if (!liveExpectancy.available) {
22462
+ return unavailableStatistic(liveExpectancy.reason, `calculateBacktestDivergenceZ: live expectancy unavailable (${liveExpectancy.detail})`, sampleCohort(liveExpectancy.requestedCount, liveExpectancy.sampleCount));
22463
+ }
22464
+ if (!Number.isFinite(liveExpectancy.value)) {
22465
+ throw new Error("calculateBacktestDivergenceZ: inputs must be finite numbers");
22466
+ }
22467
+ return availableStatistic((liveExpectancy.value - backtestExpectancy) / backtestStddev, sampleCohort(liveExpectancy.requestedCount, liveExpectancy.sampleCount));
22299
22468
  }
22300
22469
 
22301
22470
  var strategyNs = /*#__PURE__*/Object.freeze({
@@ -62339,13 +62508,18 @@ const DEFAULT_PAGINATION_DELAY_MS = 300;
62339
62508
  */
62340
62509
  const MAX_ORDERS_PER_REQUEST = 500;
62341
62510
  /**
62342
- * Order statuses that are considered "open"
62511
+ * Order statuses that are considered "open".
62512
+ *
62513
+ * `held` is included because a held conditional leg (a bracket's stop-loss,
62514
+ * say) is a working order resting at the broker, returned by Alpaca's own
62515
+ * `status=open` listing and cancelable like any other open order.
62343
62516
  */
62344
62517
  const OPEN_ORDER_STATUSES = [
62345
62518
  "new",
62346
62519
  "accepted",
62347
62520
  "pending_new",
62348
62521
  "accepted_for_bidding",
62522
+ "held",
62349
62523
  "partially_filled",
62350
62524
  ];
62351
62525
  /**
@@ -62353,13 +62527,15 @@ const OPEN_ORDER_STATUSES = [
62353
62527
  */
62354
62528
  const FILLED_ORDER_STATUSES = ["filled"];
62355
62529
  /**
62356
- * Order statuses that can still potentially be filled
62530
+ * Order statuses that can still potentially be filled. A `held` leg fills once
62531
+ * its parent fills or its trigger is met.
62357
62532
  */
62358
62533
  const FILLABLE_ORDER_STATUSES = [
62359
62534
  "new",
62360
62535
  "accepted",
62361
62536
  "pending_new",
62362
62537
  "accepted_for_bidding",
62538
+ "held",
62363
62539
  "partially_filled",
62364
62540
  ];
62365
62541
  /**
@@ -72167,11 +72343,35 @@ class CircuitBreakerRegistry {
72167
72343
  * Register that an attempt is starting, so half-open probes stay bounded.
72168
72344
  *
72169
72345
  * @param routeKey The route's stable key.
72170
- * @returns void
72346
+ * @returns Whether the attempt took a half-open probe slot. A caller holding
72347
+ * one must end the attempt with {@link onSuccess}, {@link onFailure} or
72348
+ * {@link onAttemptAbandoned}, or the slot is never returned.
72171
72349
  */
72172
72350
  onAttemptStart(routeKey) {
72173
72351
  if (this.stateOf(routeKey) === "half-open") {
72174
72352
  this.recordFor(routeKey).probesInFlight += 1;
72353
+ return true;
72354
+ }
72355
+ return false;
72356
+ }
72357
+ /**
72358
+ * Return a half-open probe slot whose attempt ended without a verdict.
72359
+ *
72360
+ * A probe that never tested the provider — refused by the client's own
72361
+ * pacing guard, cancelled by its caller, or found to be the wrong leg for the
72362
+ * request — says nothing about whether the route has recovered, so neither a
72363
+ * success nor a failure is recorded. The slot must still come back. Without
72364
+ * it the half-open route admits no further probe, no probe can ever close or
72365
+ * re-open the breaker, and the route stays excluded for the life of the
72366
+ * process while its traffic is quietly served by the next leg.
72367
+ *
72368
+ * @param routeKey The route's stable key.
72369
+ * @returns void
72370
+ */
72371
+ onAttemptAbandoned(routeKey) {
72372
+ const record = this.records.get(routeKey);
72373
+ if (record !== undefined && record.probesInFlight > 0) {
72374
+ record.probesInFlight -= 1;
72175
72375
  }
72176
72376
  }
72177
72377
  /**
@@ -72442,11 +72642,13 @@ var defaults$1 = {
72442
72642
  var providers$1 = {
72443
72643
  anthropic: {
72444
72644
  basis: "conservative-default",
72645
+ scope: "model",
72646
+ scope_source: "https://platform.claude.com/docs/en/api/rate-limits",
72445
72647
  requests_per_minute: 120,
72446
72648
  max_concurrent: 12,
72447
72649
  acquire_timeout_ms: 15000,
72448
72650
  source: null,
72449
- note: "Raised 2026-09-15 after the 4-concurrent ceiling was measured starving the live equity decision path: the alias chain reported 'exhausted its fallback chain' with every leg skipped by this guard (deepinfra primary+secondary and the anthropic incumbent), 173 of 473 signal-coordination calls failed (36.6%), and decisions were lost outright. Corroborating evidence at the time: ZERO 429s observed on any provider, the engine's own global fan-out gate permits 100 concurrent with 17 active, and 877 signal-analysis calls had been admitted to that gate. Concurrency and RPM are raised TOGETHER because they bind in series - lifting max_concurrent alone would only move the bottleneck to the token bucket. STILL conservative-default, NOT published: no provider console was read for these numbers, so they remain a deliberate under-estimate of an unknown ceiling. Transcribe the real tier limits (W3-07) and set basis to published."
72651
+ note: "The unit is per model: the scope_source states 'Rate limits are applied separately for each model; therefore you can use different models up to their respective limits simultaneously', measured as requests, input tokens and output tokens per minute for each model class, with no concurrency ceiling. Each model therefore gets its own guard, so traffic on one model class (an Opus incumbent serving background aliases) cannot refuse calls to another (the Haiku incumbent of the hot-path alias). The NUMBERS stay conservative-default: the organisation's usage tier sets the real ceilings and is read only from the Claude Console rate-limits page, which has not been transcribed. The lowest standard tier listed at the source allows 1,000 requests per minute per model; organisations with limited history can start on an evaluation tier below that, which is why these values are held well under it. Transcribe the tier's limits (W3-07) and set basis to published."
72450
72652
  },
72451
72653
  openai: {
72452
72654
  basis: "conservative-default",
@@ -72465,12 +72667,14 @@ var providers$1 = {
72465
72667
  note: "Raised 2026-09-15 after the 4-concurrent ceiling was measured starving the live equity decision path: the alias chain reported 'exhausted its fallback chain' with every leg skipped by this guard (deepinfra primary+secondary and the anthropic incumbent), 173 of 473 signal-coordination calls failed (36.6%), and decisions were lost outright. Corroborating evidence at the time: ZERO 429s observed on any provider, the engine's own global fan-out gate permits 100 concurrent with 17 active, and 877 signal-analysis calls had been admitted to that gate. Concurrency and RPM are raised TOGETHER because they bind in series - lifting max_concurrent alone would only move the bottleneck to the token bucket. STILL conservative-default, NOT published: no provider console was read for these numbers, so they remain a deliberate under-estimate of an unknown ceiling. Transcribe the real tier limits (W3-07) and set basis to published."
72466
72668
  },
72467
72669
  deepinfra: {
72468
- basis: "conservative-default",
72670
+ basis: "published",
72671
+ scope: "model",
72469
72672
  requests_per_minute: 240,
72470
- max_concurrent: 24,
72673
+ requests_per_minute_basis: "conservative-default",
72674
+ max_concurrent: 200,
72471
72675
  acquire_timeout_ms: 15000,
72472
- source: null,
72473
- note: "Raised 2026-09-15 after the 4-concurrent ceiling was measured starving the live equity decision path: the alias chain reported 'exhausted its fallback chain' with every leg skipped by this guard (deepinfra primary+secondary and the anthropic incumbent), 173 of 473 signal-coordination calls failed (36.6%), and decisions were lost outright. Corroborating evidence at the time: ZERO 429s observed on any provider, the engine's own global fan-out gate permits 100 concurrent with 17 active, and 877 signal-analysis calls had been admitted to that gate. Concurrency and RPM are raised TOGETHER because they bind in series - lifting max_concurrent alone would only move the bottleneck to the token bucket. STILL conservative-default, NOT published: no provider console was read for these numbers, so they remain a deliberate under-estimate of an unknown ceiling. Transcribe the real tier limits (W3-07) and set basis to published."
72676
+ source: "https://docs.deepinfra.com/account/rate-limits",
72677
+ note: "Transcribed 2026-09-23 from the source, which states 'Every account has a default limit of 200 concurrent requests per model', that two models queried simultaneously allow 400 in total (200 per model), and that 'The rate limit is on concurrent requests, not per-minute volume.' Concurrency is therefore the bound DeepInfra enforces, and it is enforced per MODEL, so each model gets its own guard at 200. One guard shared by the whole account enforced a ceiling DeepInfra does not impose, and because an alias's primary and secondary are both served from this account, it refused the secondary exactly when the primary's queue was full. DeepInfra publishes no per-minute ceiling, so requests_per_minute is the client's own pacing backstop (requests_per_minute_basis: conservative-default), keyed per model like the bound DeepInfra does enforce. Exceeding the ceiling returns HTTP 429, and a very busy model can return 429 below it. The ceiling belongs to the account and this guard to one process, so every process calling the same model through the same account shares the 200."
72474
72678
  },
72475
72679
  fireworks: {
72476
72680
  basis: "conservative-default",
@@ -72520,7 +72724,10 @@ var limitsConfig = {
72520
72724
  * provider's circuit breaker, fail over to a more expensive leg, and keep doing
72521
72725
  * so — converting a self-inflicted pacing problem into a permanent routing
72522
72726
  * change nobody chose. Pacing at the client is what keeps the breaker measuring
72523
- * the provider rather than measuring us.
72727
+ * the provider rather than measuring us. The same reasoning bounds the guard
72728
+ * from the other side: a client held far BELOW the provider's ceiling refuses
72729
+ * calls the provider would have served, and the chain answers those refusals by
72730
+ * failing over — the same unchosen routing change, arrived at by under-driving.
72524
72731
  *
72525
72732
  * Two distinct bounds are applied because they fail differently. The rate bound
72526
72733
  * (requests per minute) protects the provider's published ceiling. The
@@ -72529,8 +72736,18 @@ var limitsConfig = {
72529
72736
  * every one of them blows its latency budget and the fan-out produces a hundred
72530
72737
  * timeouts instead of a queue.
72531
72738
  *
72532
- * Limits live in `provider-limits.json`, not here. A rate limit discovered
72533
- * during an incident should be correctable by config, not by a release.
72739
+ * Each guard is keyed by the unit its provider enforces limits in. A provider
72740
+ * that publishes its ceilings per model gets one independent guard per model.
72741
+ * Sharing one guard across its models would enforce a ceiling the provider does
72742
+ * not impose, and — when a chain's primary and secondary are served by the same
72743
+ * provider — would refuse the secondary at exactly the moment the primary's
72744
+ * queue is full, so the fallback that exists for that moment is never reached.
72745
+ *
72746
+ * Limits live in `provider-limits.json` rather than in code, each beside the
72747
+ * source it was transcribed from, so a published ceiling and a conservative
72748
+ * guess can never be mistaken for one another in review. The file is bundled
72749
+ * at build time: changing a limit is a release of this package, not a runtime
72750
+ * switch.
72534
72751
  *
72535
72752
  * @module llm/rate-guard
72536
72753
  */
@@ -72570,64 +72787,141 @@ class RateGuardTimeoutError extends Error {
72570
72787
  provider;
72571
72788
  /** Which of the two bounds the caller waited on. */
72572
72789
  bound;
72790
+ /** The model whose guard refused the call, when the provider's limits apply per model. */
72791
+ modelId;
72792
+ /** Whether the caller stopped waiting before the guard's own wait budget ran out. */
72793
+ abandoned;
72573
72794
  /**
72574
72795
  * @param provider The provider.
72575
72796
  * @param bound Which bound was binding.
72576
- * @param waitedMs How long the caller waited.
72797
+ * @param waitedMs How long the caller was prepared to wait.
72798
+ * @param detail The model, and whether the caller left before the budget ran out.
72577
72799
  */
72578
- constructor(provider, bound, waitedMs) {
72579
- super(`client-side ${bound} guard for provider "${provider}" did not admit the call within ${waitedMs} ms. ` +
72800
+ constructor(provider, bound, waitedMs, detail = {}) {
72801
+ const guard = detail.modelId === undefined
72802
+ ? `client-side ${bound} guard for provider "${provider}"`
72803
+ : `client-side ${bound} guard for provider "${provider}", model "${detail.modelId}",`;
72804
+ const outcome = detail.abandoned === true
72805
+ ? `was left by its caller before it could admit the call (wait budget ${waitedMs} ms)`
72806
+ : `did not admit the call within ${waitedMs} ms`;
72807
+ super(`${guard} ${outcome}. ` +
72580
72808
  "The provider was never contacted, so this says nothing about its health.");
72581
72809
  this.name = "RateGuardTimeoutError";
72582
72810
  this.provider = provider;
72583
72811
  this.bound = bound;
72812
+ this.modelId = detail.modelId;
72813
+ this.abandoned = detail.abandoned === true;
72584
72814
  }
72585
72815
  }
72816
+ /**
72817
+ * The guard a call is held by.
72818
+ *
72819
+ * A call to a per-model provider that names no model shares one provider-wide
72820
+ * guard held at the per-model ceiling. That is never looser than the limit of
72821
+ * any single model it might reach, so the fallback errs toward pacing.
72822
+ *
72823
+ * @param provider The provider key.
72824
+ * @param modelId The model the call is addressed to, if known.
72825
+ * @returns The guard's identity.
72826
+ */
72827
+ function guardIdentity(provider, modelId) {
72828
+ const perModel = limitsFor(provider).scope === "model" && modelId !== undefined && modelId.length > 0;
72829
+ return perModel
72830
+ ? { key: `${provider}/${modelId}`, provider, modelId }
72831
+ : { key: provider, provider, modelId: undefined };
72832
+ }
72586
72833
  /**
72587
72834
  * A counting semaphore bounding simultaneous in-flight calls.
72588
72835
  *
72589
- * Written here rather than pulled from a dependency because it is fifteen lines
72590
- * and because the waiting behaviour matters: a waiter that times out must be
72591
- * removed from the queue, or a burst of abandoned callers permanently consumes
72592
- * the permits that later callers need.
72836
+ * Written here rather than pulled from a dependency because the waiting
72837
+ * behaviour is the point. A waiter that times out must be removed from the
72838
+ * queue, or a burst of abandoned callers permanently consumes the permits that
72839
+ * later callers need. And a waiter whose caller has stopped waiting must leave
72840
+ * at once: left queued, it holds its caller until the wait budget expires and
72841
+ * is then handed a permit it can only waste.
72593
72842
  */
72594
72843
  class ConcurrencyGate {
72595
72844
  inFlight = 0;
72596
72845
  waiters = [];
72597
72846
  limit;
72598
- provider;
72847
+ identity;
72599
72848
  /**
72600
- * @param provider The provider this gate guards.
72849
+ * @param identity The guard this gate implements.
72601
72850
  * @param limit Maximum simultaneous in-flight calls.
72602
72851
  */
72603
- constructor(provider, limit) {
72604
- this.provider = provider;
72852
+ constructor(identity, limit) {
72853
+ this.identity = identity;
72605
72854
  this.limit = limit;
72606
72855
  }
72607
72856
  /**
72608
72857
  * Wait for a permit.
72609
72858
  *
72610
72859
  * @param timeoutMs How long the caller is willing to queue.
72860
+ * @param signal The caller's cancellation; firing it takes the caller out of the queue.
72611
72861
  * @returns A release function the caller must invoke exactly once.
72862
+ * @throws {RateGuardTimeoutError} When no permit was granted in time, or the caller stopped waiting.
72612
72863
  */
72613
- async acquire(timeoutMs) {
72864
+ async acquire(timeoutMs, signal) {
72865
+ if (signal?.aborted === true) {
72866
+ // Nobody is waiting for this answer. Taking a permit for it would spend
72867
+ // capacity a live caller needs on a call that can only be torn down.
72868
+ throw this.refusal(timeoutMs, true);
72869
+ }
72614
72870
  if (this.inFlight < this.limit) {
72615
72871
  this.inFlight += 1;
72616
72872
  return () => this.release();
72617
72873
  }
72618
72874
  await new Promise((resolve, reject) => {
72619
- const timer = setTimeout(() => {
72620
- const index = this.waiters.findIndex((waiter) => waiter.timer === timer);
72621
- if (index !== -1) {
72622
- this.waiters.splice(index, 1);
72875
+ /**
72876
+ * Take this waiter out of the queue and refuse it. A waiter that `release`
72877
+ * has already admitted is no longer queued; it now holds a permit, which
72878
+ * its call returns, so there is nothing to undo here.
72879
+ *
72880
+ * @param abandoned Whether the caller left before the wait budget ran out.
72881
+ * @returns void
72882
+ */
72883
+ const leave = (abandoned) => {
72884
+ const index = this.waiters.indexOf(waiter);
72885
+ if (index === -1) {
72886
+ return;
72623
72887
  }
72624
- reject(new RateGuardTimeoutError(this.provider, "concurrency", timeoutMs));
72888
+ this.waiters.splice(index, 1);
72889
+ clearTimeout(timer);
72890
+ signal?.removeEventListener("abort", onAbort);
72891
+ reject(this.refusal(timeoutMs, abandoned));
72892
+ };
72893
+ const onAbort = () => {
72894
+ leave(true);
72895
+ };
72896
+ const timer = setTimeout(() => {
72897
+ leave(false);
72625
72898
  }, timeoutMs);
72626
- this.waiters.push({ resolve, reject, timer });
72899
+ const waiter = {
72900
+ admit: () => {
72901
+ clearTimeout(timer);
72902
+ signal?.removeEventListener("abort", onAbort);
72903
+ resolve();
72904
+ },
72905
+ };
72906
+ this.waiters.push(waiter);
72907
+ signal?.addEventListener("abort", onAbort, { once: true });
72627
72908
  });
72628
72909
  this.inFlight += 1;
72629
72910
  return () => this.release();
72630
72911
  }
72912
+ /**
72913
+ * Build the refusal for a caller this gate did not admit.
72914
+ *
72915
+ * @param timeoutMs The wait budget the caller had.
72916
+ * @param abandoned Whether the caller left before the budget ran out.
72917
+ * @returns The error to raise.
72918
+ */
72919
+ refusal(timeoutMs, abandoned) {
72920
+ return new RateGuardTimeoutError(this.identity.provider, "concurrency", timeoutMs, {
72921
+ modelId: this.identity.modelId,
72922
+ abandoned,
72923
+ });
72924
+ }
72631
72925
  /**
72632
72926
  * Return a permit and admit the next waiter.
72633
72927
  *
@@ -72637,8 +72931,7 @@ class ConcurrencyGate {
72637
72931
  this.inFlight -= 1;
72638
72932
  const next = this.waiters.shift();
72639
72933
  if (next !== undefined) {
72640
- clearTimeout(next.timer);
72641
- next.resolve();
72934
+ next.admit();
72642
72935
  }
72643
72936
  }
72644
72937
  /**
@@ -72654,44 +72947,47 @@ class ConcurrencyGate {
72654
72947
  return this.waiters.length;
72655
72948
  }
72656
72949
  }
72657
- /** Per-provider guards, created on first use and shared process-wide. */
72950
+ /** Guards, created on first use and shared process-wide, keyed by {@link GuardIdentity.key}. */
72658
72951
  const rateLimiters = new Map();
72659
72952
  const concurrencyGates = new Map();
72953
+ const guardIdentities = new Map();
72660
72954
  /**
72661
- * The rate limiter for a provider.
72955
+ * The rate limiter for a guard.
72662
72956
  *
72663
72957
  * Shared process-wide rather than per-call-site, because the provider's ceiling
72664
72958
  * applies to the process as a whole. Per-call-site limiters would each stay
72665
72959
  * under the ceiling while their sum sailed past it.
72666
72960
  *
72667
- * @param provider The provider key.
72961
+ * @param identity The guard.
72668
72962
  * @returns Its limiter.
72669
72963
  */
72670
- function rateLimiterFor(provider) {
72671
- let limiter = rateLimiters.get(provider);
72964
+ function rateLimiterFor(identity) {
72965
+ let limiter = rateLimiters.get(identity.key);
72672
72966
  if (limiter === undefined) {
72673
- const limits = limitsFor(provider);
72967
+ const limits = limitsFor(identity.provider);
72674
72968
  limiter = new TokenBucketRateLimiter({
72675
72969
  maxTokens: limits.requests_per_minute,
72676
72970
  refillRate: limits.requests_per_minute / SECONDS_PER_MINUTE,
72677
- label: `llm:${provider}`,
72971
+ label: `llm:${identity.key}`,
72678
72972
  timeoutMs: limits.acquire_timeout_ms,
72679
72973
  });
72680
- rateLimiters.set(provider, limiter);
72974
+ rateLimiters.set(identity.key, limiter);
72975
+ guardIdentities.set(identity.key, identity);
72681
72976
  }
72682
72977
  return limiter;
72683
72978
  }
72684
72979
  /**
72685
- * The concurrency gate for a provider.
72980
+ * The concurrency gate for a guard.
72686
72981
  *
72687
- * @param provider The provider key.
72982
+ * @param identity The guard.
72688
72983
  * @returns Its gate.
72689
72984
  */
72690
- function concurrencyGateFor(provider) {
72691
- let gate = concurrencyGates.get(provider);
72985
+ function concurrencyGateFor(identity) {
72986
+ let gate = concurrencyGates.get(identity.key);
72692
72987
  if (gate === undefined) {
72693
- gate = new ConcurrencyGate(provider, limitsFor(provider).max_concurrent);
72694
- concurrencyGates.set(provider, gate);
72988
+ gate = new ConcurrencyGate(identity, limitsFor(identity.provider).max_concurrent);
72989
+ concurrencyGates.set(identity.key, gate);
72990
+ guardIdentities.set(identity.key, identity);
72695
72991
  }
72696
72992
  return gate;
72697
72993
  }
@@ -72713,21 +73009,26 @@ function concurrencyGateFor(provider) {
72713
73009
  * @param provider The provider key.
72714
73010
  * @param call The work to run once admitted.
72715
73011
  * @param maxWaitMs Ceiling on queue time; the configured guard timeout applies when lower.
73012
+ * @param scope The model the call addresses, and the caller's cancellation.
72716
73013
  * @returns The call's result.
72717
- * @throws {RateGuardTimeoutError} When neither bound admitted the call in time.
73014
+ * @throws {RateGuardTimeoutError} When neither bound admitted the call in time,
73015
+ * or the caller stopped waiting first.
72718
73016
  */
72719
- async function withProviderGuards(provider, call, maxWaitMs) {
73017
+ async function withProviderGuards(provider, call, maxWaitMs, scope = {}) {
72720
73018
  const limits = limitsFor(provider);
73019
+ const identity = guardIdentity(provider, scope.modelId);
72721
73020
  const waitBudgetMs = maxWaitMs === undefined
72722
73021
  ? limits.acquire_timeout_ms
72723
73022
  : Math.min(maxWaitMs, limits.acquire_timeout_ms);
72724
73023
  try {
72725
- await rateLimiterFor(provider).acquire();
73024
+ await rateLimiterFor(identity).acquire();
72726
73025
  }
72727
73026
  catch {
72728
- throw new RateGuardTimeoutError(provider, "rate", waitBudgetMs);
73027
+ throw new RateGuardTimeoutError(provider, "rate", waitBudgetMs, {
73028
+ modelId: identity.modelId,
73029
+ });
72729
73030
  }
72730
- const release = await concurrencyGateFor(provider).acquire(waitBudgetMs);
73031
+ const release = await concurrencyGateFor(identity).acquire(waitBudgetMs, scope.signal);
72731
73032
  try {
72732
73033
  return await call();
72733
73034
  }
@@ -72741,16 +73042,20 @@ async function withProviderGuards(provider, call, maxWaitMs) {
72741
73042
  /**
72742
73043
  * Inspect the guards currently in use.
72743
73044
  *
72744
- * @returns A snapshot per provider that has been used, sorted by provider.
73045
+ * @returns A snapshot per guard that has been used, sorted by guard key.
72745
73046
  */
72746
73047
  function guardSnapshots() {
72747
- const providers = new Set([...rateLimiters.keys(), ...concurrencyGates.keys()]);
72748
- return [...providers].sort().map((provider) => {
72749
- const limits = limitsFor(provider);
72750
- const limiter = rateLimiters.get(provider);
72751
- const gate = concurrencyGates.get(provider);
73048
+ return [...guardIdentities.values()]
73049
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
73050
+ .map((identity) => {
73051
+ const limits = limitsFor(identity.provider);
73052
+ const limiter = rateLimiters.get(identity.key);
73053
+ const gate = concurrencyGates.get(identity.key);
72752
73054
  return {
72753
- provider,
73055
+ key: identity.key,
73056
+ provider: identity.provider,
73057
+ modelId: identity.modelId,
73058
+ scope: limits.scope ?? "provider",
72754
73059
  basis: limits.basis,
72755
73060
  requestsPerMinute: limits.requests_per_minute,
72756
73061
  maxConcurrent: limits.max_concurrent,
@@ -72775,6 +73080,105 @@ function resetProviderGuards() {
72775
73080
  }
72776
73081
  rateLimiters.clear();
72777
73082
  concurrencyGates.clear();
73083
+ guardIdentities.clear();
73084
+ }
73085
+
73086
+ /**
73087
+ * Interpretation of a model's answer to a structured (JSON) request.
73088
+ *
73089
+ * A JSON request is a promise about the answer's SHAPE, and providers keep it
73090
+ * in different ways. An OpenAI-compatible host given `json_object` constrains
73091
+ * its decoder, so its answer is bare JSON. A provider with no schema-less JSON
73092
+ * mode — Anthropic, reached through the gateway, supports structured output
73093
+ * only against a caller-supplied schema — receives nothing but the prompt's
73094
+ * instructions for a `json` request, and a model following them commonly
73095
+ * returns the object inside one markdown code fence. The fence is presentation,
73096
+ * not content: the object inside it is the answer the model gave.
73097
+ *
73098
+ * So exactly ONE enclosing fence is removed before parsing, and nothing else is
73099
+ * forgiven. Prose before or after the fence, two fenced blocks, a fence that
73100
+ * never closes (a truncated answer), and a fence declaring another language all
73101
+ * still fail. Each of those is an answer whose meaning a parser would have to
73102
+ * guess, and a guessed object is a decision made on data no model produced.
73103
+ *
73104
+ * Content that is not fenced is parsed exactly as it always was: JSON cannot
73105
+ * begin with a backtick, so every answer that parsed before this unwrapping
73106
+ * existed takes the same path and yields the same value.
73107
+ *
73108
+ * @module llm/structured-content
73109
+ */
73110
+ /**
73111
+ * One markdown fence enclosing the whole answer: an opening line of three
73112
+ * backticks, optionally labelled `json`, then the body, then three closing
73113
+ * backticks, with nothing but whitespace outside them. The body is anchored at
73114
+ * both ends, so an answer holding two fenced blocks captures the text between
73115
+ * them and fails to parse instead of yielding either block.
73116
+ */
73117
+ const SINGLE_ENCLOSING_JSON_FENCE = /^\s*```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n?[ \t]*```\s*$/i;
73118
+ /**
73119
+ * Thrown when a provider answered a structured request with content that does
73120
+ * not parse.
73121
+ *
73122
+ * Carries the usage the provider billed for that answer. The tokens were spent
73123
+ * whether or not the content parsed, and a chain that dropped them would report
73124
+ * a failed attempt as free — understating spend by exactly the calls that went
73125
+ * wrong.
73126
+ */
73127
+ class LlmResponseFormatError extends Error {
73128
+ /** The format the caller asked for. */
73129
+ responseFormat;
73130
+ /** What the provider billed for the answer that did not parse. */
73131
+ usage;
73132
+ /** Whether the answer sat inside one enclosing fence that was removed before parsing. */
73133
+ fenced;
73134
+ /**
73135
+ * @param responseFormat The format the caller asked for.
73136
+ * @param usage What the provider billed for the answer.
73137
+ * @param fenced Whether one enclosing fence was removed before parsing.
73138
+ * @param cause The parser's own complaint.
73139
+ */
73140
+ constructor(responseFormat, usage, fenced, cause) {
73141
+ super(`LLM returned content that is not valid JSON for a ${responseFormat} request` +
73142
+ (fenced ? " (inside one enclosing markdown fence)" : "") +
73143
+ `: ${cause instanceof Error ? cause.message : String(cause)}`);
73144
+ this.name = "LlmResponseFormatError";
73145
+ this.responseFormat = responseFormat;
73146
+ this.usage = usage;
73147
+ this.fenced = fenced;
73148
+ }
73149
+ }
73150
+ /**
73151
+ * The body of the one markdown fence that encloses an answer, if exactly one does.
73152
+ *
73153
+ * @param text The model's answer.
73154
+ * @returns The fenced body, or null when the answer is not wholly one fenced block.
73155
+ */
73156
+ function unwrapSingleJsonFence(text) {
73157
+ const match = SINGLE_ENCLOSING_JSON_FENCE.exec(text);
73158
+ return match === null ? null : match[1];
73159
+ }
73160
+ /**
73161
+ * Parse a model's answer to a structured request.
73162
+ *
73163
+ * A JSON format that does not parse is an error, not an empty object. Returning
73164
+ * a default here would hand the caller a well-typed value that means nothing,
73165
+ * and the failure would surface much later as a decision made on absent data.
73166
+ *
73167
+ * @param content The raw content of the model's message.
73168
+ * @param responseFormat The structured format the caller asked for.
73169
+ * @param usage What the provider billed for this answer, carried on failure.
73170
+ * @returns The parsed value.
73171
+ * @throws {LlmResponseFormatError} When the content is not JSON, fenced or not.
73172
+ */
73173
+ function parseStructuredContent(content, responseFormat, usage) {
73174
+ const text = typeof content === "string" ? content : "";
73175
+ const fencedBody = unwrapSingleJsonFence(text);
73176
+ try {
73177
+ return JSON.parse(fencedBody ?? text);
73178
+ }
73179
+ catch (error) {
73180
+ throw new LlmResponseFormatError(typeof responseFormat === "string" ? responseFormat : "json_schema", usage, fencedBody !== null, error);
73181
+ }
72778
73182
  }
72779
73183
 
72780
73184
  /**
@@ -72911,7 +73315,9 @@ async function runLeg(leg, params, execution) {
72911
73315
  // The guards wrap the transport rather than the whole leg, so the per-leg
72912
73316
  // timeout above still bounds the total wait: a caller queued behind the
72913
73317
  // rate limiter is spending its budget just as surely as one waiting on the
72914
- // provider, and only one clock should govern both.
73318
+ // provider, and only one clock should govern both. The leg's own signal is
73319
+ // handed to the guard as well, so a leg whose budget or caller is gone
73320
+ // leaves the queue at once instead of holding its place in it.
72915
73321
  return await withProviderGuards(leg.route.providerName, () => leg.transport.execute({
72916
73322
  route: leg.route,
72917
73323
  content: execution.content,
@@ -72921,7 +73327,7 @@ async function runLeg(leg, params, execution) {
72921
73327
  context: execution.context,
72922
73328
  signal: controller.signal,
72923
73329
  correlationId: execution.correlationId,
72924
- }), budgetMs);
73330
+ }), budgetMs, { modelId: leg.route.modelId, signal: controller.signal });
72925
73331
  }
72926
73332
  finally {
72927
73333
  clearTimeout(timer);
@@ -73033,7 +73439,7 @@ async function executeChain(alias, execution) {
73033
73439
  continue;
73034
73440
  }
73035
73441
  const startedAt = now();
73036
- execution.breakers.onAttemptStart(route.routeKey);
73442
+ const holdsProbe = execution.breakers.onAttemptStart(route.routeKey);
73037
73443
  try {
73038
73444
  const response = await runLeg(leg, leg.params, execution);
73039
73445
  execution.breakers.onSuccess(route.routeKey);
@@ -73056,6 +73462,15 @@ async function executeChain(alias, execution) {
73056
73462
  if (countsAgainstHealth) {
73057
73463
  execution.breakers.onFailure(route.routeKey);
73058
73464
  }
73465
+ else if (holdsProbe) {
73466
+ // No verdict on the route's health, but the probe slot this attempt
73467
+ // took must come back, or a half-open route admits no probe ever again.
73468
+ execution.breakers.onAttemptAbandoned(route.routeKey);
73469
+ }
73470
+ // A provider that answered with unparseable content still billed for the
73471
+ // answer; the spend belongs in the total whether or not a later leg serves.
73472
+ const billed = error instanceof LlmResponseFormatError ? error.usage : undefined;
73473
+ totalUsage = sumUsage(totalUsage, billed);
73059
73474
  const record = {
73060
73475
  routeKey: route.routeKey,
73061
73476
  role: route.role,
@@ -73064,6 +73479,7 @@ async function executeChain(alias, execution) {
73064
73479
  outcome,
73065
73480
  durationMs: now() - startedAt,
73066
73481
  reason,
73482
+ ...(billed === undefined ? {} : { usage: billed }),
73067
73483
  };
73068
73484
  attempts.push(record);
73069
73485
  execution.onAttempt?.(record);
@@ -74357,9 +74773,13 @@ function createGatewayTransport(config) {
74357
74773
  const payload = (await response.json());
74358
74774
  const choices = payload.choices;
74359
74775
  const message = choices?.[0]?.message;
74776
+ // Usage is read before the content is interpreted. The provider billed for
74777
+ // this answer whether or not it parses, and a parse failure that dropped
74778
+ // the count would report the attempt as free.
74779
+ const usage = readUsage(payload, request);
74360
74780
  return {
74361
- response: parseContent(message?.content, request.responseFormat),
74362
- usage: readUsage(payload, request),
74781
+ response: interpretContent(message?.content, request.responseFormat, usage),
74782
+ usage,
74363
74783
  tool_calls: Array.isArray(message?.tool_calls)
74364
74784
  ? message.tool_calls
74365
74785
  : undefined,
@@ -74394,25 +74814,22 @@ function buildMessages(request) {
74394
74814
  /**
74395
74815
  * Interpret the model's content according to the requested format.
74396
74816
  *
74397
- * A JSON format that does not parse is an error, not an empty object. Returning
74398
- * a default here would hand the caller a well-typed value that means nothing,
74399
- * and the failure would surface much later as a decision made on absent data.
74817
+ * Text is returned as sent. A structured format is parsed under the strict
74818
+ * single-fence rule of {@link parseStructuredContent}; a structured answer that
74819
+ * does not parse is an error carrying what the provider billed for it, never an
74820
+ * empty object.
74400
74821
  *
74401
74822
  * @param content The raw content.
74402
74823
  * @param responseFormat The format the caller asked for.
74403
- * @returns The parsed value.
74824
+ * @param usage What the provider billed for this answer.
74825
+ * @returns The interpreted value.
74826
+ * @throws {LlmResponseFormatError} When a structured answer does not parse.
74404
74827
  */
74405
- function parseContent(content, responseFormat) {
74406
- const text = typeof content === "string" ? content : "";
74828
+ function interpretContent(content, responseFormat, usage) {
74407
74829
  if (responseFormat === "text") {
74408
- return text;
74409
- }
74410
- try {
74411
- return JSON.parse(text);
74412
- }
74413
- catch (error) {
74414
- throw new Error(`LLM returned content that is not valid JSON for a ${typeof responseFormat === "string" ? responseFormat : "json_schema"} request: ${error instanceof Error ? error.message : String(error)}`);
74830
+ return (typeof content === "string" ? content : "");
74415
74831
  }
74832
+ return parseStructuredContent(content, responseFormat, usage);
74416
74833
  }
74417
74834
 
74418
74835
  /**
@@ -78507,6 +78924,7 @@ const OrderStatusSchema = enumType([
78507
78924
  "accepted",
78508
78925
  "pending_new",
78509
78926
  "accepted_for_bidding",
78927
+ "held",
78510
78928
  "stopped",
78511
78929
  "rejected",
78512
78930
  "suspended",
@@ -80163,6 +80581,7 @@ exports.GatewayUnreachableError = GatewayUnreachableError;
80163
80581
  exports.HttpClientError = HttpClientError;
80164
80582
  exports.HttpServerError = HttpServerError;
80165
80583
  exports.KEEP_ALIVE_DEFAULTS = KEEP_ALIVE_DEFAULTS;
80584
+ exports.LlmResponseFormatError = LlmResponseFormatError;
80166
80585
  exports.MARKET_DATA_API = MARKET_DATA_API;
80167
80586
  exports.MassiveAggregatesResponseSchema = MassiveAggregatesResponseSchema;
80168
80587
  exports.MassiveApiError = MassiveApiError;
@@ -80210,6 +80629,7 @@ exports.alpaca = alpaca;
80210
80629
  exports.analyzeBars = analyzeBars;
80211
80630
  exports.approximateImpliedVolatility = approximateImpliedVolatility;
80212
80631
  exports.atr = atrNs;
80632
+ exports.availableStatistic = availableStatistic;
80213
80633
  exports.bracketOrders = bracketOrders;
80214
80634
  exports.buildOCCSymbol = buildOCCSymbol;
80215
80635
  exports.buildOptionSymbol = buildOptionSymbol;
@@ -80383,6 +80803,7 @@ exports.hasSufficientVolume = hasSufficientVolume;
80383
80803
  exports.httpAgent = httpAgent;
80384
80804
  exports.httpsAgent = httpsAgent;
80385
80805
  exports.isAlpacaBrokerCredentials = isAlpacaBrokerCredentials;
80806
+ exports.isAvailable = isAvailable;
80386
80807
  exports.isContractTradable = isContractTradable;
80387
80808
  exports.isCryptoPair = isCryptoPair;
80388
80809
  exports.isExpiringWithin = isExpiringWithin;
@@ -80429,6 +80850,7 @@ exports.routeKeyFor = routeKeyFor;
80429
80850
  exports.routeSupports = routeSupports;
80430
80851
  exports.routeTable = routeTable;
80431
80852
  exports.safeValidateResponse = safeValidateResponse;
80853
+ exports.sampleCohort = sampleCohort;
80432
80854
  exports.searchNews = searchNews;
80433
80855
  exports.sellAllCrypto = sellAllCrypto;
80434
80856
  exports.sellCryptoNotional = sellCryptoNotional;
@@ -80442,6 +80864,7 @@ exports.strategy = strategyNs;
80442
80864
  exports.sumUsage = sumUsage;
80443
80865
  exports.tradingPolicy = index;
80444
80866
  exports.trailingStops = trailingStops;
80867
+ exports.unavailableStatistic = unavailableStatistic;
80445
80868
  exports.updateAccountConfiguration = updateAccountConfiguration;
80446
80869
  exports.updateTrailingStop = updateTrailingStop;
80447
80870
  exports.validateAlpacaCredentials = validateAlpacaCredentials;