@mastra/observability 1.19.0-alpha.4 → 1.20.0-alpha.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
@@ -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. */
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",
@@ -1156,100 +1292,154 @@ const BEDROCK_GEOGRAPHY_PREFIXES = /* @__PURE__ */ new Set([
1156
1292
  const AI_SDK_VERCEL_GATEWAY_PROVIDER_ID = "gateway";
1157
1293
  const VERCEL_PRICING_PROVIDER_ID = "vercel";
1158
1294
  const AI_SDK_PROVIDER_NAMESPACE_ALIASES = /* @__PURE__ */ new Map([
1295
+ ["fireworks", "fireworks-ai"],
1159
1296
  ["google.vertex", "google-vertex"],
1160
1297
  ["vertex.anthropic", "google-vertex-anthropic"],
1161
1298
  ["vertex.maas", "google-vertex"]
1162
1299
  ]);
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
1300
  let cachedLoadError = null;
1301
+ let globalRegistry = null;
1302
+ let bundledRegistry = null;
1303
+ let refreshedAt = 0;
1304
+ let lastRefreshAttemptAt = 0;
1305
+ let refreshEtag;
1306
+ let refreshInFlight = false;
1176
1307
  var PricingRegistry = class PricingRegistry {
1177
1308
  pricingModels;
1178
- static globalRegistry = null;
1179
1309
  constructor(pricingModels) {
1180
1310
  this.pricingModels = pricingModels;
1181
1311
  }
1182
- static fromText(pricingModelText) {
1183
- return new PricingRegistry(parsePricingModelText(pricingModelText));
1312
+ /** A registry from models.dev costs, in the shape of `pricing-data.json` or `api.json`. */
1313
+ static fromModelsDev(costs) {
1314
+ return new PricingRegistry(modelsDevToPricingModels(costs));
1184
1315
  }
1316
+ /**
1317
+ * The registry used for cost estimates. It uses the bundled snapshot. When refresh is on (under
1318
+ * `mastra dev`, or with `MASTRA_AUTO_REFRESH_PRICING=true`), it starts from the models.dev copy
1319
+ * cached in `~/.cache/mastra` instead, and refreshes from models.dev in the background once a day
1320
+ * and when a model has no price. Lookups never wait for a refresh.
1321
+ */
1185
1322
  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;
1323
+ if (!globalRegistry) {
1324
+ const cachedPricingModels = loadCachedPricingModels();
1325
+ globalRegistry = cachedPricingModels ? new PricingRegistry(cachedPricingModels) : getBundledRegistry();
1326
+ }
1327
+ if (Date.now() - refreshedAt >= REFRESH_INTERVAL_MS) refreshGlobalPricing();
1328
+ return globalRegistry;
1191
1329
  }
1192
1330
  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
- }
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;
1335
+ refreshGlobalPricing();
1336
+ return null;
1337
+ }
1338
+ find(args) {
1339
+ for (const provider of getPricingProviderCandidates(args.provider, args.model)) for (const variant of getModelVariants(args.model, provider)) {
1340
+ const match = this.pricingModels.get(makePricingKey({
1341
+ provider,
1342
+ model: variant
1343
+ }));
1344
+ if (match) return match;
1203
1345
  }
1204
1346
  return null;
1205
1347
  }
1206
1348
  };
