@mastra/observability 1.20.0-alpha.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1150,7 +1150,7 @@ const MODELS_DEV_API_URL = "https://models.dev/api.json";
1150
1150
  * not replace the pricing in use. The full catalog prices roughly 8,000 models.
1151
1151
  */
1152
1152
  const MIN_PRICED_MODELS = 1e3;
1153
- /** models.dev `cost` keys (USD per 1M tokens) mapped to pricing meters. */
1153
+ /** models.dev `cost` keys (USD per 1M tokens) mapped to pricing meters. Keyed by core's `ModelPricingRates` so the two cannot drift. */
1154
1154
  const COST_KEY_TO_METER = {
1155
1155
  input: "input_tokens",
1156
1156
  output: "output_tokens",
@@ -1290,6 +1290,7 @@ const BEDROCK_GEOGRAPHY_PREFIXES = /* @__PURE__ */ new Set([
1290
1290
  "au"
1291
1291
  ]);
1292
1292
  const AI_SDK_VERCEL_GATEWAY_PROVIDER_ID = "gateway";
1293
+ const AZURE_OPENAI_GATEWAY_ID = "azure-openai";
1293
1294
  const VERCEL_PRICING_PROVIDER_ID = "vercel";
1294
1295
  const AI_SDK_PROVIDER_NAMESPACE_ALIASES = /* @__PURE__ */ new Map([
1295
1296
  ["fireworks", "fireworks-ai"],
@@ -1300,6 +1301,11 @@ const AI_SDK_PROVIDER_NAMESPACE_ALIASES = /* @__PURE__ */ new Map([
1300
1301
  let cachedLoadError = null;
1301
1302
  let globalRegistry = null;
1302
1303
  let bundledRegistry = null;
1304
+ let gatewayRegistry = null;
1305
+ let gatewayCosts = {};
1306
+ let gatewaySource = null;
1307
+ const gatewayLoadAttempts = /* @__PURE__ */ new Map();
1308
+ let gatewayLoadInFlight = false;
1303
1309
  let refreshedAt = 0;
1304
1310
  let lastRefreshAttemptAt = 0;
1305
1311
  let refreshEtag;
@@ -1328,10 +1334,10 @@ var PricingRegistry = class PricingRegistry {
1328
1334
  return globalRegistry;
1329
1335
  }
1330
1336
  get(args) {
1331
- const match = this.find(args);
1332
- if (match || this !== globalRegistry) return match;
1333
- const bundledMatch = this === bundledRegistry ? null : getBundledRegistry()?.find(args);
1334
- if (bundledMatch) return bundledMatch;
1337
+ if (this !== globalRegistry) return this.find(args);
1338
+ const match = this.find(args) ?? (this === bundledRegistry ? null : getBundledRegistry()?.find(args)) ?? gatewayRegistry?.find(args);
1339
+ if (match) return match;
1340
+ loadGatewayPricing();
1335
1341
  refreshGlobalPricing();
1336
1342
  return null;
1337
1343
  }
@@ -1354,12 +1360,70 @@ function getBundledRegistry() {
1354
1360
  return bundledRegistry;
1355
1361
  }
1356
1362
  /**
1363
+ * Where to find the model router gateways whose `pricing` fills the gaps in models.dev. Prices
1364
+ * load on the first lookup that has no price, so production does not call gateways at startup;
1365
+ * under the same switches as the models.dev refresh (`mastra dev`, or
1366
+ * `MASTRA_AUTO_REFRESH_PRICING=true`) they also load right away, off the hot path, so the first
1367
+ * model call is priced. Each gateway is read once; a failed read is retried at most once an hour.
1368
+ */
1369
+ function setPricingGateways(source) {
1370
+ gatewaySource = source;
1371
+ if (isRefreshEnabled()) loadGatewayPricing();
1372
+ }
1373
+ /** Load `pricing` from every custom gateway not loaded yet. Off under `MASTRA_OFFLINE`. */
1374
+ async function loadGatewayPricing() {
1375
+ if (!gatewaySource || gatewayLoadInFlight || isOffline()) return;
1376
+ gatewayLoadInFlight = true;
1377
+ try {
1378
+ const builtInIds = await getBuiltInGatewayIds();
1379
+ let changed = false;
1380
+ for (const gateway of gatewaySource()) {
1381
+ const gatewayId = gateway.getId?.() ?? gateway.id;
1382
+ const lastAttempt = gatewayLoadAttempts.get(gateway) ?? 0;
1383
+ if (builtInIds.has(gatewayId) || Date.now() - lastAttempt < MIN_REFRESH_ATTEMPT_INTERVAL_MS) continue;
1384
+ try {
1385
+ const providers = await gateway.fetchProviders();
1386
+ for (const [providerId, config] of Object.entries(providers ?? {})) for (const [modelId, cost] of Object.entries(config?.pricing ?? {})) (gatewayCosts[providerId] ??= { models: {} }).models[modelId] = { cost };
1387
+ gatewayLoadAttempts.set(gateway, Infinity);
1388
+ changed = true;
1389
+ } catch (error) {
1390
+ gatewayLoadAttempts.set(gateway, Date.now());
1391
+ console.warn(`Failed to load model prices from gateway ${gatewayId}:`, error);
1392
+ }
1393
+ }
1394
+ if (changed) gatewayRegistry = new PricingRegistry(modelsDevToPricingModels(gatewayCosts));
1395
+ } finally {
1396
+ gatewayLoadInFlight = false;
1397
+ }
1398
+ }
1399
+ /**
1400
+ * The gateways core ships: the ones Mastra registers on its own, plus Azure OpenAI, which users
1401
+ * construct themselves. Their `fetchProviders()` can hit the network and never carries prices.
1402
+ * Read from core when it exports the list (1.46+), else the known ids.
1403
+ */
1404
+ async function getBuiltInGatewayIds() {
1405
+ try {
1406
+ const { defaultGateways, getGatewayId } = await import("@mastra/core/llm");
1407
+ return /* @__PURE__ */ new Set([...defaultGateways.map(getGatewayId), AZURE_OPENAI_GATEWAY_ID]);
1408
+ } catch {
1409
+ return /* @__PURE__ */ new Set([
1410
+ "models.dev",
1411
+ "netlify",
1412
+ "mastra",
1413
+ AZURE_OPENAI_GATEWAY_ID
1414
+ ]);
1415
+ }
1416
+ }
1417
+ function isOffline() {
1418
+ const offline = process.env.MASTRA_OFFLINE;
1419
+ return offline === "true" || offline === "1";
1420
+ }
1421
+ /**
1357
1422
  * Same switches as the model router's auto-refresh in `@mastra/core`: on by default only under
1358
1423
  * `mastra dev` (`MASTRA_DEV`), opt-in elsewhere, and always off with `MASTRA_OFFLINE`.
1359
1424
  */
1360
1425
  function isRefreshEnabled() {
1361
- const offline = process.env.MASTRA_OFFLINE;
1362
- if (offline === "true" || offline === "1") return false;
1426
+ if (isOffline()) return false;
1363
1427
  const isDev = process.env.MASTRA_DEV === "true" || process.env.MASTRA_DEV === "1";
1364
1428
  const setting = process.env.MASTRA_AUTO_REFRESH_PRICING;
1365
1429
  return setting === "true" || setting !== "false" && isDev;
@@ -4000,6 +4064,8 @@ function resolveTracingStorageStrategy$1(config, observabilityStorage, storageNa
4000
4064
  return observabilityStrategy.preferred;
4001
4065
  }
4002
4066
  const FLUSH_SUCCEEDED$1 = { failed: false };
4067
+ /** Max join/write rounds a single flush() makes before it returns under continuous traffic */
4068
+ const MAX_FLUSH_ROUNDS$1 = 3;
4003
4069
  /**
4004
4070
  * @deprecated Use `MastraStorageExporter` from `@mastra/observability` instead.
4005
4071
  * This class is preserved unchanged so existing integrations (including code
@@ -4019,6 +4085,8 @@ var DefaultExporter = class extends BaseExporter {
4019
4085
  #observabilityStorage;
4020
4086
  #resolvedStrategy;
4021
4087
  #flushTimer;
4088
+ /** Storage writes (timer, size trigger, or flush()) that haven't finished yet */
4089
+ #inFlightFlushes = /* @__PURE__ */ new Set();
4022
4090
  #emitDropEvent;
4023
4091
  #unsupportedSignals = /* @__PURE__ */ new Set();
4024
4092
  constructor(config = {}) {
@@ -4093,7 +4161,7 @@ var DefaultExporter = class extends BaseExporter {
4093
4161
  if (this.#flushTimer) clearTimeout(this.#flushTimer);
4094
4162
  this.#flushTimer = setTimeout(() => {
4095
4163
  this.#flushTimer = void 0;
4096
- this.flushBuffer().catch((error) => {
4164
+ this.trackFlushBuffer().catch((error) => {
4097
4165
  this.logger.error("Scheduled flush failed", { error: error instanceof Error ? error.message : String(error) });
4098
4166
  });
4099
4167
  }, delayMs);
@@ -4123,7 +4191,7 @@ var DefaultExporter = class extends BaseExporter {
4123
4191
  * Returns the flush promise when flushing so callers can await it.
4124
4192
  */
4125
4193
  async handleBatchedFlush() {
4126
- if (this.shouldFlush()) await this.flushBuffer();
4194
+ if (this.shouldFlush()) await this.trackFlushBuffer();
4127
4195
  else if (this.#eventBuffer.totalSize === 1) this.scheduleFlush();
4128
4196
  }
4129
4197
  sanitizeDropError(error) {
@@ -4227,6 +4295,15 @@ var DefaultExporter = class extends BaseExporter {
4227
4295
  }
4228
4296
  }
4229
4297
  /**
4298
+ * Runs flushBuffer() and tracks its promise so flush() can wait for writes
4299
+ * that a timer or size trigger started on their own.
4300
+ */
4301
+ trackFlushBuffer() {
4302
+ const promise = this.flushBuffer().finally(() => this.#inFlightFlushes.delete(promise));
4303
+ this.#inFlightFlushes.add(promise);
4304
+ return promise;
4305
+ }
4306
+ /**
4230
4307
  * Flushes the current buffer to storage.
4231
4308
  *
4232
4309
  * Creates are flushed first, then their span keys are added to allCreatedSpans.
@@ -4385,10 +4462,16 @@ var DefaultExporter = class extends BaseExporter {
4385
4462
  * are exported before the runtime instance is terminated.
4386
4463
  */
4387
4464
  async flush() {
4388
- if (this.#eventBuffer.totalSize > 0) {
4389
- this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4390
- await this.flushBuffer();
4465
+ for (let round = 0; round < MAX_FLUSH_ROUNDS$1; round++) {
4466
+ if (this.#inFlightFlushes.size > 0) await Promise.allSettled([...this.#inFlightFlushes]);
4467
+ if (this.#eventBuffer.totalSize > 0) {
4468
+ this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4469
+ await this.trackFlushBuffer();
4470
+ }
4471
+ if (this.#inFlightFlushes.size === 0) return;
4391
4472
  }
4473
+ await Promise.allSettled([...this.#inFlightFlushes]);
4474
+ this.logger.warn(`flush() stopped after ${MAX_FLUSH_ROUNDS$1} rounds; new writes kept starting`, { bufferedEventsLeftBehind: this.#eventBuffer.totalSize });
4392
4475
  }
4393
4476
  async shutdown() {
4394
4477
  if (this.#flushTimer) {
@@ -4422,6 +4505,8 @@ function resolveTracingStorageStrategy(config, observabilityStorage, storageName
4422
4505
  return observabilityStrategy.preferred;
4423
4506
  }
4424
4507
  const FLUSH_SUCCEEDED = { failed: false };
4508
+ /** Max join/write rounds a single flush() makes before it returns under continuous traffic */
4509
+ const MAX_FLUSH_ROUNDS = 3;
4425
4510
  /**
4426
4511
  * Storage-backed exporter. Buffers observability events and flushes them in
4427
4512
  * batches to the configured ObservabilityStorage backend with retry support.
@@ -4436,6 +4521,8 @@ var MastraStorageExporter = class extends BaseExporter {
4436
4521
  #observabilityStorage;
4437
4522
  #resolvedStrategy;
4438
4523
  #flushTimer;
4524
+ /** Storage writes (timer, size trigger, or flush()) that haven't finished yet */
4525
+ #inFlightFlushes = /* @__PURE__ */ new Set();
4439
4526
  #emitDropEvent;
4440
4527
  #unsupportedSignals = /* @__PURE__ */ new Set();
4441
4528
  constructor(config = {}) {
@@ -4525,7 +4612,7 @@ var MastraStorageExporter = class extends BaseExporter {
4525
4612
  if (this.#flushTimer) clearTimeout(this.#flushTimer);
4526
4613
  this.#flushTimer = setTimeout(() => {
4527
4614
  this.#flushTimer = void 0;
4528
- this.flushBuffer().catch((error) => {
4615
+ this.trackFlushBuffer().catch((error) => {
4529
4616
  this.logger.error("Scheduled flush failed", { error: error instanceof Error ? error.message : String(error) });
4530
4617
  });
4531
4618
  }, delayMs);
@@ -4555,7 +4642,7 @@ var MastraStorageExporter = class extends BaseExporter {
4555
4642
  * Returns the flush promise when flushing so callers can await it.
4556
4643
  */
4557
4644
  async handleBatchedFlush() {
4558
- if (this.shouldFlush()) await this.flushBuffer();
4645
+ if (this.shouldFlush()) await this.trackFlushBuffer();
4559
4646
  else if (this.#eventBuffer.totalSize === 1) this.scheduleFlush();
4560
4647
  }
4561
4648
  sanitizeDropError(error) {
@@ -4659,6 +4746,15 @@ var MastraStorageExporter = class extends BaseExporter {
4659
4746
  }
4660
4747
  }
4661
4748
  /**
4749
+ * Runs flushBuffer() and tracks its promise so flush() can wait for writes
4750
+ * that a timer or size trigger started on their own.
4751
+ */
4752
+ trackFlushBuffer() {
4753
+ const promise = this.flushBuffer().finally(() => this.#inFlightFlushes.delete(promise));
4754
+ this.#inFlightFlushes.add(promise);
4755
+ return promise;
4756
+ }
4757
+ /**
4662
4758
  * Flushes the current buffer to storage.
4663
4759
  *
4664
4760
  * Creates are flushed first, then their span keys are added to allCreatedSpans.
@@ -4806,10 +4902,16 @@ var MastraStorageExporter = class extends BaseExporter {
4806
4902
  * are exported before the runtime instance is terminated.
4807
4903
  */
4808
4904
  async flush() {
4809
- if (this.#eventBuffer.totalSize > 0) {
4810
- this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4811
- await this.flushBuffer();
4905
+ for (let round = 0; round < MAX_FLUSH_ROUNDS; round++) {
4906
+ if (this.#inFlightFlushes.size > 0) await Promise.allSettled([...this.#inFlightFlushes]);
4907
+ if (this.#eventBuffer.totalSize > 0) {
4908
+ this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4909
+ await this.trackFlushBuffer();
4910
+ }
4911
+ if (this.#inFlightFlushes.size === 0) return;
4812
4912
  }
4913
+ await Promise.allSettled([...this.#inFlightFlushes]);
4914
+ this.logger.warn(`flush() stopped after ${MAX_FLUSH_ROUNDS} rounds; new writes kept starting`, { bufferedEventsLeftBehind: this.#eventBuffer.totalSize });
4813
4915
  }
4814
4916
  async shutdown() {
4815
4917
  if (this.#flushTimer) {
@@ -9931,6 +10033,7 @@ var Observability = class extends MastraBase {
9931
10033
  const instances = this.listInstances();
9932
10034
  const { mastra } = options;
9933
10035
  this.#mastra = mastra;
10036
+ setPricingGateways(() => Object.values(mastra.listGateways?.() ?? {}));
9934
10037
  const mastraEnvironment = mastra.getEnvironment?.();
9935
10038
  instances.forEach((instance) => {
9936
10039
  instance.__setMastraEnvironment?.(mastraEnvironment);