@adaptic/utils 0.0.1015 → 0.0.1016

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 (152) hide show
  1. package/dist/index.cjs +2823 -25
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.mjs +2783 -25
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/types/__tests__/llm/client/support/rejections.d.ts +21 -0
  6. package/dist/types/__tests__/llm/client/support/rejections.d.ts.map +1 -0
  7. package/dist/types/__tests__/llm/client/support/routes.d.ts +67 -0
  8. package/dist/types/__tests__/llm/client/support/routes.d.ts.map +1 -0
  9. package/dist/types/__tests__/llm/client/support/streams.d.ts +74 -0
  10. package/dist/types/__tests__/llm/client/support/streams.d.ts.map +1 -0
  11. package/dist/types/__tests__/llm/client/support/transports.d.ts +105 -0
  12. package/dist/types/__tests__/llm/client/support/transports.d.ts.map +1 -0
  13. package/dist/types/index.d.ts +1 -0
  14. package/dist/types/index.d.ts.map +1 -1
  15. package/dist/types/llm/alias-client.d.ts +64 -0
  16. package/dist/types/llm/alias-client.d.ts.map +1 -0
  17. package/dist/types/llm/circuit-breaker.d.ts +124 -0
  18. package/dist/types/llm/circuit-breaker.d.ts.map +1 -0
  19. package/dist/types/llm/eval/comparators.d.ts +127 -0
  20. package/dist/types/llm/eval/comparators.d.ts.map +1 -0
  21. package/dist/types/llm/eval/coverage.d.ts +44 -0
  22. package/dist/types/llm/eval/coverage.d.ts.map +1 -0
  23. package/dist/types/llm/eval/golden-set.d.ts +47 -0
  24. package/dist/types/llm/eval/golden-set.d.ts.map +1 -0
  25. package/dist/types/llm/eval/index.d.ts +25 -0
  26. package/dist/types/llm/eval/index.d.ts.map +1 -0
  27. package/dist/types/llm/eval/json-shape.d.ts +74 -0
  28. package/dist/types/llm/eval/json-shape.d.ts.map +1 -0
  29. package/dist/types/llm/eval/judge.d.ts +131 -0
  30. package/dist/types/llm/eval/judge.d.ts.map +1 -0
  31. package/dist/types/llm/eval/metrics.d.ts +51 -0
  32. package/dist/types/llm/eval/metrics.d.ts.map +1 -0
  33. package/dist/types/llm/eval/run.d.ts +97 -0
  34. package/dist/types/llm/eval/run.d.ts.map +1 -0
  35. package/dist/types/llm/eval/types.d.ts +242 -0
  36. package/dist/types/llm/eval/types.d.ts.map +1 -0
  37. package/dist/types/llm/fallback-chain.d.ts +97 -0
  38. package/dist/types/llm/fallback-chain.d.ts.map +1 -0
  39. package/dist/types/llm/index.d.ts +30 -0
  40. package/dist/types/llm/index.d.ts.map +1 -0
  41. package/dist/types/llm/param-matrix.d.ts +65 -0
  42. package/dist/types/llm/param-matrix.d.ts.map +1 -0
  43. package/dist/types/llm/rate-guard.d.ts +119 -0
  44. package/dist/types/llm/rate-guard.d.ts.map +1 -0
  45. package/dist/types/llm/route-table.d.ts +155 -0
  46. package/dist/types/llm/route-table.d.ts.map +1 -0
  47. package/dist/types/llm/schema-retry.d.ts +80 -0
  48. package/dist/types/llm/schema-retry.d.ts.map +1 -0
  49. package/dist/types/llm/streaming.d.ts +93 -0
  50. package/dist/types/llm/streaming.d.ts.map +1 -0
  51. package/dist/types/llm/transports/direct.d.ts +92 -0
  52. package/dist/types/llm/transports/direct.d.ts.map +1 -0
  53. package/dist/types/llm/transports/gateway.d.ts +73 -0
  54. package/dist/types/llm/transports/gateway.d.ts.map +1 -0
  55. package/dist/types/llm/types.d.ts +292 -0
  56. package/dist/types/llm/types.d.ts.map +1 -0
  57. package/dist/types/schemas/alpaca-schemas.d.ts +6 -6
  58. package/package.json +1 -1
  59. package/dist/types/__tests__/alpaca-broker-error-preservation.test.d.ts +0 -2
  60. package/dist/types/__tests__/alpaca-broker-error-preservation.test.d.ts.map +0 -1
  61. package/dist/types/__tests__/alpaca-client-order-id.test.d.ts +0 -2
  62. package/dist/types/__tests__/alpaca-client-order-id.test.d.ts.map +0 -1
  63. package/dist/types/__tests__/alpaca-functions.test.d.ts +0 -2
  64. package/dist/types/__tests__/alpaca-functions.test.d.ts.map +0 -1
  65. package/dist/types/__tests__/alpaca-market-data-retry.test.d.ts +0 -2
  66. package/dist/types/__tests__/alpaca-market-data-retry.test.d.ts.map +0 -1
  67. package/dist/types/__tests__/alpaca-order-idempotency.test.d.ts +0 -2
  68. package/dist/types/__tests__/alpaca-order-idempotency.test.d.ts.map +0 -1
  69. package/dist/types/__tests__/alpaca-trading-api.test.d.ts +0 -2
  70. package/dist/types/__tests__/alpaca-trading-api.test.d.ts.map +0 -1
  71. package/dist/types/__tests__/api-endpoints.test.d.ts +0 -2
  72. package/dist/types/__tests__/api-endpoints.test.d.ts.map +0 -1
  73. package/dist/types/__tests__/asset-allocation.test.d.ts +0 -2
  74. package/dist/types/__tests__/asset-allocation.test.d.ts.map +0 -1
  75. package/dist/types/__tests__/atr.test.d.ts +0 -2
  76. package/dist/types/__tests__/atr.test.d.ts.map +0 -1
  77. package/dist/types/__tests__/auth-validator.test.d.ts +0 -2
  78. package/dist/types/__tests__/auth-validator.test.d.ts.map +0 -1
  79. package/dist/types/__tests__/broker-factory.test.d.ts +0 -2
  80. package/dist/types/__tests__/broker-factory.test.d.ts.map +0 -1
  81. package/dist/types/__tests__/broker-types.test.d.ts +0 -2
  82. package/dist/types/__tests__/broker-types.test.d.ts.map +0 -1
  83. package/dist/types/__tests__/cache.test.d.ts +0 -2
  84. package/dist/types/__tests__/cache.test.d.ts.map +0 -1
  85. package/dist/types/__tests__/errors.test.d.ts +0 -2
  86. package/dist/types/__tests__/errors.test.d.ts.map +0 -1
  87. package/dist/types/__tests__/financial-regression.test.d.ts +0 -2
  88. package/dist/types/__tests__/financial-regression.test.d.ts.map +0 -1
  89. package/dist/types/__tests__/format-tools.test.d.ts +0 -2
  90. package/dist/types/__tests__/format-tools.test.d.ts.map +0 -1
  91. package/dist/types/__tests__/http-keep-alive.test.d.ts +0 -2
  92. package/dist/types/__tests__/http-keep-alive.test.d.ts.map +0 -1
  93. package/dist/types/__tests__/http-timeout.test.d.ts +0 -2
  94. package/dist/types/__tests__/http-timeout.test.d.ts.map +0 -1
  95. package/dist/types/__tests__/index.test.d.ts +0 -2
  96. package/dist/types/__tests__/index.test.d.ts.map +0 -1
  97. package/dist/types/__tests__/legacy-auth.test.d.ts +0 -2
  98. package/dist/types/__tests__/legacy-auth.test.d.ts.map +0 -1
  99. package/dist/types/__tests__/logger.test.d.ts +0 -2
  100. package/dist/types/__tests__/logger.test.d.ts.map +0 -1
  101. package/dist/types/__tests__/logging.test.d.ts +0 -2
  102. package/dist/types/__tests__/logging.test.d.ts.map +0 -1
  103. package/dist/types/__tests__/market-time.test.d.ts +0 -2
  104. package/dist/types/__tests__/market-time.test.d.ts.map +0 -1
  105. package/dist/types/__tests__/massive.test.d.ts +0 -2
  106. package/dist/types/__tests__/massive.test.d.ts.map +0 -1
  107. package/dist/types/__tests__/metrics-calcs-direction.test.d.ts +0 -2
  108. package/dist/types/__tests__/metrics-calcs-direction.test.d.ts.map +0 -1
  109. package/dist/types/__tests__/misc-utils.test.d.ts +0 -2
  110. package/dist/types/__tests__/misc-utils.test.d.ts.map +0 -1
  111. package/dist/types/__tests__/paginator.test.d.ts +0 -2
  112. package/dist/types/__tests__/paginator.test.d.ts.map +0 -1
  113. package/dist/types/__tests__/performance-metrics-fees.test.d.ts +0 -2
  114. package/dist/types/__tests__/performance-metrics-fees.test.d.ts.map +0 -1
  115. package/dist/types/__tests__/performance-metrics.test.d.ts +0 -2
  116. package/dist/types/__tests__/performance-metrics.test.d.ts.map +0 -1
  117. package/dist/types/__tests__/price-utils-fees.test.d.ts +0 -2
  118. package/dist/types/__tests__/price-utils-fees.test.d.ts.map +0 -1
  119. package/dist/types/__tests__/price-utils.test.d.ts +0 -2
  120. package/dist/types/__tests__/price-utils.test.d.ts.map +0 -1
  121. package/dist/types/__tests__/property-based-financial.test.d.ts +0 -2
  122. package/dist/types/__tests__/property-based-financial.test.d.ts.map +0 -1
  123. package/dist/types/__tests__/protective-order-sides.test.d.ts +0 -2
  124. package/dist/types/__tests__/protective-order-sides.test.d.ts.map +0 -1
  125. package/dist/types/__tests__/rate-limiter.test.d.ts +0 -2
  126. package/dist/types/__tests__/rate-limiter.test.d.ts.map +0 -1
  127. package/dist/types/__tests__/retry-classification.test.d.ts +0 -2
  128. package/dist/types/__tests__/retry-classification.test.d.ts.map +0 -1
  129. package/dist/types/__tests__/retry.test.d.ts +0 -2
  130. package/dist/types/__tests__/retry.test.d.ts.map +0 -1
  131. package/dist/types/__tests__/risk-free-rate.test.d.ts +0 -2
  132. package/dist/types/__tests__/risk-free-rate.test.d.ts.map +0 -1
  133. package/dist/types/__tests__/risk-metrics.test.d.ts +0 -2
  134. package/dist/types/__tests__/risk-metrics.test.d.ts.map +0 -1
  135. package/dist/types/__tests__/schema-validation.test.d.ts +0 -2
  136. package/dist/types/__tests__/schema-validation.test.d.ts.map +0 -1
  137. package/dist/types/__tests__/stampede-load-timeout.test.d.ts +0 -2
  138. package/dist/types/__tests__/stampede-load-timeout.test.d.ts.map +0 -1
  139. package/dist/types/__tests__/strategy-metrics.test.d.ts +0 -2
  140. package/dist/types/__tests__/strategy-metrics.test.d.ts.map +0 -1
  141. package/dist/types/__tests__/technical-analysis-totality.test.d.ts +0 -2
  142. package/dist/types/__tests__/technical-analysis-totality.test.d.ts.map +0 -1
  143. package/dist/types/__tests__/technical-analysis.test.d.ts +0 -2
  144. package/dist/types/__tests__/technical-analysis.test.d.ts.map +0 -1
  145. package/dist/types/__tests__/time-utils.test.d.ts +0 -2
  146. package/dist/types/__tests__/time-utils.test.d.ts.map +0 -1
  147. package/dist/types/__tests__/trading-policy-schemas.test.d.ts +0 -2
  148. package/dist/types/__tests__/trading-policy-schemas.test.d.ts.map +0 -1
  149. package/dist/types/__tests__/trailing-stops-portfolio.test.d.ts +0 -2
  150. package/dist/types/__tests__/trailing-stops-portfolio.test.d.ts.map +0 -1
  151. package/dist/types/__tests__/volatility.test.d.ts +0 -2
  152. package/dist/types/__tests__/volatility.test.d.ts.map +0 -1
package/dist/index.mjs CHANGED
@@ -3288,7 +3288,7 @@ const MIN_WAKE_DELAY_MS = 1;
3288
3288
  * Number of whole tokens required to release a single queued request. The token
3289
3289
  * bucket consumes exactly one token per admitted request.
3290
3290
  */
3291
- const TOKENS_PER_REQUEST = 1;
3291
+ const TOKENS_PER_REQUEST$1 = 1;
3292
3292
  /**
3293
3293
  * Token bucket rate limiter implementation
3294
3294
  *
@@ -3360,8 +3360,8 @@ class TokenBucketRateLimiter {
3360
3360
  // Require a WHOLE token: refill() accrues fractionally, and admitting on
3361
3361
  // any positive fraction would release a full request per accrual tick,
3362
3362
  // driving the bucket negative and overrunning the configured rate.
3363
- if (this.tokens >= TOKENS_PER_REQUEST) {
3364
- this.tokens -= TOKENS_PER_REQUEST;
3363
+ if (this.tokens >= TOKENS_PER_REQUEST$1) {
3364
+ this.tokens -= TOKENS_PER_REQUEST$1;
3365
3365
  logger.debug(`Rate limit token acquired for ${this.config.label}`, {
3366
3366
  remainingTokens: this.tokens,
3367
3367
  queueLength: this.queue.length,
@@ -3406,7 +3406,7 @@ class TokenBucketRateLimiter {
3406
3406
  if (this.wakeTimer !== null || this.queue.length === 0) {
3407
3407
  return;
3408
3408
  }
3409
- const tokensNeeded = Math.max(0, TOKENS_PER_REQUEST - this.tokens);
3409
+ const tokensNeeded = Math.max(0, TOKENS_PER_REQUEST$1 - this.tokens);
3410
3410
  const deficitMs = Math.max(MIN_WAKE_DELAY_MS, Math.ceil((tokensNeeded / this.config.refillRate) * MS_PER_SECOND$1));
3411
3411
  const timer = setTimeout(() => {
3412
3412
  this.wakeTimer = null;
@@ -3458,8 +3458,8 @@ class TokenBucketRateLimiter {
3458
3458
  const logger = getLogger();
3459
3459
  try {
3460
3460
  // Whole-token admission — see the matching guard in acquire().
3461
- while (this.queue.length > 0 && this.tokens >= TOKENS_PER_REQUEST) {
3462
- this.tokens -= TOKENS_PER_REQUEST;
3461
+ while (this.queue.length > 0 && this.tokens >= TOKENS_PER_REQUEST$1) {
3462
+ this.tokens -= TOKENS_PER_REQUEST$1;
3463
3463
  const next = this.queue.shift();
3464
3464
  if (next) {
3465
3465
  clearTimeout(next.timeoutHandle);
@@ -3541,7 +3541,7 @@ class TokenBucketRateLimiter {
3541
3541
  * await rateLimiters.alphaVantage.acquire();
3542
3542
  * ```
3543
3543
  */
