@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.js CHANGED
@@ -6,6 +6,7 @@ import { DEFAULT_BLOCKED_LABELS, EntityType, InternalSpans, SpanType, TracingEve
6
6
  import { fetchWithRetry, getNestedValue, setNestedValue } from "@mastra/core/utils";
7
7
  import { z } from "zod/v4";
8
8
  import fs from "fs";
9
+ import os from "os";
9
10
  import path from "path";
10
11
  import { TransformStream } from "stream/web";
11
12
  import { coreFeatures } from "@mastra/core/features";
@@ -1123,14 +1124,12 @@ var PricingModel = class {
1123
1124
  id;
1124
1125
  provider;
1125
1126
  model;
1126
- schema;
1127
1127
  currency;
1128
1128
  tiers;
1129
1129
  constructor(args) {
1130
1130
  this.id = args.id;
1131
1131
  this.provider = args.provider;
1132
1132
  this.model = args.model;
1133
- this.schema = args.schema;
1134
1133
  this.currency = args.currency;
1135
1134
  this.tiers = args.tiers;
1136
1135
  }
@@ -1143,8 +1142,145 @@ var PricingModel = class {
1143
1142
  }
1144
1143
  };
1145
1144
  //#endregion
1145
+ //#region src/metrics/models-dev.ts
1146
+ /** Same catalog the model router reads in `@mastra/core` (`gateways/models-dev.ts`). */
1147
+ const MODELS_DEV_API_URL = "https://models.dev/api.json";
1148
+ /**
1149
+ * A models.dev catalog with fewer prices than this has changed shape or is broken, so it must
1150
+ * not replace the pricing in use. The full catalog prices roughly 8,000 models.
1151
+ */
1152
+ const MIN_PRICED_MODELS = 1e3;
1153
+ /** models.dev `cost` keys (USD per 1M tokens) mapped to pricing meters. Keyed by core's `ModelPricingRates` so the two cannot drift. */
1154
+ const COST_KEY_TO_METER = {
1155
+ input: "input_tokens",
1156
+ output: "output_tokens",
1157
+ cache_read: "input_cache_read_tokens",
1158
+ cache_write: "input_cache_write_tokens",
1159
+ input_audio: "input_audio_tokens",
1160
+ output_audio: "output_audio_tokens",
1161
+ reasoning: "output_reasoning_tokens"
1162
+ };
1163
+ const PRICE_SIGNIFICANT_DIGITS = 12;
1164
+ /** Keep only each model's `cost` from a models.dev `api.json` payload, sorted by id so diffs stay small. */
1165
+ function pickModelsDevCosts(catalog) {
1166
+ const costs = {};
1167
+ for (const [providerId, providerInfo] of sortedEntries(catalog)) {
1168
+ if (!isRecord(providerInfo)) continue;
1169
+ for (const [modelId, modelInfo] of sortedEntries(providerInfo.models)) if (isRecord(modelInfo) && isRecord(modelInfo.cost)) (costs[providerId] ??= { models: {} }).models[modelId] = { cost: modelInfo.cost };
1170
+ }
1171
+ return costs;
1172
+ }
1173
+ /**
1174
+ * Build the pricing lookup from models.dev costs, keyed by `provider::model`.
1175
+ *
1176
+ * Each model is keyed by its own id (lowercased, dots and slashes flattened to dashes, matching
1177
+ * the lookup variants in `pricing-registry.ts`). Each model is also keyed by an alias with region,
1178
+ * vendor, version and date parts stripped, but only when no model owns that key itself, so the
1179
+ * dated `gpt-4o-2024-05-13` never replaces `gpt-4o`.
1180
+ */
1181
+ function modelsDevToPricingModels(costs) {
1182
+ const pricingModels = /* @__PURE__ */ new Map();
1183
+ const aliases = [];
1184
+ for (const [providerId, providerInfo] of Object.entries(isRecord(costs) ? costs : {})) {
1185
+ if (!isRecord(providerInfo) || !isRecord(providerInfo.models)) continue;
1186
+ const provider = normalizeProviderId(providerId);
1187
+ for (const [modelId, modelInfo] of Object.entries(providerInfo.models)) {
1188
+ const tiers = isRecord(modelInfo) ? toTiers(modelInfo.cost, modelId) : null;
1189
+ if (!tiers) continue;
1190
+ const model = modelId.toLowerCase().replace(/[./]/g, "-");
1191
+ const pricingModel = new PricingModel({
1192
+ id: `${providerId}/${modelId}`,
1193
+ provider,
1194
+ model,
1195
+ currency: "USD",
1196
+ tiers
1197
+ });
1198
+ pricingModels.set(`${provider}::${model}`, pricingModel);
1199
+ const alias = toAliasModelKey(modelId);
1200
+ if (alias && alias !== model) aliases.push({
1201
+ alias,
1202
+ modelId,
1203
+ pricingModel
1204
+ });
1205
+ }
1206
+ }
1207
+ aliases.sort((a, b) => a.modelId.length - b.modelId.length || b.modelId.localeCompare(a.modelId));
1208
+ for (const { alias, pricingModel } of aliases) {
1209
+ const key = `${pricingModel.provider}::${alias}`;
1210
+ if (!pricingModels.has(key)) {
1211
+ const { id, provider, currency, tiers } = pricingModel;
1212
+ pricingModels.set(key, new PricingModel({
1213
+ id,
1214
+ provider,
1215
+ model: alias,
1216
+ currency,
1217
+ tiers
1218
+ }));
1219
+ }
1220
+ }
1221
+ return pricingModels;
1222
+ }
1223
+ function toTiers(cost, modelId) {
1224
+ const isClaude = modelId.toLowerCase().includes("claude");
1225
+ const baseRates = toRates(cost, isClaude);
1226
+ if (!baseRates || !isRecord(cost)) return null;
1227
+ const contextTiers = (Array.isArray(cost.tiers) ? cost.tiers : []).flatMap((tier) => {
1228
+ const band = isRecord(tier) && isRecord(tier.tier) ? tier.tier : null;
1229
+ const rates = toRates(tier, isClaude);
1230
+ return band && (band.type ?? "context") === "context" && typeof band.size === "number" && band.size > 0 && rates ? [{
1231
+ size: band.size,
1232
+ rates
1233
+ }] : [];
1234
+ }).sort((a, b) => b.size - a.size);
1235
+ return [new PricingTier({
1236
+ index: 0,
1237
+ rates: baseRates
1238
+ }), ...contextTiers.map(({ size, rates }, i) => new PricingTier({
1239
+ index: i + 1,
1240
+ when: [{
1241
+ field: "total_input_tokens",
1242
+ op: "gte",
1243
+ value: size
1244
+ }],
1245
+ rates
1246
+ }))];
1247
+ }
1248
+ /** Zero prices are dropped (free endpoints and flat-fee plans), as the old platform pipeline did. */
1249
+ function toRates(cost, isClaude) {
1250
+ if (!isRecord(cost)) return null;
1251
+ const rates = {};
1252
+ for (const [costKey, meter] of Object.entries(COST_KEY_TO_METER)) {
1253
+ const pricePerMillion = cost[costKey];
1254
+ if (typeof pricePerMillion === "number" && Number.isFinite(pricePerMillion) && pricePerMillion > 0) rates[meter] = Number((pricePerMillion / 1e6).toPrecision(PRICE_SIGNIFICANT_DIGITS));
1255
+ }
1256
+ const cacheWriteRate = rates.input_cache_write_tokens;
1257
+ if (isClaude && cacheWriteRate !== void 0) {
1258
+ rates.input_cache_write_5m_tokens = cacheWriteRate;
1259
+ rates.input_cache_write_1h_tokens = cacheWriteRate * 1.6;
1260
+ }
1261
+ return rates.input_tokens && rates.output_tokens ? rates : null;
1262
+ }
1263
+ function normalizeProviderId(value) {
1264
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1265
+ }
1266
+ /** Port of the old platform pipeline's model-key normalization, used only for alias keys. */
1267
+ function toAliasModelKey(modelId) {
1268
+ 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();
1269
+ }
1270
+ function sortedEntries(value) {
1271
+ return Object.entries(isRecord(value) ? value : {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1272
+ }
1273
+ function isRecord(value) {
1274
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1275
+ }
1276
+ //#endregion
1146
1277
  //#region src/metrics/pricing-registry.ts
1147
- const DATA_FILE_NAME = "pricing-data.jsonl";
1278
+ const DATA_FILE_NAME = "pricing-data.json";
1279
+ const REFRESH_INTERVAL_MS = 1440 * 60 * 1e3;
1280
+ const MIN_REFRESH_ATTEMPT_INTERVAL_MS = 3600 * 1e3;
1281
+ const REFRESH_TIMEOUT_MS = 5e3;
1282
+ const CACHE_FILE = () => path.join(os.homedir(), ".cache", "mastra", "pricing-data.json");
1283
+ const CACHE_VERSION = 1;
1148
1284
  const BEDROCK_GEOGRAPHY_PREFIXES = /* @__PURE__ */ new Set([
1149
1285
  "global",
1150
1286
  "us",
@@ -1154,102 +1290,220 @@ const BEDROCK_GEOGRAPHY_PREFIXES = /* @__PURE__ */ new Set([
1154
1290
  "au"
1155
1291
  ]);
1156
1292
  const AI_SDK_VERCEL_GATEWAY_PROVIDER_ID = "gateway";
1293
+ const AZURE_OPENAI_GATEWAY_ID = "azure-openai";
1157
1294
  const VERCEL_PRICING_PROVIDER_ID = "vercel";
1158
1295
  const AI_SDK_PROVIDER_NAMESPACE_ALIASES = /* @__PURE__ */ new Map([
1296
+ ["fireworks", "fireworks-ai"],
1159
1297
  ["google.vertex", "google-vertex"],
1160
1298
  ["vertex.anthropic", "google-vertex-anthropic"],
1161
1299
  ["vertex.maas", "google-vertex"]
1162
1300
  ]);
1163
- const MINIFIED_METER_TO_CANONICAL = {
1164
- it: "input_tokens",
1165
- ot: "output_tokens",
1166
- icrt: "input_cache_read_tokens",
1167
- icwt: "input_cache_write_tokens",
1168
- icwt5m: "input_cache_write_5m_tokens",
1169
- icwt1h: "input_cache_write_1h_tokens",
1170
- iat: "input_audio_tokens",
1171
- oat: "output_audio_tokens",
1172
- ort: "output_reasoning_tokens"
1173
- };
1174
- const MINIFIED_CONDITION_FIELD_TO_CANONICAL = { tit: "total_input_tokens" };
1175
1301
  let cachedLoadError = null;
1302
+ let globalRegistry = null;
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;
1309
+ let refreshedAt = 0;
1310
+ let lastRefreshAttemptAt = 0;
1311
+ let refreshEtag;
1312
+ let refreshInFlight = false;
1176
1313
  var PricingRegistry = class PricingRegistry {
1177
1314
  pricingModels;
1178
- static globalRegistry = null;
1179
1315
  constructor(pricingModels) {
1180
1316
  this.pricingModels = pricingModels;
1181
1317
  }
1182
- static fromText(pricingModelText) {
1183
- return new PricingRegistry(parsePricingModelText(pricingModelText));
1318
+ /** A registry from models.dev costs, in the shape of `pricing-data.json` or `api.json`. */
1319
+ static fromModelsDev(costs) {
1320
+ return new PricingRegistry(modelsDevToPricingModels(costs));
1184
1321
  }
1322
+ /**
1323
+ * The registry used for cost estimates. It uses the bundled snapshot. When refresh is on (under
1324
+ * `mastra dev`, or with `MASTRA_AUTO_REFRESH_PRICING=true`), it starts from the models.dev copy
1325
+ * cached in `~/.cache/mastra` instead, and refreshes from models.dev in the background once a day
1326
+ * and when a model has no price. Lookups never wait for a refresh.
1327
+ */
1185
1328
  static getGlobal() {
1186
- if (PricingRegistry.globalRegistry) return PricingRegistry.globalRegistry;
1187
- const pricingModels = loadPricingModels();
1188
- if (!pricingModels) return null;
1189
- PricingRegistry.globalRegistry = new PricingRegistry(pricingModels);
1190
- return PricingRegistry.globalRegistry;
1329
+ if (!globalRegistry) {
1330
+ const cachedPricingModels = loadCachedPricingModels();
1331
+ globalRegistry = cachedPricingModels ? new PricingRegistry(cachedPricingModels) : getBundledRegistry();
1332
+ }
1333
+ if (Date.now() - refreshedAt >= REFRESH_INTERVAL_MS) refreshGlobalPricing();
1334
+ return globalRegistry;
1191
1335
  }
1192
1336
  get(args) {
1193
- for (const provider of getPricingProviderCandidates(args.provider, args.model)) {
1194
- const variants = getModelVariants(args.model, provider);
1195
- for (const variant of variants) {
1196
- const key = makePricingKey({
1197
- provider,
1198
- model: variant
1199
- });
1200
- const match = this.pricingModels.get(key);
1201
- if (match) return match;
1202
- }
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();
1341
+ refreshGlobalPricing();
1342
+ return null;
1343
+ }
1344
+ find(args) {
1345
+ for (const provider of getPricingProviderCandidates(args.provider, args.model)) for (const variant of getModelVariants(args.model, provider)) {
1346
+ const match = this.pricingModels.get(makePricingKey({
1347
+ provider,
1348
+ model: variant
1349
+ }));
1350
+ if (match) return match;
1203
1351
  }
1204
1352
  return null;
1205
1353
  }
1206
1354
  };
1355
+ function getBundledRegistry() {
1356
+ if (!bundledRegistry) {
1357
+ const pricingModels = loadPricingModels();
1358
+ if (pricingModels) bundledRegistry = new PricingRegistry(pricingModels);
1359
+ }
1360
+ return bundledRegistry;
1361
+ }
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
+ /**
1422
+ * Same switches as the model router's auto-refresh in `@mastra/core`: on by default only under
1423
+ * `mastra dev` (`MASTRA_DEV`), opt-in elsewhere, and always off with `MASTRA_OFFLINE`.
1424
+ */
1425
+ function isRefreshEnabled() {
1426
+ if (isOffline()) return false;
1427
+ const isDev = process.env.MASTRA_DEV === "true" || process.env.MASTRA_DEV === "1";
1428
+ const setting = process.env.MASTRA_AUTO_REFRESH_PRICING;
1429
+ return setting === "true" || setting !== "false" && isDev;
1430
+ }
1431
+ /**
1432
+ * Start a background refresh from models.dev, unless refresh is off, one is already
1433
+ * running, or the last attempt was less than an hour ago.
1434
+ */
1435
+ function refreshGlobalPricing() {
1436
+ if (refreshInFlight || !isRefreshEnabled() || Date.now() - lastRefreshAttemptAt < MIN_REFRESH_ATTEMPT_INTERVAL_MS) return;
1437
+ refreshInFlight = true;
1438
+ lastRefreshAttemptAt = Date.now();
1439
+ fetchModelsDevPricing().catch(() => {}).finally(() => {
1440
+ refreshInFlight = false;
1441
+ });
1442
+ }
1443
+ async function fetchModelsDevPricing() {
1444
+ const response = await fetch(MODELS_DEV_API_URL, {
1445
+ headers: refreshEtag ? { "if-none-match": refreshEtag } : void 0,
1446
+ signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS)
1447
+ });
1448
+ if (response.status === 304) {
1449
+ refreshedAt = Date.now();
1450
+ await touchCacheFile();
1451
+ return;
1452
+ }
1453
+ if (!response.ok) throw new Error(`models.dev responded with ${response.status}`);
1454
+ const costs = pickModelsDevCosts(await response.json());
1455
+ const pricingModels = modelsDevToPricingModels(costs);
1456
+ if (pricingModels.size < 1e3) throw new Error(`models.dev returned ${pricingModels.size} prices, expected at least ${MIN_PRICED_MODELS}`);
1457
+ globalRegistry = new PricingRegistry(pricingModels);
1458
+ refreshedAt = Date.now();
1459
+ refreshEtag = response.headers.get("etag") ?? void 0;
1460
+ await writeCacheFile(JSON.stringify({
1461
+ version: CACHE_VERSION,
1462
+ etag: refreshEtag,
1463
+ costs
1464
+ }));
1465
+ }
1466
+ /** The cache file's mtime records when its prices were last confirmed against models.dev. */
1467
+ function loadCachedPricingModels() {
1468
+ if (!isRefreshEnabled()) return null;
1469
+ try {
1470
+ const cacheFile = CACHE_FILE();
1471
+ const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
1472
+ if (cached.version !== CACHE_VERSION) return null;
1473
+ const pricingModels = modelsDevToPricingModels(cached.costs);
1474
+ if (pricingModels.size < 1e3) return null;
1475
+ refreshEtag = typeof cached.etag === "string" ? cached.etag : void 0;
1476
+ refreshedAt = fs.statSync(cacheFile).mtimeMs;
1477
+ return pricingModels;
1478
+ } catch {
1479
+ return null;
1480
+ }
1481
+ }
1482
+ /** Write-to-temp-then-rename, like the model router's cache writes, so readers never see a partial file. */
1483
+ async function writeCacheFile(content) {
1484
+ try {
1485
+ const cacheFile = CACHE_FILE();
1486
+ const tempFile = `${cacheFile}.${process.pid}.${Date.now()}.tmp`;
1487
+ await fs.promises.mkdir(path.dirname(cacheFile), { recursive: true });
1488
+ await fs.promises.writeFile(tempFile, content, "utf-8");
1489
+ await fs.promises.rename(tempFile, cacheFile);
1490
+ } catch {}
1491
+ }
1492
+ async function touchCacheFile() {
1493
+ try {
1494
+ const now = /* @__PURE__ */ new Date();
1495
+ await fs.promises.utimes(CACHE_FILE(), now, now);
1496
+ } catch {}
1497
+ }
1207
1498
  function loadPricingModels() {
1208
1499
  if (cachedLoadError) return null;
1209
1500
  try {
1210
- return parsePricingModelText(fs.readFileSync(resolvePricingModelPath(), "utf-8"));
1501
+ return modelsDevToPricingModels(JSON.parse(fs.readFileSync(resolvePricingModelPath(), "utf-8")));
1211
1502
  } catch (error) {
1212
1503
  cachedLoadError = error instanceof Error ? error.message : String(error);
1213
1504
  return null;
1214
1505
  }
1215
1506
  }
1216
- function parsePricingModelText(content) {
1217
- const pricingModels = /* @__PURE__ */ new Map();
1218
- for (const line of content.split("\n")) {
1219
- const trimmed = line.trim();
1220
- if (!trimmed) continue;
1221
- const pricingModel = expandPricingModelRow(JSON.parse(trimmed));
1222
- pricingModels.set(makePricingKey(pricingModel), pricingModel);
1223
- }
1224
- return pricingModels;
1225
- }
1226
- function expandRates(row, tier) {
1227
- const rates = Object.fromEntries(Object.entries(tier.r).map(([meter, value]) => [MINIFIED_METER_TO_CANONICAL[meter], value.c]));
1228
- const cacheWriteRate = rates.input_cache_write_tokens;
1229
- if (row.m.includes("claude") && typeof cacheWriteRate === "number") {
1230
- rates.input_cache_write_5m_tokens ??= cacheWriteRate;
1231
- rates.input_cache_write_1h_tokens ??= cacheWriteRate * 1.6;
1232
- }
1233
- return rates;
1234
- }
1235
- function expandPricingModelRow(row) {
1236
- return new PricingModel({
1237
- id: row.i,
1238
- provider: row.p,
1239
- model: row.m,
1240
- schema: row.s.v,
1241
- currency: row.s.d.u,
1242
- tiers: row.s.d.t.map((tier, index) => new PricingTier({
1243
- index,
1244
- when: tier.w?.map((condition) => ({
1245
- field: MINIFIED_CONDITION_FIELD_TO_CANONICAL[condition.f],
1246
- op: condition.op,
1247
- value: condition.value
1248
- })),
1249
- rates: expandRates(row, tier)
1250
- }))
1251
- });
1252
- }
1253
1507
  function resolvePricingModelPath() {
1254
1508
  const packageRoot = getPackageRoot();
1255
1509
  const candidates = [
@@ -1259,7 +1513,7 @@ function resolvePricingModelPath() {
1259
1513
  path.join(process.cwd(), "src", "metrics", DATA_FILE_NAME)
1260
1514
  ];
1261
1515
  for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate;
1262
- throw new Error(`Unable to locate pricing data JSONL at any known path: ${candidates.join(", ")}`);
1516
+ throw new Error(`Unable to locate pricing data at any known path: ${candidates.join(", ")}`);
1263
1517
  }
1264
1518
  function getPackageRoot() {
1265
1519
  try {
@@ -2880,6 +3134,17 @@ function isSpanInternal(spanType, flags) {
2880
3134
  }
2881
3135
  }
2882
3136
  /**
3137
+ * Copies the links whose IDs are valid W3C trace and span IDs (32 and 16 lowercase
3138
+ * hex characters, not all zeros); `undefined` when none remain.
3139
+ */
3140
+ function validLinks(links) {
3141
+ const valid = links?.filter((link) => /^(?!0+$)[0-9a-f]{32}$/.test(link?.traceId ?? "") && /^(?!0+$)[0-9a-f]{16}$/.test(link?.spanId ?? ""));
3142
+ return valid?.length ? valid.map(({ traceId, spanId }) => ({
3143
+ traceId,
3144
+ spanId
3145
+ })) : void 0;
3146
+ }
3147
+ /**
2883
3148
  * Get the external parent span ID from CreateSpanOptions.
2884
3149
  *
2885
3150
  * If the parent is internal or excluded from export (`excludeSpanTypes`),
@@ -2929,6 +3194,7 @@ var BaseSpan = class BaseSpan {
2929
3194
  metadata;
2930
3195
  requestContext;
2931
3196
  tags;
3197
+ links;
2932
3198
  traceState;
2933
3199
  /** Entity type that created the span (e.g., agent, workflow) */
2934
3200
  entityType;
@@ -2994,6 +3260,7 @@ var BaseSpan = class BaseSpan {
2994
3260
  this.#openChildrenOwner = parent;
2995
3261
  }
2996
3262
  this.tags = !options.parent && options.tags?.length ? options.tags : void 0;
3263
+ this.links = validLinks(options.links);
2997
3264
  const entityParent = this.getParentSpan(false);
2998
3265
  this.entityType = options.entityType ?? entityParent?.entityType;
2999
3266
  this.entityId = options.entityId ?? entityParent?.entityId;
@@ -3194,6 +3461,7 @@ var BaseSpan = class BaseSpan {
3194
3461
  const hideInput = this.traceState?.hideInput ?? false;
3195
3462
  const hideOutput = this.traceState?.hideOutput ?? false;
3196
3463
  const nestedUnderParent = this.traceState?.nestedUnderParent ?? false;
3464
+ const links = validLinks(this.links);
3197
3465
  return {
3198
3466
  id: this.id,
3199
3467
  traceId: this.traceId,
@@ -3216,6 +3484,7 @@ var BaseSpan = class BaseSpan {
3216
3484
  parentSpanId: this.getParentSpanId(includeInternalSpans),
3217
3485
  externalParentSpanId: this.getExportedExternalParentSpanId(includeInternalSpans),
3218
3486
  ...nestedUnderParent ? { nestedUnderParent: true } : {},
3487
+ ...links ? { links } : {},
3219
3488
  ...this.isRootSpan && !nestedUnderParent && this.tags?.length ? { tags: this.tags } : {}
3220
3489
  };
3221
3490
  }
@@ -3795,6 +4064,8 @@ function resolveTracingStorageStrategy$1(config, observabilityStorage, storageNa
3795
4064
  return observabilityStrategy.preferred;
3796
4065
  }
3797
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;
3798
4069
  /**
3799
4070
  * @deprecated Use `MastraStorageExporter` from `@mastra/observability` instead.
3800
4071
  * This class is preserved unchanged so existing integrations (including code
@@ -3814,6 +4085,8 @@ var DefaultExporter = class extends BaseExporter {
3814
4085
  #observabilityStorage;
3815
4086
  #resolvedStrategy;
3816
4087
  #flushTimer;
4088
+ /** Storage writes (timer, size trigger, or flush()) that haven't finished yet */
4089
+ #inFlightFlushes = /* @__PURE__ */ new Set();
3817
4090
  #emitDropEvent;
3818
4091
  #unsupportedSignals = /* @__PURE__ */ new Set();
3819
4092
  constructor(config = {}) {
@@ -3888,7 +4161,7 @@ var DefaultExporter = class extends BaseExporter {
3888
4161
  if (this.#flushTimer) clearTimeout(this.#flushTimer);
3889
4162
  this.#flushTimer = setTimeout(() => {
3890
4163
  this.#flushTimer = void 0;
3891
- this.flushBuffer().catch((error) => {
4164
+ this.trackFlushBuffer().catch((error) => {
3892
4165
  this.logger.error("Scheduled flush failed", { error: error instanceof Error ? error.message : String(error) });
3893
4166
  });
3894
4167
  }, delayMs);
@@ -3918,7 +4191,7 @@ var DefaultExporter = class extends BaseExporter {
3918
4191
  * Returns the flush promise when flushing so callers can await it.
3919
4192
  */
3920
4193
  async handleBatchedFlush() {
3921
- if (this.shouldFlush()) await this.flushBuffer();
4194
+ if (this.shouldFlush()) await this.trackFlushBuffer();
3922
4195
  else if (this.#eventBuffer.totalSize === 1) this.scheduleFlush();
3923
4196
  }
3924
4197
  sanitizeDropError(error) {
@@ -4022,6 +4295,15 @@ var DefaultExporter = class extends BaseExporter {
4022
4295
  }
4023
4296
  }
4024
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
+ /**
4025
4307
  * Flushes the current buffer to storage.
4026
4308
  *
4027
4309
  * Creates are flushed first, then their span keys are added to allCreatedSpans.
@@ -4180,10 +4462,16 @@ var DefaultExporter = class extends BaseExporter {
4180
4462
  * are exported before the runtime instance is terminated.
4181
4463
  */
4182
4464
  async flush() {
4183
- if (this.#eventBuffer.totalSize > 0) {
4184
- this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4185
- 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;
4186
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 });
4187
4475
  }
4188
4476
  async shutdown() {
4189
4477
  if (this.#flushTimer) {
@@ -4217,6 +4505,8 @@ function resolveTracingStorageStrategy(config, observabilityStorage, storageName
4217
4505
  return observabilityStrategy.preferred;
4218
4506
  }
4219
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;
4220
4510
  /**
4221
4511
  * Storage-backed exporter. Buffers observability events and flushes them in
4222
4512
  * batches to the configured ObservabilityStorage backend with retry support.
@@ -4231,6 +4521,8 @@ var MastraStorageExporter = class extends BaseExporter {
4231
4521
  #observabilityStorage;
4232
4522
  #resolvedStrategy;
4233
4523
  #flushTimer;
4524
+ /** Storage writes (timer, size trigger, or flush()) that haven't finished yet */
4525
+ #inFlightFlushes = /* @__PURE__ */ new Set();
4234
4526
  #emitDropEvent;
4235
4527
  #unsupportedSignals = /* @__PURE__ */ new Set();
4236
4528
  constructor(config = {}) {
@@ -4320,7 +4612,7 @@ var MastraStorageExporter = class extends BaseExporter {
4320
4612
  if (this.#flushTimer) clearTimeout(this.#flushTimer);
4321
4613
  this.#flushTimer = setTimeout(() => {
4322
4614
  this.#flushTimer = void 0;
4323
- this.flushBuffer().catch((error) => {
4615
+ this.trackFlushBuffer().catch((error) => {
4324
4616
  this.logger.error("Scheduled flush failed", { error: error instanceof Error ? error.message : String(error) });
4325
4617
  });
4326
4618
  }, delayMs);
@@ -4350,7 +4642,7 @@ var MastraStorageExporter = class extends BaseExporter {
4350
4642
  * Returns the flush promise when flushing so callers can await it.
4351
4643
  */
4352
4644
  async handleBatchedFlush() {
4353
- if (this.shouldFlush()) await this.flushBuffer();
4645
+ if (this.shouldFlush()) await this.trackFlushBuffer();
4354
4646
  else if (this.#eventBuffer.totalSize === 1) this.scheduleFlush();
4355
4647
  }
4356
4648
  sanitizeDropError(error) {
@@ -4454,6 +4746,15 @@ var MastraStorageExporter = class extends BaseExporter {
4454
4746
  }
4455
4747
  }
4456
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
+ /**
4457
4758
  * Flushes the current buffer to storage.
4458
4759
  *
4459
4760
  * Creates are flushed first, then their span keys are added to allCreatedSpans.
@@ -4601,10 +4902,16 @@ var MastraStorageExporter = class extends BaseExporter {
4601
4902
  * are exported before the runtime instance is terminated.
4602
4903
  */
4603
4904
  async flush() {
4604
- if (this.#eventBuffer.totalSize > 0) {
4605
- this.logger.debug("Flushing buffered events", { bufferedEvents: this.#eventBuffer.totalSize });
4606
- 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;
4607
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 });
4608
4915
  }
4609
4916
  async shutdown() {
4610
4917
  if (this.#flushTimer) {
@@ -4804,6 +5111,7 @@ var BaseObservabilityInstance = class extends MastraBase {
4804
5111
  spanId: cached.id,
4805
5112
  parentSpanId: cached.parentSpanId,
4806
5113
  externalParentSpanId: cached.externalParentSpanId,
5114
+ links: cached.links,
4807
5115
  startTime: cached.startTime instanceof Date ? cached.startTime : new Date(cached.startTime),
4808
5116
  input: cached.input,
4809
5117
  attributes: cached.attributes,
@@ -9725,6 +10033,7 @@ var Observability = class extends MastraBase {
9725
10033
  const instances = this.listInstances();
9726
10034
  const { mastra } = options;
9727
10035
  this.#mastra = mastra;
10036
+ setPricingGateways(() => Object.values(mastra.listGateways?.() ?? {}));
9728
10037
  const mastraEnvironment = mastra.getEnvironment?.();
9729
10038
  instances.forEach((instance) => {
9730
10039
  instance.__setMastraEnvironment?.(mastraEnvironment);