@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/default.d.ts.map +1 -1
- package/dist/exporters/default.d.ts +5 -0
- package/dist/exporters/default.d.ts.map +1 -1
- package/dist/exporters/mastra-storage.d.ts +5 -0
- package/dist/exporters/mastra-storage.d.ts.map +1 -1
- package/dist/index.cjs +392 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +391 -82
- package/dist/index.js.map +1 -1
- package/dist/instances/base.d.ts.map +1 -1
- package/dist/metrics/models-dev.d.ts +26 -0
- package/dist/metrics/models-dev.d.ts.map +1 -0
- package/dist/metrics/pricing-data.json +69397 -0
- package/dist/metrics/pricing-model.d.ts +0 -2
- package/dist/metrics/pricing-model.d.ts.map +1 -1
- package/dist/metrics/pricing-registry.d.ts +24 -3
- package/dist/metrics/pricing-registry.d.ts.map +1 -1
- package/dist/spans/base.d.ts +2 -1
- package/dist/spans/base.d.ts.map +1 -1
- package/package.json +7 -5
- package/dist/metrics/pricing-data.jsonl +0 -9132
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.
|
|
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
|
-
|
|
1208
|
-
|
|
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 (
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4209
|
-
this.
|
|
4210
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4630
|
-
this.
|
|
4631
|
-
|
|
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);
|