@mastra/observability 1.19.0 → 1.20.0-alpha.1

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.cjs CHANGED
@@ -30,6 +30,8 @@ let zod_v4 = require("zod/v4");
30
30
  let fs = require("fs");
31
31
  fs = __toESM(fs, 1);
32
32
  let module$1 = require("module");
33
+ let os = require("os");
34
+ os = __toESM(os, 1);
33
35
  let path = require("path");
34
36
  path = __toESM(path, 1);
35
37
  let stream_web = require("stream/web");
@@ -1148,14 +1150,12 @@ var PricingModel = class {
1148
1150
  id;
1149
1151
  provider;
1150
1152
  model;
1151
- schema;
1152
1153
  currency;
1153
1154
  tiers;
1154
1155
  constructor(args) {
1155
1156
  this.id = args.id;
1156
1157
  this.provider = args.provider;
1157
1158
  this.model = args.model;
1158
- this.schema = args.schema;
1159
1159
  this.currency = args.currency;
1160
1160
  this.tiers = args.tiers;
1161
1161
  }
@@ -1168,8 +1168,145 @@ var PricingModel = class {
1168
1168
  }
1169
1169
  };
1170
1170
  //#endregion
1171
+ //#region src/metrics/models-dev.ts
1172
+ /** Same catalog the model router reads in `@mastra/core` (`gateways/models-dev.ts`). */
1173
+ const MODELS_DEV_API_URL = "https://models.dev/api.json";
1174
+ /**
1175
+ * A models.dev catalog with fewer prices than this has changed shape or is broken, so it must
1176
+ * not replace the pricing in use. The full catalog prices roughly 8,000 models.
1177
+ */
1178
+ const MIN_PRICED_MODELS = 1e3;
1179
+ /** models.dev `cost` keys (USD per 1M tokens) mapped to pricing meters. Keyed by core's `ModelPricingRates` so the two cannot drift. */
1180
+ const COST_KEY_TO_METER = {
1181
+ input: "input_tokens",
1182
+ output: "output_tokens",
1183
+ cache_read: "input_cache_read_tokens",
1184
+ cache_write: "input_cache_write_tokens",
1185
+ input_audio: "input_audio_tokens",
1186
+ output_audio: "output_audio_tokens",
1187
+ reasoning: "output_reasoning_tokens"
1188
+ };
1189
+ const PRICE_SIGNIFICANT_DIGITS = 12;
1190
+ /** Keep only each model's `cost` from a models.dev `api.json` payload, sorted by id so diffs stay small. */
1191
+ function pickModelsDevCosts(catalog) {
1192
+ const costs = {};
1193
+ for (const [providerId, providerInfo] of sortedEntries(catalog)) {
1194
+ if (!isRecord(providerInfo)) continue;
1195
+ for (const [modelId, modelInfo] of sortedEntries(providerInfo.models)) if (isRecord(modelInfo) && isRecord(modelInfo.cost)) (costs[providerId] ??= { models: {} }).models[modelId] = { cost: modelInfo.cost };
1196
+ }
1197
+ return costs;
1198
+ }
1199
+ /**
1200
+ * Build the pricing lookup from models.dev costs, keyed by `provider::model`.
1201
+ *
1202
+ * Each model is keyed by its own id (lowercased, dots and slashes flattened to dashes, matching
1203
+ * the lookup variants in `pricing-registry.ts`). Each model is also keyed by an alias with region,
1204
+ * vendor, version and date parts stripped, but only when no model owns that key itself, so the
1205
+ * dated `gpt-4o-2024-05-13` never replaces `gpt-4o`.
1206
+ */
1207
+ function modelsDevToPricingModels(costs) {
1208
+ const pricingModels = /* @__PURE__ */ new Map();
1209
+ const aliases = [];
1210
+ for (const [providerId, providerInfo] of Object.entries(isRecord(costs) ? costs : {})) {
1211
+ if (!isRecord(providerInfo) || !isRecord(providerInfo.models)) continue;
1212
+ const provider = normalizeProviderId(providerId);
1213
+ for (const [modelId, modelInfo] of Object.entries(providerInfo.models)) {
1214
+ const tiers = isRecord(modelInfo) ? toTiers(modelInfo.cost, modelId) : null;
1215
+ if (!tiers) continue;
1216
+ const model = modelId.toLowerCase().replace(/[./]/g, "-");
1217
+ const pricingModel = new PricingModel({
1218
+ id: `${providerId}/${modelId}`,
1219
+ provider,
1220
+ model,
1221
+ currency: "USD",
1222
+ tiers
1223
+ });
1224
+ pricingModels.set(`${provider}::${model}`, pricingModel);
1225
+ const alias = toAliasModelKey(modelId);
1226
+ if (alias && alias !== model) aliases.push({
1227
+ alias,
1228
+ modelId,
1229
+ pricingModel
1230
+ });
1231
+ }
1232
+ }
1233
+ aliases.sort((a, b) => a.modelId.length - b.modelId.length || b.modelId.localeCompare(a.modelId));
1234
+ for (const { alias, pricingModel } of aliases) {
1235
+ const key = `${pricingModel.provider}::${alias}`;
1236
+ if (!pricingModels.has(key)) {
1237
+ const { id, provider, currency, tiers } = pricingModel;
1238
+ pricingModels.set(key, new PricingModel({
1239
+ id,
1240
+ provider,
1241
+ model: alias,
1242
+ currency,
1243
+ tiers
1244
+ }));
1245
+ }
1246
+ }
1247
+ return pricingModels;
1248
+ }
1249
+ function toTiers(cost, modelId) {
1250
+ const isClaude = modelId.toLowerCase().includes("claude");
1251
+ const baseRates = toRates(cost, isClaude);
1252
+ if (!baseRates || !isRecord(cost)) return null;
1253
+ const contextTiers = (Array.isArray(cost.tiers) ? cost.tiers : []).flatMap((tier) => {
1254
+ const band = isRecord(tier) && isRecord(tier.tier) ? tier.tier : null;
1255
+ const rates = toRates(tier, isClaude);
1256
+ return band && (band.type ?? "context") === "context" && typeof band.size === "number" && band.size > 0 && rates ? [{
1257
+ size: band.size,
1258
+ rates
1259
+ }] : [];
1260
+ }).sort((a, b) => b.size - a.size);
1261
+ return [new PricingTier({
1262
+ index: 0,
1263
+ rates: baseRates
1264
+ }), ...contextTiers.map(({ size, rates }, i) => new PricingTier({
1265
+ index: i + 1,
1266
+ when: [{
1267
+ field: "total_input_tokens",
1268
+ op: "gte",
1269
+ value: size
1270
+ }],
1271
+ rates
1272
+ }))];
1273
+ }
1274
+ /** Zero prices are dropped (free endpoints and flat-fee plans), as the old platform pipeline did. */
1275
+ function toRates(cost, isClaude) {
1276
+ if (!isRecord(cost)) return null;
1277
+ const rates = {};
1278
+ for (const [costKey, meter] of Object.entries(COST_KEY_TO_METER)) {
1279
+ const pricePerMillion = cost[costKey];
1280
+ if (typeof pricePerMillion === "number" && Number.isFinite(pricePerMillion) && pricePerMillion > 0) rates[meter] = Number((pricePerMillion / 1e6).toPrecision(PRICE_SIGNIFICANT_DIGITS));
1281
+ }
1282
+ const cacheWriteRate = rates.input_cache_write_tokens;
1283
+ if (isClaude && cacheWriteRate !== void 0) {
1284
+ rates.input_cache_write_5m_tokens = cacheWriteRate;
1285
+ rates.input_cache_write_1h_tokens = cacheWriteRate * 1.6;
1286
+ }
1287
+ return rates.input_tokens && rates.output_tokens ? rates : null;
1288
+ }
1289
+ function normalizeProviderId(value) {
1290
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1291
+ }
1292
+ /** Port of the old platform pipeline's model-key normalization, used only for alias keys. */
1293
+ function toAliasModelKey(modelId) {
1294
+ return modelId.replace(/^accounts\/[^/]+\/models\//i, "").replace(/^(global|us|eu|apac)\./i, "").replace(/^[a-z0-9-]+\.(openai|anthropic|google|xai|minimax)\./i, "").replace(/^(openai|anthropic|google|xai|x-ai|minimax|minimaxai)[/:._-]+/i, "").replace(/-v\d+(?::0)?$/i, "").replace(/@\d{8}$/, "").replace(/@default$/i, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").replace(/-20\d{6}$/, "").replace(/_20\d{6}$/, "").replace(/[/.:_\s]+/g, "-").replace(/[^a-zA-Z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
1295
+ }
1296
+ function sortedEntries(value) {
1297
+ return Object.entries(isRecord(value) ? value : {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1298
+ }
1299
+ function isRecord(value) {
1300
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1301
+ }
1302
+ //#endregion
1171
1303
  //#region src/metrics/pricing-registry.ts
1172
- const DATA_FILE_NAME = "pricing-data.jsonl";
1304
+ const DATA_FILE_NAME = "pricing-data.json";
1305
+ const REFRESH_INTERVAL_MS = 1440 * 60 * 1e3;
1306
+ const MIN_REFRESH_ATTEMPT_INTERVAL_MS = 3600 * 1e3;
1307
+ const REFRESH_TIMEOUT_MS = 5e3;
1308
+ const CACHE_FILE = () => path.default.join(os.default.homedir(), ".cache", "mastra", "pricing-data.json");
1309
+ const CACHE_VERSION = 1;
1173
1310
  const BEDROCK_GEOGRAPHY_PREFIXES = /* @__PURE__ */ new Set([
1174
1311
  "global",
1175
1312
  "us",
@@ -1179,102 +1316,220 @@ const BEDROCK_GEOGRAPHY_PREFIXES = /* @__PURE__ */ new Set([
1179
1316
  "au"
1180
1317
  ]);
1181
1318
  const AI_SDK_VERCEL_GATEWAY_PROVIDER_ID = "gateway";
1319
+ const AZURE_OPENAI_GATEWAY_ID = "azure-openai";
1182
1320
  const VERCEL_PRICING_PROVIDER_ID = "vercel";
1183
1321
  const AI_SDK_PROVIDER_NAMESPACE_ALIASES = /* @__PURE__ */ new Map([
1322
+ ["fireworks", "fireworks-ai"],
1184
1323
  ["google.vertex", "google-vertex"],
1185
1324
  ["vertex.anthropic", "google-vertex-anthropic"],
1186
1325
  ["vertex.maas", "google-vertex"]
1187
1326
  ]);
1188
- const MINIFIED_METER_TO_CANONICAL = {
1189
- it: "input_tokens",
1190
- ot: "output_tokens",
1191
- icrt: "input_cache_read_tokens",
1192
- icwt: "input_cache_write_tokens",
1193
- icwt5m: "input_cache_write_5m_tokens",
1194
- icwt1h: "input_cache_write_1h_tokens",
1195
- iat: "input_audio_tokens",
1196
- oat: "output_audio_tokens",
1197
- ort: "output_reasoning_tokens"
1198
- };
1199
- const MINIFIED_CONDITION_FIELD_TO_CANONICAL = { tit: "total_input_tokens" };
1200
1327
  let cachedLoadError = null;
1328
+ let globalRegistry = null;
1329
+ let bundledRegistry = null;
1330
+ let gatewayRegistry = null;
1331
+ let gatewayCosts = {};
1332
+ let gatewaySource = null;
1333
+ const gatewayLoadAttempts = /* @__PURE__ */ new Map();
1334
+ let gatewayLoadInFlight = false;
1335
+ let refreshedAt = 0;
1336
+ let lastRefreshAttemptAt = 0;
1337
+ let refreshEtag;
1338
+ let refreshInFlight = false;
1201
1339
  var PricingRegistry = class PricingRegistry {
1202
1340
  pricingModels;
1203
- static globalRegistry = null;
1204
1341
  constructor(pricingModels) {
1205
1342
  this.pricingModels = pricingModels;
1206
1343
  }
1207
- static fromText(pricingModelText) {
1208
- return new PricingRegistry(parsePricingModelText(pricingModelText));
1344
+ /** A registry from models.dev costs, in the shape of `pricing-data.json` or `api.json`. */
1345
+ static fromModelsDev(costs) {
1346
+ return new PricingRegistry(modelsDevToPricingModels(costs));
1209
1347
  }
1348
+ /**
1349
+ * The registry used for cost estimates. It uses the bundled snapshot. When refresh is on (under
1350
+ * `mastra dev`, or with `MASTRA_AUTO_REFRESH_PRICING=true`), it starts from the models.dev copy
1351
+ * cached in `~/.cache/mastra` instead, and refreshes from models.dev in the background once a day
1352
+ * and when a model has no price. Lookups never wait for a refresh.
1353
+ */
1210
1354
  static getGlobal() {
1211
- if (PricingRegistry.globalRegistry) return PricingRegistry.globalRegistry;
1212
- const pricingModels = loadPricingModels();
1213
- if (!pricingModels) return null;
1214
- PricingRegistry.globalRegistry = new PricingRegistry(pricingModels);
1215
- return PricingRegistry.globalRegistry;
1355
+ if (!globalRegistry) {
1356
+ const cachedPricingModels = loadCachedPricingModels();
1357
+ globalRegistry = cachedPricingModels ? new PricingRegistry(cachedPricingModels) : getBundledRegistry();
1358
+ }
1359
+ if (Date.now() - refreshedAt >= REFRESH_INTERVAL_MS) refreshGlobalPricing();
1360
+ return globalRegistry;
1216
1361
  }
1217
1362
  get(args) {
1218
- for (const provider of getPricingProviderCandidates(args.provider, args.model)) {
1219
- const variants = getModelVariants(args.model, provider);
1220
- for (const variant of variants) {
1221
- const key = makePricingKey({
1222
- provider,
1223
- model: variant
1224
- });
1225
- const match = this.pricingModels.get(key);
1226
- if (match) return match;
1227
- }
1363
+ if (this !== globalRegistry) return this.find(args);
1364
+ const match = this.find(args) ?? (this === bundledRegistry ? null : getBundledRegistry()?.find(args)) ?? gatewayRegistry?.find(args);
1365
+ if (match) return match;
1366
+ loadGatewayPricing();
1367
+ refreshGlobalPricing();
1368
+ return null;
1369
+ }
1370
+ find(args) {
1371
+ for (const provider of getPricingProviderCandidates(args.provider, args.model)) for (const variant of getModelVariants(args.model, provider)) {
1372
+ const match = this.pricingModels.get(makePricingKey({
1373
+ provider,
1374
+ model: variant
1375
+ }));
1376
+ if (match) return match;
1228
1377
  }
1229
1378
  return null;
1230
1379
  }
1231
1380
  };
1381
+ function getBundledRegistry() {
1382
+ if (!bundledRegistry) {
1383
+ const pricingModels = loadPricingModels();
1384
+ if (pricingModels) bundledRegistry = new PricingRegistry(pricingModels);
1385
+ }
1386
+ return bundledRegistry;
1387
+ }
1388
+ /**
1389
+ * Where to find the model router gateways whose `pricing` fills the gaps in models.dev. Prices
1390
+ * load on the first lookup that has no price, so production does not call gateways at startup;
1391
+ * under the same switches as the models.dev refresh (`mastra dev`, or
1392
+ * `MASTRA_AUTO_REFRESH_PRICING=true`) they also load right away, off the hot path, so the first
1393
+ * model call is priced. Each gateway is read once; a failed read is retried at most once an hour.
1394
+ */
1395
+ function setPricingGateways(source) {
1396
+ gatewaySource = source;
1397
+ if (isRefreshEnabled()) loadGatewayPricing();
1398
+ }
1399
+ /** Load `pricing` from every custom gateway not loaded yet. Off under `MASTRA_OFFLINE`. */
1400
+ async function loadGatewayPricing() {
1401
+ if (!gatewaySource || gatewayLoadInFlight || isOffline()) return;
1402
+ gatewayLoadInFlight = true;
1403
+ try {
1404
+ const builtInIds = await getBuiltInGatewayIds();
1405
+ let changed = false;
1406
+ for (const gateway of gatewaySource()) {
1407
+ const gatewayId = gateway.getId?.() ?? gateway.id;
1408
+ const lastAttempt = gatewayLoadAttempts.get(gateway) ?? 0;
1409
+ if (builtInIds.has(gatewayId) || Date.now() - lastAttempt < MIN_REFRESH_ATTEMPT_INTERVAL_MS) continue;
1410
+ try {
1411
+ const providers = await gateway.fetchProviders();
1412
+ for (const [providerId, config] of Object.entries(providers ?? {})) for (const [modelId, cost] of Object.entries(config?.pricing ?? {})) (gatewayCosts[providerId] ??= { models: {} }).models[modelId] = { cost };
1413
+ gatewayLoadAttempts.set(gateway, Infinity);
1414
+ changed = true;
1415
+ } catch (error) {
1416
+ gatewayLoadAttempts.set(gateway, Date.now());
1417
+ console.warn(`Failed to load model prices from gateway ${gatewayId}:`, error);
1418
+ }
1419
+ }
1420
+ if (changed) gatewayRegistry = new PricingRegistry(modelsDevToPricingModels(gatewayCosts));
1421
+ } finally {
1422
+ gatewayLoadInFlight = false;
1423
+ }
1424
+ }
1425
+ /**
1426
+ * The gateways core ships: the ones Mastra registers on its own, plus Azure OpenAI, which users
1427
+ * construct themselves. Their `fetchProviders()` can hit the network and never carries prices.
1428
+ * Read from core when it exports the list (1.46+), else the known ids.
1429
+ */
1430
+ async function getBuiltInGatewayIds() {
1431
+ try {
1432
+ const { defaultGateways, getGatewayId } = await import("@mastra/core/llm");
1433
+ return /* @__PURE__ */ new Set([...defaultGateways.map(getGatewayId), AZURE_OPENAI_GATEWAY_ID]);
1434
+ } catch {
1435
+ return /* @__PURE__ */ new Set([
1436
+ "models.dev",
1437
+ "netlify",
1438
+ "mastra",
1439
+ AZURE_OPENAI_GATEWAY_ID
1440
+ ]);
1441
+ }
1442
+ }
1443
+ function isOffline() {
1444
+ const offline = process.env.MASTRA_OFFLINE;
1445
+ return offline === "true" || offline === "1";
1446
+ }
1447
+ /**
1448
+ * Same switches as the model router's auto-refresh in `@mastra/core`: on by default only under
1449
+ * `mastra dev` (`MASTRA_DEV`), opt-in elsewhere, and always off with `MASTRA_OFFLINE`.
1450
+ */
1451
+ function isRefreshEnabled() {
1452
+ if (isOffline()) return false;
1453
+ const isDev = process.env.MASTRA_DEV === "true" || process.env.MASTRA_DEV === "1";
1454
+ const setting = process.env.MASTRA_AUTO_REFRESH_PRICING;
1455
+ return setting === "true" || setting !== "false" && isDev;
1456
+ }
1457
+ /**
1458
+ * Start a background refresh from models.dev, unless refresh is off, one is already
1459
+ * running, or the last attempt was less than an hour ago.
1460
+ */
1461
+ function refreshGlobalPricing() {
1462
+ if (refreshInFlight || !isRefreshEnabled() || Date.now() - lastRefreshAttemptAt < MIN_REFRESH_ATTEMPT_INTERVAL_MS) return;
1463
+ refreshInFlight = true;
1464
+ lastRefreshAttemptAt = Date.now();
1465
+ fetchModelsDevPricing().catch(() => {}).finally(() => {
1466
+ refreshInFlight = false;
1467
+ });
1468
+ }
1469
+ async function fetchModelsDevPricing() {
1470
+ const response = await fetch(MODELS_DEV_API_URL, {
1471
+ headers: refreshEtag ? { "if-none-match": refreshEtag } : void 0,
1472
+ signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS)
1473
+ });
1474
+ if (response.status === 304) {
1475
+ refreshedAt = Date.now();
1476
+ await touchCacheFile();
1477
+ return;
1478
+ }
1479
+ if (!response.ok) throw new Error(`models.dev responded with ${response.status}`);
1480
+ const costs = pickModelsDevCosts(await response.json());
1481
+ const pricingModels = modelsDevToPricingModels(costs);
1482
+ if (pricingModels.size < 1e3) throw new Error(`models.dev returned ${pricingModels.size} prices, expected at least ${MIN_PRICED_MODELS}`);
1483
+ globalRegistry = new PricingRegistry(pricingModels);
1484
+ refreshedAt = Date.now();
1485
+ refreshEtag = response.headers.get("etag") ?? void 0;
1486
+ await writeCacheFile(JSON.stringify({
1487
+ version: CACHE_VERSION,
1488
+ etag: refreshEtag,
1489
+ costs
1490
+ }));
1491
+ }
1492
+ /** The cache file's mtime records when its prices were last confirmed against models.dev. */
1493
+ function loadCachedPricingModels() {
1494
+ if (!isRefreshEnabled()) return null;
1495
+ try {
1496
+ const cacheFile = CACHE_FILE();
1497
+ const cached = JSON.parse(fs.default.readFileSync(cacheFile, "utf-8"));
1498
+ if (cached.version !== CACHE_VERSION) return null;
1499
+ const pricingModels = modelsDevToPricingModels(cached.costs);
1500
+ if (pricingModels.size < 1e3) return null;
1501
+ refreshEtag = typeof cached.etag === "string" ? cached.etag : void 0;
1502
+ refreshedAt = fs.default.statSync(cacheFile).mtimeMs;
1503
+ return pricingModels;
1504
+ } catch {
1505
+ return null;
1506
+ }
1507
+ }
1508
+ /** Write-to-temp-then-rename, like the model router's cache writes, so readers never see a partial file. */
1509
+ async function writeCacheFile(content) {
1510
+ try {
1511
+ const cacheFile = CACHE_FILE();
1512
+ const tempFile = `${cacheFile}.${process.pid}.${Date.now()}.tmp`;
1513
+ await fs.default.promises.mkdir(path.default.dirname(cacheFile), { recursive: true });
1514
+ await fs.default.promises.writeFile(tempFile, content, "utf-8");
1515
+ await fs.default.promises.rename(tempFile, cacheFile);
1516
+ } catch {}
1517
+ }
1518
+ async function touchCacheFile() {
1519
+ try {
1520
+ const now = /* @__PURE__ */ new Date();
1521
+ await fs.default.promises.utimes(CACHE_FILE(), now, now);
1522
+ } catch {}
1523
+ }
1232
1524
  function loadPricingModels() {
1233
1525
  if (cachedLoadError) return null;
1234
1526
  try {
1235
- return parsePricingModelText(fs.default.readFileSync(resolvePricingModelPath(), "utf-8"));
1527
+ return modelsDevToPricingModels(JSON.parse(fs.default.readFileSync(resolvePricingModelPath(), "utf-8")));
1236
1528
  } catch (error) {
1237
1529
  cachedLoadError = error instanceof Error ? error.message : String(error);
1238
1530
  return null;
1239
1531
  }
1240
1532
  }
1241
- function parsePricingModelText(content) {
1242
- const pricingModels = /* @__PURE__ */ new Map();
1243
- for (const line of content.split("\n")) {
1244
- const trimmed = line.trim();
1245
- if (!trimmed) continue;
1246
- const pricingModel = expandPricingModelRow(JSON.parse(trimmed));
1247
- pricingModels.set(makePricingKey(pricingModel), pricingModel);
1248
- }
1249
- return pricingModels;
1250
- }
1251
- function expandRates(row, tier) {
1252
- const rates = Object.fromEntries(Object.entries(tier.r).map(([meter, value]) => [MINIFIED_METER_TO_CANONICAL[meter], value.c]));
1253
- const cacheWriteRate = rates.input_cache_write_tokens;
1254
- if (row.m.includes("claude") && typeof cacheWriteRate === "number") {
1255
- rates.input_cache_write_5m_tokens ??= cacheWriteRate;
1256
- rates.input_cache_write_1h_tokens ??= cacheWriteRate * 1.6;
1257
- }
1258
- return rates;
1259
- }
1260
- function expandPricingModelRow(row) {
1261
- return new PricingModel({
1262
- id: row.i,
1263
- provider: row.p,
1264
- model: row.m,
1265
- schema: row.s.v,
1266
- currency: row.s.d.u,
1267
- tiers: row.s.d.t.map((tier, index) => new PricingTier({
1268
- index,
1269
- when: tier.w?.map((condition) => ({
1270
- field: MINIFIED_CONDITION_FIELD_TO_CANONICAL[condition.f],
1271
- op: condition.op,
1272
- value: condition.value
1273
- })),
1274
- rates: expandRates(row, tier)
1275
- }))
1276
- });
1277
- }
1278
1533
  function resolvePricingModelPath() {
1279
1534
  const packageRoot = getPackageRoot();
1280
1535
  const candidates = [
@@ -1284,7 +1539,7 @@ function resolvePricingModelPath() {
1284
1539
  path.default.join(process.cwd(), "src", "metrics", DATA_FILE_NAME)
1285
1540
  ];
1286
1541
  for (const candidate of candidates) if (fs.default.existsSync(candidate)) return candidate;
1287
- throw new Error(`Unable to locate pricing data JSONL at any known path: ${candidates.join(", ")}`);
1542
+ throw new Error(`Unable to locate pricing data at any known path: ${candidates.join(", ")}`);
1288
1543
  }
1289
1544
  function getPackageRoot() {
1290
1545
  try {
@@ -2905,6 +3160,17 @@ function isSpanInternal(spanType, flags) {
2905
3160
  }
2906
3161
  }
2907
3162
  /**
3163
+ * Copies the links whose IDs are valid W3C trace and span IDs (32 and 16 lowercase
3164
+ * hex characters, not all zeros); `undefined` when none remain.
3165
+ */
3166
+ function validLinks(links) {
3167
+ const valid = links?.filter((link) => /^(?!0+$)[0-9a-f]{32}$/.test(link?.traceId ?? "") && /^(?!0+$)[0-9a-f]{16}$/.test(link?.spanId ?? ""));
3168
+ return valid?.length ? valid.map(({ traceId, spanId }) => ({
3169
+ traceId,
3170
+ spanId
3171
+ })) : void 0;
3172
+ }
3173
+ /**
2908
3174
  * Get the external parent span ID from CreateSpanOptions.
2909
3175
  *
2910
3176
  * If the parent is internal or excluded from export (`excludeSpanTypes`),
@@ -2954,6 +3220,7 @@ var BaseSpan = class BaseSpan {
2954
3220
  metadata;
2955
3221
  requestContext;
2956
3222
  tags;
3223
+ links;
2957
3224
  traceState;
2958
3225
  /** Entity type that created the span (e.g., agent, workflow) */
2959
3226
  entityType;
@@ -3019,6 +3286,7 @@ var BaseSpan = class BaseSpan {
3019
3286
  this.#openChildrenOwner = parent;
3020
3287
  }
3021
3288
  this.tags = !options.parent && options.tags?.length ? options.tags : void 0;
3289
+ this.links = validLinks(options.links);
3022
3290
  const entityParent = this.getParentSpan(false);
3023
3291
  this.entityType = options.entityType ?? entityParent?.entityType;
3024
3292
  this.entityId = options.entityId ?? entityParent?.entityId;
@@ -3219,6 +3487,7 @@ var BaseSpan = class BaseSpan {
3219
3487
  const hideInput = this.traceState?.hideInput ?? false;
3220
3488
  const hideOutput = this.traceState?.hideOutput ?? false;
3221
3489
  const nestedUnderParent = this.traceState?.nestedUnderParent ?? false;
3490
+ const links = validLinks(this.links);
3222
3491
  return {
3223
3492
  id: this.id,
3224
3493
  traceId: this.traceId,
@@ -3241,6 +3510,7 @@ var BaseSpan = class BaseSpan {
3241
3510
  parentSpanId: this.getParentSpanId(includeInternalSpans),
3242
3511
  externalParentSpanId: this.getExportedExternalParentSpanId(includeInternalSpans),
3243
3512
  ...nestedUnderParent ? { nestedUnderParent: true } : {},
3513
+ ...links ? { links } : {},
3244
3514
  ...this.isRootSpan && !nestedUnderParent && this.tags?.length ? { tags: this.tags } : {}
3245
3515
  };
3246
3516
  }
@@ -3820,6 +4090,8 @@ function resolveTracingStorageStrategy$1(config, observabilityStorage, storageNa
3820
4090
  return observabilityStrategy.preferred;
3821
4091
  }
3822
4092
  const FLUSH_SUCCEEDED$1 = { failed: false };
4093
+ /** Max join/write rounds a single flush() makes before it returns under continuous traffic */
4094
+ const MAX_FLUSH_ROUNDS$1 = 3;
3823
4095
  /**
3824
4096
  * @deprecated Use `MastraStorageExporter` from `@mastra/observability` instead.
3825
4097
  * This class is preserved unchanged so existing integrations (including code
@@ -3839,6 +4111,8 @@ var DefaultExporter = class extends BaseExporter {
3839
4111
  #observabilityStorage;
3840
4112
  #resolvedStrategy;
3841
4113
  #flushTimer;
4114
+ /** Storage writes (timer, size trigger, or flush()) that haven't finished yet */
4115
+ #inFlightFlushes = /* @__PURE__ */ new Set();
3842
4116
  #emitDropEvent;
3843
4117
  #unsupportedSignals = /* @__PURE__ */ new Set();
3844
4118
  constructor(config = {}) {
@@ -3913,7 +4187,7 @@ var DefaultExporter = class extends BaseExporter {
3913
4187
  if (this.#flushTimer) clearTimeout(this.#flushTimer);
3914
4188
  this.#flushTimer = setTimeout(() => {
3915
4189
  this.#flushTimer = void 0;
3916
- this.flushBuffer().catch((error) => {
4190
+ this.trackFlushBuffer().catch((error) => {
3917
4191
  this.logger.error("Scheduled flush failed", { error: error instanceof Error ? error.message : String(error) });
3918
4192
  });
3919
4193
  }, delayMs);
@@ -3943,7 +4217,7 @@ var DefaultExporter = class extends BaseExporter {
3943
4217
  * Returns the flush promise when flushing so callers can await it.
3944
4218
  */
3945
4219
  async handleBatchedFlush() {
3946
- if (this.shouldFlush()) await this.flushBuffer();
4220
+ if (this.shouldFlush()) await this.trackFlushBuffer();
3947
4221
  else if (this.#eventBuffer.totalSize === 1) this.scheduleFlush();
3948
4222
  }
3949
4223
  sanitizeDropError(error) {
@@ -4047,6 +4321,15 @@ var DefaultExporter = class extends BaseExporter {
4047
4321
  }
4048
4322
  }
4049
4323
  /**
4324
+ * Runs flushBuffer() and tracks its promise so flush() can wait for writes
4325
+ * that a timer or size trigger started on their own.
4326
+ */
4327
+ trackFlushBuffer() {
4328
+ const promise = this.flushBuffer().finally(() => this.#inFlightFlushes.delete(promise));
4329
+ this.#inFlightFlushes.add(promise);
4330
+ return promise;
4331
+ }
4332
+ /**
4050
4333
  * Flushes the current buffer to storage.
4051
4334
  *
4052
4335
  * Creates are flushed first, then their span keys are added to allCreatedSpans.
@@ -4205,10 +4488,16 @@ var DefaultExporter = class extends BaseExporter {
4205
4488
  * are exported before the runtime instance is terminated.
4206
4489
  */
4207
4490
  async flush() {
4208
- if (this.#eventBuffer.totalSize > 0) {
4209
- this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4210
- await this.flushBuffer();
4491
+ for (let round = 0; round < MAX_FLUSH_ROUNDS$1; round++) {
4492
+ if (this.#inFlightFlushes.size > 0) await Promise.allSettled([...this.#inFlightFlushes]);
4493
+ if (this.#eventBuffer.totalSize > 0) {
4494
+ this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4495
+ await this.trackFlushBuffer();
4496
+ }
4497
+ if (this.#inFlightFlushes.size === 0) return;
4211
4498
  }
4499
+ await Promise.allSettled([...this.#inFlightFlushes]);
4500
+ this.logger.warn(`flush() stopped after ${MAX_FLUSH_ROUNDS$1} rounds; new writes kept starting`, { bufferedEventsLeftBehind: this.#eventBuffer.totalSize });
4212
4501
  }
4213
4502
  async shutdown() {
4214
4503
  if (this.#flushTimer) {
@@ -4242,6 +4531,8 @@ function resolveTracingStorageStrategy(config, observabilityStorage, storageName
4242
4531
  return observabilityStrategy.preferred;
4243
4532
  }
4244
4533
  const FLUSH_SUCCEEDED = { failed: false };
4534
+ /** Max join/write rounds a single flush() makes before it returns under continuous traffic */
4535
+ const MAX_FLUSH_ROUNDS = 3;
4245
4536
  /**
4246
4537
  * Storage-backed exporter. Buffers observability events and flushes them in
4247
4538
  * batches to the configured ObservabilityStorage backend with retry support.
@@ -4256,6 +4547,8 @@ var MastraStorageExporter = class extends BaseExporter {
4256
4547
  #observabilityStorage;
4257
4548
  #resolvedStrategy;
4258
4549
  #flushTimer;
4550
+ /** Storage writes (timer, size trigger, or flush()) that haven't finished yet */
4551
+ #inFlightFlushes = /* @__PURE__ */ new Set();
4259
4552
  #emitDropEvent;
4260
4553
  #unsupportedSignals = /* @__PURE__ */ new Set();
4261
4554
  constructor(config = {}) {
@@ -4345,7 +4638,7 @@ var MastraStorageExporter = class extends BaseExporter {
4345
4638
  if (this.#flushTimer) clearTimeout(this.#flushTimer);
4346
4639
  this.#flushTimer = setTimeout(() => {
4347
4640
  this.#flushTimer = void 0;
4348
- this.flushBuffer().catch((error) => {
4641
+ this.trackFlushBuffer().catch((error) => {
4349
4642
  this.logger.error("Scheduled flush failed", { error: error instanceof Error ? error.message : String(error) });
4350
4643
  });
4351
4644
  }, delayMs);
@@ -4375,7 +4668,7 @@ var MastraStorageExporter = class extends BaseExporter {
4375
4668
  * Returns the flush promise when flushing so callers can await it.
4376
4669
  */
4377
4670
  async handleBatchedFlush() {
4378
- if (this.shouldFlush()) await this.flushBuffer();
4671
+ if (this.shouldFlush()) await this.trackFlushBuffer();
4379
4672
  else if (this.#eventBuffer.totalSize === 1) this.scheduleFlush();
4380
4673
  }
4381
4674
  sanitizeDropError(error) {
@@ -4479,6 +4772,15 @@ var MastraStorageExporter = class extends BaseExporter {
4479
4772
  }
4480
4773
  }
4481
4774
  /**
4775
+ * Runs flushBuffer() and tracks its promise so flush() can wait for writes
4776
+ * that a timer or size trigger started on their own.
4777
+ */
4778
+ trackFlushBuffer() {
4779
+ const promise = this.flushBuffer().finally(() => this.#inFlightFlushes.delete(promise));
4780
+ this.#inFlightFlushes.add(promise);
4781
+ return promise;
4782
+ }
4783
+ /**
4482
4784
  * Flushes the current buffer to storage.
4483
4785
  *
4484
4786
  * Creates are flushed first, then their span keys are added to allCreatedSpans.
@@ -4626,10 +4928,16 @@ var MastraStorageExporter = class extends BaseExporter {
4626
4928
  * are exported before the runtime instance is terminated.
4627
4929
  */
4628
4930
  async flush() {
4629
- if (this.#eventBuffer.totalSize > 0) {
4630
- this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4631
- await this.flushBuffer();
4931
+ for (let round = 0; round < MAX_FLUSH_ROUNDS; round++) {
4932
+ if (this.#inFlightFlushes.size > 0) await Promise.allSettled([...this.#inFlightFlushes]);
4933
+ if (this.#eventBuffer.totalSize > 0) {
4934
+ this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4935
+ await this.trackFlushBuffer();
4936
+ }
4937
+ if (this.#inFlightFlushes.size === 0) return;
4632
4938
  }
4939
+ await Promise.allSettled([...this.#inFlightFlushes]);
4940
+ this.logger.warn(`flush() stopped after ${MAX_FLUSH_ROUNDS} rounds; new writes kept starting`, { bufferedEventsLeftBehind: this.#eventBuffer.totalSize });
4633
4941
  }
4634
4942
  async shutdown() {
4635
4943
  if (this.#flushTimer) {
@@ -4829,6 +5137,7 @@ var BaseObservabilityInstance = class extends _mastra_core_base.MastraBase {
4829
5137
  spanId: cached.id,
4830
5138
  parentSpanId: cached.parentSpanId,
4831
5139
  externalParentSpanId: cached.externalParentSpanId,
5140
+ links: cached.links,
4832
5141
  startTime: cached.startTime instanceof Date ? cached.startTime : new Date(cached.startTime),
4833
5142
  input: cached.input,
4834
5143
  attributes: cached.attributes,
@@ -9750,6 +10059,7 @@ var Observability = class extends _mastra_core_base.MastraBase {
9750
10059
  const instances = this.listInstances();
9751
10060
  const { mastra } = options;
9752
10061
  this.#mastra = mastra;
10062
+ setPricingGateways(() => Object.values(mastra.listGateways?.() ?? {}));
9753
10063
  const mastraEnvironment = mastra.getEnvironment?.();
9754
10064
  instances.forEach((instance) => {
9755
10065
  instance.__setMastraEnvironment?.(mastraEnvironment);