omnigateway 0.9.1 → 0.9.2

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 (33) hide show
  1. package/README.md +12 -4
  2. package/bin/omni.js +37 -5
  3. package/gateway.js +618 -62
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-DJkUCwNJ.js → Chip-Did4Zhvj.js} +1 -1
  6. package/public/assets/{CopyValue-CJ3ZYgvs.js → CopyValue-Cjg9QXrD.js} +1 -1
  7. package/public/assets/{Lamp-D7hJcQ90.js → Lamp-C2Q3t5Mn.js} +1 -1
  8. package/public/assets/{Rack-CaxD9IY1.js → Rack-D0_N-xsd.js} +1 -1
  9. package/public/assets/{RequestTable-bXapHDrR.js → RequestTable-i9paP_7e.js} +1 -1
  10. package/public/assets/{SummaryDeck-Dt_qbFtg.js → SummaryDeck-g4yLDeXB.js} +1 -1
  11. package/public/assets/{Toggle-C_iC6W50.js → Toggle-D-NDlneh.js} +1 -1
  12. package/public/assets/{TokenBreakdown-C96x_g6d.js → TokenBreakdown-D5ZIngM6.js} +1 -1
  13. package/public/assets/{WindowChart-D4dcv4DX.js → WindowChart-BsonO5Py.js} +1 -1
  14. package/public/assets/{_app.accounts-CGFOyYZc.js → _app.accounts-DFhyE62a.js} +1 -1
  15. package/public/assets/{_app.console-DrCT8eLd.js → _app.console-BfHcfQMp.js} +1 -1
  16. package/public/assets/{_app.database-D32je-GL.js → _app.database-Cgw9z3rK.js} +1 -1
  17. package/public/assets/{_app.index-BG6mGxkv.js → _app.index-Blw3S4-3.js} +1 -1
  18. package/public/assets/{_app.keys-C1AjjeHc.js → _app.keys-DrdfnfIk.js} +1 -1
  19. package/public/assets/{_app.logs-BDLj_H_j.js → _app.logs-DUOqlJzB.js} +1 -1
  20. package/public/assets/{_app.models-Cgg7gVSF.js → _app.models-Dm83HRaM.js} +1 -1
  21. package/public/assets/{_app.plugins._pluginId-C5FSNOfN.js → _app.plugins._pluginId-BmJGDYxw.js} +1 -1
  22. package/public/assets/{_app.settings-B7lKNDoh.js → _app.settings-66rm4vHJ.js} +1 -1
  23. package/public/assets/{_app.usage-DjqlIHDL.js → _app.usage-BCufy5XR.js} +1 -1
  24. package/public/assets/{chevron-right-DJj6SEIT.js → chevron-right-BZ4QmUDM.js} +1 -1
  25. package/public/assets/{client-Q2fvA2EK.js → client-De1_cEH5.js} +1 -1
  26. package/public/assets/{index-D1ewvjU0.js → index-PP8vsvw3.js} +3 -3
  27. package/public/assets/{login-Br_AimWR.js → login-CkZlwcdd.js} +1 -1
  28. package/public/assets/plus-D5AwIXj3.js +1 -0
  29. package/public/assets/{preload-helper-92RRH0nq.js → preload-helper-BC9bKUg1.js} +1 -1
  30. package/public/assets/{shared-Cp_kzH_a.js → shared-C47BfV_u.js} +1 -1
  31. package/public/assets/{trash-2-Chg8CwQf.js → trash-2-BLc5o7DN.js} +1 -1
  32. package/public/index.html +4 -4
  33. package/public/assets/plus-CCwmaxw3.js +0 -1