1349
+ function getBundledRegistry() {
1350
+ if (!bundledRegistry) {
1351
+ const pricingModels = loadPricingModels();
1352
+ if (pricingModels) bundledRegistry = new PricingRegistry(pricingModels);
1353
+ }
1354
+ return bundledRegistry;
1355
+ }
1356
+ /**
1357
+ * Same switches as the model router's auto-refresh in `@mastra/core`: on by default only under
1358
+ * `mastra dev` (`MASTRA_DEV`), opt-in elsewhere, and always off with `MASTRA_OFFLINE`.
1359
+ */
1360
+ function isRefreshEnabled() {
1361
+ const offline = process.env.MASTRA_OFFLINE;
1362
+ if (offline === "true" || offline === "1") return false;
1363
+ const isDev = process.env.MASTRA_DEV === "true" || process.env.MASTRA_DEV === "1";
1364
+ const setting = process.env.MASTRA_AUTO_REFRESH_PRICING;
1365
+ return setting === "true" || setting !== "false" && isDev;
1366
+ }
1367
+ /**
1368
+ * Start a background refresh from models.dev, unless refresh is off, one is already
1369
+ * running, or the last attempt was less than an hour ago.
1370
+ */
1371
+ function refreshGlobalPricing() {
1372
+ if (refreshInFlight || !isRefreshEnabled() || Date.now() - lastRefreshAttemptAt < MIN_REFRESH_ATTEMPT_INTERVAL_MS) return;
1373
+ refreshInFlight = true;
1374
+ lastRefreshAttemptAt = Date.now();
1375
+ fetchModelsDevPricing().catch(() => {}).finally(() => {
1376
+ refreshInFlight = false;
1377
+ });
1378
+ }
1379
+ async function fetchModelsDevPricing() {
1380
+ const response = await fetch(MODELS_DEV_API_URL, {
1381
+ headers: refreshEtag ? { "if-none-match": refreshEtag } : void 0,
1382
+ signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS)
1383
+ });
1384
+ if (response.status === 304) {
1385
+ refreshedAt = Date.now();
1386
+ await touchCacheFile();
1387
+ return;
1388
+ }
1389
+ if (!response.ok) throw new Error(`models.dev responded with ${response.status}`);
1390
+ const costs = pickModelsDevCosts(await response.json());
1391
+ const pricingModels = modelsDevToPricingModels(costs);
1392
+ if (pricingModels.size < 1e3) throw new Error(`models.dev returned ${pricingModels.size} prices, expected at least ${MIN_PRICED_MODELS}`);
1393
+ globalRegistry = new PricingRegistry(pricingModels);
1394
+ refreshedAt = Date.now();
1395
+ refreshEtag = response.headers.get("etag") ?? void 0;
1396
+ await writeCacheFile(JSON.stringify({
1397
+ version: CACHE_VERSION,
1398
+ etag: refreshEtag,
1399
+ costs
1400
+ }));
1401
+ }
1402
+ /** The cache file's mtime records when its prices were last confirmed against models.dev. */
1403
+ function loadCachedPricingModels() {
1404
+ if (!isRefreshEnabled()) return null;
1405
+ try {
1406
+ const cacheFile = CACHE_FILE();
1407
+ const cached = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
1408
+ if (cached.version !== CACHE_VERSION) return null;
1409
+ const pricingModels = modelsDevToPricingModels(cached.costs);
1410
+ if (pricingModels.size < 1e3) return null;
1411
+ refreshEtag = typeof cached.etag === "string" ? cached.etag : void 0;
1412
+ refreshedAt = fs.statSync(cacheFile).mtimeMs;
1413
+ return pricingModels;
1414
+ } catch {
1415
+ return null;
1416
+ }
1417
+ }
1418
+ /** Write-to-temp-then-rename, like the model router's cache writes, so readers never see a partial file. */
1419
+ async function writeCacheFile(content) {
1420
+ try {
1421
+ const cacheFile = CACHE_FILE();
1422
+ const tempFile = `${cacheFile}.${process.pid}.${Date.now()}.tmp`;
1423
+ await fs.promises.mkdir(path.dirname(cacheFile), { recursive: true });
1424
+ await fs.promises.writeFile(tempFile, content, "utf-8");
1425
+ await fs.promises.rename(tempFile, cacheFile);
1426
+ } catch {}
1427
+ }
1428
+ async function touchCacheFile() {
1429
+ try {
1430
+ const now = /* @__PURE__ */ new Date();
1431
+ await fs.promises.utimes(CACHE_FILE(), now, now);
1432
+ } catch {}
1433
+ }
1207
1434
  function loadPricingModels() {
1208
1435
  if (cachedLoadError) return null;
1209
1436
  try {
1210
- return parsePricingModelText(fs.readFileSync(resolvePricingModelPath(), "utf-8"));
1437
+ return modelsDevToPricingModels(JSON.parse(fs.readFileSync(resolvePricingModelPath(), "utf-8")));
1211
1438
  } catch (error) {
1212
1439
  cachedLoadError = error instanceof Error ? error.message : String(error);
1213
1440
  return null;
1214
1441
  }
1215
1442
  }
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
1443
  function resolvePricingModelPath() {
1254
1444
  const packageRoot = getPackageRoot();
1255
1445
  const candidates = [
@@ -1259,7 +1449,7 @@ function resolvePricingModelPath() {
1259
1449
  path.join(process.cwd(), "src", "metrics", DATA_FILE_NAME)
1260
1450
  ];
1261
1451
  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(", ")}`);
1452
+ throw new Error(`Unable to locate pricing data at any known path: ${candidates.join(", ")}`);
1263
1453
  }
1264
1454
  function getPackageRoot() {
1265
1455
  try {
@@ -2880,6 +3070,17 @@ function isSpanInternal(spanType, flags) {
2880
3070
  }
2881
3071
  }
2882
3072
  /**
3073
+ * Copies the links whose IDs are valid W3C trace and span IDs (32 and 16 lowercase
3074
+ * hex characters, not all zeros); `undefined` when none remain.
3075
+ */
3076
+ function validLinks(links) {
3077
+ const valid = links?.filter((link) => /^(?!0+$)[0-9a-f]{32}$/.test(link?.traceId ?? "") && /^(?!0+$)[0-9a-f]{16}$/.test(link?.spanId ?? ""));
3078
+ return valid?.length ? valid.map(({ traceId, spanId }) => ({
3079
+ traceId,
3080
+ spanId
3081
+ })) : void 0;
3082
+ }
3083
+ /**
2883
3084
  * Get the external parent span ID from CreateSpanOptions.
2884
3085
  *
2885
3086
  * If the parent is internal or excluded from export (`excludeSpanTypes`),
@@ -2929,6 +3130,7 @@ var BaseSpan = class BaseSpan {
2929
3130
  metadata;
2930
3131
  requestContext;
2931
3132
  tags;
3133
+ links;
2932
3134
  traceState;
2933
3135
  /** Entity type that created the span (e.g., agent, workflow) */
2934
3136
  entityType;
@@ -2994,6 +3196,7 @@ var BaseSpan = class BaseSpan {
2994
3196
  this.#openChildrenOwner = parent;
2995
3197
  }
2996
3198
  this.tags = !options.parent && options.tags?.length ? options.tags : void 0;
3199
+ this.links = validLinks(options.links);
2997
3200
  const entityParent = this.getParentSpan(false);
2998
3201
  this.entityType = options.entityType ?? entityParent?.entityType;
2999
3202
  this.entityId = options.entityId ?? entityParent?.entityId;
@@ -3194,6 +3397,7 @@ var BaseSpan = class BaseSpan {
3194
3397
  const hideInput = this.traceState?.hideInput ?? false;
3195
3398
  const hideOutput = this.traceState?.hideOutput ?? false;
3196
3399
  const nestedUnderParent = this.traceState?.nestedUnderParent ?? false;
3400
+ const links = validLinks(this.links);
3197
3401
  return {
3198
3402
  id: this.id,
3199
3403
  traceId: this.traceId,
@@ -3216,6 +3420,7 @@ var BaseSpan = class BaseSpan {
3216
3420
  parentSpanId: this.getParentSpanId(includeInternalSpans),
3217
3421
  externalParentSpanId: this.getExportedExternalParentSpanId(includeInternalSpans),
3218
3422
  ...nestedUnderParent ? { nestedUnderParent: true } : {},
3423
+ ...links ? { links } : {},
3219
3424
  ...this.isRootSpan && !nestedUnderParent && this.tags?.length ? { tags: this.tags } : {}
3220
3425
  };
3221
3426
  }
@@ -4804,6 +5009,7 @@ var BaseObservabilityInstance = class extends MastraBase {
4804
5009
  spanId: cached.id,
4805
5010
  parentSpanId: cached.parentSpanId,
4806
5011
  externalParentSpanId: cached.externalParentSpanId,
5012
+ links: cached.links,
4807
5013
  startTime: cached.startTime instanceof Date ? cached.startTime : new Date(cached.startTime),
4808
5014
  input: cached.input,
4809
5015
  attributes: cached.attributes,