3544
- const rateLimiters = {
3544
+ const rateLimiters$1 = {
3545
3545
  /**
3546
3546
  * Alpaca API rate limiter
3547
3547
  *
@@ -4147,7 +4147,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
4147
4147
  // bucket so concurrent callers (bar fetches, quotes, options, snapshots)
4148
4148
  // can't overrun Alpaca's server-side rate limit. Prior to this, parallel
4149
4149
  // historical-bar fan-out produced ~125 server-side 429s per minute.
4150
- await rateLimiters.alpaca.acquire();
4150
+ await rateLimiters$1.alpaca.acquire();
4151
4151
  // Retry ONLY transient connection faults, and only on GET (every
4152
4152
  // market-data read here is idempotent). A non-2xx response is a real
4153
4153
  // answer from Alpaca and is never retried — that path still throws on
@@ -4193,7 +4193,7 @@ class AlpacaMarketDataAPI extends EventEmitter {
4193
4193
  await new Promise((resolve) => setTimeout(resolve, delayMs));
4194
4194
  // Re-acquire the rate-limit token so a retry storm cannot overrun
4195
4195
  // Alpaca's server-side limit.
4196
- await rateLimiters.alpaca.acquire();
4196
+ await rateLimiters$1.alpaca.acquire();
4197
4197
  }
4198
4198
  }
4199
4199
  if (!response) {
@@ -9900,7 +9900,7 @@ const fetchTickerInfo = async (symbol, options) => {
9900
9900
  apiKey,
9901
9901
  });
9902
9902
  return massiveLimit(async () => {
9903
- await rateLimiters.massive.acquire();
9903
+ await rateLimiters$1.massive.acquire();
9904
9904
  try {
9905
9905
  const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 1000);
9906
9906
  const data = await response.json();
@@ -10013,7 +10013,7 @@ const fetchLastTradeImpl = async (symbol, options) => {
10013
10013
  order: "desc",
10014
10014
  });
10015
10015
  return massiveLimit(async () => {
10016
- await rateLimiters.massive.acquire();
10016
+ await rateLimiters$1.massive.acquire();
10017
10017
  try {
10018
10018
  const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 1000);
10019
10019
  const data = (await response.json());
@@ -10084,7 +10084,7 @@ const fetchLastQuote = async (symbol, options) => {
10084
10084
  order: "desc",
10085
10085
  });
10086
10086
  return massiveLimit(async () => {
10087
- await rateLimiters.massive.acquire();
10087
+ await rateLimiters$1.massive.acquire();
10088
10088
  try {
10089
10089
  const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 1000);
10090
10090
  const data = (await response.json());
@@ -10173,7 +10173,7 @@ const fetchPrices = async (params, options) => {
10173
10173
  let aggregatedStatus = "OK";
10174
10174
  while (nextUrl) {
10175
10175
  //getLogger().info(`Debug: Fetching ${nextUrl}`);
10176
- await rateLimiters.massive.acquire();
10176
+ await rateLimiters$1.massive.acquire();
10177
10177
  const response = await fetchWithRetry(nextUrl, { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 1000);
10178
10178
  const data = await response.json();
10179
10179
  if (!MASSIVE_VALID_STATUSES.has(data.status)) {
@@ -10360,7 +10360,7 @@ const fetchGroupedDaily = async (date, options) => {
10360
10360
  include_otc: options?.includeOTC ? "true" : "false",
10361
10361
  });
10362
10362
  return massiveLimit(async () => {
10363
- await rateLimiters.massive.acquire();
10363
+ await rateLimiters$1.massive.acquire();
10364
10364
  try {
10365
10365
  const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 1000);
10366
10366
  const data = await response.json();
@@ -10454,7 +10454,7 @@ symbol, date = new Date(), options) => {
10454
10454
  adjusted: (options?.adjusted ?? true).toString(),
10455
10455
  });
10456
10456
  return massiveLimit(async () => {
10457
- await rateLimiters.massive.acquire();
10457
+ await rateLimiters$1.massive.acquire();
10458
10458
  try {
10459
10459
  const response = await fetchWithRetry(`${baseUrl}?${params.toString()}`, { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 1000);
10460
10460
  const data = await response.json();
@@ -10528,7 +10528,7 @@ const fetchTrades = async (symbol, options) => {
10528
10528
  if (options?.sort)
10529
10529
  params.append("sort", options.sort);
10530
10530
  return massiveLimit(async () => {
10531
- await rateLimiters.massive.acquire();
10531
+ await rateLimiters$1.massive.acquire();
10532
10532
  const url = `${baseUrl}?${params.toString()}`;
10533
10533
  try {
10534
10534
  logIfDebug(`Fetching trades for ${symbol} from ${url}`);
@@ -10611,7 +10611,7 @@ const fetchIndicesAggregates = async (params, options) => {
10611
10611
  }
10612
10612
  url.search = queryParams.toString();
10613
10613
  return massiveIndicesLimit(async () => {
10614
- await rateLimiters.massive.acquire();
10614
+ await rateLimiters$1.massive.acquire();
10615
10615
  try {
10616
10616
  const response = await fetchWithRetry(url.toString(), { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 300);
10617
10617
  const data = await response.json();
@@ -10641,7 +10641,7 @@ const fetchIndicesPreviousClose = async (indicesTicker, options) => {
10641
10641
  queryParams.append("apiKey", apiKey);
10642
10642
  url.search = queryParams.toString();
10643
10643
  return massiveIndicesLimit(async () => {
10644
- await rateLimiters.massive.acquire();
10644
+ await rateLimiters$1.massive.acquire();
10645
10645
  try {
10646
10646
  const response = await fetchWithRetry(url.toString(), { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 300);
10647
10647
  const data = await response.json();
@@ -10672,7 +10672,7 @@ const fetchIndicesDailyOpenClose = async (indicesTicker, date, options) => {
10672
10672
  queryParams.append("apiKey", apiKey);
10673
10673
  url.search = queryParams.toString();
10674
10674
  return massiveIndicesLimit(async () => {
10675
- await rateLimiters.massive.acquire();
10675
+ await rateLimiters$1.massive.acquire();
10676
10676
  try {
10677
10677
  const response = await fetchWithRetry(url.toString(), { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 300);
10678
10678
  const data = await response.json();
@@ -10714,7 +10714,7 @@ const fetchIndicesSnapshot = async (params, options) => {
10714
10714
  }
10715
10715
  url.search = queryParams.toString();
10716
10716
  return massiveIndicesLimit(async () => {
10717
- await rateLimiters.massive.acquire();
10717
+ await rateLimiters$1.massive.acquire();
10718
10718
  try {
10719
10719
  const response = await fetchWithRetry(url.toString(), { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 300);
10720
10720
  const data = await response.json();
@@ -10763,7 +10763,7 @@ const fetchUniversalSnapshot = async (tickers, options) => {
10763
10763
  }
10764
10764
  url.search = queryParams.toString();
10765
10765
  return massiveIndicesLimit(async () => {
10766
- await rateLimiters.massive.acquire();
10766
+ await rateLimiters$1.massive.acquire();
10767
10767
  try {
10768
10768
  const response = await fetchWithRetry(url.toString(), { signal: createTimeoutSignal(DEFAULT_TIMEOUTS.MASSIVE_API) }, 3, 300);
10769
10769
  const data = await response.json();
@@ -52145,7 +52145,7 @@ class AlpacaClient {
52145
52145
  * @returns Result of the operation
52146
52146
  */
52147
52147
  async executeWithRateLimit(operation, label) {
52148
- await rateLimiters.alpaca.acquire();
52148
+ await rateLimiters$1.alpaca.acquire();
52149
52149
  return withRetry(operation, {
52150
52150
  maxRetries: 2,
52151
52151
  baseDelayMs: 1000,
@@ -52204,7 +52204,7 @@ class AlpacaClient {
52204
52204
  * @returns Response data
52205
52205
  */
52206
52206
  async makeRequest(endpoint, method = "GET", body) {
52207
- await rateLimiters.alpaca.acquire();
52207
+ await rateLimiters$1.alpaca.acquire();
52208
52208
  const url = `${this.apiBaseUrl}${endpoint}`;
52209
52209
  const options = {
52210
52210
  method,
@@ -62909,6 +62909,2764 @@ const alpaca = {
62909
62909
  streams: streams$1,
62910
62910
  };
62911
62911
 
62912
+ /**
62913
+ * Per-route circuit breaker for the alias client.
62914
+ *
62915
+ * A provider that has just failed five calls will almost certainly fail the
62916
+ * sixth, and every attempt spends latency budget the next leg of the chain
62917
+ * needs. The breaker converts that repeated discovery into a decision made
62918
+ * once: an unhealthy leg is skipped outright until it has had time to recover,
62919
+ * so a chain reaches its working leg quickly instead of paying the full
62920
+ * timeout of each dead one first.
62921
+ *
62922
+ * Breakers are keyed by chain POSITION, not by provider. Two consequences
62923
+ * follow, and both are intended. Reverting an alias to a different model at the
62924
+ * same position inherits that position's health rather than starting blind. And
62925
+ * an isolated route never shares a breaker with the shared one, so isolated
62926
+ * traffic can neither trip nor be tripped by traffic on the other side of the
62927
+ * PD-9 boundary.
62928
+ *
62929
+ * The clock is injected. Breaker behaviour is entirely about elapsed time, and
62930
+ * a test that must sleep to observe a cooldown is a test nobody runs.
62931
+ *
62932
+ * @module llm/circuit-breaker
62933
+ */
62934
+ /**
62935
+ * Tracks route health and decides whether a leg may be attempted.
62936
+ */
62937
+ class CircuitBreakerRegistry {
62938
+ records = new Map();
62939
+ config;
62940
+ now;
62941
+ /**
62942
+ * @param config Failure threshold, cooldown and half-open probe budget.
62943
+ * @param now Clock, injected so cooldowns are testable without waiting.
62944
+ */
62945
+ constructor(config, now = Date.now) {
62946
+ this.config = config;
62947
+ this.now = now;
62948
+ }
62949
+ /**
62950
+ * Current state of a route's breaker.
62951
+ *
62952
+ * The transition from open to half-open is computed from elapsed time at read
62953
+ * time rather than scheduled with a timer. A timer would keep the process
62954
+ * awake for every route that ever failed, and would drift whenever the
62955
+ * process was busy — which is exactly when a breaker matters most.
62956
+ *
62957
+ * @param routeKey The route's stable key.
62958
+ * @returns Its state.
62959
+ */
62960
+ stateOf(routeKey) {
62961
+ const record = this.records.get(routeKey);
62962
+ if (record === undefined || record.openedAtMs === null) {
62963
+ return "closed";
62964
+ }
62965
+ const elapsed = this.now() - record.openedAtMs;
62966
+ return elapsed >= this.config.cooldown_ms ? "half-open" : "open";
62967
+ }
62968
+ /**
62969
+ * Whether a route may be attempted now.
62970
+ *
62971
+ * A half-open route admits a bounded number of probes at once. Letting the
62972
+ * whole queue through the moment a cooldown expires would re-hammer a
62973
+ * provider that is still recovering, which is how a breaker turns into a
62974
+ * synchronised retry storm.
62975
+ *
62976
+ * @param routeKey The route's stable key.
62977
+ * @returns Whether an attempt is permitted.
62978
+ */
62979
+ allows(routeKey) {
62980
+ const state = this.stateOf(routeKey);
62981
+ if (state === "closed") {
62982
+ return true;
62983
+ }
62984
+ if (state === "open") {
62985
+ return false;
62986
+ }
62987
+ const record = this.recordFor(routeKey);
62988
+ return record.probesInFlight < this.config.half_open_probes;
62989
+ }
62990
+ /**
62991
+ * Register that an attempt is starting, so half-open probes stay bounded.
62992
+ *
62993
+ * @param routeKey The route's stable key.
62994
+ * @returns void
62995
+ */
62996
+ onAttemptStart(routeKey) {
62997
+ if (this.stateOf(routeKey) === "half-open") {
62998
+ this.recordFor(routeKey).probesInFlight += 1;
62999
+ }
63000
+ }
63001
+ /**
63002
+ * Record a success, closing the breaker.
63003
+ *
63004
+ * A single success closes it fully rather than decrementing the failure
63005
+ * count. The breaker's question is "is this route working now", and one
63006
+ * working call answers it; requiring several would keep a recovered provider
63007
+ * excluded while the chain paid for slower legs.
63008
+ *
63009
+ * @param routeKey The route's stable key.
63010
+ * @returns void
63011
+ */
63012
+ onSuccess(routeKey) {
63013
+ this.records.set(routeKey, {
63014
+ consecutiveFailures: 0,
63015
+ openedAtMs: null,
63016
+ probesInFlight: 0,
63017
+ });
63018
+ }
63019
+ /**
63020
+ * Record a failure, opening the breaker once the threshold is reached.
63021
+ *
63022
+ * A failure while half-open re-opens immediately without waiting to
63023
+ * re-accumulate the threshold: the probe was the test, and it failed.
63024
+ *
63025
+ * @param routeKey The route's stable key.
63026
+ * @returns void
63027
+ */
63028
+ onFailure(routeKey) {
63029
+ const wasHalfOpen = this.stateOf(routeKey) === "half-open";
63030
+ const record = this.recordFor(routeKey);
63031
+ record.probesInFlight = 0;
63032
+ record.consecutiveFailures += 1;
63033
+ if (wasHalfOpen || record.consecutiveFailures >= this.config.failure_threshold) {
63034
+ record.openedAtMs = this.now();
63035
+ }
63036
+ }
63037
+ /**
63038
+ * Inspect a route's breaker.
63039
+ *
63040
+ * @param routeKey The route's stable key.
63041
+ * @returns A snapshot.
63042
+ */
63043
+ snapshot(routeKey) {
63044
+ const record = this.records.get(routeKey) ?? {
63045
+ consecutiveFailures: 0,
63046
+ openedAtMs: null,
63047
+ probesInFlight: 0,
63048
+ };
63049
+ return {
63050
+ routeKey,
63051
+ state: this.stateOf(routeKey),
63052
+ consecutiveFailures: record.consecutiveFailures,
63053
+ openedAtMs: record.openedAtMs,
63054
+ probesInFlight: record.probesInFlight,
63055
+ };
63056
+ }
63057
+ /**
63058
+ * Every route the registry has observed.
63059
+ *
63060
+ * @returns Snapshots, sorted by route key for stable output.
63061
+ */
63062
+ snapshotAll() {
63063
+ return [...this.records.keys()]
63064
+ .sort()
63065
+ .map((routeKey) => this.snapshot(routeKey));
63066
+ }
63067
+ /**
63068
+ * Discard all breaker state.
63069
+ *
63070
+ * @returns void
63071
+ */
63072
+ reset() {
63073
+ this.records.clear();
63074
+ }
63075
+ /**
63076
+ * @param routeKey The route's stable key.
63077
+ * @returns The mutable record, created on first use.
63078
+ */
63079
+ recordFor(routeKey) {
63080
+ let record = this.records.get(routeKey);
63081
+ if (record === undefined) {
63082
+ record = { consecutiveFailures: 0, openedAtMs: null, probesInFlight: 0 };
63083
+ this.records.set(routeKey, record);
63084
+ }
63085
+ return record;
63086
+ }
63087
+ }
63088
+
63089
+ /**
63090
+ * Parameter normalisation across providers.
63091
+ *
63092
+ * A caller passes one option bag and the chain may serve it from any of three
63093
+ * providers, so the same request has to be expressible to all of them. Vendors
63094
+ * disagree about more than spelling: some reject a sampling parameter they do
63095
+ * not support with a hard 400 rather than ignoring it, some name the output
63096
+ * cap differently, and reasoning models take an effort knob that non-reasoning
63097
+ * models refuse. Left unnormalised, a fallback would fail on the leg it fell
63098
+ * back to — turning the mechanism that exists to survive an outage into a
63099
+ * second way to fail.
63100
+ *
63101
+ * The rule throughout is that an unsupported parameter is OMITTED, never sent
63102
+ * with a default. Sending a default asserts a value the caller did not choose;
63103
+ * omitting it lets the provider apply its own, which is what "unsupported"
63104
+ * actually means.
63105
+ *
63106
+ * @module llm/param-matrix
63107
+ */
63108
+ /**
63109
+ * Providers that name the output cap `max_tokens` rather than
63110
+ * `max_completion_tokens`.
63111
+ *
63112
+ * The split is a wire-format fact about each API, so it is recorded as data
63113
+ * next to the translation that uses it rather than inferred from a version
63114
+ * number that will move.
63115
+ */
63116
+ const MAX_TOKENS_PARAM_BY_API_STYLE = {
63117
+ anthropic: "max_tokens",
63118
+ "openai-compatible": "max_completion_tokens",
63119
+ };
63120
+ /** The response-format key an OpenAI-compatible provider expects. */
63121
+ const RESPONSE_FORMAT_KEY = "response_format";
63122
+ /**
63123
+ * Normalise a caller's options into the exact parameter set one leg accepts.
63124
+ *
63125
+ * @param options The caller's options.
63126
+ * @param route The leg the request is being prepared for.
63127
+ * @param responseFormat The response shape the caller asked for.
63128
+ * @returns Parameters ready to send verbatim.
63129
+ */
63130
+ function normaliseParams(options, route, responseFormat) {
63131
+ const params = {};
63132
+ const capabilities = route.params;
63133
+ // Temperature. A model that accepts only its default value returns a hard
63134
+ // error for the parameter's mere presence, so support is checked before the
63135
+ // caller's preference is consulted at all.
63136
+ const temperature = options.temperature ?? capabilities.temperature ?? undefined;
63137
+ if (capabilities.supports_temperature !== false && typeof temperature === "number") {
63138
+ params.temperature = temperature;
63139
+ }
63140
+ // Output cap. The caller's value wins where present; the route's declared cap
63141
+ // is the ceiling, because exceeding it is a provider error rather than a
63142
+ // larger answer.
63143
+ const routeCap = capabilities.max_output_tokens ?? undefined;
63144
+ const requested = options.maxOutputTokens ?? routeCap;
63145
+ if (typeof requested === "number") {
63146
+ const capped = typeof routeCap === "number" ? Math.min(requested, routeCap) : requested;
63147
+ const key = MAX_TOKENS_PARAM_BY_API_STYLE[route.provider.api_style] ?? "max_tokens";
63148
+ params[key] = capped;
63149
+ }
63150
+ // Reasoning effort is meaningful only where the route declares it, and is
63151
+ // dropped elsewhere rather than translated into a temperature.
63152
+ const effort = options.reasoningEffort ?? capabilities.reasoning_effort ?? undefined;
63153
+ if (typeof effort === "string" && capabilities.reasoning_effort !== undefined) {
63154
+ params.reasoning_effort = effort;
63155
+ }
63156
+ const format = normaliseResponseFormat(responseFormat, route);
63157
+ if (format !== undefined) {
63158
+ params[RESPONSE_FORMAT_KEY] = format;
63159
+ }
63160
+ if (options.tools !== undefined && options.tools.length > 0) {
63161
+ if (capabilities.supports_tools === false) {
63162
+ throw new UnsupportedCapabilityError(route, "tools");
63163
+ }
63164
+ params.tools = [...options.tools];
63165
+ // Parallel tool calls are off by default: a decision path that fans out
63166
+ // tool calls concurrently reorders its own effects, and ordering is part
63167
+ // of the meaning of a sequence of trading actions.
63168
+ params.parallel_tool_calls = false;
63169
+ }
63170
+ if (options.metadata !== undefined) {
63171
+ params.metadata = { ...options.metadata };
63172
+ }
63173
+ return params;
63174
+ }
63175
+ /**
63176
+ * Translate the requested response shape into the parameter a route accepts.
63177
+ *
63178
+ * A strict JSON schema is a real capability, not a formatting preference: a
63179
+ * route that cannot enforce one would return prose where the caller's parser
63180
+ * expects an object. Rather than silently degrading to free JSON — which fails
63181
+ * later, further from the cause — a route without the capability refuses the
63182
+ * request so the chain advances to one that has it.
63183
+ *
63184
+ * @param responseFormat What the caller asked for.
63185
+ * @param route The leg being prepared.
63186
+ * @returns The provider-facing value, or undefined for plain text.
63187
+ */
63188
+ function normaliseResponseFormat(responseFormat, route) {
63189
+ if (responseFormat === "text") {
63190
+ return undefined;
63191
+ }
63192
+ if (responseFormat === "json") {
63193
+ return { type: "json_object" };
63194
+ }
63195
+ if (route.params.supports_json_schema === false) {
63196
+ throw new UnsupportedCapabilityError(route, "json_schema");
63197
+ }
63198
+ return {
63199
+ type: "json_schema",
63200
+ json_schema: {
63201
+ name: "structured_response",
63202
+ strict: true,
63203
+ schema: responseFormat.schema,
63204
+ },
63205
+ };
63206
+ }
63207
+ /**
63208
+ * Thrown when a leg cannot honour a capability the caller requires.
63209
+ *
63210
+ * A distinct type rather than a generic error, because the chain treats it
63211
+ * differently from a provider outage: the leg is not broken, it is simply the
63212
+ * wrong leg for this request, and no retry against it will help.
63213
+ */
63214
+ class UnsupportedCapabilityError extends Error {
63215
+ /** The leg that cannot serve the request. */
63216
+ routeKey;
63217
+ /** The capability it lacks. */
63218
+ capability;
63219
+ /**
63220
+ * @param route The leg.
63221
+ * @param capability The missing capability.
63222
+ */
63223
+ constructor(route, capability) {
63224
+ super(`route ${route.routeKey} (${route.providerName}/${route.modelId}) does not support ${capability}; ` +
63225
+ "the chain advances rather than degrading the request, so the caller's contract is never silently weakened");
63226
+ this.name = "UnsupportedCapabilityError";
63227
+ this.routeKey = route.routeKey;
63228
+ this.capability = capability;
63229
+ }
63230
+ }
63231
+ /**
63232
+ * Whether a leg can serve a request needing the given capabilities at all.
63233
+ *
63234
+ * Used to skip a leg before spending a network round trip on it. Checking
63235
+ * up front rather than reacting to the provider's rejection keeps a
63236
+ * capability mismatch from consuming the caller's latency budget.
63237
+ *
63238
+ * @param route The leg.
63239
+ * @param needs Capabilities the request requires.
63240
+ * @returns Whether the leg is a candidate.
63241
+ */
63242
+ function routeSupports(route, needs) {
63243
+ if (needs.tools === true && route.params.supports_tools === false) {
63244
+ return false;
63245
+ }
63246
+ if (needs.jsonSchema === true && route.params.supports_json_schema === false) {
63247
+ return false;
63248
+ }
63249
+ if (needs.vision === true && route.params.supports_vision !== true) {
63250
+ return false;
63251
+ }
63252
+ if (needs.cacheControl === true && route.params.supports_cache_control !== true) {
63253
+ return false;
63254
+ }
63255
+ return true;
63256
+ }
63257
+
63258
+ var defaults$1 = {
63259
+ _readme: "Applied to any provider whose published ceiling is unknown. Chosen to be comfortably below the slowest plausible published limit: being slower than necessary costs latency, while being faster than permitted costs 429s that the fallback chain will read as provider ill-health and use to open a circuit breaker. The asymmetry is what makes the conservative side the correct default.",
63260
+ basis: "conservative-default",
63261
+ requests_per_minute: 60,
63262
+ max_concurrent: 4,
63263
+ acquire_timeout_ms: 15000
63264
+ };
63265
+ var providers$1 = {
63266
+ anthropic: {
63267
+ basis: "conservative-default",
63268
+ requests_per_minute: 50,
63269
+ max_concurrent: 4,
63270
+ acquire_timeout_ms: 15000,
63271
+ source: null,
63272
+ note: "Anthropic publishes tier-dependent limits; the account's tier is not recorded here. Transcribe the real ceiling from the console at W3-07 and set basis to published."
63273
+ },
63274
+ openai: {
63275
+ basis: "conservative-default",
63276
+ requests_per_minute: 60,
63277
+ max_concurrent: 4,
63278
+ acquire_timeout_ms: 15000,
63279
+ source: null,
63280
+ note: "Tier-dependent. No alias routes here today; the entry exists so a revert to an OpenAI incumbent inherits a bounded client rather than an unbounded one."
63281
+ },
63282
+ deepseek: {
63283
+ basis: "conservative-default",
63284
+ requests_per_minute: 60,
63285
+ max_concurrent: 4,
63286
+ acquire_timeout_ms: 30000,
63287
+ source: null,
63288
+ note: "Serves llm.extract, a batch-class alias, so a longer acquire timeout is appropriate: a batch caller can afford to queue where a hot-path caller cannot."
63289
+ },
63290
+ deepinfra: {
63291
+ basis: "conservative-default",
63292
+ requests_per_minute: 60,
63293
+ max_concurrent: 4,
63294
+ acquire_timeout_ms: 15000,
63295
+ source: null,
63296
+ note: "Awaiting W3-02. Transcribe the published ceiling then."
63297
+ },
63298
+ fireworks: {
63299
+ basis: "conservative-default",
63300
+ requests_per_minute: 60,
63301
+ max_concurrent: 4,
63302
+ acquire_timeout_ms: 15000,
63303
+ source: null,
63304
+ note: "The backlog calls out recording Fireworks' published RPM ceiling specifically (W3-03 -> W4-04). Do that at onboarding."
63305
+ },
63306
+ zai: {
63307
+ basis: "conservative-default",
63308
+ requests_per_minute: 60,
63309
+ max_concurrent: 4,
63310
+ acquire_timeout_ms: 15000,
63311
+ source: null,
63312
+ note: "Awaiting W3-04."
63313
+ },
63314
+ groq: {
63315
+ basis: "conservative-default",
63316
+ requests_per_minute: 30,
63317
+ max_concurrent: 2,
63318
+ acquire_timeout_ms: 10000,
63319
+ source: null,
63320
+ note: "Declared primary of the hot-path llm.fast alias but blocked on open item OI-01. Held tighter than the default because a hot-path caller cannot afford to queue: if the limit binds, failing fast into the chain beats waiting."
63321
+ },
63322
+ openrouter: {
63323
+ basis: "conservative-default",
63324
+ requests_per_minute: 30,
63325
+ max_concurrent: 2,
63326
+ acquire_timeout_ms: 15000,
63327
+ source: null,
63328
+ note: "Aggregator backstop, reached by explicit route only. Held tight because its own limits are a function of whichever upstream it selects, which the client cannot observe."
63329
+ }
63330
+ };
63331
+ var limitsConfig = {
63332
+ defaults: defaults$1,
63333
+ providers: providers$1
63334
+ };
63335
+
63336
+ /**
63337
+ * Client-side rate and concurrency guards, per provider (W4-04).
63338
+ *
63339
+ * A provider's rate limit is enforced at the provider whether or not the client
63340
+ * respects it. The reason to respect it here is what a 429 means once it
63341
+ * arrives: to the fallback chain it is indistinguishable from provider
63342
+ * ill-health, so a client that over-drives a healthy provider will open that
63343
+ * provider's circuit breaker, fail over to a more expensive leg, and keep doing
63344
+ * so — converting a self-inflicted pacing problem into a permanent routing
63345
+ * change nobody chose. Pacing at the client is what keeps the breaker measuring
63346
+ * the provider rather than measuring us.
63347
+ *
63348
+ * Two distinct bounds are applied because they fail differently. The rate bound
63349
+ * (requests per minute) protects the provider's published ceiling. The
63350
+ * concurrency bound protects the caller: a hundred simultaneous in-flight
63351
+ * requests will each wait behind the other ninety-nine at the provider, so
63352
+ * every one of them blows its latency budget and the fan-out produces a hundred
63353
+ * timeouts instead of a queue.
63354
+ *
63355
+ * Limits live in `provider-limits.json`, not here. A rate limit discovered
63356
+ * during an incident should be correctable by config, not by a release.
63357
+ *
63358
+ * @module llm/rate-guard
63359
+ */
63360
+ /** Seconds in a minute, converting a published per-minute ceiling to a refill rate. */
63361
+ const SECONDS_PER_MINUTE = 60;
63362
+ /** One request consumes one token. */
63363
+ const TOKENS_PER_REQUEST = 1;
63364
+ const config$1 = limitsConfig;
63365
+ /**
63366
+ * Resolve the limits that apply to a provider.
63367
+ *
63368
+ * An unregistered provider falls back to the conservative defaults rather than
63369
+ * to no limit at all. Treating "unknown" as "unlimited" would make every newly
63370
+ * onboarded provider the one most likely to be over-driven, which is exactly
63371
+ * backwards: a new provider is the one whose real ceiling is least understood.
63372
+ *
63373
+ * @param provider The provider key.
63374
+ * @returns Its limits.
63375
+ */
63376
+ function limitsFor(provider) {
63377
+ return config$1.providers[provider] ?? config$1.defaults;
63378
+ }
63379
+ /** Every provider with a recorded limit, plus whether it is published or a default. */
63380
+ function limitsInventory() {
63381
+ return Object.keys(config$1.providers)
63382
+ .sort()
63383
+ .map((provider) => ({ provider, limits: config$1.providers[provider] }));
63384
+ }
63385
+ /**
63386
+ * Thrown when a caller could not acquire a slot within its budget.
63387
+ *
63388
+ * Distinguished from a provider failure so the chain does not count it against
63389
+ * route health: the provider was never asked, so nothing was learned about it.
63390
+ */
63391
+ class RateGuardTimeoutError extends Error {
63392
+ /** The provider whose guard could not admit the call. */
63393
+ provider;
63394
+ /** Which of the two bounds the caller waited on. */
63395
+ bound;
63396
+ /**
63397
+ * @param provider The provider.
63398
+ * @param bound Which bound was binding.
63399
+ * @param waitedMs How long the caller waited.
63400
+ */
63401
+ constructor(provider, bound, waitedMs) {
63402
+ super(`client-side ${bound} guard for provider "${provider}" did not admit the call within ${waitedMs} ms. ` +
63403
+ "The provider was never contacted, so this says nothing about its health.");
63404
+ this.name = "RateGuardTimeoutError";
63405
+ this.provider = provider;
63406
+ this.bound = bound;
63407
+ }
63408
+ }
63409
+ /**
63410
+ * A counting semaphore bounding simultaneous in-flight calls.
63411
+ *
63412
+ * Written here rather than pulled from a dependency because it is fifteen lines
63413
+ * and because the waiting behaviour matters: a waiter that times out must be
63414
+ * removed from the queue, or a burst of abandoned callers permanently consumes
63415
+ * the permits that later callers need.
63416
+ */
63417
+ class ConcurrencyGate {
63418
+ inFlight = 0;
63419
+ waiters = [];
63420
+ limit;
63421
+ provider;
63422
+ /**
63423
+ * @param provider The provider this gate guards.
63424
+ * @param limit Maximum simultaneous in-flight calls.
63425
+ */
63426
+ constructor(provider, limit) {
63427
+ this.provider = provider;
63428
+ this.limit = limit;
63429
+ }
63430
+ /**
63431
+ * Wait for a permit.
63432
+ *
63433
+ * @param timeoutMs How long the caller is willing to queue.
63434
+ * @returns A release function the caller must invoke exactly once.
63435
+ */
63436
+ async acquire(timeoutMs) {
63437
+ if (this.inFlight < this.limit) {
63438
+ this.inFlight += 1;
63439
+ return () => this.release();
63440
+ }
63441
+ await new Promise((resolve, reject) => {
63442
+ const timer = setTimeout(() => {
63443
+ const index = this.waiters.findIndex((waiter) => waiter.timer === timer);
63444
+ if (index !== -1) {
63445
+ this.waiters.splice(index, 1);
63446
+ }
63447
+ reject(new RateGuardTimeoutError(this.provider, "concurrency", timeoutMs));
63448
+ }, timeoutMs);
63449
+ this.waiters.push({ resolve, reject, timer });
63450
+ });
63451
+ this.inFlight += 1;
63452
+ return () => this.release();
63453
+ }
63454
+ /**
63455
+ * Return a permit and admit the next waiter.
63456
+ *
63457
+ * @returns void
63458
+ */
63459
+ release() {
63460
+ this.inFlight -= 1;
63461
+ const next = this.waiters.shift();
63462
+ if (next !== undefined) {
63463
+ clearTimeout(next.timer);
63464
+ next.resolve();
63465
+ }
63466
+ }
63467
+ /**
63468
+ * @returns How many calls are currently in flight.
63469
+ */
63470
+ inFlightCount() {
63471
+ return this.inFlight;
63472
+ }
63473
+ /**
63474
+ * @returns How many callers are queued.
63475
+ */
63476
+ queueLength() {
63477
+ return this.waiters.length;
63478
+ }
63479
+ }
63480
+ /** Per-provider guards, created on first use and shared process-wide. */
63481
+ const rateLimiters = new Map();
63482
+ const concurrencyGates = new Map();
63483
+ /**
63484
+ * The rate limiter for a provider.
63485
+ *
63486
+ * Shared process-wide rather than per-call-site, because the provider's ceiling
63487
+ * applies to the process as a whole. Per-call-site limiters would each stay
63488
+ * under the ceiling while their sum sailed past it.
63489
+ *
63490
+ * @param provider The provider key.
63491
+ * @returns Its limiter.
63492
+ */
63493
+ function rateLimiterFor(provider) {
63494
+ let limiter = rateLimiters.get(provider);
63495
+ if (limiter === undefined) {
63496
+ const limits = limitsFor(provider);
63497
+ limiter = new TokenBucketRateLimiter({
63498
+ maxTokens: limits.requests_per_minute,
63499
+ refillRate: limits.requests_per_minute / SECONDS_PER_MINUTE,
63500
+ label: `llm:${provider}`,
63501
+ timeoutMs: limits.acquire_timeout_ms,
63502
+ });
63503
+ rateLimiters.set(provider, limiter);
63504
+ }
63505
+ return limiter;
63506
+ }
63507
+ /**
63508
+ * The concurrency gate for a provider.
63509
+ *
63510
+ * @param provider The provider key.
63511
+ * @returns Its gate.
63512
+ */
63513
+ function concurrencyGateFor(provider) {
63514
+ let gate = concurrencyGates.get(provider);
63515
+ if (gate === undefined) {
63516
+ gate = new ConcurrencyGate(provider, limitsFor(provider).max_concurrent);
63517
+ concurrencyGates.set(provider, gate);
63518
+ }
63519
+ return gate;
63520
+ }
63521
+ /**
63522
+ * Run a call under a provider's rate and concurrency guards.
63523
+ *
63524
+ * The concurrency permit is taken AFTER the rate token. Taking it first would
63525
+ * let callers hold scarce permits while idling in the rate queue, which
63526
+ * throttles the provider twice over and turns a pacing bound into a deadlock
63527
+ * shaped like slowness.
63528
+ *
63529
+ * `maxWaitMs` bounds how long a caller may queue. It exists because the queue
63530
+ * spends the SAME budget the call itself does: a caller that waits out its whole
63531
+ * deadline in a rate queue has failed just as completely as one that waited on
63532
+ * the provider, and worse, it never reached the fallback chain that could have
63533
+ * answered it. Passing the leg's own timeout keeps one clock governing the
63534
+ * whole attempt.
63535
+ *
63536
+ * @param provider The provider key.
63537
+ * @param call The work to run once admitted.
63538
+ * @param maxWaitMs Ceiling on queue time; the configured guard timeout applies when lower.
63539
+ * @returns The call's result.
63540
+ * @throws {RateGuardTimeoutError} When neither bound admitted the call in time.
63541
+ */
63542
+ async function withProviderGuards(provider, call, maxWaitMs) {
63543
+ const limits = limitsFor(provider);
63544
+ const waitBudgetMs = maxWaitMs === undefined
63545
+ ? limits.acquire_timeout_ms
63546
+ : Math.min(maxWaitMs, limits.acquire_timeout_ms);
63547
+ try {
63548
+ await rateLimiterFor(provider).acquire();
63549
+ }
63550
+ catch {
63551
+ throw new RateGuardTimeoutError(provider, "rate", waitBudgetMs);
63552
+ }
63553
+ const release = await concurrencyGateFor(provider).acquire(waitBudgetMs);
63554
+ try {
63555
+ return await call();
63556
+ }
63557
+ finally {
63558
+ // Released on every path. A permit leaked on the error path would shrink
63559
+ // the effective limit by one on each failure until nothing could run — and
63560
+ // failures cluster exactly when throughput matters most.
63561
+ release();
63562
+ }
63563
+ }
63564
+ /**
63565
+ * Inspect the guards currently in use.
63566
+ *
63567
+ * @returns A snapshot per provider that has been used, sorted by provider.
63568
+ */
63569
+ function guardSnapshots() {
63570
+ const providers = new Set([...rateLimiters.keys(), ...concurrencyGates.keys()]);
63571
+ return [...providers].sort().map((provider) => {
63572
+ const limits = limitsFor(provider);
63573
+ const limiter = rateLimiters.get(provider);
63574
+ const gate = concurrencyGates.get(provider);
63575
+ return {
63576
+ provider,
63577
+ basis: limits.basis,
63578
+ requestsPerMinute: limits.requests_per_minute,
63579
+ maxConcurrent: limits.max_concurrent,
63580
+ inFlight: gate?.inFlightCount() ?? 0,
63581
+ rateQueueLength: limiter?.getQueueLength() ?? 0,
63582
+ concurrencyQueueLength: gate?.queueLength() ?? 0,
63583
+ availableTokens: limiter?.getAvailableTokens() ?? limits.requests_per_minute * TOKENS_PER_REQUEST,
63584
+ };
63585
+ });
63586
+ }
63587
+ /**
63588
+ * Discard all guard state.
63589
+ *
63590
+ * Exists so a test can start from a known position; a shared process-wide
63591
+ * limiter is otherwise carried between tests and makes their order matter.
63592
+ *
63593
+ * @returns void
63594
+ */
63595
+ function resetProviderGuards() {
63596
+ for (const limiter of rateLimiters.values()) {
63597
+ limiter.reset();
63598
+ }
63599
+ rateLimiters.clear();
63600
+ concurrencyGates.clear();
63601
+ }
63602
+
63603
+ /**
63604
+ * Ordered execution of an alias's fallback chain (PD-3).
63605
+ *
63606
+ * Every call walks the chain primary -> secondary -> closed incumbent, and each
63607
+ * leg runs under a hard timeout and a circuit breaker. Those three controls are
63608
+ * one mechanism rather than three features: a chain without timeouts never
63609
+ * reaches its second leg, a chain without a breaker pays a dead provider's full
63610
+ * timeout on every call, and a timeout without a chain is just a slower
63611
+ * failure. Timeout cascades are this system's known brown-out mode, which is
63612
+ * why the budget is enforced here — at the only place that knows both the
63613
+ * caller's deadline and how many legs are left to spend it on.
63614
+ *
63615
+ * Nothing here ever substitutes a value for an outcome. When every leg is
63616
+ * exhausted the caller gets a typed error naming each leg and why it failed,
63617
+ * because a default returned in place of an answer is a wrong answer that
63618
+ * nobody is told about.
63619
+ *
63620
+ * @module llm/fallback-chain
63621
+ */
63622
+ /** Zero-valued usage, used as the identity when summing across attempts. */
63623
+ const EMPTY_USAGE = {
63624
+ prompt_tokens: 0,
63625
+ completion_tokens: 0,
63626
+ provider: "none",
63627
+ model: "none",
63628
+ cost: 0,
63629
+ };
63630
+ /**
63631
+ * Thrown when every leg of a chain has been tried and none produced an answer.
63632
+ *
63633
+ * Carries the full attempt record rather than only the last error. The last
63634
+ * error is usually the least informative one — the incumbent timing out says
63635
+ * nothing about why the two legs before it were skipped — and an operator
63636
+ * reading only that would go looking in the wrong place.
63637
+ */
63638
+ class ChainExhaustedError extends Error {
63639
+ /** The alias whose chain was exhausted. */
63640
+ alias;
63641
+ /** Every leg tried, in order, with its outcome. */
63642
+ attempts;
63643
+ /** Usage spent across the failed attempts, so the spend is still accounted for. */
63644
+ totalUsage;
63645
+ /**
63646
+ * @param alias The alias.
63647
+ * @param attempts The attempt record.
63648
+ * @param totalUsage Usage spent across all attempts.
63649
+ */
63650
+ constructor(alias, attempts, totalUsage) {
63651
+ const detail = attempts
63652
+ .map((attempt) => `${attempt.role}(${attempt.provider}/${attempt.modelId}): ${attempt.outcome}` +
63653
+ (attempt.reason === undefined ? "" : ` — ${attempt.reason}`))
63654
+ .join("; ");
63655
+ super(`LLM alias "${alias}" exhausted its fallback chain. Attempts: ${detail || "(no leg was servable)"}`);
63656
+ this.name = "ChainExhaustedError";
63657
+ this.alias = alias;
63658
+ this.attempts = attempts;
63659
+ this.totalUsage = totalUsage;
63660
+ }
63661
+ }
63662
+ /**
63663
+ * Add two usage records.
63664
+ *
63665
+ * Attribution keeps the LAST attempt's provider and model, because that is the
63666
+ * one that produced the answer the caller is holding, while the token counts
63667
+ * accumulate across every attempt. Charging only the successful attempt would
63668
+ * understate spend by exactly the amount the failures cost — which is the
63669
+ * amount a fallback chain is most likely to run up.
63670
+ *
63671
+ * @param a The running total.
63672
+ * @param b The attempt to add, if any.
63673
+ * @returns The combined usage.
63674
+ */
63675
+ function sumUsage(a, b) {
63676
+ if (b === undefined) {
63677
+ return a;
63678
+ }
63679
+ return {
63680
+ prompt_tokens: a.prompt_tokens + b.prompt_tokens,
63681
+ completion_tokens: a.completion_tokens + b.completion_tokens,
63682
+ reasoning_tokens: a.reasoning_tokens === undefined && b.reasoning_tokens === undefined
63683
+ ? undefined
63684
+ : (a.reasoning_tokens ?? 0) + (b.reasoning_tokens ?? 0),
63685
+ cached_tokens: a.cached_tokens === undefined && b.cached_tokens === undefined
63686
+ ? undefined
63687
+ : (a.cached_tokens ?? 0) + (b.cached_tokens ?? 0),
63688
+ provider: b.provider,
63689
+ model: b.model,
63690
+ cost: a.cost + b.cost,
63691
+ };
63692
+ }
63693
+ /** Raised internally when a leg exceeds its budget. */
63694
+ class LegTimeoutError extends Error {
63695
+ /**
63696
+ * @param routeKey The leg that timed out.
63697
+ * @param budgetMs Its budget in milliseconds.
63698
+ */
63699
+ constructor(routeKey, budgetMs) {
63700
+ super(`route ${routeKey} exceeded its ${budgetMs} ms budget`);
63701
+ this.name = "LegTimeoutError";
63702
+ }
63703
+ }
63704
+ /**
63705
+ * Run one leg under a hard timeout, honouring the caller's own cancellation.
63706
+ *
63707
+ * The timer is always cleared and the abort listener always removed, including
63708
+ * on the success path. A long-lived process that leaked one timer per LLM call
63709
+ * would accumulate them at exactly the rate it does useful work.
63710
+ *
63711
+ * @param leg The leg to run.
63712
+ * @param params Normalised parameters for this leg.
63713
+ * @param execution The call context.
63714
+ * @returns The provider's answer.
63715
+ */
63716
+ async function runLeg(leg, params, execution) {
63717
+ const controller = new AbortController();
63718
+ const budgetMs = leg.route.timeoutMs;
63719
+ const timer = setTimeout(() => {
63720
+ controller.abort(new LegTimeoutError(leg.route.routeKey, budgetMs));
63721
+ }, budgetMs);
63722
+ const forwardAbort = () => {
63723
+ controller.abort(execution.callerSignal?.reason);
63724
+ };
63725
+ if (execution.callerSignal !== undefined) {
63726
+ if (execution.callerSignal.aborted) {
63727
+ forwardAbort();
63728
+ }
63729
+ else {
63730
+ execution.callerSignal.addEventListener("abort", forwardAbort, { once: true });
63731
+ }
63732
+ }
63733
+ try {
63734
+ // The guards wrap the transport rather than the whole leg, so the per-leg
63735
+ // timeout above still bounds the total wait: a caller queued behind the
63736
+ // rate limiter is spending its budget just as surely as one waiting on the
63737
+ // provider, and only one clock should govern both.
63738
+ return await withProviderGuards(leg.route.providerName, () => leg.transport.execute({
63739
+ route: leg.route,
63740
+ content: execution.content,
63741
+ responseFormat: execution.responseFormat,
63742
+ params,
63743
+ signal: controller.signal,
63744
+ correlationId: execution.correlationId,
63745
+ }), budgetMs);
63746
+ }
63747
+ finally {
63748
+ clearTimeout(timer);
63749
+ execution.callerSignal?.removeEventListener("abort", forwardAbort);
63750
+ }
63751
+ }
63752
+ /**
63753
+ * Classify why a leg failed.
63754
+ *
63755
+ * The distinction matters to the breaker: a timeout and a 5xx are evidence the
63756
+ * provider is unhealthy, while the caller cancelling is not. Counting a
63757
+ * cancellation as a provider failure would let a burst of user-cancelled
63758
+ * requests open the breaker on a perfectly healthy route.
63759
+ *
63760
+ * @param error The thrown value.
63761
+ * @param callerSignal The caller's cancellation signal, if any.
63762
+ * @returns The outcome and whether it counts against route health.
63763
+ */
63764
+ function classify(error, callerSignal) {
63765
+ if (callerSignal !== undefined && callerSignal.aborted) {
63766
+ return {
63767
+ outcome: "skipped",
63768
+ reason: "caller cancelled",
63769
+ countsAgainstHealth: false,
63770
+ };
63771
+ }
63772
+ if (error instanceof LegTimeoutError) {
63773
+ return { outcome: "timeout", reason: error.message, countsAgainstHealth: true };
63774
+ }
63775
+ if (error instanceof UnsupportedCapabilityError) {
63776
+ return { outcome: "skipped", reason: error.message, countsAgainstHealth: false };
63777
+ }
63778
+ if (error instanceof RateGuardTimeoutError) {
63779
+ // Self-inflicted pacing, not provider ill-health. Counting it would let the
63780
+ // client's own throttling open a breaker on a perfectly healthy provider
63781
+ // and permanently reroute traffic nobody chose to reroute.
63782
+ return { outcome: "skipped", reason: error.message, countsAgainstHealth: false };
63783
+ }
63784
+ const reason = error instanceof Error ? error.message : String(error);
63785
+ if (/abort/i.test(reason)) {
63786
+ return {
63787
+ outcome: "timeout",
63788
+ reason: `aborted: ${reason}`,
63789
+ countsAgainstHealth: true,
63790
+ };
63791
+ }
63792
+ return { outcome: "error", reason, countsAgainstHealth: true };
63793
+ }
63794
+ /**
63795
+ * Whether the caller has stopped waiting.
63796
+ *
63797
+ * Read through a function rather than inline, because `AbortSignal.aborted` is
63798
+ * a live getter: it can flip to true while a leg is in flight, but a compiler
63799
+ * that narrowed it at the top of the loop would prove the later check
63800
+ * unreachable and invite its removal. The check is not redundant — it is the
63801
+ * only thing that stops the chain spending money on an answer nobody will read.
63802
+ *
63803
+ * @param signal The caller's signal, if any.
63804
+ * @returns Whether the call has been cancelled.
63805
+ */
63806
+ function isAborted$1(signal) {
63807
+ return signal !== undefined && signal.aborted;
63808
+ }
63809
+ /**
63810
+ * Walk a chain until a leg answers.
63811
+ *
63812
+ * @param alias The alias being served, for error attribution.
63813
+ * @param execution The call context.
63814
+ * @returns The first successful leg's answer, with the full attempt record.
63815
+ * @throws {ChainExhaustedError} When no leg produced an answer.
63816
+ */
63817
+ async function executeChain(alias, execution) {
63818
+ const now = execution.now ?? Date.now;
63819
+ const attempts = [];
63820
+ let totalUsage = EMPTY_USAGE;
63821
+ for (const leg of execution.legs) {
63822
+ const { route } = leg;
63823
+ if (isAborted$1(execution.callerSignal)) {
63824
+ // The caller has stopped waiting. Continuing to walk the chain would
63825
+ // spend money on an answer nobody will read.
63826
+ break;
63827
+ }
63828
+ if (leg.params instanceof UnsupportedCapabilityError) {
63829
+ const record = {
63830
+ routeKey: route.routeKey,
63831
+ role: route.role,
63832
+ provider: route.providerName,
63833
+ modelId: route.modelId,
63834
+ outcome: "skipped",
63835
+ durationMs: 0,
63836
+ reason: leg.params.message,
63837
+ };
63838
+ attempts.push(record);
63839
+ execution.onAttempt?.(record);
63840
+ continue;
63841
+ }
63842
+ if (!execution.breakers.allows(route.routeKey)) {
63843
+ const record = {
63844
+ routeKey: route.routeKey,
63845
+ role: route.role,
63846
+ provider: route.providerName,
63847
+ modelId: route.modelId,
63848
+ outcome: "breaker-open",
63849
+ durationMs: 0,
63850
+ reason: `circuit breaker is ${execution.breakers.stateOf(route.routeKey)}`,
63851
+ };
63852
+ attempts.push(record);
63853
+ execution.onAttempt?.(record);
63854
+ continue;
63855
+ }
63856
+ const startedAt = now();
63857
+ execution.breakers.onAttemptStart(route.routeKey);
63858
+ try {
63859
+ const response = await runLeg(leg, leg.params, execution);
63860
+ execution.breakers.onSuccess(route.routeKey);
63861
+ totalUsage = sumUsage(totalUsage, response.usage);
63862
+ const record = {
63863
+ routeKey: route.routeKey,
63864
+ role: route.role,
63865
+ provider: route.providerName,
63866
+ modelId: route.modelId,
63867
+ outcome: "ok",
63868
+ durationMs: now() - startedAt,
63869
+ usage: response.usage,
63870
+ };
63871
+ attempts.push(record);
63872
+ execution.onAttempt?.(record);
63873
+ return { response, servedBy: route, attempts, totalUsage };
63874
+ }
63875
+ catch (error) {
63876
+ const { outcome, reason, countsAgainstHealth } = classify(error, execution.callerSignal);
63877
+ if (countsAgainstHealth) {
63878
+ execution.breakers.onFailure(route.routeKey);
63879
+ }
63880
+ const record = {
63881
+ routeKey: route.routeKey,
63882
+ role: route.role,
63883
+ provider: route.providerName,
63884
+ modelId: route.modelId,
63885
+ outcome,
63886
+ durationMs: now() - startedAt,
63887
+ reason,
63888
+ };
63889
+ attempts.push(record);
63890
+ execution.onAttempt?.(record);
63891
+ if (outcome === "skipped" && isAborted$1(execution.callerSignal)) {
63892
+ break;
63893
+ }
63894
+ }
63895
+ }
63896
+ throw new ChainExhaustedError(alias, attempts, totalUsage);
63897
+ }
63898
+
63899
+ var schema_version = 1;
63900
+ var policy_source = "docs/llm-provider-migration.md#3-routing-policy (v1.1)";
63901
+ var revised = "2026-09-10";
63902
+ var defaults = {
63903
+ request_timeout_ms: {
63904
+ "hot-path": 30000,
63905
+ background: 90000,
63906
+ batch: 300000
63907
+ },
63908
+ retries_per_leg: 1,
63909
+ circuit_breaker: {
63910
+ failure_threshold: 5,
63911
+ cooldown_ms: 60000,
63912
+ half_open_probes: 1
63913
+ }
63914
+ };
63915
+ var providers = {
63916
+ anthropic: {
63917
+ display_name: "Anthropic",
63918
+ tier: "closed",
63919
+ api_style: "anthropic",
63920
+ base_url: null,
63921
+ base_url_env: null,
63922
+ api_key_env: "ANTHROPIC_API_KEY",
63923
+ secret_path: "llm/anthropic/apiKey",
63924
+ account_status: "live",
63925
+ lumic_provider: "anthropic",
63926
+ published_rpm: null,
63927
+ published_tpm: null,
63928
+ limits_source: null,
63929
+ docs_url: "https://docs.claude.com/en/api/overview",
63930
+ notes: "Permanent designated tier. Retained, keyed, budgeted and continuously exercised as the revert target of every alias (PD-11)."
63931
+ },
63932
+ openai: {
63933
+ display_name: "OpenAI",
63934
+ tier: "closed",
63935
+ api_style: "openai-compatible",
63936
+ base_url: null,
63937
+ base_url_env: null,
63938
+ api_key_env: "OPENAI_API_KEY",
63939
+ secret_path: "llm/openai/apiKey",
63940
+ account_status: "live",
63941
+ lumic_provider: "openai",
63942
+ published_rpm: null,
63943
+ published_tpm: null,
63944
+ limits_source: null,
63945
+ docs_url: "https://platform.openai.com/docs/api-reference",
63946
+ notes: "Permanent designated tier. No alias routes to it today; it stays keyed and budgeted so a per-alias revert to an OpenAI incumbent is a config change (PD-11)."
63947
+ },
63948
+ deepinfra: {
63949
+ display_name: "DeepInfra",
63950
+ tier: "open",
63951
+ api_style: "openai-compatible",
63952
+ base_url: "https://api.deepinfra.com/v1/openai",
63953
+ base_url_env: "DEEPINFRA_BASE_URL",
63954
+ api_key_env: "DEEPINFRA_API_KEY",
63955
+ secret_path: "llm/deepinfra/apiKey",
63956
+ account_status: "pending-onboarding",
63957
+ lumic_provider: null,
63958
+ published_rpm: null,
63959
+ published_tpm: null,
63960
+ limits_source: null,
63961
+ docs_url: "https://deepinfra.com/docs",
63962
+ notes: "Rate limits and exact model ids are transcribed from the provider console at W3-02, never guessed."
63963
+ },
63964
+ fireworks: {
63965
+ display_name: "Fireworks AI",
63966
+ tier: "open",
63967
+ api_style: "openai-compatible",
63968
+ base_url: "https://api.fireworks.ai/inference/v1",
63969
+ base_url_env: "FIREWORKS_BASE_URL",
63970
+ api_key_env: "FIREWORKS_API_KEY",
63971
+ secret_path: "llm/fireworks/apiKey",
63972
+ account_status: "pending-onboarding",
63973
+ lumic_provider: null,
63974
+ published_rpm: null,
63975
+ published_tpm: null,
63976
+ limits_source: null,
63977
+ docs_url: "https://docs.fireworks.ai",
63978
+ notes: "Onboarded as a capacity backstop. No alias routes to it until its published RPM ceiling is recorded in provider-limits (W4-04)."
63979
+ },
63980
+ zai: {
63981
+ display_name: "Z.ai",
63982
+ tier: "open",
63983
+ api_style: "openai-compatible",
63984
+ base_url: "https://api.z.ai/api/paas/v4",
63985
+ base_url_env: "ZAI_BASE_URL",
63986
+ api_key_env: "ZAI_API_KEY",
63987
+ secret_path: "llm/zai/apiKey",
63988
+ account_status: "pending-onboarding",
63989
+ lumic_provider: null,
63990
+ published_rpm: null,
63991
+ published_tpm: null,
63992
+ limits_source: null,
63993
+ docs_url: "https://docs.z.ai",
63994
+ notes: "First-party GLM host. Section 3 anchors first-party GLM below the converged multi-host price, so it is the primary leg for llm.reason."
63995
+ },
63996
+ deepseek: {
63997
+ display_name: "DeepSeek",
63998
+ tier: "open",
63999
+ api_style: "openai-compatible",
64000
+ base_url: "https://api.deepseek.com",
64001
+ base_url_env: "DEEPSEEK_BASE_URL",
64002
+ api_key_env: "DEEPSEEK_API_KEY",
64003
+ secret_path: "llm/deepseek/apiKey",
64004
+ account_status: "live",
64005
+ lumic_provider: "deepseek",
64006
+ published_rpm: null,
64007
+ published_tpm: null,
64008
+ limits_source: null,
64009
+ docs_url: "https://api-docs.deepseek.com",
64010
+ notes: "Already a registered lumic provider, so its legs can also be served by the degraded direct transport. Off-peak windows are captured in gateway config at W3-05 for batch scheduling."
64011
+ },
64012
+ groq: {
64013
+ display_name: "Groq",
64014
+ tier: "open",
64015
+ api_style: "openai-compatible",
64016
+ base_url: "https://api.groq.com/openai/v1",
64017
+ base_url_env: "GROQ_BASE_URL",
64018
+ api_key_env: "GROQ_API_KEY",
64019
+ secret_path: "llm/groq/apiKey",
64020
+ account_status: "not-in-scope",
64021
+ lumic_provider: null,
64022
+ published_rpm: null,
64023
+ published_tpm: null,
64024
+ limits_source: null,
64025
+ docs_url: "https://console.groq.com/docs",
64026
+ notes: "Section 3 names Groq or Cerebras as the llm.fast primary, but the W3 onboarding list does not include either. See open item OI-01: until an owner resolves it, llm.fast's primary leg is unreachable and the alias serves from its secondary and closed legs."
64027
+ },
64028
+ openrouter: {
64029
+ display_name: "OpenRouter",
64030
+ tier: "aggregator",
64031
+ api_style: "openai-compatible",
64032
+ base_url: "https://openrouter.ai/api/v1",
64033
+ base_url_env: "OPENROUTER_BASE_URL",
64034
+ api_key_env: "OPENROUTER_API_KEY",
64035
+ secret_path: "llm/openrouter/apiKey",
64036
+ account_status: "pending-onboarding",
64037
+ lumic_provider: null,
64038
+ published_rpm: null,
64039
+ published_tpm: null,
64040
+ limits_source: null,
64041
+ docs_url: "https://openrouter.ai/docs",
64042
+ notes: "Aggregator backstop. Reaches an open-weight model when its dedicated host is down, without a new account. Not a chain leg by default: an aggregator hides which upstream served a call, which defeats per-provider attribution."
64043
+ }
64044
+ };
64045
+ var aliases = {
64046
+ "llm.reason": {
64047
+ workload: "Deep reasoning, audit loops, config tuning",
64048
+ latency_class: "background",
64049
+ criticality: "ops",
64050
+ isolation_capable: true,
64051
+ eval_gate: "free-text-judge",
64052
+ budget: {
64053
+ basis: "provisional-pre-baseline",
64054
+ monthly_usd: 750,
64055
+ alert_pct: [
64056
+ 50,
64057
+ 80,
64058
+ 100
64059
+ ]
64060
+ },
64061
+ routes: [
64062
+ {
64063
+ role: "primary",
64064
+ provider: "zai",
64065
+ model_id: null,
64066
+ model_id_status: "pending-provider-confirmation",
64067
+ model_id_source: null,
64068
+ model_family: "GLM-5.3",
64069
+ lumic_model: null,
64070
+ params: {
64071
+ temperature: null,
64072
+ max_output_tokens: null,
64073
+ supports_temperature: true,
64074
+ supports_json_schema: true,
64075
+ supports_tools: true,
64076
+ supports_cache_control: false,
64077
+ supports_vision: false
64078
+ },
64079
+ price_per_mtok: {
64080
+ input: 1.09,
64081
+ output: 3.43,
64082
+ as_of: "2026-09-10",
64083
+ source: "docs/llm-provider-migration.md#3 first-party GLM-5.3 anchor; re-verify at W2-02"
64084
+ }
64085
+ },
64086
+ {
64087
+ role: "secondary",
64088
+ provider: "deepinfra",
64089
+ model_id: null,
64090
+ model_id_status: "pending-provider-confirmation",
64091
+ model_id_source: null,
64092
+ model_family: "DeepSeek V4 Pro",
64093
+ lumic_model: null,
64094
+ params: {
64095
+ temperature: null,
64096
+ max_output_tokens: null,
64097
+ supports_temperature: true,
64098
+ supports_json_schema: true,
64099
+ supports_tools: true,
64100
+ supports_cache_control: false,
64101
+ supports_vision: false
64102
+ },
64103
+ price_per_mtok: {
64104
+ input: 1.3,
64105
+ output: 2.6,
64106
+ as_of: "2026-09-10",
64107
+ source: "docs/llm-provider-migration.md#3 DeepInfra anchor; re-verify at W2-02"
64108
+ }
64109
+ },
64110
+ {
64111
+ role: "closed_incumbent",
64112
+ provider: "anthropic",
64113
+ model_id: "claude-opus-4-7",
64114
+ model_id_status: "confirmed",
64115
+ model_id_source: "lumic-utils/src/types/openai-types.ts SUPPORTED_MODELS — already routed on in production",
64116
+ model_family: "Opus-class",
64117
+ lumic_model: "claude-opus-4-7",
64118
+ params: {
64119
+ temperature: null,
64120
+ max_output_tokens: 128000,
64121
+ supports_temperature: true,
64122
+ supports_json_schema: true,
64123
+ supports_tools: true,
64124
+ supports_cache_control: true,
64125
+ supports_vision: true,
64126
+ context_window: 1000000
64127
+ },
64128
+ price_per_mtok: {
64129
+ input: 5,
64130
+ output: 25,
64131
+ as_of: "2026-09-10",
64132
+ source: "docs/llm-provider-migration.md#3 Opus-class anchor; matches lumic-utils/src/functions/llm-config.ts"
64133
+ }
64134
+ }
64135
+ ]
64136
+ },
64137
+ "llm.agentic": {
64138
+ workload: "Hardest long-horizon agentic work",
64139
+ latency_class: "background",
64140
+ criticality: "ops",
64141
+ isolation_capable: true,
64142
+ eval_gate: "tool-call",
64143
+ policy_note: "Section 3 names the primary as Fable-class. The strongest Anthropic model registered in the lumic model registry — the table this codebase actually routes on — is claude-opus-4-7, so that is the configured id. Registering a Fable-class model in @adaptic/lumic-utils would change live model selection for every consumer of the advanced tier and is therefore a separate, evidence-gated decision, recorded as open item OI-02 rather than smuggled in here.",
64144
+ budget: {
64145
+ basis: "provisional-pre-baseline",
64146
+ monthly_usd: 1500,
64147
+ alert_pct: [
64148
+ 50,
64149
+ 80,
64150
+ 100
64151
+ ]
64152
+ },
64153
+ routes: [
64154
+ {
64155
+ role: "primary",
64156
+ provider: "anthropic",
64157
+ model_id: "claude-opus-4-7",
64158
+ model_id_status: "confirmed",
64159
+ model_id_source: "lumic-utils/src/types/openai-types.ts SUPPORTED_MODELS — already routed on in production",
64160
+ model_family: "Fable-class (see policy_note)",
64161
+ lumic_model: "claude-opus-4-7",
64162
+ params: {
64163
+ temperature: null,
64164
+ max_output_tokens: 128000,
64165
+ supports_temperature: true,
64166
+ supports_json_schema: true,
64167
+ supports_tools: true,
64168
+ supports_cache_control: true,
64169
+ supports_vision: true,
64170
+ context_window: 1000000
64171
+ },
64172
+ price_per_mtok: {
64173
+ input: 10,
64174
+ output: 50,
64175
+ as_of: "2026-09-10",
64176
+ source: "docs/llm-provider-migration.md#3 Fable-class anchor"
64177
+ }
64178
+ },
64179
+ {
64180
+ role: "secondary",
64181
+ provider: "deepinfra",
64182
+ model_id: null,
64183
+ model_id_status: "pending-provider-confirmation",
64184
+ model_id_source: null,
64185
+ model_family: "Kimi K3",
64186
+ lumic_model: null,
64187
+ shadow_only: true,
64188
+ params: {
64189
+ temperature: null,
64190
+ max_output_tokens: null,
64191
+ supports_temperature: true,
64192
+ supports_json_schema: true,
64193
+ supports_tools: true,
64194
+ supports_cache_control: false,
64195
+ supports_vision: false
64196
+ },
64197
+ price_per_mtok: {
64198
+ input: 2.85,
64199
+ output: 14.25,
64200
+ as_of: "2026-09-10",
64201
+ source: "docs/llm-provider-migration.md#3 DeepInfra anchor; re-verify at W2-02"
64202
+ },
64203
+ notes: "Shadow-eval only per Section 3. Configured and scored, never served to a caller, so the hardest agentic path keeps its closed primary while the open candidate accrues evidence."
64204
+ }
64205
+ ]
64206
+ },
64207
+ "llm.fast": {
64208
+ workload: "Latency-critical paths",
64209
+ latency_class: "hot-path",
64210
+ criticality: "trading-adjacent",
64211
+ isolation_capable: false,
64212
+ eval_gate: "structured-output",
64213
+ policy_note: "Trading-adjacent and hot-path, so under Section 7 this alias is the LAST to promote: no hot-path alias reaches LIVE until every background alias has completed W5-03 and held steady state for a week.",
64214
+ budget: {
64215
+ basis: "provisional-pre-baseline",
64216
+ monthly_usd: 400,
64217
+ alert_pct: [
64218
+ 50,
64219
+ 80,
64220
+ 100
64221
+ ]
64222
+ },
64223
+ routes: [
64224
+ {
64225
+ role: "primary",
64226
+ provider: "groq",
64227
+ model_id: null,
64228
+ model_id_status: "pending-provider-confirmation",
64229
+ model_id_source: null,
64230
+ model_family: "gpt-oss-120B",
64231
+ lumic_model: null,
64232
+ params: {
64233
+ temperature: null,
64234
+ max_output_tokens: null,
64235
+ supports_temperature: true,
64236
+ supports_json_schema: true,
64237
+ supports_tools: true,
64238
+ supports_cache_control: false,
64239
+ supports_vision: false
64240
+ },
64241
+ price_per_mtok: {
64242
+ input: 0.039,
64243
+ output: 0.19,
64244
+ as_of: "2026-09-10",
64245
+ source: "docs/llm-provider-migration.md#3 gpt-oss-120B anchor (DeepInfra price; Groq price unconfirmed pending OI-01)"
64246
+ },
64247
+ notes: "Unreachable until OI-01 is resolved — Groq is not on the W3 onboarding list."
64248
+ },
64249
+ {
64250
+ role: "secondary",
64251
+ provider: "deepinfra",
64252
+ model_id: null,
64253
+ model_id_status: "pending-provider-confirmation",
64254
+ model_id_source: null,
64255
+ model_family: "gpt-oss-120B",
64256
+ lumic_model: null,
64257
+ params: {
64258
+ temperature: null,
64259
+ max_output_tokens: null,
64260
+ supports_temperature: true,
64261
+ supports_json_schema: true,
64262
+ supports_tools: true,
64263
+ supports_cache_control: false,
64264
+ supports_vision: false
64265
+ },
64266
+ price_per_mtok: {
64267
+ input: 0.039,
64268
+ output: 0.19,
64269
+ as_of: "2026-09-10",
64270
+ source: "docs/llm-provider-migration.md#3 DeepInfra anchor; re-verify at W2-02"
64271
+ }
64272
+ },
64273
+ {
64274
+ role: "closed_incumbent",
64275
+ provider: "anthropic",
64276
+ model_id: "claude-haiku-4-5",
64277
+ model_id_status: "confirmed",
64278
+ model_id_source: "lumic-utils/src/types/openai-types.ts SUPPORTED_MODELS — already routed on in production",
64279
+ model_family: "Haiku-class",
64280
+ lumic_model: "claude-haiku-4-5",
64281
+ params: {
64282
+ temperature: null,
64283
+ max_output_tokens: 64000,
64284
+ supports_temperature: true,
64285
+ supports_json_schema: true,
64286
+ supports_tools: true,
64287
+ supports_cache_control: true,
64288
+ supports_vision: true,
64289
+ context_window: 200000
64290
+ },
64291
+ price_per_mtok: {
64292
+ input: 1,
64293
+ output: 5,
64294
+ as_of: "2026-09-10",
64295
+ source: "docs/llm-provider-migration.md#3 Haiku-class anchor"
64296
+ }
64297
+ }
64298
+ ]
64299
+ },
64300
+ "llm.extract": {
64301
+ workload: "High-volume extraction, classification, log analysis",
64302
+ latency_class: "batch",
64303
+ criticality: "internal",
64304
+ isolation_capable: true,
64305
+ eval_gate: "structured-output",
64306
+ budget: {
64307
+ basis: "provisional-pre-baseline",
64308
+ monthly_usd: 300,
64309
+ alert_pct: [
64310
+ 50,
64311
+ 80,
64312
+ 100
64313
+ ]
64314
+ },
64315
+ routes: [
64316
+ {
64317
+ role: "primary",
64318
+ provider: "deepseek",
64319
+ model_id: "deepseek-v4-flash",
64320
+ model_id_status: "confirmed",
64321
+ model_id_source: "lumic-utils/src/types/openai-types.ts SUPPORTED_MODELS — already routed on in production",
64322
+ model_family: "DeepSeek V4 Flash",
64323
+ lumic_model: "deepseek-v4-flash",
64324
+ params: {
64325
+ temperature: null,
64326
+ max_output_tokens: 64000,
64327
+ supports_temperature: true,
64328
+ supports_json_schema: true,
64329
+ supports_tools: true,
64330
+ supports_cache_control: true,
64331
+ supports_vision: false,
64332
+ context_window: 1000000
64333
+ },
64334
+ price_per_mtok: {
64335
+ input: 0.14,
64336
+ output: 0.28,
64337
+ as_of: "2026-09-10",
64338
+ source: "docs/llm-provider-migration.md#3 V4 Flash anchor; re-verify at W2-02"
64339
+ }
64340
+ },
64341
+ {
64342
+ role: "secondary",
64343
+ provider: "zai",
64344
+ model_id: null,
64345
+ model_id_status: "pending-provider-confirmation",
64346
+ model_id_source: null,
64347
+ model_family: "GLM 5.3 Flash",
64348
+ lumic_model: null,
64349
+ params: {
64350
+ temperature: null,
64351
+ max_output_tokens: null,
64352
+ supports_temperature: true,
64353
+ supports_json_schema: true,
64354
+ supports_tools: true,
64355
+ supports_cache_control: false,
64356
+ supports_vision: false
64357
+ },
64358
+ price_per_mtok: {
64359
+ input: 1.09,
64360
+ output: 3.43,
64361
+ as_of: "2026-09-10",
64362
+ source: "docs/llm-provider-migration.md#3 GLM first-party anchor; Flash-tier price unconfirmed, re-verify at W2-02"
64363
+ }
64364
+ },
64365
+ {
64366
+ role: "closed_incumbent",
64367
+ provider: "anthropic",
64368
+ model_id: "claude-haiku-4-5",
64369
+ model_id_status: "confirmed",
64370
+ model_id_source: "lumic-utils/src/types/openai-types.ts SUPPORTED_MODELS — already routed on in production",
64371
+ model_family: "Haiku-class",
64372
+ lumic_model: "claude-haiku-4-5",
64373
+ params: {
64374
+ temperature: null,
64375
+ max_output_tokens: 64000,
64376
+ supports_temperature: true,
64377
+ supports_json_schema: true,
64378
+ supports_tools: true,
64379
+ supports_cache_control: true,
64380
+ supports_vision: true,
64381
+ context_window: 200000
64382
+ },
64383
+ price_per_mtok: {
64384
+ input: 1,
64385
+ output: 5,
64386
+ as_of: "2026-09-10",
64387
+ source: "docs/llm-provider-migration.md#3 Haiku-class anchor"
64388
+ }
64389
+ }
64390
+ ]
64391
+ },
64392
+ "llm.judge": {
64393
+ workload: "Eval judging only",
64394
+ latency_class: "batch",
64395
+ criticality: "internal",
64396
+ isolation_capable: false,
64397
+ pinned: true,
64398
+ eval_gate: "none-pinned-judge",
64399
+ policy_note: "PD-6: pinned and never auto-swapped, and deliberately carries no fallback. A judge that silently failed over would grade one model's output against another model's standard, which would corrupt every gate decided by it — so it fails loud instead.",
64400
+ budget: {
64401
+ basis: "provisional-pre-baseline",
64402
+ monthly_usd: 150,
64403
+ alert_pct: [
64404
+ 50,
64405
+ 80,
64406
+ 100
64407
+ ]
64408
+ },
64409
+ routes: [
64410
+ {
64411
+ role: "primary",
64412
+ provider: "anthropic",
64413
+ model_id: "claude-sonnet-4-6",
64414
+ model_id_status: "confirmed",
64415
+ model_id_source: "lumic-utils/src/types/openai-types.ts SUPPORTED_MODELS — already routed on in production",
64416
+ model_family: "Sonnet-class",
64417
+ lumic_model: "claude-sonnet-4-6",
64418
+ params: {
64419
+ temperature: 0,
64420
+ max_output_tokens: 64000,
64421
+ supports_temperature: true,
64422
+ supports_json_schema: true,
64423
+ supports_tools: true,
64424
+ supports_cache_control: true,
64425
+ supports_vision: true,
64426
+ context_window: 1000000
64427
+ },
64428
+ price_per_mtok: {
64429
+ input: 3,
64430
+ output: 15,
64431
+ as_of: "2026-09-10",
64432
+ source: "lumic-utils/src/functions/llm-config.ts anthropicModelCosts"
64433
+ }
64434
+ }
64435
+ ]
64436
+ }
64437
+ };
64438
+ var open_items = [
64439
+ {
64440
+ id: "OI-01",
64441
+ question: "Section 3 makes Groq or Cerebras the llm.fast primary, but neither appears in the W3 onboarding provider list (DeepInfra, Fireworks, Z.ai, DeepSeek, aggregator). Which host serves llm.fast's primary leg, and is it added to W3?",
64442
+ blocks: "llm.fast primary reachability; W5-03 canary for llm.fast",
64443
+ owner: "CEO"
64444
+ },
64445
+ {
64446
+ id: "OI-02",
64447
+ question: "Section 3 names a Fable-class primary for llm.agentic, but the lumic model registry's strongest registered Anthropic model is claude-opus-4-7. Register a Fable-class model in @adaptic/lumic-utils, or accept opus-4-7 as the llm.agentic primary?",
64448
+ blocks: "llm.agentic primary fidelity to Section 3",
64449
+ owner: "CEO"
64450
+ },
64451
+ {
64452
+ id: "OI-03",
64453
+ question: "Every alias budget is provisional-pre-baseline because Section 9's baseline monthly spend is still FILL. Which caps apply once W1-03 produces the billing baseline?",
64454
+ blocks: "PD-7 layer 3 calibration; Section 9 savings target measurement",
64455
+ owner: "CEO"
64456
+ },
64457
+ {
64458
+ id: "OI-04",
64459
+ question: "llm.agentic's serving chain is one leg long because Section 3 makes its primary the closed incumbent and its only secondary shadow-eval-only. An Anthropic outage therefore leaves the alias with no configured fallback. Widen it (a second closed provider, or promoting the Kimi leg out of shadow), or accept the single leg?",
64460
+ blocks: "llm.agentic availability under a primary-provider outage",
64461
+ owner: "CEO"
64462
+ }
64463
+ ];
64464
+ var rawTable = {
64465
+ schema_version: schema_version,
64466
+ policy_source: policy_source,
64467
+ revised: revised,
64468
+ defaults: defaults,
64469
+ providers: providers,
64470
+ aliases: aliases,
64471
+ open_items: open_items
64472
+ };
64473
+
64474
+ /**
64475
+ * Typed access to the canonical alias route table.
64476
+ *
64477
+ * The table is imported rather than fetched so an alias always resolves, even
64478
+ * when the gateway is unreachable. A client that could only learn its routes
64479
+ * from the gateway would have no way to fall back when the gateway itself is
64480
+ * the thing that failed, which would make the mandatory fallback chain of PD-3
64481
+ * conditional on the very component it exists to survive.
64482
+ *
64483
+ * Resolution is deliberately conservative in three ways. An unknown alias is an
64484
+ * error rather than a default, because a typo silently served by whichever
64485
+ * model happened to be configured is worse than a loud failure. A route whose
64486
+ * vendor model id is unconfirmed is excluded, because a guessed id fails at the
64487
+ * first live call rather than at review. And a shadow-only route is never
64488
+ * served, because a candidate that can answer a live request has stopped being
64489
+ * a candidate.
64490
+ *
64491
+ * @module llm/route-table
64492
+ */
64493
+ /** Chain order, fixed by role rather than by authoring order in the file. */
64494
+ const ROLE_ORDER = ["primary", "secondary", "closed_incumbent"];
64495
+ /** Suffix naming an alias's research-isolated variant (PD-9). */
64496
+ const ISOLATED_SUFFIX = ".isolated";
64497
+ /**
64498
+ * The canonical route table.
64499
+ *
64500
+ * Exposed as a readonly view so a consumer can inspect routing without being
64501
+ * able to mutate it. A table that could be edited at runtime would let one
64502
+ * caller change every other caller's routing.
64503
+ */
64504
+ const routeTable = rawTable;
64505
+ /**
64506
+ * Every alias the table defines.
64507
+ *
64508
+ * @returns The alias names, sorted for stable iteration.
64509
+ */
64510
+ function listAliases() {
64511
+ return Object.keys(routeTable.aliases).sort();
64512
+ }
64513
+ /**
64514
+ * Look up an alias definition.
64515
+ *
64516
+ * @param alias The alias to resolve.
64517
+ * @returns Its definition.
64518
+ * @throws When the alias is not defined by the route table.
64519
+ */
64520
+ function aliasDefinition(alias) {
64521
+ const definition = routeTable.aliases[alias];
64522
+ if (definition === undefined) {
64523
+ throw new UnknownAliasError(alias, listAliases());
64524
+ }
64525
+ return definition;
64526
+ }
64527
+ /**
64528
+ * Look up a provider registry entry.
64529
+ *
64530
+ * @param name The provider key.
64531
+ * @returns Its registry entry.
64532
+ * @throws When the provider is not registered.
64533
+ */
64534
+ function providerEntry(name) {
64535
+ const provider = routeTable.providers[name];
64536
+ if (provider === undefined) {
64537
+ throw new Error(`route table names provider "${name}", which is not in the provider registry`);
64538
+ }
64539
+ return provider;
64540
+ }
64541
+ /** Thrown when a caller names an alias the route table does not define. */
64542
+ class UnknownAliasError extends Error {
64543
+ /** The alias that was requested. */
64544
+ alias;
64545
+ /** The aliases that do exist, so the message is actionable. */
64546
+ known;
64547
+ /**
64548
+ * @param alias The unrecognised alias.
64549
+ * @param known The aliases the table defines.
64550
+ */
64551
+ constructor(alias, known) {
64552
+ super(`unknown LLM alias "${alias}". Known aliases: ${known.join(", ")}. ` +
64553
+ "Application code names aliases only; adding one is a change to the route table.");
64554
+ this.name = "UnknownAliasError";
64555
+ this.alias = alias;
64556
+ this.known = known;
64557
+ }
64558
+ }
64559
+ /** Thrown when an alias exists but has no leg that can currently serve a caller. */
64560
+ class NoServableRouteError extends Error {
64561
+ /** The alias that could not be served. */
64562
+ alias;
64563
+ /** Why each of its legs was excluded, in chain order. */
64564
+ exclusions;
64565
+ /**
64566
+ * @param alias The alias.
64567
+ * @param exclusions Per-leg reasons, in chain order.
64568
+ */
64569
+ constructor(alias, exclusions) {
64570
+ super(`alias "${alias}" has no servable route. Legs excluded: ${exclusions.join("; ")}`);
64571
+ this.name = "NoServableRouteError";
64572
+ this.alias = alias;
64573
+ this.exclusions = exclusions;
64574
+ }
64575
+ }
64576
+ /**
64577
+ * Order an alias's routes into the chain the client walks.
64578
+ *
64579
+ * @param definition The alias definition.
64580
+ * @returns Routes ordered primary -> secondary -> closed incumbent.
64581
+ */
64582
+ function orderedRoutes(definition) {
64583
+ return [...definition.routes].sort((a, b) => ROLE_ORDER.indexOf(a.role) - ROLE_ORDER.indexOf(b.role));
64584
+ }
64585
+ /**
64586
+ * Stable identity for one leg, used to key its circuit breaker and its metrics.
64587
+ *
64588
+ * Keyed by alias, isolation and role rather than by provider and model, because
64589
+ * the breaker guards a position in a chain: reverting an alias to a different
64590
+ * model at the same position should inherit that position's health rather than
64591
+ * start blind, and the isolated variant must never share a breaker with the
64592
+ * shared one.
64593
+ *
64594
+ * @param alias The alias.
64595
+ * @param isolated Whether this is the isolated variant.
64596
+ * @param role The leg's role.
64597
+ * @returns The route key.
64598
+ */
64599
+ function routeKeyFor(alias, isolated, role) {
64600
+ return `${alias}${isolated ? ISOLATED_SUFFIX : ""}#${role}`;
64601
+ }
64602
+ /**
64603
+ * Resolve an alias to the ordered chain of legs that can serve it today.
64604
+ *
64605
+ * Exclusions are returned rather than discarded so an exhausted chain can say
64606
+ * why each leg was unavailable. "No route available" without that detail sends
64607
+ * an operator to read config; with it, the answer is in the error.
64608
+ *
64609
+ * @param alias The alias to resolve.
64610
+ * @param options Resolution options.
64611
+ * @param options.isolated Route through the isolated variant (PD-9).
64612
+ * @param options.timeoutMsOverride Per-leg budget override, never wider than the caller's own deadline.
64613
+ * @returns The resolved chain.
64614
+ * @throws {UnknownAliasError} When the alias is not defined.
64615
+ */
64616
+ function resolveChain(alias, options = {}) {
64617
+ const definition = aliasDefinition(alias);
64618
+ const isolated = options.isolated === true;
64619
+ if (isolated && !definition.isolation_capable) {
64620
+ throw new Error(`alias "${alias}" is not isolation-capable, so it has no isolated variant. ` +
64621
+ "PD-9 forbids serving isolated work from a shared route, so this fails rather than falling back.");
64622
+ }
64623
+ const timeoutMs = options.timeoutMsOverride ??
64624
+ routeTable.defaults.request_timeout_ms[definition.latency_class];
64625
+ const resolved = [];
64626
+ const exclusions = [];
64627
+ for (const route of orderedRoutes(definition)) {
64628
+ const provider = providerEntry(route.provider);
64629
+ if (route.shadow_only === true) {
64630
+ exclusions.push({
64631
+ role: route.role,
64632
+ provider: route.provider,
64633
+ reason: "shadow-only: configured and scored, never served to a caller",
64634
+ });
64635
+ continue;
64636
+ }
64637
+ if (route.model_id_status !== "confirmed" || route.model_id === null) {
64638
+ exclusions.push({
64639
+ role: route.role,
64640
+ provider: route.provider,
64641
+ reason: `model id unconfirmed (${route.model_family}); it is transcribed from the provider console at onboarding, never guessed`,
64642
+ });
64643
+ continue;
64644
+ }
64645
+ if (provider.account_status !== "live") {
64646
+ exclusions.push({
64647
+ role: route.role,
64648
+ provider: route.provider,
64649
+ reason: `provider account status is "${provider.account_status}"`,
64650
+ });
64651
+ continue;
64652
+ }
64653
+ resolved.push({
64654
+ alias,
64655
+ isolated,
64656
+ role: route.role,
64657
+ providerName: route.provider,
64658
+ provider,
64659
+ modelId: route.model_id,
64660
+ lumicModel: route.lumic_model ?? null,
64661
+ params: route.params ?? {},
64662
+ routeKey: routeKeyFor(alias, isolated, route.role),
64663
+ timeoutMs,
64664
+ retriesPerLeg: routeTable.defaults.retries_per_leg,
64665
+ });
64666
+ }
64667
+ return { alias, isolated, routes: resolved, exclusions };
64668
+ }
64669
+ /**
64670
+ * The permanent closed-incumbent leg of an alias (PD-11).
64671
+ *
64672
+ * Found by provider tier rather than by role, because an alias whose primary is
64673
+ * already a closed vendor has that primary as its revert target. Both shapes
64674
+ * therefore hold the same guarantee: there is always a configured route that
64675
+ * needs no new account and no code change to fall back to.
64676
+ *
64677
+ * @param chain A resolved chain.
64678
+ * @returns The closed leg, or undefined when none is currently servable.
64679
+ */
64680
+ function closedIncumbentLeg(chain) {
64681
+ return chain.routes.find((route) => route.provider.tier === "closed");
64682
+ }
64683
+ /**
64684
+ * The name the gateway knows a leg by.
64685
+ *
64686
+ * The head of a chain is addressed by the bare alias so a caller never learns a
64687
+ * leg name; every other leg carries the derived name the renderer gives it.
64688
+ * Keeping this derivation beside the resolver — rather than in the transport —
64689
+ * is what stops the client and the gateway config from disagreeing about a name.
64690
+ *
64691
+ * @param route The resolved leg.
64692
+ * @param chain The chain it belongs to.
64693
+ * @returns The gateway `model_name`.
64694
+ */
64695
+ function gatewayModelNameFor(route, chain) {
64696
+ const base = `${route.alias}${route.isolated ? ISOLATED_SUFFIX : ""}`;
64697
+ return chain.routes[0]?.routeKey === route.routeKey
64698
+ ? base
64699
+ : `${base}.fallback.${route.role}`;
64700
+ }
64701
+
64702
+ /**
64703
+ * Bounded validate-and-retry for structured output.
64704
+ *
64705
+ * A model asked for a schema-shaped answer sometimes returns something close
64706
+ * but wrong. Feeding the validator's own complaint back once fixes most of
64707
+ * those, because the model can see precisely what it got wrong. A second retry
64708
+ * almost never helps: if the model is still wrong after being told exactly what
64709
+ * was wrong, it is persistently wrong for this prompt, and further attempts
64710
+ * spend budget and latency to arrive at the same place.
64711
+ *
64712
+ * The retry lives ahead of the fallback chain rather than inside it. A payload
64713
+ * that fails validation is evidence about the PROMPT, not about the provider's
64714
+ * health, so it must not open a circuit breaker or advance to the next leg —
64715
+ * the next leg would receive the same prompt and be no likelier to satisfy it.
64716
+ *
64717
+ * Exhaustion throws. Returning a partial or defaulted object would hand the
64718
+ * caller a well-typed value that no model ever produced, and the resulting
64719
+ * decision would be made on invented data with nothing to show it.
64720
+ *
64721
+ * @module llm/schema-retry
64722
+ */
64723
+ /** Attempts allowed: the first, plus exactly one feedback retry. */
64724
+ const MAX_ATTEMPTS = 2;
64725
+ /** Characters of an invalid payload echoed back to the model. */
64726
+ const PAYLOAD_EXCERPT = 2000;
64727
+ /**
64728
+ * Thrown when both attempts failed validation.
64729
+ *
64730
+ * Carries both rejection reasons, because the pair is what distinguishes a
64731
+ * flaky answer from a prompt the model cannot satisfy: two different complaints
64732
+ * suggest instability, while the same complaint twice points at the prompt or
64733
+ * the schema.
64734
+ */
64735
+ class SchemaRetryExhaustedError extends Error {
64736
+ /** Why the first attempt was rejected. */
64737
+ firstReason;
64738
+ /** Why the retry was rejected. */
64739
+ secondReason;
64740
+ /** Usage spent across both attempts, so the spend is still accounted for. */
64741
+ totalUsage;
64742
+ /**
64743
+ * @param firstReason Validator's complaint about attempt one.
64744
+ * @param secondReason Validator's complaint about attempt two.
64745
+ * @param totalUsage Usage across both attempts.
64746
+ */
64747
+ constructor(firstReason, secondReason, totalUsage) {
64748
+ super("LLM structured output failed validation twice. " +
64749
+ `First: ${firstReason}. After feedback: ${secondReason}. ` +
64750
+ "No value is returned: a defaulted object would be data no model produced.");
64751
+ this.name = "SchemaRetryExhaustedError";
64752
+ this.firstReason = firstReason;
64753
+ this.secondReason = secondReason;
64754
+ this.totalUsage = totalUsage;
64755
+ }
64756
+ }
64757
+ /**
64758
+ * Build the retry prompt.
64759
+ *
64760
+ * The rejected payload is echoed back alongside the complaint, because a model
64761
+ * asked to "fix the error" without seeing what it produced will usually
64762
+ * regenerate from scratch and reproduce the same mistake.
64763
+ *
64764
+ * @param originalPrompt The prompt that produced the invalid payload.
64765
+ * @param rejected The payload that failed.
64766
+ * @param reason The validator's complaint.
64767
+ * @returns The retry prompt.
64768
+ */
64769
+ function buildRetryPrompt(originalPrompt, rejected, reason) {
64770
+ const excerpt = typeof rejected === "string"
64771
+ ? rejected
64772
+ : JSON.stringify(rejected, null, 2) ?? String(rejected);
64773
+ return [
64774
+ originalPrompt,
64775
+ "",
64776
+ "Your previous response was rejected by a schema validator.",
64777
+ "",
64778
+ "Previous response:",
64779
+ excerpt.slice(0, PAYLOAD_EXCERPT),
64780
+ "",
64781
+ `Validator rejection: ${reason}`,
64782
+ "",
64783
+ "Return a corrected response that satisfies the schema. Return only the corrected response.",
64784
+ ].join("\n");
64785
+ }
64786
+ /**
64787
+ * Run a call with one validator-feedback retry.
64788
+ *
64789
+ * @param options Retry options.
64790
+ * @param options.prompt The original prompt.
64791
+ * @param options.validate Validator applied to each attempt's payload.
64792
+ * @param options.call Executes one attempt with the given prompt.
64793
+ * @returns The validated value with combined usage.
64794
+ * @throws {SchemaRetryExhaustedError} When both attempts fail validation.
64795
+ */
64796
+ async function callWithValidation(options) {
64797
+ const first = await options.call(options.prompt);
64798
+ const firstOutcome = options.validate(first.response);
64799
+ if (firstOutcome.ok) {
64800
+ return {
64801
+ value: firstOutcome.value,
64802
+ attempts: 1,
64803
+ response: first,
64804
+ totalUsage: first.usage,
64805
+ };
64806
+ }
64807
+ const retryPrompt = buildRetryPrompt(options.prompt, first.response, firstOutcome.reason);
64808
+ const second = await options.call(retryPrompt);
64809
+ const combined = sumUsage(first.usage, second.usage);
64810
+ const secondOutcome = options.validate(second.response);
64811
+ if (secondOutcome.ok) {
64812
+ return {
64813
+ value: secondOutcome.value,
64814
+ attempts: MAX_ATTEMPTS,
64815
+ response: second,
64816
+ totalUsage: combined,
64817
+ };
64818
+ }
64819
+ throw new SchemaRetryExhaustedError(firstOutcome.reason, secondOutcome.reason, combined);
64820
+ }
64821
+
64822
+ /**
64823
+ * Degraded direct transport: the path used when the gateway itself is gone.
64824
+ *
64825
+ * Routing every call through one proxy concentrates a great deal of value — one
64826
+ * place to swap a model, one place to bound spend, one place to see cost. It
64827
+ * also concentrates risk: without this path, a gateway outage would take every
64828
+ * LLM call in the system down at once, which is a worse failure than any of the
64829
+ * provider outages the gateway exists to survive.
64830
+ *
64831
+ * Two constraints keep this a safety net rather than a second routing policy.
64832
+ * It serves CLOSED-tier legs only, so the degraded path can never be the thing
64833
+ * that silently promotes an open-weight model past its evaluation gates. And it
64834
+ * resolves the model from the same route table the gateway is rendered from, so
64835
+ * degraded traffic reaches the same model the gateway would have chosen.
64836
+ *
64837
+ * The provider SDK is reached through an injected caller, resolved lazily. A
64838
+ * static import would make `@adaptic/utils` load a provider SDK for every
64839
+ * consumer, including those that never make an LLM call, and would harden a
64840
+ * package cycle that is currently only a declaration.
64841
+ *
64842
+ * @module llm/transports/direct
64843
+ */
64844
+ /**
64845
+ * Thrown when the degraded path is asked to serve a leg it must not serve.
64846
+ *
64847
+ * Refusing loudly rather than serving the leg anyway is the point: the whole
64848
+ * value of restricting this path is lost if it quietly widens under pressure,
64849
+ * and pressure is exactly when it runs.
64850
+ */
64851
+ class DirectTransportRefusedError extends Error {
64852
+ /**
64853
+ * @param routeKey The leg that was refused.
64854
+ * @param reason Why it cannot be served directly.
64855
+ */
64856
+ constructor(routeKey, reason) {
64857
+ super(`the degraded direct transport refuses route ${routeKey}: ${reason}. ` +
64858
+ "It exists so a gateway outage does not stop every LLM call, not as a second routing policy.");
64859
+ this.name = "DirectTransportRefusedError";
64860
+ }
64861
+ }
64862
+ /**
64863
+ * Build the degraded direct transport.
64864
+ *
64865
+ * @param config Transport configuration.
64866
+ * @returns A transport that reaches a closed-tier provider without the gateway.
64867
+ */
64868
+ function createDirectTransport(config) {
64869
+ return {
64870
+ name: "direct",
64871
+ async execute(request) {
64872
+ const { route } = request;
64873
+ if (route.provider.tier !== "closed") {
64874
+ throw new DirectTransportRefusedError(route.routeKey, `provider "${route.providerName}" is ${route.provider.tier}-tier; only a closed incumbent may be served without the gateway, so a degraded call can never promote an open route past its evaluation gates`);
64875
+ }
64876
+ if (route.lumicModel === null) {
64877
+ throw new DirectTransportRefusedError(route.routeKey, "the route names no lumic_model, so there is no registered model to call directly");
64878
+ }
64879
+ const call = await config.resolveCaller();
64880
+ const result = await call(request.content, request.responseFormat, {
64881
+ ...request.params,
64882
+ model: route.lumicModel,
64883
+ signal: request.signal,
64884
+ timeout: route.timeoutMs,
64885
+ });
64886
+ return {
64887
+ response: result.response,
64888
+ usage: readUsage$1(result.usage, request),
64889
+ tool_calls: Array.isArray(result.tool_calls)
64890
+ ? result.tool_calls
64891
+ : undefined,
64892
+ };
64893
+ },
64894
+ };
64895
+ }
64896
+ /**
64897
+ * Normalise the incumbent client's usage shape.
64898
+ *
64899
+ * Missing counts stay zero rather than being estimated, for the same reason
64900
+ * they do on the gateway path: an invented token count flows straight into the
64901
+ * budget accounting the spend controls depend on.
64902
+ *
64903
+ * @param usage The incumbent client's usage, if any.
64904
+ * @param request The request it answers.
64905
+ * @returns The normalised usage record.
64906
+ */
64907
+ function readUsage$1(usage, request) {
64908
+ return {
64909
+ prompt_tokens: usage?.prompt_tokens ?? 0,
64910
+ completion_tokens: usage?.completion_tokens ?? 0,
64911
+ reasoning_tokens: usage?.reasoning_tokens,
64912
+ cached_tokens: usage?.cached_tokens,
64913
+ provider: usage?.provider ?? request.route.providerName,
64914
+ model: usage?.model ?? request.route.modelId,
64915
+ cost: usage?.cost ?? 0,
64916
+ };
64917
+ }
64918
+ /**
64919
+ * Package providing the default provider client, resolved at runtime.
64920
+ *
64921
+ * Assembled rather than written as a literal so the module specifier is opaque
64922
+ * to the compiler and the bundler. That is not a trick to dodge a type error:
64923
+ * this package is genuinely OPTIONAL. The stable lineage of `@adaptic/utils`
64924
+ * does not depend on `@adaptic/lumic-utils` at all, the transport is injectable
64925
+ * precisely so a consumer can supply its own, and the default exists only as a
64926
+ * convenience for consumers that already have it installed. A static specifier
64927
+ * would assert a dependency that does not exist and would harden the
64928
+ * utils/lumic-utils package cycle from a declaration into a build-time fact.
64929
+ */
64930
+ const DEFAULT_PROVIDER_CLIENT_PACKAGE = ["@adaptic", "lumic-utils"].join("/");
64931
+ /**
64932
+ * The default caller: the incumbent client in `@adaptic/lumic-utils`.
64933
+ *
64934
+ * Resolved with a dynamic import so this module has no load-time dependency on
64935
+ * that package. When it cannot be loaded the failure names the degraded path
64936
+ * explicitly, because "cannot find module" during an outage is otherwise a
64937
+ * confusing second mystery on top of the first — and a consumer that does not
64938
+ * ship that package is expected to register its own transport rather than to
64939
+ * discover this at the moment the gateway fails.
64940
+ *
64941
+ * @returns The provider-calling function.
64942
+ */
64943
+ async function resolveDefaultDirectCaller() {
64944
+ try {
64945
+ const lumic = (await import(
64946
+ /* @vite-ignore */ DEFAULT_PROVIDER_CLIENT_PACKAGE));
64947
+ const call = lumic.lumic?.llm?.call;
64948
+ if (typeof call !== "function") {
64949
+ throw new Error(`${DEFAULT_PROVIDER_CLIENT_PACKAGE} exposes no lumic.llm.call`);
64950
+ }
64951
+ return call;
64952
+ }
64953
+ catch (error) {
64954
+ throw new Error("the degraded direct transport could not load its provider client: " +
64955
+ `${error instanceof Error ? error.message : String(error)}. ` +
64956
+ "Register a direct transport explicitly via configureLlmClient() where the default is unavailable.");
64957
+ }
64958
+ }
64959
+
64960
+ /**
64961
+ * Gateway transport: the normal path for every LLM call.
64962
+ *
64963
+ * The client sends an alias to the LiteLLM proxy and the proxy resolves it. The
64964
+ * vendor model string therefore exists only in the gateway's configuration and
64965
+ * never in application code (PD-5), which is what makes a model swap a config
64966
+ * change rather than a deploy.
64967
+ *
64968
+ * The client still walks its own chain on top of the gateway's, and the
64969
+ * duplication is deliberate. The gateway's fallbacks cover a provider being
64970
+ * down; the client's cover the gateway being down. Only one of those two can
64971
+ * cover the other, so the outer chain is the one that must exist.
64972
+ *
64973
+ * The gateway key is read from the environment by NAME at call time and never
64974
+ * stored, logged, or included in an error (PD-2). Reading it per call rather
64975
+ * than caching it at import means a rotation takes effect without a restart.
64976
+ *
64977
+ * @module llm/transports/gateway
64978
+ */
64979
+ /** HTTP statuses that mean "try the next leg" rather than "this request is wrong". */
64980
+ const RETRYABLE_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
64981
+ /** Maximum characters of an error body echoed into a message. */
64982
+ const ERROR_BODY_EXCERPT = 400;
64983
+ /**
64984
+ * Thrown when the gateway itself is unreachable, as opposed to a provider
64985
+ * behind it failing.
64986
+ *
64987
+ * The distinction is what licenses the degraded direct path: a 502 from a
64988
+ * provider means try the next leg, while a connection refused from the proxy
64989
+ * means the whole gateway is gone and the chain cannot be walked through it at
64990
+ * all.
64991
+ */
64992
+ class GatewayUnreachableError extends Error {
64993
+ /**
64994
+ * @param baseUrl The gateway that could not be reached.
64995
+ * @param cause The underlying transport error.
64996
+ */
64997
+ constructor(baseUrl, cause) {
64998
+ super(`LLM gateway at ${baseUrl} is unreachable: ${cause instanceof Error ? cause.message : String(cause)}`);
64999
+ this.name = "GatewayUnreachableError";
65000
+ }
65001
+ }
65002
+ /** Thrown when the gateway answered, but with a failure. */
65003
+ class GatewayResponseError extends Error {
65004
+ /** The HTTP status. */
65005
+ status;
65006
+ /** Whether advancing to the next leg could plausibly help. */
65007
+ retryable;
65008
+ /**
65009
+ * @param status The HTTP status.
65010
+ * @param body A bounded excerpt of the response body.
65011
+ */
65012
+ constructor(status, body) {
65013
+ super(`LLM gateway returned ${status}: ${body.slice(0, ERROR_BODY_EXCERPT)}`);
65014
+ this.name = "GatewayResponseError";
65015
+ this.status = status;
65016
+ this.retryable = RETRYABLE_STATUSES.has(status);
65017
+ }
65018
+ }
65019
+ /**
65020
+ * Read the gateway key from the environment by name.
65021
+ *
65022
+ * @param envVar The variable's NAME.
65023
+ * @returns The key.
65024
+ * @throws When the variable is unset, because an unauthenticated call would
65025
+ * reach the gateway as an anonymous caller and be rejected there anyway —
65026
+ * later, and with a less useful message.
65027
+ */
65028
+ function readGatewayKey(envVar) {
65029
+ const value = process.env[envVar];
65030
+ if (value === undefined || value.length === 0) {
65031
+ throw new Error(`${envVar} is unset, so the LLM gateway cannot be authenticated against. ` +
65032
+ "Provision it from the secrets manager; it is never read from a file or a default.");
65033
+ }
65034
+ return value;
65035
+ }
65036
+ /**
65037
+ * Extract usage from a chat-completion response.
65038
+ *
65039
+ * Absent counts stay zero rather than being estimated. A fabricated token count
65040
+ * would flow straight into the budget accounting that the spend controls are
65041
+ * built on, and a budget computed from invented numbers is worse than one that
65042
+ * knows it is missing a call.
65043
+ *
65044
+ * @param payload The parsed response body.
65045
+ * @param request The request it answers.
65046
+ * @returns The usage record.
65047
+ */
65048
+ function readUsage(payload, request) {
65049
+ const usage = (payload.usage ?? {});
65050
+ const details = (usage.prompt_tokens_details ?? {});
65051
+ const cached = details.cached_tokens;
65052
+ const reasoningDetails = (usage.completion_tokens_details ?? {});
65053
+ const reasoning = reasoningDetails.reasoning_tokens;
65054
+ return {
65055
+ prompt_tokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
65056
+ completion_tokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
65057
+ reasoning_tokens: typeof reasoning === "number" ? reasoning : undefined,
65058
+ cached_tokens: typeof cached === "number" ? cached : undefined,
65059
+ provider: request.route.providerName,
65060
+ model: request.route.modelId,
65061
+ cost: typeof usage.response_cost === "number" ? usage.response_cost : 0,
65062
+ };
65063
+ }
65064
+ /**
65065
+ * Build the gateway transport.
65066
+ *
65067
+ * @param config Transport configuration.
65068
+ * @returns A transport that executes one leg through the proxy.
65069
+ */
65070
+ function createGatewayTransport(config) {
65071
+ const doFetch = config.fetchImpl ?? fetch;
65072
+ return {
65073
+ name: "gateway",
65074
+ async execute(request) {
65075
+ const messages = buildMessages(request);
65076
+ const body = {
65077
+ model: config.modelNameFor(request),
65078
+ messages,
65079
+ ...request.params,
65080
+ };
65081
+ let response;
65082
+ try {
65083
+ response = await doFetch(`${config.baseUrl.replace(/\/$/, "")}/chat/completions`, {
65084
+ method: "POST",
65085
+ headers: {
65086
+ "content-type": "application/json",
65087
+ authorization: `Bearer ${readGatewayKey(config.apiKeyEnv)}`,
65088
+ ...(request.correlationId === undefined
65089
+ ? {}
65090
+ : { "x-correlation-id": request.correlationId }),
65091
+ },
65092
+ body: JSON.stringify(body),
65093
+ signal: request.signal,
65094
+ });
65095
+ }
65096
+ catch (error) {
65097
+ // A transport-level throw means the proxy was never reached. Abort is
65098
+ // re-thrown untouched so the chain's timeout classification stays
65099
+ // accurate rather than being masked as a gateway outage.
65100
+ if (request.signal.aborted) {
65101
+ throw error;
65102
+ }
65103
+ throw new GatewayUnreachableError(config.baseUrl, error);
65104
+ }
65105
+ if (!response.ok) {
65106
+ throw new GatewayResponseError(response.status, await response.text());
65107
+ }
65108
+ const payload = (await response.json());
65109
+ const choices = payload.choices;
65110
+ const message = choices?.[0]?.message;
65111
+ return {
65112
+ response: parseContent(message?.content, request.responseFormat),
65113
+ usage: readUsage(payload, request),
65114
+ tool_calls: Array.isArray(message?.tool_calls)
65115
+ ? message.tool_calls
65116
+ : undefined,
65117
+ };
65118
+ },
65119
+ };
65120
+ }
65121
+ /**
65122
+ * Compose the message array for a request.
65123
+ *
65124
+ * @param request The request.
65125
+ * @returns Chat messages.
65126
+ */
65127
+ function buildMessages(request) {
65128
+ const content = typeof request.content === "string" ? request.content : [...request.content];
65129
+ return [{ role: "user", content }];
65130
+ }
65131
+ /**
65132
+ * Interpret the model's content according to the requested format.
65133
+ *
65134
+ * A JSON format that does not parse is an error, not an empty object. Returning
65135
+ * a default here would hand the caller a well-typed value that means nothing,
65136
+ * and the failure would surface much later as a decision made on absent data.
65137
+ *
65138
+ * @param content The raw content.
65139
+ * @param responseFormat The format the caller asked for.
65140
+ * @returns The parsed value.
65141
+ */
65142
+ function parseContent(content, responseFormat) {
65143
+ const text = typeof content === "string" ? content : "";
65144
+ if (responseFormat === "text") {
65145
+ return text;
65146
+ }
65147
+ try {
65148
+ return JSON.parse(text);
65149
+ }
65150
+ catch (error) {
65151
+ 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)}`);
65152
+ }
65153
+ }
65154
+
65155
+ /**
65156
+ * The alias-resolving LLM client.
65157
+ *
65158
+ * This is the only supported way to reach a language model from application
65159
+ * code. Its signature mirrors the incumbent client's — content, response
65160
+ * format, options — so migrating a call site is replacing a model string with
65161
+ * an alias, and nothing else. That similarity is the point: a migration that
65162
+ * required rewriting call sites would be a migration that stalls half-done,
65163
+ * leaving some calls inside the timeout, breaker and fallback controls and
65164
+ * some outside them, which is worse than either end state.
65165
+ *
65166
+ * There is deliberately no way to name a model. PD-5 makes vendor strings a CI
65167
+ * failure in application code, and an option that accepted one would let a call
65168
+ * site opt out of the routing policy without anyone noticing.
65169
+ *
65170
+ * Every call gets, in order: alias resolution against the canonical route
65171
+ * table, per-provider parameter normalisation, a hard per-leg timeout, a
65172
+ * per-route circuit breaker, an ordered fallback chain ending at the closed
65173
+ * incumbent, and — where the caller supplies a validator — one schema-feedback
65174
+ * retry ahead of the chain. None of them is optional, because a control that a
65175
+ * caller can switch off is a control that will be off on the call that needed
65176
+ * it.
65177
+ *
65178
+ * @module llm/alias-client
65179
+ */
65180
+ /** Env var naming the gateway's base URL. */
65181
+ const GATEWAY_BASE_URL_ENV = "LLM_GATEWAY_BASE_URL";
65182
+ /** Env var NAME holding the gateway key. The key itself is never read here. */
65183
+ const DEFAULT_GATEWAY_KEY_ENV = "LLM_GATEWAY_API_KEY";
65184
+ /** Process-wide breaker registry, so route health is shared across call sites. */
65185
+ let breakers = new CircuitBreakerRegistry(routeTable.defaults.circuit_breaker);
65186
+ /** Active runtime wiring. */
65187
+ let config = {};
65188
+ /** Lazily built transports, rebuilt whenever configuration changes. */
65189
+ let gatewayTransport = null;
65190
+ let directTransport = null;
65191
+ /**
65192
+ * Wire the client.
65193
+ *
65194
+ * Called once at process start. Transports are injectable so a consumer can
65195
+ * supply its own instrumented client, and so tests can exercise the chain
65196
+ * without a network — a fallback chain that could only be observed against live
65197
+ * providers would in practice never be observed at all.
65198
+ *
65199
+ * @param next Runtime wiring; unspecified fields fall back to the environment.
65200
+ * @returns void
65201
+ */
65202
+ function configureLlmClient(next) {
65203
+ config = { ...next };
65204
+ gatewayTransport = null;
65205
+ directTransport = null;
65206
+ breakers = new CircuitBreakerRegistry(routeTable.defaults.circuit_breaker, next.now);
65207
+ }
65208
+ /**
65209
+ * Inspect route health.
65210
+ *
65211
+ * @returns The live breaker registry.
65212
+ */
65213
+ function llmBreakers() {
65214
+ return breakers;
65215
+ }
65216
+ /**
65217
+ * Resolve the gateway transport, building it on first use.
65218
+ *
65219
+ * @param chain The chain being served, used to derive each leg's gateway name.
65220
+ * @returns The transport, or null when no gateway is configured.
65221
+ */
65222
+ function gatewayFor(chain) {
65223
+ if (config.gatewayTransport !== undefined) {
65224
+ return config.gatewayTransport;
65225
+ }
65226
+ const baseUrl = config.gatewayBaseUrl ?? process.env[GATEWAY_BASE_URL_ENV];
65227
+ if (baseUrl === undefined || baseUrl.length === 0) {
65228
+ return null;
65229
+ }
65230
+ if (gatewayTransport === null) {
65231
+ gatewayTransport = createGatewayTransport({
65232
+ baseUrl,
65233
+ apiKeyEnv: config.gatewayApiKeyEnv ?? DEFAULT_GATEWAY_KEY_ENV,
65234
+ modelNameFor: (request) => gatewayModelNameFor(request.route, chain),
65235
+ });
65236
+ }
65237
+ return gatewayTransport;
65238
+ }
65239
+ /**
65240
+ * Resolve the degraded direct transport, building it on first use.
65241
+ *
65242
+ * @returns The transport.
65243
+ */
65244
+ function directFor() {
65245
+ if (config.directTransport !== undefined) {
65246
+ return config.directTransport;
65247
+ }
65248
+ if (directTransport === null) {
65249
+ directTransport = createDirectTransport({
65250
+ resolveCaller: resolveDefaultDirectCaller,
65251
+ });
65252
+ }
65253
+ return directTransport;
65254
+ }
65255
+ /**
65256
+ * Prepare each leg, normalising parameters up front.
65257
+ *
65258
+ * Normalisation happens before the walk rather than inside it so a capability
65259
+ * mismatch is known without spending a network round trip on it, and so a leg
65260
+ * that cannot serve the request is recorded as skipped rather than counted
65261
+ * against the provider's health.
65262
+ *
65263
+ * @param chain The resolved chain.
65264
+ * @param options The caller's options.
65265
+ * @param responseFormat The requested response shape.
65266
+ * @param transport The transport to carry every leg.
65267
+ * @returns Prepared legs, in chain order.
65268
+ */
65269
+ function prepareLegs(chain, options, responseFormat, transport) {
65270
+ return chain.routes.map((route) => {
65271
+ try {
65272
+ return {
65273
+ route,
65274
+ transport,
65275
+ params: normaliseParams(options, route, responseFormat),
65276
+ };
65277
+ }
65278
+ catch (error) {
65279
+ if (error instanceof UnsupportedCapabilityError) {
65280
+ return { route, transport, params: error };
65281
+ }
65282
+ throw error;
65283
+ }
65284
+ });
65285
+ }
65286
+ /**
65287
+ * Call a language model by semantic alias.
65288
+ *
65289
+ * @param content The prompt, or multi-part content for a vision-capable route.
65290
+ * @param responseFormat The response shape. Defaults to plain text.
65291
+ * @param options Call options; `alias` is required and there is no model option.
65292
+ * @returns The answer, with the routing decision and full attempt record attached.
65293
+ * @throws {UnknownAliasError} When the alias is not in the route table.
65294
+ * @throws {ChainExhaustedError} When no leg produced an answer.
65295
+ * @throws {SchemaRetryExhaustedError} When a validated payload failed twice.
65296
+ */
65297
+ async function callLLMByAlias(content, responseFormat = "text", options) {
65298
+ const chain = resolveChain(options.alias, {
65299
+ isolated: options.isolated,
65300
+ timeoutMsOverride: options.timeoutMs,
65301
+ });
65302
+ if (chain.routes.length === 0) {
65303
+ // Nothing is servable. The exclusions say why each leg was unavailable,
65304
+ // which is the difference between an operator reading config and an
65305
+ // operator reading the error.
65306
+ throw new ChainExhaustedError(options.alias, chain.exclusions.map((exclusion) => ({
65307
+ routeKey: `${options.alias}#${exclusion.role}`,
65308
+ role: exclusion.role,
65309
+ provider: exclusion.provider,
65310
+ modelId: "(unresolved)",
65311
+ outcome: "skipped",
65312
+ durationMs: 0,
65313
+ reason: exclusion.reason,
65314
+ })), {
65315
+ prompt_tokens: 0,
65316
+ completion_tokens: 0,
65317
+ provider: "none",
65318
+ model: "none",
65319
+ cost: 0,
65320
+ });
65321
+ }
65322
+ const gateway = gatewayFor(chain);
65323
+ const attemptLog = [];
65324
+ /**
65325
+ * Run the chain, falling back from the gateway to the degraded direct path
65326
+ * only when the gateway itself is unreachable.
65327
+ *
65328
+ * @param prompt The prompt for this attempt.
65329
+ * @returns The transport response and the routing facts about it.
65330
+ */
65331
+ const runOnce = async (prompt) => {
65332
+ const boundContent = prompt;
65333
+ if (gateway !== null) {
65334
+ try {
65335
+ const outcome = await executeChain(options.alias, {
65336
+ legs: prepareLegs(chain, options, responseFormat, gateway),
65337
+ content: boundContent,
65338
+ responseFormat,
65339
+ breakers,
65340
+ correlationId: options.correlationId,
65341
+ callerSignal: options.signal,
65342
+ now: config.now,
65343
+ onAttempt: (record) => attemptLog.push(record),
65344
+ });
65345
+ return { ...outcome, degraded: false };
65346
+ }
65347
+ catch (error) {
65348
+ if (!isGatewayOutage(error)) {
65349
+ throw error;
65350
+ }
65351
+ // The proxy is gone, not a provider behind it. Walking the chain again
65352
+ // through the gateway would repeat the same failure on every leg, so
65353
+ // the degraded path takes over — restricted to the closed incumbent.
65354
+ }
65355
+ }
65356
+ const direct = directFor();
65357
+ const closedLegs = chain.routes.filter((route) => route.provider.tier === "closed");
65358
+ if (closedLegs.length === 0) {
65359
+ throw new ChainExhaustedError(options.alias, attemptLog, {
65360
+ prompt_tokens: 0,
65361
+ completion_tokens: 0,
65362
+ provider: "none",
65363
+ model: "none",
65364
+ cost: 0,
65365
+ });
65366
+ }
65367
+ const outcome = await executeChain(options.alias, {
65368
+ legs: prepareLegs({ ...chain, routes: closedLegs }, options, responseFormat, direct),
65369
+ content: boundContent,
65370
+ responseFormat,
65371
+ breakers,
65372
+ correlationId: options.correlationId,
65373
+ callerSignal: options.signal,
65374
+ now: config.now,
65375
+ onAttempt: (record) => attemptLog.push(record),
65376
+ });
65377
+ return { ...outcome, degraded: true };
65378
+ };
65379
+ if (options.validate === undefined) {
65380
+ const outcome = await runOnce(content);
65381
+ return {
65382
+ response: outcome.response.response,
65383
+ usage: outcome.response.usage,
65384
+ tool_calls: outcome.response.tool_calls,
65385
+ servedBy: outcome.servedBy,
65386
+ attempts: attemptLog,
65387
+ degraded: outcome.degraded,
65388
+ totalUsage: outcome.totalUsage,
65389
+ };
65390
+ }
65391
+ // A validator is only meaningful against a text prompt, because the retry has
65392
+ // to be able to append the validator's complaint to it.
65393
+ if (typeof content !== "string") {
65394
+ throw new Error("a validator requires a string prompt: the feedback retry appends the validator's rejection to the original prompt");
65395
+ }
65396
+ let lastRouting = null;
65397
+ const validated = await callWithValidation({
65398
+ prompt: content,
65399
+ validate: options.validate,
65400
+ call: async (prompt) => {
65401
+ const outcome = await runOnce(prompt);
65402
+ lastRouting = {
65403
+ servedBy: outcome.servedBy,
65404
+ degraded: outcome.degraded,
65405
+ totalUsage: outcome.totalUsage,
65406
+ };
65407
+ return outcome.response;
65408
+ },
65409
+ });
65410
+ if (lastRouting === null) {
65411
+ throw new Error("validated call completed without recording a routing decision");
65412
+ }
65413
+ const routing = lastRouting;
65414
+ return {
65415
+ response: validated.value,
65416
+ usage: validated.response.usage,
65417
+ tool_calls: validated.response.tool_calls,
65418
+ servedBy: routing.servedBy,
65419
+ attempts: attemptLog,
65420
+ degraded: routing.degraded,
65421
+ totalUsage: validated.totalUsage,
65422
+ };
65423
+ }
65424
+ /**
65425
+ * Whether an error means the gateway itself is gone.
65426
+ *
65427
+ * Only a transport-level failure to reach the proxy qualifies. A provider error
65428
+ * relayed BY the proxy is a normal chain event and must not trigger the
65429
+ * degraded path, or a single flaky provider would quietly move every call onto
65430
+ * the closed incumbent — a fallback the routing policy reserves for last.
65431
+ *
65432
+ * @param error The error to classify.
65433
+ * @returns Whether the gateway is unreachable.
65434
+ */
65435
+ function isGatewayOutage(error) {
65436
+ if (error instanceof GatewayUnreachableError) {
65437
+ return true;
65438
+ }
65439
+ if (error instanceof ChainExhaustedError) {
65440
+ return (error.attempts.length > 0 &&
65441
+ error.attempts.every((attempt) => attempt.reason === undefined
65442
+ ? false
65443
+ : attempt.reason.includes("is unreachable")));
65444
+ }
65445
+ return false;
65446
+ }
65447
+ /**
65448
+ * The aliases application code may name.
65449
+ *
65450
+ * @returns The alias names, sorted.
65451
+ */
65452
+ function llmAliases() {
65453
+ return Object.keys(routeTable.aliases).sort();
65454
+ }
65455
+
65456
+ /**
65457
+ * Streaming normalisation across provider wire formats.
65458
+ *
65459
+ * A caller that streams wants one thing — text as it arrives — but the wire
65460
+ * formats disagree about how to say it. OpenAI-compatible providers emit
65461
+ * server-sent events whose payload nests the increment under
65462
+ * `choices[0].delta.content` and end with a literal `[DONE]` sentinel;
65463
+ * Anthropic emits typed events where the increment is `delta.text` and the end
65464
+ * is an explicit `message_stop`. A consumer written against one shape breaks on
65465
+ * the other, which would make a fallback across providers fail precisely when
65466
+ * the fallback was needed.
65467
+ *
65468
+ * The one behaviour that matters more than the format is how a stream ENDS. A
65469
+ * stream cut short mid-answer looks exactly like a short answer: the consumer
65470
+ * has already received and probably already acted on the text. So a stream
65471
+ * that stops without its terminal event raises rather than returning what it
65472
+ * had — a truncated answer accepted as complete is a wrong answer that nothing
65473
+ * reports.
65474
+ *
65475
+ * @module llm/streaming
65476
+ */
65477
+ /** SSE payload that marks the end of an OpenAI-compatible stream. */
65478
+ const SSE_DONE_SENTINEL = "[DONE]";
65479
+ /** Prefix carrying the payload on an SSE line. */
65480
+ const SSE_DATA_PREFIX = "data:";
65481
+ /** Anthropic event type carrying a text increment. */
65482
+ const ANTHROPIC_DELTA_EVENT = "content_block_delta";
65483
+ /** Anthropic event type marking the end of a message. */
65484
+ const ANTHROPIC_STOP_EVENT = "message_stop";
65485
+ /** Anthropic event type carrying a mid-stream error. */
65486
+ const ANTHROPIC_ERROR_EVENT = "error";
65487
+ /**
65488
+ * Thrown when a stream ends without its terminal event.
65489
+ *
65490
+ * A distinct type because the caller's correct response differs from a normal
65491
+ * failure: the partial text exists and may be worth logging for diagnosis, but
65492
+ * it must never be treated as the answer.
65493
+ */
65494
+ class StreamTruncatedError extends Error {
65495
+ /** Text received before the stream stopped. Present for diagnosis only. */
65496
+ partialText;
65497
+ /**
65498
+ * @param partialText What had arrived when the stream stopped.
65499
+ * @param reason Why it stopped, when known.
65500
+ */
65501
+ constructor(partialText, reason) {
65502
+ super(`LLM stream ended without a terminal event after ${partialText.length} characters: ${reason}. ` +
65503
+ "The partial text is not returned as an answer: a truncated answer accepted as complete is a wrong answer nothing reports.");
65504
+ this.name = "StreamTruncatedError";
65505
+ this.partialText = partialText;
65506
+ }
65507
+ }
65508
+ /** Thrown when a provider reports an error inside an already-open stream. */
65509
+ class StreamProviderError extends Error {
65510
+ /** Text received before the error. */
65511
+ partialText;
65512
+ /**
65513
+ * @param partialText What had arrived when the error appeared.
65514
+ * @param detail The provider's message.
65515
+ */
65516
+ constructor(partialText, detail) {
65517
+ super(`LLM stream failed mid-response: ${detail}`);
65518
+ this.name = "StreamProviderError";
65519
+ this.partialText = partialText;
65520
+ }
65521
+ }
65522
+ /**
65523
+ * Split a byte stream into complete lines.
65524
+ *
65525
+ * Chunk boundaries fall wherever the network puts them, not on line breaks, so
65526
+ * a partial line is carried across chunks. Parsing each network chunk as if it
65527
+ * were whole would drop or corrupt every event unlucky enough to be split.
65528
+ *
65529
+ * @param source The response body stream.
65530
+ * @returns An async iterable of complete lines.
65531
+ */
65532
+ async function* toLines(source) {
65533
+ const decoder = new TextDecoder();
65534
+ let buffer = "";
65535
+ for await (const chunk of source) {
65536
+ buffer += decoder.decode(chunk, { stream: true });
65537
+ let newline = buffer.indexOf("\n");
65538
+ while (newline !== -1) {
65539
+ yield buffer.slice(0, newline).replace(/\r$/, "");
65540
+ buffer = buffer.slice(newline + 1);
65541
+ newline = buffer.indexOf("\n");
65542
+ }
65543
+ }
65544
+ buffer += decoder.decode();
65545
+ if (buffer.length > 0) {
65546
+ yield buffer;
65547
+ }
65548
+ }
65549
+ /**
65550
+ * Normalise an OpenAI-compatible SSE stream.
65551
+ *
65552
+ * @param source The response body stream.
65553
+ * @returns Normalised chunks.
65554
+ * @throws {StreamTruncatedError} When the stream ends without `[DONE]`.
65555
+ * @throws {StreamProviderError} When an error event appears mid-stream.
65556
+ */
65557
+ async function* normaliseOpenAiStream(source) {
65558
+ let text = "";
65559
+ let sawDone = false;
65560
+ for await (const line of toLines(source)) {
65561
+ if (!line.startsWith(SSE_DATA_PREFIX)) {
65562
+ continue;
65563
+ }
65564
+ const payload = line.slice(SSE_DATA_PREFIX.length).trim();
65565
+ if (payload === SSE_DONE_SENTINEL) {
65566
+ sawDone = true;
65567
+ break;
65568
+ }
65569
+ if (payload.length === 0) {
65570
+ continue;
65571
+ }
65572
+ let event;
65573
+ try {
65574
+ event = JSON.parse(payload);
65575
+ }
65576
+ catch (error) {
65577
+ throw new StreamProviderError(text, `unparseable event: ${error instanceof Error ? error.message : String(error)}`);
65578
+ }
65579
+ if (event.error !== undefined) {
65580
+ throw new StreamProviderError(text, JSON.stringify(event.error));
65581
+ }
65582
+ const choices = event.choices;
65583
+ const delta = choices?.[0]?.delta?.content;
65584
+ if (typeof delta === "string" && delta.length > 0) {
65585
+ text += delta;
65586
+ yield { delta, text };
65587
+ }
65588
+ }
65589
+ if (!sawDone) {
65590
+ throw new StreamTruncatedError(text, "no [DONE] sentinel was received");
65591
+ }
65592
+ }
65593
+ /**
65594
+ * Normalise an Anthropic event stream.
65595
+ *
65596
+ * @param source The response body stream.
65597
+ * @returns Normalised chunks.
65598
+ * @throws {StreamTruncatedError} When the stream ends without `message_stop`.
65599
+ * @throws {StreamProviderError} When an error event appears mid-stream.
65600
+ */
65601
+ async function* normaliseAnthropicStream(source) {
65602
+ let text = "";
65603
+ let sawStop = false;
65604
+ for await (const line of toLines(source)) {
65605
+ if (!line.startsWith(SSE_DATA_PREFIX)) {
65606
+ continue;
65607
+ }
65608
+ const payload = line.slice(SSE_DATA_PREFIX.length).trim();
65609
+ if (payload.length === 0) {
65610
+ continue;
65611
+ }
65612
+ let event;
65613
+ try {
65614
+ event = JSON.parse(payload);
65615
+ }
65616
+ catch (error) {
65617
+ throw new StreamProviderError(text, `unparseable event: ${error instanceof Error ? error.message : String(error)}`);
65618
+ }
65619
+ const type = event.type;
65620
+ if (type === ANTHROPIC_ERROR_EVENT) {
65621
+ throw new StreamProviderError(text, JSON.stringify(event.error ?? event));
65622
+ }
65623
+ if (type === ANTHROPIC_STOP_EVENT) {
65624
+ sawStop = true;
65625
+ break;
65626
+ }
65627
+ if (type === ANTHROPIC_DELTA_EVENT) {
65628
+ const delta = event.delta?.text;
65629
+ if (typeof delta === "string" && delta.length > 0) {
65630
+ text += delta;
65631
+ yield { delta, text };
65632
+ }
65633
+ }
65634
+ }
65635
+ if (!sawStop) {
65636
+ throw new StreamTruncatedError(text, "no message_stop event was received");
65637
+ }
65638
+ }
65639
+ /**
65640
+ * Normalise a stream according to the wire format its provider speaks.
65641
+ *
65642
+ * Selecting on the route's declared API style rather than sniffing the payload
65643
+ * keeps the decision with the route table, which is the thing that actually
65644
+ * knows which provider is answering.
65645
+ *
65646
+ * @param apiStyle The provider's wire format.
65647
+ * @param source The response body stream.
65648
+ * @returns Normalised chunks, identical in shape across providers.
65649
+ */
65650
+ function normaliseStream(apiStyle, source) {
65651
+ return apiStyle === "anthropic"
65652
+ ? normaliseAnthropicStream(source)
65653
+ : normaliseOpenAiStream(source);
65654
+ }
65655
+ /**
65656
+ * Drain a normalised stream into its complete text.
65657
+ *
65658
+ * @param stream A normalised stream.
65659
+ * @returns The full text.
65660
+ * @throws Whatever the stream raises; a truncated stream never resolves to text.
65661
+ */
65662
+ async function collectStream(stream) {
65663
+ let text = "";
65664
+ for await (const chunk of stream) {
65665
+ text = chunk.text;
65666
+ }
65667
+ return text;
65668
+ }
65669
+
62912
65670
  /**
62913
65671
  * @module LRUCache
62914
65672
  */
@@ -72033,10 +74791,10 @@ const adaptic = {
72033
74791
  },
72034
74792
  rateLimiter: {
72035
74793
  TokenBucketRateLimiter,
72036
- limiters: rateLimiters,
74794
+ limiters: rateLimiters$1,
72037
74795
  },
72038
74796
  };
72039
74797
  const adptc = adaptic;
72040
74798
 
72041
- export { API_RETRY_CONFIGS, AVNewsArticleSchema, AVNewsResponseSchema, AdapticUtilsError, AlpacaAccountDetailsSchema, AlpacaApiError, AlpacaBarSchema, AlpacaClient, AlpacaCryptoBarsResponseSchema, AlpacaHistoricalBarsResponseSchema, AlpacaLatestBarsResponseSchema, AlpacaLatestQuotesResponseSchema, AlpacaLatestTradesResponseSchema, AlpacaMarketDataAPI, AlpacaNewsArticleSchema, AlpacaNewsResponseSchema, AlpacaOrderSchema, AlpacaOrdersArraySchema, AlpacaPortfolioHistoryResponseSchema, AlpacaPositionSchema, AlpacaPositionsArraySchema, AlpacaQuoteSchema, AlpacaTradeSchema, AlpacaTradingAPI, AlphaVantageError, AlphaVantageQuoteResponseSchema, AssetAllocationEngine, AuthenticationError, AutonomyMode, BTC_PAIRS, BarError, CircuitOpenError, CryptoDataError, CryptoOrderError, DEFAULT_CACHE_OPTIONS, DEFAULT_RISK_FREE_RATE, DEFAULT_TIMEOUTS, DEFAULT_TRADING_POLICY, DataFormatError, DecisionMemoryOutcome, DecisionOutcome, DecisionRecordStatus, DuplicateClientOrderIdError, HttpClientError, HttpServerError, KEEP_ALIVE_DEFAULTS, LlmProvider, MARKET_DATA_API, MassiveAggregatesResponseSchema, MassiveApiError, MassiveDailyOpenCloseSchema, MassiveErrorResponseSchema, MassiveGroupedDailyResponseSchema, MassiveLastTradeResponseSchema, MassiveTickerDetailsResponseSchema, MassiveTickerInfoSchema, MassiveTradeSchema as MassiveTradeZodSchema, MassiveTradesResponseSchema, NetworkError, NewsError, OptionStrategyError, OptionsDataError, OverlaySeverity, OverlayStatus, OverlayType, QuoteError, RISK_FREE_RATE_TTL_MS, RateLimitError, RawMassivePriceDataSchema, StampedeProtectedCache, TRADING_API, TimeoutError, TokenBucketRateLimiter, TradeError, TrailingStopValidationError, USDC_PAIRS, USDT_PAIRS, USD_PAIRS, UnsupportedBrokerError, ValidationError, ValidationResponseError, WEBSOCKET_STREAMS, WebSocketError, account, adaptic, adptc, alpaca, analyzeBars, approximateImpliedVolatility, atrNs as atr, bracketOrders, buildOCCSymbol, buildOptionSymbol, buyCryptoNotional, buyToClose, buyToOpen, buyWithStopLoss, buyWithTrailingStop, calculateMoneyness, calculateOrderValue, calculatePeriodPerformance, calculatePutCallRatio, calculateTotalFilledValue, cancelAllCryptoOrders, cancelOCOOrder, cancelOTOOrder, cancelTrailingStop, cancelTrailingStopsForSymbol, checkTradingEligibility, clearClientCache, clock, closeAllOptionPositions, closeOptionPosition, createAlpacaClient, createAlpacaMarketDataAPI, createAlpacaTradingAPI, createBracketOrder, createBrokerClient, createButterflySpread, createClientFromEnv, createCoveredCall, createCryptoLimitOrder, createCryptoMarketOrder, createCryptoOrder, createCryptoStopLimitOrder, createCryptoStopOrder, createExecutorFromTradingAPI, createIronCondor$1 as createIronCondor, createIronCondor as createIronCondorAdvanced, createMultiLegOptionOrder, createOCOOrder, createOTOOrder, createOptionOrder, createPortfolioTrailingStops, createProtectiveBracket, createStampedeProtectedCache, createStraddle$1 as createStraddle, createStraddle as createStraddleAdvanced, createStrangle$1 as createStrangle, createStrangle as createStrangleAdvanced, createStreamManager, createTimeoutSignal, createTrailingStop, createVerticalSpread$1 as createVerticalSpread, createVerticalSpread as createVerticalSpreadAdvanced, enrichAlpacaError, entryWithPercentStopLoss, exerciseOption, extractAlpacaBrokerError, extractGreeks, filterByExpiration, filterByStrike, filterByType, filterOrdersByDateRange, findATMOptions, findATMStrikes, findNearestExpiration, findOptionsByDelta, formatOrderForLog, formatOrderSummary, generateOptimalAllocation, getAccountConfiguration, getAccountDetails, getAccountSummary, getAgentPoolStatus, getAllOrders, getAlpacaBrokerErrorCode, getAlpacaBrokerErrorDetail, getAlpacaCalendar, getAlpacaClock, getAverageDailyVolume, getBars, getBuyingPower, getCachedRiskFreeRateSync, getCachedRiskFreeRateSyncWithProvenance, getCrypto24HourChange, getCryptoBars, getCryptoDailyPrices, getCryptoPairsByQuote, getCryptoPrice, getCryptoSnapshots, getCryptoSpread, getCryptoStreamUrl, getCryptoTrades, getCurrentPrice, getCurrentPrices, getDailyPrices, getDailyReturns, getDaysToExpiration, getDefaultRiskProfile, getEquityCurve, getExpirationDates, getFilledOrders, getGroupedOptionChain, getHistoricalOptionsBars, getHistoricalTrades, getIntradayPrices, getLatestBars, getLatestCryptoQuotes, getLatestCryptoTrades, getLatestNews, getLatestOptionsQuotes, getLatestOptionsTrades, getLatestQuote, getLatestQuotes, getLatestTrade, getLatestTrades, getLogger, getMarginInfo, getNews, getNewsForSymbols, getOCOOrderStatus, getOTOOrderStatus, getOpenCryptoOrders, getOpenOrders$1 as getOpenOrdersQuery, getOpenTrailingStops, getOptionChain, getOptionContract, getOptionContracts, getOptionSpread, getOptionsChain, getOptionsSnapshots, getOptionsStreamUrl, getOptionsTradingLevel, getOrderHistory, getOrdersBySymbol, getPDTStatus, getPopularCryptoPairs, getPortfolioHistory, getPreviousClose, getPriceRange, getRiskFreeRate, getRiskFreeRateWithProvenance, getSpread, getSpreads, getStockStreamUrl, getStrikePrices, getSupportedCryptoPairs, getSymbolSentiment, getTimeout, getTradeVolume, getTradingApiUrl, getTradingWebSocketUrl, getTrailingStopHWM, groupOrdersByStatus, groupOrdersBySymbol, hasActiveTrailingStop, hasGoodLiquidity as hasOptionLiquidity, hasGoodLiquidity$1 as hasStockLiquidity, hasSufficientVolume, httpAgent, httpsAgent, isAlpacaBrokerCredentials, isContractTradable, isCryptoPair, isExpiringWithin, isMarginAccount, isOptionOrderCancelable, isOptionOrderTerminal, isOrderFillable, isOrderFilled, isOrderOpen, isOrderTerminal$1 as isOrderTerminalStatus, isSupportedCryptoPair, isTransientNetworkError, index$1 as legacyApi, limitBuyWithTakeProfit, ocoOrders, orderUtils, otoOrders, paginate, paginateAll, parseOCCSymbol, protectLongPosition, protectShortPosition, rateLimiters, resetLogger, resetRiskFreeRateCache, riskNs as risk, rollOptionPosition, roundPriceForAlpaca$3 as roundPriceForAlpaca, roundPriceForAlpacaNumber, safeValidateResponse, searchNews, sellAllCrypto, sellCryptoNotional, sellToClose, sellToOpen, setLogger, setRiskFreeRate, shortWithStopLoss, sortOrdersByDate, strategyNs as strategy, index as tradingPolicy, trailingStops, updateAccountConfiguration, updateTrailingStop, validateAlpacaCredentials, validateAlphaVantageApiKey, validateMassiveApiKey$1 as validateMassiveApiKey, validateMultiLegOrder, validateResponse, verifyFetchKeepAlive, volatilityNs as volatility, waitForOrderFill, withRetry, withTimeout };
74799
+ export { API_RETRY_CONFIGS, AVNewsArticleSchema, AVNewsResponseSchema, AdapticUtilsError, AlpacaAccountDetailsSchema, AlpacaApiError, AlpacaBarSchema, AlpacaClient, AlpacaCryptoBarsResponseSchema, AlpacaHistoricalBarsResponseSchema, AlpacaLatestBarsResponseSchema, AlpacaLatestQuotesResponseSchema, AlpacaLatestTradesResponseSchema, AlpacaMarketDataAPI, AlpacaNewsArticleSchema, AlpacaNewsResponseSchema, AlpacaOrderSchema, AlpacaOrdersArraySchema, AlpacaPortfolioHistoryResponseSchema, AlpacaPositionSchema, AlpacaPositionsArraySchema, AlpacaQuoteSchema, AlpacaTradeSchema, AlpacaTradingAPI, AlphaVantageError, AlphaVantageQuoteResponseSchema, AssetAllocationEngine, AuthenticationError, AutonomyMode, BTC_PAIRS, BarError, ChainExhaustedError, CircuitBreakerRegistry, CircuitOpenError, CryptoDataError, CryptoOrderError, DEFAULT_CACHE_OPTIONS, DEFAULT_RISK_FREE_RATE, DEFAULT_TIMEOUTS, DEFAULT_TRADING_POLICY, DataFormatError, DecisionMemoryOutcome, DecisionOutcome, DecisionRecordStatus, DirectTransportRefusedError, DuplicateClientOrderIdError, GatewayResponseError, GatewayUnreachableError, HttpClientError, HttpServerError, KEEP_ALIVE_DEFAULTS, LlmProvider, MARKET_DATA_API, MassiveAggregatesResponseSchema, MassiveApiError, MassiveDailyOpenCloseSchema, MassiveErrorResponseSchema, MassiveGroupedDailyResponseSchema, MassiveLastTradeResponseSchema, MassiveTickerDetailsResponseSchema, MassiveTickerInfoSchema, MassiveTradeSchema as MassiveTradeZodSchema, MassiveTradesResponseSchema, NetworkError, NewsError, NoServableRouteError, OptionStrategyError, OptionsDataError, OverlaySeverity, OverlayStatus, OverlayType, QuoteError, RISK_FREE_RATE_TTL_MS, RateGuardTimeoutError, RateLimitError, RawMassivePriceDataSchema, SchemaRetryExhaustedError, StampedeProtectedCache, StreamProviderError, StreamTruncatedError, TRADING_API, TimeoutError, TokenBucketRateLimiter, TradeError, TrailingStopValidationError, USDC_PAIRS, USDT_PAIRS, USD_PAIRS, UnknownAliasError, UnsupportedBrokerError, UnsupportedCapabilityError, ValidationError, ValidationResponseError, WEBSOCKET_STREAMS, WebSocketError, account, adaptic, adptc, alpaca, analyzeBars, approximateImpliedVolatility, atrNs as atr, bracketOrders, buildOCCSymbol, buildOptionSymbol, buildRetryPrompt, buyCryptoNotional, buyToClose, buyToOpen, buyWithStopLoss, buyWithTrailingStop, calculateMoneyness, calculateOrderValue, calculatePeriodPerformance, calculatePutCallRatio, calculateTotalFilledValue, callLLMByAlias, callWithValidation, cancelAllCryptoOrders, cancelOCOOrder, cancelOTOOrder, cancelTrailingStop, cancelTrailingStopsForSymbol, checkTradingEligibility, clearClientCache, clock, closeAllOptionPositions, closeOptionPosition, closedIncumbentLeg, collectStream, configureLlmClient, createAlpacaClient, createAlpacaMarketDataAPI, createAlpacaTradingAPI, createBracketOrder, createBrokerClient, createButterflySpread, createClientFromEnv, createCoveredCall, createCryptoLimitOrder, createCryptoMarketOrder, createCryptoOrder, createCryptoStopLimitOrder, createCryptoStopOrder, createDirectTransport, createExecutorFromTradingAPI, createGatewayTransport, createIronCondor$1 as createIronCondor, createIronCondor as createIronCondorAdvanced, createMultiLegOptionOrder, createOCOOrder, createOTOOrder, createOptionOrder, createPortfolioTrailingStops, createProtectiveBracket, createStampedeProtectedCache, createStraddle$1 as createStraddle, createStraddle as createStraddleAdvanced, createStrangle$1 as createStrangle, createStrangle as createStrangleAdvanced, createStreamManager, createTimeoutSignal, createTrailingStop, createVerticalSpread$1 as createVerticalSpread, createVerticalSpread as createVerticalSpreadAdvanced, enrichAlpacaError, entryWithPercentStopLoss, exerciseOption, extractAlpacaBrokerError, extractGreeks, filterByExpiration, filterByStrike, filterByType, filterOrdersByDateRange, findATMOptions, findATMStrikes, findNearestExpiration, findOptionsByDelta, formatOrderForLog, formatOrderSummary, gatewayModelNameFor, generateOptimalAllocation, getAccountConfiguration, getAccountDetails, getAccountSummary, getAgentPoolStatus, getAllOrders, getAlpacaBrokerErrorCode, getAlpacaBrokerErrorDetail, getAlpacaCalendar, getAlpacaClock, getAverageDailyVolume, getBars, getBuyingPower, getCachedRiskFreeRateSync, getCachedRiskFreeRateSyncWithProvenance, getCrypto24HourChange, getCryptoBars, getCryptoDailyPrices, getCryptoPairsByQuote, getCryptoPrice, getCryptoSnapshots, getCryptoSpread, getCryptoStreamUrl, getCryptoTrades, getCurrentPrice, getCurrentPrices, getDailyPrices, getDailyReturns, getDaysToExpiration, getDefaultRiskProfile, getEquityCurve, getExpirationDates, getFilledOrders, getGroupedOptionChain, getHistoricalOptionsBars, getHistoricalTrades, getIntradayPrices, getLatestBars, getLatestCryptoQuotes, getLatestCryptoTrades, getLatestNews, getLatestOptionsQuotes, getLatestOptionsTrades, getLatestQuote, getLatestQuotes, getLatestTrade, getLatestTrades, getLogger, getMarginInfo, getNews, getNewsForSymbols, getOCOOrderStatus, getOTOOrderStatus, getOpenCryptoOrders, getOpenOrders$1 as getOpenOrdersQuery, getOpenTrailingStops, getOptionChain, getOptionContract, getOptionContracts, getOptionSpread, getOptionsChain, getOptionsSnapshots, getOptionsStreamUrl, getOptionsTradingLevel, getOrderHistory, getOrdersBySymbol, getPDTStatus, getPopularCryptoPairs, getPortfolioHistory, getPreviousClose, getPriceRange, getRiskFreeRate, getRiskFreeRateWithProvenance, getSpread, getSpreads, getStockStreamUrl, getStrikePrices, getSupportedCryptoPairs, getSymbolSentiment, getTimeout, getTradeVolume, getTradingApiUrl, getTradingWebSocketUrl, getTrailingStopHWM, groupOrdersByStatus, groupOrdersBySymbol, guardSnapshots, hasActiveTrailingStop, hasGoodLiquidity as hasOptionLiquidity, hasGoodLiquidity$1 as hasStockLiquidity, hasSufficientVolume, httpAgent, httpsAgent, isAlpacaBrokerCredentials, isContractTradable, isCryptoPair, isExpiringWithin, isMarginAccount, isOptionOrderCancelable, isOptionOrderTerminal, isOrderFillable, isOrderFilled, isOrderOpen, isOrderTerminal$1 as isOrderTerminalStatus, isSupportedCryptoPair, isTransientNetworkError, index$1 as legacyApi, limitBuyWithTakeProfit, limitsFor, limitsInventory, listAliases, llmAliases, llmBreakers, normaliseAnthropicStream, normaliseOpenAiStream, normaliseParams, normaliseStream, ocoOrders, orderUtils, orderedRoutes, otoOrders, paginate, paginateAll, parseOCCSymbol, protectLongPosition, protectShortPosition, rateLimiters$1 as rateLimiters, resetLogger, resetProviderGuards, resetRiskFreeRateCache, resolveChain, resolveDefaultDirectCaller, riskNs as risk, rollOptionPosition, roundPriceForAlpaca$3 as roundPriceForAlpaca, roundPriceForAlpacaNumber, routeKeyFor, routeSupports, routeTable, safeValidateResponse, searchNews, sellAllCrypto, sellCryptoNotional, sellToClose, sellToOpen, setLogger, setRiskFreeRate, shortWithStopLoss, sortOrdersByDate, strategyNs as strategy, sumUsage, index as tradingPolicy, trailingStops, updateAccountConfiguration, updateTrailingStop, validateAlpacaCredentials, validateAlphaVantageApiKey, validateMassiveApiKey$1 as validateMassiveApiKey, validateMultiLegOrder, validateResponse, verifyFetchKeepAlive, volatilityNs as volatility, waitForOrderFill, withProviderGuards, withRetry, withTimeout };
72042
74800
  //# sourceMappingURL=index.mjs.map