package/gateway.js CHANGED
@@ -29520,6 +29520,19 @@ function loadConfig(env) {
29520
29520
  const bodyLoggingAllowed = TRUTHY.has((env.OMNI_BODY_LOGGING_ALLOWED ?? "").trim().toLowerCase());
29521
29521
  const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
29522
29522
  const logLevel = parseLogLevel(rawLogLevel);
29523
+ const metricsToken = env.OMNI_METRICS_TOKEN?.trim() || null;
29524
+ const rawMaxSeries = env.OMNI_METRICS_MAX_SERIES ?? "5000";
29525
+ const metricsMaxSeries = Number(rawMaxSeries);
29526
+ if (!DECIMAL_INTEGER.test(rawMaxSeries) || metricsMaxSeries < 1) {
29527
+ throw new Error(`OMNI_METRICS_MAX_SERIES must be a positive integer, got "${rawMaxSeries}"`);
29528
+ }
29529
+ const otlpEndpoint = env.OMNI_OTLP_ENDPOINT?.trim().replace(/\/+$/, "") || null;
29530
+ const otlpHeaders = env.OMNI_OTLP_HEADERS?.trim() || null;
29531
+ const rawTraceSample = env.OMNI_TRACE_SAMPLE ?? "1.0";
29532
+ const traceSample = Number(rawTraceSample);
29533
+ if (!Number.isFinite(traceSample) || traceSample < 0 || traceSample > 1) {
29534
+ throw new Error(`OMNI_TRACE_SAMPLE must be between 0 and 1, got "${rawTraceSample}"`);
29535
+ }
29523
29536
  return {
29524
29537
  logLevel: logLevel ?? "info",
29525
29538
  logLevelFallbackFrom: logLevel === null && rawLogLevel ? rawLogLevel : null,
@@ -29533,7 +29546,12 @@ function loadConfig(env) {
29533
29546
  logFile: logFile === undefined || logFile.length === 0 ? null : logFile,
29534
29547
  clusterMode,
29535
29548
  databaseUrl: clusterMode ? databaseUrl ?? null : null,
29536
- redisUrl: clusterMode ? redisUrl ?? null : null
29549
+ redisUrl: clusterMode ? redisUrl ?? null : null,
29550
+ metricsToken,
29551
+ metricsMaxSeries,
29552
+ otlpEndpoint,
29553
+ otlpHeaders,
29554
+ traceSample
29537
29555
  };
29538
29556
  }
29539
29557
  // packages/control/src/oauth/pending.ts
@@ -29732,7 +29750,8 @@ function codecAdapter(id, capabilities2, codec2, origins) {
29732
29750
  const res = await req.http({
29733
29751
  provider: id,
29734
29752
  ...sendable,
29735
- signal: req.signal
29753
+ signal: req.signal,
29754
+ ...req.requestId === undefined ? {} : { requestId: req.requestId }
29736
29755
  });
29737
29756
  if (res.status < 200 || res.status >= 300) {
29738
29757
  const text = await res.text().catch(() => "");
@@ -33117,15 +33136,28 @@ function nodeHttpClient(options = {}) {
33117
33136
  const startedAt = now();
33118
33137
  let traced = false;
33119
33138
  const trace = (status, failed = false) => {
33120
- if (traced || !logger2.enabled("debug"))
33139
+ if (traced)
33121
33140
  return;
33122
33141
  traced = true;
33142
+ const durationMs = now() - startedAt;
33143
+ try {
33144
+ options.onResponseHead?.({
33145
+ provider: req.provider,
33146
+ host: url2.host,
33147
+ path: url2.pathname,
33148
+ ...status === undefined ? {} : { status },
33149
+ durationMs,
33150
+ ...req.requestId === undefined ? {} : { requestId: req.requestId }
33151
+ });
33152
+ } catch {}
33153
+ if (!logger2.enabled("debug"))
33154
+ return;
33123
33155
  logger2.debug("upstream http", {
33124
33156
  provider: req.provider,
33125
33157
  status,
33126
33158
  host: url2.host,
33127
33159
  path: url2.pathname,
33128
- durationMs: now() - startedAt,
33160
+ durationMs,
33129
33161
  reason: failed ? "transport error" : undefined
33130
33162
  });
33131
33163
  };
@@ -36847,6 +36879,7 @@ var WINDOW_MS2 = {
36847
36879
  "1w": 7 * 24 * 60 * 60 * 1000
36848
36880
  };
36849
36881
  // apps/gateway/src/app.ts
36882
+ import { timingSafeEqual } from "crypto";
36850
36883
  import { existsSync, realpathSync as realpathSync3 } from "fs";
36851
36884
  import { resolve as resolve3, sep as sep3 } from "path";
36852
36885
 
@@ -51324,11 +51357,13 @@ class ApiKeyRateLimiter {
51324
51357
  store;
51325
51358
  now;
51326
51359
  logger;
51360
+ onRejected;
51327
51361
  constructor(deps) {
51328
51362
  this.store = deps.store;
51329
51363
  this.now = deps.now;
51330
51364
  this.logger = deps.logger ?? noopLogger;
51331
51365
  this.coord = deps.coord ?? memoryCoord();
51366
+ this.onRejected = deps.onRejected;
51332
51367
  }
51333
51368
  async admit(keyId, limits, requestId) {
51334
51369
  if (!anyLimit(limits))
@@ -51403,6 +51438,9 @@ class ApiKeyRateLimiter {
51403
51438
  const exact = await this.exactReset(keyId, window2, now, requestId);
51404
51439
  const resolved = exact === null ? violation : { ...violation, resetAt: exact };
51405
51440
  const headroom = exact === null || window2 === null ? decision.headroom : withReset(decision.headroom, window2, exact);
51441
+ try {
51442
+ this.onRejected?.(violation.dimension, violation.window);
51443
+ } catch {}
51406
51444
  throw new RateLimitExceeded(retryAfterMs(resolved, now), headroom);
51407
51445
  }
51408
51446
  async exactReset(keyId, window2, now, requestId) {
@@ -51501,6 +51539,61 @@ function withReset(headroom, window2, resetAt) {
51501
51539
  return corrected;
51502
51540
  }
51503
51541
 
51542
+ // apps/gateway/src/dispatch/loadRegistry.ts
51543
+ var PREFIX6 = "load:";
51544
+ var SLOT_TTL_MS = 300000;
51545
+ function createLoadRegistry(coord = memoryCoord()) {
51546
+ const local = new Map;
51547
+ const providers = new Map;
51548
+ let remote = new Map;
51549
+ return {
51550
+ acquire(credentialId, model, provider) {
51551
+ const key = healthKey(credentialId, model);
51552
+ local.set(key, (local.get(key) ?? 0) + 1);
51553
+ if (provider !== undefined)
51554
+ providers.set(provider, (providers.get(provider) ?? 0) + 1);
51555
+ coord.gauge.acquire(PREFIX6 + key, SLOT_TTL_MS);
51556
+ let released = false;
51557
+ return () => {
51558
+ if (released)
51559
+ return;
51560
+ released = true;
51561
+ const next = (local.get(key) ?? 0) - 1;
51562
+ if (next > 0)
51563
+ local.set(key, next);
51564
+ else
51565
+ local.delete(key);
51566
+ if (provider !== undefined) {
51567
+ const providerCount = (providers.get(provider) ?? 0) - 1;
51568
+ if (providerCount > 0)
51569
+ providers.set(provider, providerCount);
51570
+ else
51571
+ providers.delete(provider);
51572
+ }
51573
+ coord.gauge.release(PREFIX6 + key);
51574
+ };
51575
+ },
51576
+ counts() {
51577
+ const out = new Map(local);
51578
+ for (const [key, count] of remote) {
51579
+ if (count > (out.get(key) ?? 0))
51580
+ out.set(key, count);
51581
+ }
51582
+ return out;
51583
+ },
51584
+ localCounts() {
51585
+ return new Map(providers);
51586
+ },
51587
+ async refresh() {
51588
+ const held = await coord.gauge.snapshot(PREFIX6);
51589
+ const sample = new Map;
51590
+ for (const [key, count] of held)
51591
+ sample.set(key.slice(PREFIX6.length), count);
51592
+ remote = sample;
51593
+ }
51594
+ };
51595
+ }
51596
+
51504
51597
  // apps/gateway/src/dispatch/snapshotCache.ts
51505
51598
  function createRoutingSnapshotCache(store, logger2 = noopLogger) {
51506
51599
  let snapshot = null;
@@ -55473,7 +55566,7 @@ async function routeLog(store, requestId, target, logger2 = noopLogger, broadcas
55473
55566
  }
55474
55567
  }
55475
55568
  }
55476
- async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit, emit, broadcast) {
55569
+ async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit, emit, broadcast, telemetry, trace) {
55477
55570
  let appended = true;
55478
55571
  try {
55479
55572
  await store.usage.append({ ...log, apiKeyId: keyId });
@@ -55517,15 +55610,108 @@ async function finishLog(store, log, keyId, logger2 = noopLogger, bodies, debit,
55517
55610
  report(logger2, "failed to publish a resource invalidation", log.id, error61);
55518
55611
  }
55519
55612
  }
55520
- if (bodies === undefined)
55521
- return;
55613
+ if (bodies !== undefined) {
55614
+ try {
55615
+ await bodies(log);
55616
+ } catch (error61) {
55617
+ report(logger2, "failed to persist request bodies", log.id, error61);
55618
+ }
55619
+ }
55522
55620
  try {
55523
- await bodies(log);
55621
+ telemetry?.record(log, keyId, trace ?? null);
55524
55622
  } catch (error61) {
55525
- report(logger2, "failed to persist request bodies", log.id, error61);
55623
+ report(logger2, "failed to record request telemetry", log.id, error61);
55624
+ }
55625
+ try {
55626
+ telemetry?.flush(log.id, trace ?? null);
55627
+ } catch (error61) {
55628
+ report(logger2, "failed to export request telemetry", log.id, error61);
55526
55629
  }
55527
55630
  }
55528
55631
 
55632
+ // apps/gateway/src/telemetry/spans.ts
55633
+ var TRACEPARENT = /^[\da-f]{2}-([\da-f]{32})-([\da-f]{16})-([\da-f]{2})(?:-[\da-f]+)*$/i;
55634
+ var ZERO_TRACE = "0".repeat(32);
55635
+ var ZERO_SPAN = "0".repeat(16);
55636
+ function parseTraceparent(value) {
55637
+ if (value === null)
55638
+ return null;
55639
+ const match = TRACEPARENT.exec(value);
55640
+ const traceId = match?.[1]?.toLowerCase();
55641
+ const parentSpanId = match?.[2]?.toLowerCase();
55642
+ const flags = match?.[3];
55643
+ if (traceId === undefined || parentSpanId === undefined || flags === undefined)
55644
+ return null;
55645
+ if (traceId === ZERO_TRACE || parentSpanId === ZERO_SPAN)
55646
+ return null;
55647
+ return { traceId, parentSpanId, sampled: (Number.parseInt(flags, 16) & 1) === 1 };
55648
+ }
55649
+ function randomHex(bytes) {
55650
+ const out = new Uint8Array(bytes);
55651
+ crypto.getRandomValues(out);
55652
+ return [...out].map((byte2) => byte2.toString(16).padStart(2, "0")).join("");
55653
+ }
55654
+ function createTrace(opts) {
55655
+ const id = opts.id ?? (() => randomHex(8));
55656
+ return {
55657
+ startedAt: opts.startedAt,
55658
+ traceId: opts.traceparent?.traceId ?? `${id()}${id()}`.slice(0, 32),
55659
+ parentSpanId: opts.traceparent?.parentSpanId ?? null,
55660
+ spanIds: [],
55661
+ spans: [],
55662
+ activeAttempt: null,
55663
+ routeSpan: null,
55664
+ nextId: id
55665
+ };
55666
+ }
55667
+ function addSpan(trace, name, parent, startMs, endMs, attrs) {
55668
+ try {
55669
+ const id = trace.spans.length;
55670
+ trace.spanIds.push(trace.nextId());
55671
+ trace.spans.push({ id, parent, name, startMs, endMs, attrs });
55672
+ return id;
55673
+ } catch {
55674
+ return -1;
55675
+ }
55676
+ }
55677
+ function otlpAttrs(attrs) {
55678
+ const out = [];
55679
+ for (const [key, value] of Object.entries(attrs)) {
55680
+ if (value === undefined)
55681
+ continue;
55682
+ out.push(typeof value === "number" ? { key, value: { intValue: String(value) } } : { key, value: { stringValue: value } });
55683
+ }
55684
+ return out;
55685
+ }
55686
+ function encodeTrace(trace, serviceVersion) {
55687
+ const spans = trace.spans.map((span) => {
55688
+ const parentSpanId = span.parent === null ? trace.parentSpanId : trace.spanIds[span.parent] ?? trace.parentSpanId;
55689
+ return {
55690
+ traceId: trace.traceId,
55691
+ spanId: trace.spanIds[span.id] ?? trace.nextId(),
55692
+ ...parentSpanId === null ? {} : { parentSpanId },
55693
+ name: span.name,
55694
+ startTimeUnixNano: String(BigInt(Math.round(trace.startedAt + span.startMs)) * 1000000n),
55695
+ endTimeUnixNano: String(BigInt(Math.round(trace.startedAt + span.endMs)) * 1000000n),
55696
+ attributes: otlpAttrs(span.attrs),
55697
+ status: { code: span.attrs.code === undefined ? 0 : 2 }
55698
+ };
55699
+ });
55700
+ return {
55701
+ resourceSpans: [
55702
+ {
55703
+ resource: {
55704
+ attributes: [
55705
+ { key: "service.name", value: { stringValue: "omnigateway" } },
55706
+ { key: "service.version", value: { stringValue: serviceVersion } }
55707
+ ]
55708
+ },
55709
+ scopeSpans: [{ spans }]
55710
+ }
55711
+ ]
55712
+ };
55713
+ }
55714
+
55529
55715
  // apps/gateway/src/dispatch/attempt.ts
55530
55716
  async function attempt(opts) {
55531
55717
  const { candidate, adapter, http, now, signal, refresh, refreshLeadMs } = opts;
@@ -55708,6 +55894,7 @@ async function dispatch(request2, deps, signal, requestId) {
55708
55894
  return fail(code, describeError(error61, "unresolvable model"));
55709
55895
  }
55710
55896
  await deps.loadRegistry.refresh();
55897
+ const routeStartedAt = deps.now();
55711
55898
  const { candidates, excluded } = rank({
55712
55899
  request: dispatchRequest,
55713
55900
  model,
@@ -55717,6 +55904,15 @@ async function dispatch(request2, deps, signal, requestId) {
55717
55904
  load: deps.loadRegistry.counts(),
55718
55905
  providers: deps.providers
55719
55906
  });
55907
+ if (deps.trace !== null && deps.trace !== undefined) {
55908
+ deps.trace.routeSpan = addSpan(deps.trace, "dispatch.route", 0, routeStartedAt - startedAt, deps.now() - startedAt, {
55909
+ candidates: candidates.length,
55910
+ ...candidates[0] === undefined ? {} : {
55911
+ chosen_provider: candidates[0].target.provider,
55912
+ chosen_model: candidates[0].target.model
55913
+ }
55914
+ });
55915
+ }
55720
55916
  logger2.debug("routing candidates ranked", {
55721
55917
  requestId,
55722
55918
  requestedModel: request2.model,
@@ -55744,7 +55940,7 @@ async function dispatch(request2, deps, signal, requestId) {
55744
55940
  const eager = {
55745
55941
  credentialId: head.credential.id,
55746
55942
  model: head.target.model,
55747
- release: deps.loadRegistry.acquire(head.credential.id, head.target.model)
55943
+ release: deps.loadRegistry.acquire(head.credential.id, head.target.model, head.target.provider)
55748
55944
  };
55749
55945
  let eagerHeld = true;
55750
55946
  releaseOnAbort = () => eager.release();
@@ -55755,7 +55951,14 @@ async function dispatch(request2, deps, signal, requestId) {
55755
55951
  const persistHealth = async (candidate, transition) => {
55756
55952
  const credentialId = candidate.credential.id;
55757
55953
  const { model: model2 } = candidate.target;
55758
- await deps.store.credentials.updateHealth(credentialId, model2, (current) => transition(current ?? blankHealth(credentialId, model2)));
55954
+ await deps.store.credentials.updateHealth(credentialId, model2, (current) => {
55955
+ const before = current ?? blankHealth(credentialId, model2);
55956
+ const after = transition(before);
55957
+ try {
55958
+ deps.telemetry?.breaker(candidate.target.provider, before.breakerState, after.breakerState);
55959
+ } catch {}
55960
+ return after;
55961
+ });
55759
55962
  };
55760
55963
  async function* run() {
55761
55964
  try {
@@ -55774,7 +55977,17 @@ async function dispatch(request2, deps, signal, requestId) {
55774
55977
  const adoptable = eagerHeld && eager.credentialId === candidate.credential.id && eager.model === candidate.target.model;
55775
55978
  if (adoptable)
55776
55979
  eagerHeld = false;
55777
- const releaseSlot = adoptable ? eager.release : deps.loadRegistry.acquire(candidate.credential.id, candidate.target.model);
55980
+ const releaseSlot = adoptable ? eager.release : deps.loadRegistry.acquire(candidate.credential.id, candidate.target.model, candidate.target.provider);
55981
+ const attemptStartedAt = deps.now();
55982
+ let attemptCode;
55983
+ if (deps.trace !== null && deps.trace !== undefined) {
55984
+ deps.trace.activeAttempt = addSpan(deps.trace, "dispatch.attempt", 0, attemptStartedAt - startedAt, attemptStartedAt - startedAt, {
55985
+ attempt: i + 1,
55986
+ provider: candidate.target.provider,
55987
+ model: candidate.target.model,
55988
+ credential_id: candidate.credential.id
55989
+ });
55990
+ }
55778
55991
  try {
55779
55992
  log.attempts = i + 1;
55780
55993
  log.credentialId = candidate.credential.id;
@@ -55838,6 +56051,9 @@ async function dispatch(request2, deps, signal, requestId) {
55838
56051
  if (event.type === "blockDelta" && !committed) {
55839
56052
  committed = true;
55840
56053
  log.ttftMs = deps.now() - startedAt;
56054
+ if (deps.trace !== null && deps.trace !== undefined && deps.trace.activeAttempt !== null) {
56055
+ addSpan(deps.trace, "stream.commit", deps.trace.activeAttempt, log.ttftMs, log.ttftMs, { provider: candidate.target.provider, model: candidate.target.model });
56056
+ }
55841
56057
  logger2.debug("stream committed", {
55842
56058
  requestId,
55843
56059
  provider: candidate.target.provider,
@@ -55920,6 +56136,7 @@ async function dispatch(request2, deps, signal, requestId) {
55920
56136
  throw signal.reason;
55921
56137
  const classifiedError = deadlineAt !== null && dispatchSignal.aborted ? { code: "TIMEOUT" } : classify(error61);
55922
56138
  const { code: code2 } = classifiedError;
56139
+ attemptCode = code2;
55923
56140
  const message2 = describeError(error61, "attempt failed");
55924
56141
  lastError = rewrap(classifiedError, message2);
55925
56142
  if (error61 instanceof GatewayError)
@@ -55993,6 +56210,15 @@ async function dispatch(request2, deps, signal, requestId) {
55993
56210
  }
55994
56211
  }
55995
56212
  } finally {
56213
+ if (deps.trace !== null && deps.trace !== undefined && deps.trace.activeAttempt !== null) {
56214
+ const span = deps.trace.spans[deps.trace.activeAttempt];
56215
+ if (span !== undefined) {
56216
+ span.endMs = deps.now() - startedAt;
56217
+ if (attemptCode !== undefined)
56218
+ span.attrs = { ...span.attrs, code: attemptCode };
56219
+ }
56220
+ deps.trace.activeAttempt = null;
56221
+ }
55996
56222
  releaseSlot();
55997
56223
  }
55998
56224
  }
@@ -56064,48 +56290,6 @@ function waitForCancellation(promise2, signal) {
56064
56290
  });
56065
56291
  }
56066
56292
 
56067
- // apps/gateway/src/dispatch/loadRegistry.ts
56068
- var PREFIX6 = "load:";
56069
- var SLOT_TTL_MS = 300000;
56070
- function createLoadRegistry(coord = memoryCoord()) {
56071
- const local = new Map;
56072
- let remote = new Map;
56073
- return {
56074
- acquire(credentialId, model) {
56075
- const key = healthKey(credentialId, model);
56076
- local.set(key, (local.get(key) ?? 0) + 1);
56077
- coord.gauge.acquire(PREFIX6 + key, SLOT_TTL_MS);
56078
- let released = false;
56079
- return () => {
56080
- if (released)
56081
- return;
56082
- released = true;
56083
- const next = (local.get(key) ?? 0) - 1;
56084
- if (next > 0)
56085
- local.set(key, next);
56086
- else
56087
- local.delete(key);
56088
- coord.gauge.release(PREFIX6 + key);
56089
- };
56090
- },
56091
- counts() {
56092
- const out = new Map(local);
56093
- for (const [key, count] of remote) {
56094
- if (count > (out.get(key) ?? 0))
56095
- out.set(key, count);
56096
- }
56097
- return out;
56098
- },
56099
- async refresh() {
56100
- const held = await coord.gauge.snapshot(PREFIX6);
56101
- const sample = new Map;
56102
- for (const [key, count] of held)
56103
- sample.set(key.slice(PREFIX6.length), count);
56104
- remote = sample;
56105
- }
56106
- };
56107
- }
56108
-
56109
56293
  // apps/gateway/src/ingress/anthropicTools.ts
56110
56294
  var TOOL_FIELDS = {
56111
56295
  cache_control: cacheControlSchema.nullable(),
@@ -57330,6 +57514,7 @@ async function bodyCollectorFor(deps, key) {
57330
57514
  async function handle(deps, rateLimiter, surface, request2) {
57331
57515
  const requestId = deps.requestId();
57332
57516
  const startedAt = deps.now();
57517
+ const trace = deps.telemetry?.startRequest(requestId, startedAt, request2.headers.get("traceparent"), surface, deps.rand()) ?? null;
57333
57518
  let keyId = null;
57334
57519
  let requestedModel = "";
57335
57520
  let outcome = null;
@@ -57411,6 +57596,7 @@ async function handle(deps, rateLimiter, surface, request2) {
57411
57596
  const dispatched = await dispatch(chatRequest, {
57412
57597
  ...deps,
57413
57598
  ...captured === null ? {} : { http: captured.wrap(deps.http) },
57599
+ trace,
57414
57600
  async onRoute(target) {
57415
57601
  if (began) {
57416
57602
  await routeLog(deps.store, requestId, target, deps.logger, deps.broadcaster);
@@ -57448,7 +57634,7 @@ async function handle(deps, rateLimiter, surface, request2) {
57448
57634
  reportRejection(deps.logger, requestId, completed, gatewayError, surface);
57449
57635
  }
57450
57636
  logged = true;
57451
- await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit, deps.broadcaster);
57637
+ await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit, deps.broadcaster, deps.telemetry, trace);
57452
57638
  };
57453
57639
  if (chatRequest.stream) {
57454
57640
  const frames = surface === "anthropic" ? anthropicStream(dispatched.events, requestId) : surface === "responses" ? responsesStream(dispatched.events, render2()) : openaiStream(dispatched.events, requestId, Math.floor(deps.now() / 1000));
@@ -57489,7 +57675,7 @@ async function handle(deps, rateLimiter, surface, request2) {
57489
57675
  if (collector !== null && !logged)
57490
57676
  collector.client.response = rejection;
57491
57677
  if (!logged)
57492
- await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit, deps.broadcaster);
57678
+ await finishLog(deps.store, completed, keyId, deps.logger, writeBodies, debit, deps.emit, deps.broadcaster, deps.telemetry, trace);
57493
57679
  if (!cancelled)
57494
57680
  reportRejection(deps.logger, requestId, completed, gatewayError, surface);
57495
57681
  return jsonResponse(rejection, HTTP_STATUS[gatewayError.code], {
@@ -58457,7 +58643,7 @@ function createRing(limits) {
58457
58643
  }
58458
58644
 
58459
58645
  // apps/gateway/src/version.ts
58460
- var VERSION = "0.9.1";
58646
+ var VERSION = "0.9.2";
58461
58647
 
58462
58648
  // apps/gateway/src/app.ts
58463
58649
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
@@ -58495,10 +58681,22 @@ function createApp(deps) {
58495
58681
  const nodeId = deps.nodeId ?? crypto.randomUUID();
58496
58682
  const logger2 = deps.logger ?? noopLogger;
58497
58683
  const rand = deps.rand ?? Math.random;
58498
- const http = deps.http ?? nodeHttpClient({ logger: logger2, now });
58684
+ const http = deps.http ?? nodeHttpClient({
58685
+ logger: logger2,
58686
+ now,
58687
+ ...deps.telemetry === undefined ? {} : { onResponseHead: deps.telemetry.httpHead }
58688
+ });
58499
58689
  const adapters = deps.adapters ?? ADAPTERS;
58500
58690
  const requestId = deps.requestId ?? (() => `req_${crypto.randomUUID()}`);
58501
- const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter({ store: deps.store, now, logger: logger2, coord });
58691
+ const loadRegistry = deps.loadRegistry ?? createLoadRegistry(coord);
58692
+ const telemetry = deps.telemetry;
58693
+ const rateLimiter = deps.rateLimiter ?? new ApiKeyRateLimiter({
58694
+ store: deps.store,
58695
+ now,
58696
+ logger: logger2,
58697
+ coord,
58698
+ ...telemetry === undefined ? {} : { onRejected: telemetry.rateLimit }
58699
+ });
58502
58700
  const snapshots = createRoutingSnapshotCache(deps.store, logger2);
58503
58701
  const ROUTING_TOPIC = "routing";
58504
58702
  deps.store.routing.subscribe((change) => {
@@ -58563,7 +58761,17 @@ function createApp(deps) {
58563
58761
  ok: true,
58564
58762
  mode: deps.mode ?? "single",
58565
58763
  nodeId,
58566
- coord: "healthy" in coord && !coord.healthy() ? "fallback" : "ok"
58764
+ coord: deps.coordHealthy?.() === false ? "fallback" : "ok"
58765
+ })).use(deps.metricsToken === undefined || telemetry === undefined ? new Elysia : new Elysia().get("/metrics", ({ request: request2 }) => {
58766
+ const supplied = request2.headers.get("authorization");
58767
+ const expected = `Bearer ${deps.metricsToken}`;
58768
+ const actualBytes = Buffer.from(supplied ?? "");
58769
+ const expectedBytes = Buffer.from(expected);
58770
+ if (actualBytes.length !== expectedBytes.length || !timingSafeEqual(actualBytes, expectedBytes)) {
58771
+ return new Response(null, { status: 401 });
58772
+ }
58773
+ const coordFallback = deps.coordHealthy?.() === false;
58774
+ return new Response(telemetry.scrape(loadRegistry.localCounts?.() ?? new Map, registry2.stats(), coordFallback), { headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" } });
58567
58775
  })).use(proxyRoutes({
58568
58776
  store: deps.store,
58569
58777
  snapshots,
@@ -58576,9 +58784,10 @@ function createApp(deps) {
58576
58784
  rateLimiter,
58577
58785
  logger: logger2,
58578
58786
  coord,
58579
- ...deps.loadRegistry === undefined ? {} : { loadRegistry: deps.loadRegistry },
58787
+ loadRegistry,
58580
58788
  bodyLoggingAllowed: deps.bodyLoggingAllowed === true,
58581
58789
  ...deps.emit === undefined ? {} : { emit: deps.emit },
58790
+ ...telemetry === undefined ? {} : { telemetry },
58582
58791
  broadcaster
58583
58792
  })).use(adminRoutes({
58584
58793
  store: deps.store,
@@ -59847,6 +60056,340 @@ function startJournalPoll(deps, source, logger2) {
59847
60056
  };
59848
60057
  }
59849
60058
 
60059
+ // apps/gateway/src/telemetry/otlp.ts
60060
+ function createOtlpExporter(opts) {
60061
+ const queue = [];
60062
+ const send = opts.fetch ?? fetch;
60063
+ const schedule = opts.schedule ?? ((run, ms) => {
60064
+ const timer = setInterval(run, ms);
60065
+ timer.unref?.();
60066
+ return () => clearInterval(timer);
60067
+ });
60068
+ let reporting = false;
60069
+ const exporter = {
60070
+ enqueue(trace) {
60071
+ try {
60072
+ const encoded = JSON.stringify(encodeTrace(trace, opts.version ?? "unknown"));
60073
+ if (queue.length >= opts.capacity) {
60074
+ opts.registry.add("omni_otlp_spans_dropped_total", { reason: "queue_full" });
60075
+ return;
60076
+ }
60077
+ queue.push(encoded);
60078
+ } catch {
60079
+ opts.registry.add("omni_otlp_spans_dropped_total", { reason: "encode" });
60080
+ }
60081
+ },
60082
+ async flush() {
60083
+ if (queue.length === 0)
60084
+ return;
60085
+ const encoded = queue.splice(0, opts.batchMax);
60086
+ try {
60087
+ const bodies = encoded.map((body2) => JSON.parse(body2));
60088
+ const resourceSpans = bodies.flatMap((body2) => body2.resourceSpans);
60089
+ const response = await send(`${opts.endpoint.replace(/\/+$/, "")}/v1/traces`, {
60090
+ method: "POST",
60091
+ headers: { "content-type": "application/json", ...opts.headers },
60092
+ body: JSON.stringify({ resourceSpans })
60093
+ });
60094
+ if (!response.ok)
60095
+ throw new Error(`collector returned ${response.status}`);
60096
+ reporting = false;
60097
+ } catch {
60098
+ if (!reporting)
60099
+ opts.logger?.warn("trace export failed", { reason: "collector unavailable" });
60100
+ reporting = true;
60101
+ }
60102
+ },
60103
+ queued: () => queue.length,
60104
+ stop() {
60105
+ cancel();
60106
+ }
60107
+ };
60108
+ const cancel = schedule(() => void exporter.flush(), opts.intervalMs);
60109
+ return exporter;
60110
+ }
60111
+
60112
+ // apps/gateway/src/telemetry/registry.ts
60113
+ var DEFINITIONS = {
60114
+ omni_requests_total: { help: "Requests completed by this process.", type: "counter" },
60115
+ omni_request_duration_seconds: { help: "Request duration in seconds.", type: "histogram" },
60116
+ omni_ttft_seconds: { help: "Streaming time to first token in seconds.", type: "histogram" },
60117
+ omni_tokens_total: { help: "Billable tokens completed by this process.", type: "counter" },
60118
+ omni_cost_usd_total: { help: "Request cost in US dollars.", type: "counter" },
60119
+ omni_upstream_duration_seconds: {
60120
+ help: "Provider response-head duration in seconds.",
60121
+ type: "histogram"
60122
+ },
60123
+ omni_inflight: { help: "Provider requests in flight in this process.", type: "gauge" },
60124
+ omni_breaker_open: { help: "Credential breakers observed open in this process.", type: "gauge" },
60125
+ omni_ratelimit_rejected_total: { help: "API-key rate-limit refusals.", type: "counter" },
60126
+ omni_stream_connections: { help: "Live stream connections in this process.", type: "gauge" },
60127
+ omni_stream_queued: { help: "Stream frames queued in this process.", type: "gauge" },
60128
+ omni_stream_dropped_total: { help: "Stream frames dropped in this process.", type: "counter" },
60129
+ omni_coord_fallback: {
60130
+ help: "Whether this process is using coordination fallback.",
60131
+ type: "gauge"
60132
+ },
60133
+ omni_metrics_series_folded_total: {
60134
+ help: "Metric series folded into api_key_id other.",
60135
+ type: "counter"
60136
+ },
60137
+ omni_otlp_spans_dropped_total: { help: "Spans dropped before OTLP export.", type: "counter" },
60138
+ omni_build_info: { help: "Gateway build information.", type: "gauge" }
60139
+ };
60140
+ var HISTOGRAM_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60];
60141
+ function labelKey(labels) {
60142
+ return Object.entries(labels).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key.length}:${key}${value.length}:${value}`).join("");
60143
+ }
60144
+ function createMetricRegistry(opts) {
60145
+ const series = new Map;
60146
+ const rawSeries = new Set;
60147
+ const folded = new Set;
60148
+ const normalize = (name, labels) => {
60149
+ const copy = { ...labels };
60150
+ const raw = `${name}:${labelKey(copy)}`;
60151
+ if (rawSeries.has(raw))
60152
+ return copy;
60153
+ if (rawSeries.size < opts.maxSeries || copy.api_key_id === undefined) {
60154
+ rawSeries.add(raw);
60155
+ return copy;
60156
+ }
60157
+ copy.api_key_id = "other";
60158
+ if (!folded.has(raw)) {
60159
+ folded.add(raw);
60160
+ change("omni_metrics_series_folded_total", {}, 1, false);
60161
+ }
60162
+ return copy;
60163
+ };
60164
+ const change = (name, labels, value, replace) => {
60165
+ const normalized = normalize(name, labels);
60166
+ const key = labelKey(normalized);
60167
+ const table = series.get(name) ?? new Map;
60168
+ const current = table.get(key) ?? { labels: normalized, value: 0 };
60169
+ current.value = replace ? value : current.value + value;
60170
+ table.set(key, current);
60171
+ series.set(name, table);
60172
+ return current;
60173
+ };
60174
+ return {
60175
+ add(name, labels, value = 1) {
60176
+ change(name, labels, value, false);
60177
+ },
60178
+ set(name, labels, value) {
60179
+ change(name, labels, value, true);
60180
+ },
60181
+ observe(name, value, labels) {
60182
+ const current = change(name, labels, value, false);
60183
+ current.count = (current.count ?? 0) + 1;
60184
+ const buckets = current.buckets ?? HISTOGRAM_BUCKETS.map(() => 0);
60185
+ for (let i = 0;i < HISTOGRAM_BUCKETS.length; i++) {
60186
+ if (value <= HISTOGRAM_BUCKETS[i])
60187
+ buckets[i] = (buckets[i] ?? 0) + 1;
60188
+ }
60189
+ current.buckets = buckets;
60190
+ },
60191
+ value(name, labels) {
60192
+ return series.get(name)?.get(labelKey(labels))?.value ?? 0;
60193
+ },
60194
+ snapshot() {
60195
+ return {
60196
+ at: opts.now(),
60197
+ metrics: Object.entries(DEFINITIONS).map(([name, definition]) => ({
60198
+ name,
60199
+ ...definition,
60200
+ samples: [...series.get(name)?.values() ?? []].map((sample) => definition.type === "histogram" ? {
60201
+ labels: sample.labels,
60202
+ value: sample.value,
60203
+ sum: sample.value,
60204
+ count: sample.count ?? 0,
60205
+ buckets: sample.buckets ?? HISTOGRAM_BUCKETS.map(() => 0)
60206
+ } : { labels: sample.labels, value: sample.value })
60207
+ })).filter((metric) => metric.samples.length > 0)
60208
+ };
60209
+ }
60210
+ };
60211
+ }
60212
+
60213
+ // apps/gateway/src/telemetry/render.ts
60214
+ function escapeLabel(value) {
60215
+ return value.replaceAll("\\", "\\\\").replaceAll(`
60216
+ `, "\\n").replaceAll('"', "\\\"");
60217
+ }
60218
+ function labels(labels2, extra) {
60219
+ const entries = Object.entries(labels2);
60220
+ if (extra !== undefined)
60221
+ entries.push([extra[0], extra[1]]);
60222
+ if (entries.length === 0)
60223
+ return "";
60224
+ return `{${entries.map(([key, value]) => `${key}="${escapeLabel(value)}"`).join(",")}}`;
60225
+ }
60226
+ function number4(value) {
60227
+ if (value === Number.POSITIVE_INFINITY)
60228
+ return "+Inf";
60229
+ if (value === Number.NEGATIVE_INFINITY)
60230
+ return "-Inf";
60231
+ if (Number.isNaN(value))
60232
+ return "NaN";
60233
+ return String(value);
60234
+ }
60235
+ function renderPrometheus(snapshot) {
60236
+ const out = [];
60237
+ for (const metric of snapshot.metrics) {
60238
+ out.push(`# HELP ${metric.name} ${metric.help}`, `# TYPE ${metric.name} ${metric.type}`);
60239
+ for (const sample of metric.samples) {
60240
+ if (metric.type !== "histogram" || !("buckets" in sample)) {
60241
+ out.push(`${metric.name}${labels(sample.labels)} ${number4(sample.value)}`);
60242
+ continue;
60243
+ }
60244
+ for (let i = 0;i < HISTOGRAM_BUCKETS.length; i++) {
60245
+ out.push(`${metric.name}_bucket${labels(sample.labels, ["le", String(HISTOGRAM_BUCKETS[i])])} ${number4(sample.buckets[i] ?? 0)}`);
60246
+ }
60247
+ out.push(`${metric.name}_bucket${labels(sample.labels, ["le", "+Inf"])} ${sample.count}`, `${metric.name}_sum${labels(sample.labels)} ${number4(sample.sum)}`, `${metric.name}_count${labels(sample.labels)} ${sample.count}`);
60248
+ }
60249
+ }
60250
+ return `${out.join(`
60251
+ `)}
60252
+ `;
60253
+ }
60254
+
60255
+ // apps/gateway/src/telemetry/index.ts
60256
+ function parseOtlpHeaders(value) {
60257
+ const headers = {};
60258
+ if (value === null)
60259
+ return headers;
60260
+ for (const entry of value.split(",")) {
60261
+ const at = entry.indexOf("=");
60262
+ if (at <= 0)
60263
+ continue;
60264
+ const key = entry.slice(0, at).trim();
60265
+ if (key.length > 0)
60266
+ headers[key] = entry.slice(at + 1).trim();
60267
+ }
60268
+ return headers;
60269
+ }
60270
+ function codeAttr(value) {
60271
+ return value === "interrupted" || /^[A-Z_]{1,32}$/.test(value) ? { code: value } : {};
60272
+ }
60273
+ function createTelemetry(opts) {
60274
+ const registry2 = createMetricRegistry({ maxSeries: opts.maxSeries, now: opts.now });
60275
+ const traces = new Map;
60276
+ const openBreakers = new Map;
60277
+ const exporter = opts.exporter ?? (opts.otlpEndpoint === null ? null : createOtlpExporter({
60278
+ endpoint: opts.otlpEndpoint,
60279
+ headers: opts.otlpHeaders,
60280
+ capacity: 1e4,
60281
+ batchMax: 512,
60282
+ intervalMs: 5000,
60283
+ registry: registry2,
60284
+ ...opts.logger === undefined ? {} : { logger: opts.logger },
60285
+ version: opts.version
60286
+ }));
60287
+ if (opts.metricsEnabled)
60288
+ registry2.set("omni_build_info", { version: opts.version }, 1);
60289
+ return {
60290
+ metricsEnabled: opts.metricsEnabled,
60291
+ tracingEnabled: exporter !== null,
60292
+ registry: registry2,
60293
+ startRequest(requestId, startedAt, traceparent, surface, rand) {
60294
+ if (exporter === null)
60295
+ return null;
60296
+ const inbound = parseTraceparent(traceparent);
60297
+ if (inbound !== null ? !inbound.sampled : rand >= opts.traceSample)
60298
+ return null;
60299
+ const trace = createTrace({ startedAt, traceparent: inbound });
60300
+ addSpan(trace, "gateway.request", null, 0, 0, { surface });
60301
+ traces.set(requestId, trace);
60302
+ return trace;
60303
+ },
60304
+ record(log, keyId, trace) {
60305
+ if (opts.metricsEnabled) {
60306
+ const provider = log.resolvedProvider ?? "";
60307
+ const model = log.resolvedModel ?? log.requestedModel;
60308
+ const key = keyId ?? "";
60309
+ registry2.add("omni_requests_total", {
60310
+ provider,
60311
+ model,
60312
+ status: String(log.status),
60313
+ code: log.errorCode ?? "",
60314
+ api_key_id: key
60315
+ });
60316
+ registry2.observe("omni_request_duration_seconds", log.durationMs / 1000, {
60317
+ provider,
60318
+ model
60319
+ });
60320
+ if (log.ttftMs !== null)
60321
+ registry2.observe("omni_ttft_seconds", log.ttftMs / 1000, { provider, model });
60322
+ for (const [kind, value] of [
60323
+ ["input", log.inputTokens],
60324
+ ["output", log.outputTokens],
60325
+ ["cache_read", log.cacheReadTokens],
60326
+ ["cache_write", log.cacheWriteTokens]
60327
+ ]) {
60328
+ registry2.add("omni_tokens_total", { provider, model, api_key_id: key, kind }, value);
60329
+ }
60330
+ registry2.add("omni_cost_usd_total", { provider, model, api_key_id: key }, log.costUsd);
60331
+ }
60332
+ if (trace !== null) {
60333
+ const root = trace.spans[0];
60334
+ if (root !== undefined) {
60335
+ root.endMs = log.durationMs;
60336
+ root.attrs = {
60337
+ ...root.attrs,
60338
+ requested_model: log.requestedModel,
60339
+ api_key_id: keyId ?? "",
60340
+ status: log.status,
60341
+ ...log.errorCode === null ? {} : codeAttr(log.errorCode)
60342
+ };
60343
+ }
60344
+ }
60345
+ },
60346
+ flush(requestId, trace) {
60347
+ traces.delete(requestId);
60348
+ if (trace !== null)
60349
+ exporter?.enqueue(trace);
60350
+ },
60351
+ httpHead(event) {
60352
+ if (opts.metricsEnabled)
60353
+ registry2.observe("omni_upstream_duration_seconds", event.durationMs / 1000, {
60354
+ provider: event.provider
60355
+ });
60356
+ const trace = event.requestId === undefined ? undefined : traces.get(event.requestId);
60357
+ if (trace === undefined)
60358
+ return;
60359
+ const endMs = Math.max(0, opts.now() - trace.startedAt);
60360
+ addSpan(trace, "provider.http", trace.activeAttempt ?? null, Math.max(0, endMs - event.durationMs), endMs, {
60361
+ provider: event.provider,
60362
+ host: event.host,
60363
+ path: event.path,
60364
+ ...event.status === undefined ? {} : { status: event.status }
60365
+ });
60366
+ },
60367
+ breaker(provider, before, after) {
60368
+ if (!opts.metricsEnabled || before === after)
60369
+ return;
60370
+ const count = Math.max(0, (openBreakers.get(provider) ?? 0) + (after === "open" ? 1 : before === "open" ? -1 : 0));
60371
+ openBreakers.set(provider, count);
60372
+ registry2.set("omni_breaker_open", { provider }, count);
60373
+ },
60374
+ rateLimit(dimension, window2) {
60375
+ if (opts.metricsEnabled)
60376
+ registry2.add("omni_ratelimit_rejected_total", { dimension, window: window2 ?? "none" });
60377
+ },
60378
+ scrape(localInflight, streams, coordFallback) {
60379
+ for (const [provider, value] of localInflight)
60380
+ registry2.set("omni_inflight", { provider }, value);
60381
+ registry2.set("omni_stream_connections", {}, streams.connections);
60382
+ registry2.set("omni_stream_queued", {}, streams.queued);
60383
+ registry2.set("omni_stream_dropped_total", {}, streams.dropped);
60384
+ registry2.set("omni_coord_fallback", {}, coordFallback ? 1 : 0);
60385
+ return renderPrometheus(registry2.snapshot());
60386
+ },
60387
+ stop() {
60388
+ exporter?.stop();
60389
+ }
60390
+ };
60391
+ }
60392
+
59850
60393
  // apps/gateway/src/index.ts
59851
60394
  function stdoutLogger(level) {
59852
60395
  return createLogger({
@@ -59915,7 +60458,17 @@ async function main() {
59915
60458
  logger2.info("retired interrupted requests", { count: swept });
59916
60459
  const console2 = consoleSource(config2.logFile);
59917
60460
  logger2.info("console log source resolved", { reason: console2.source.kind });
59918
- const http = nodeHttpClient({ logger: logger2, now });
60461
+ const telemetry = createTelemetry({
60462
+ metricsEnabled: config2.metricsToken !== null,
60463
+ maxSeries: config2.metricsMaxSeries,
60464
+ otlpEndpoint: config2.otlpEndpoint,
60465
+ otlpHeaders: parseOtlpHeaders(config2.otlpHeaders),
60466
+ traceSample: config2.traceSample,
60467
+ now,
60468
+ version: VERSION,
60469
+ logger: logger2
60470
+ });
60471
+ const http = nodeHttpClient({ logger: logger2, now, onResponseHead: telemetry.httpHead });
59919
60472
  const shared = config2.clusterMode && config2.redisUrl !== null ? redisCoord({ url: config2.redisUrl, logger: logger2, now }) : null;
59920
60473
  const coord = shared ?? memoryCoord({ now });
59921
60474
  const lease = { coord, nodeId };
@@ -59986,6 +60539,8 @@ async function main() {
59986
60539
  broadcaster,
59987
60540
  ring: streamRing,
59988
60541
  channels: pluginChannels,
60542
+ telemetry,
60543
+ ...config2.metricsToken === null ? {} : { metricsToken: config2.metricsToken },
59989
60544
  bodyLoggingAllowed: config2.bodyLoggingAllowed,
59990
60545
  plugins: loadedPlugins.plugins.map((plugin) => ({ id: plugin.id, routes: plugin.routes })),
59991
60546
  pluginUi: loadedPlugins.plugins,
@@ -60043,6 +60598,7 @@ async function main() {
60043
60598
  streamRegistry.closeAll(1001, "restart");
60044
60599
  streamRegistry.stop();
60045
60600
  broadcaster.stop();
60601
+ telemetry.stop();
60046
60602
  pluginChannels.stop();
60047
60603
  }
60048
60604
  ],