mtok-relay 0.2.2 → 0.2.3

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.
Files changed (2) hide show
  1. package/dist/mtok-relay.mjs +90 -12
  2. package/package.json +1 -1
@@ -2157,7 +2157,7 @@ var secp256k1 = createCurve({
2157
2157
  }, sha256);
2158
2158
 
2159
2159
  // node_modules/viem/_esm/errors/version.js
2160
- var version = "2.55.10";
2160
+ var version = "2.56.0";
2161
2161
 
2162
2162
  // node_modules/viem/_esm/errors/base.js
2163
2163
  var errorConfig = {
@@ -4432,6 +4432,15 @@ var InvalidStructTypeError = class extends BaseError {
4432
4432
  });
4433
4433
  }
4434
4434
  };
4435
+ var InvalidTypedDataTypeError = class extends BaseError {
4436
+ constructor({ type }) {
4437
+ const canonicalType = type.replace(/^(u?int)/, "$&256");
4438
+ super(`Type "${type}" is not a valid EIP-712 type.`, {
4439
+ metaMessages: [`Use "${canonicalType}" instead.`],
4440
+ name: "InvalidTypedDataTypeError"
4441
+ });
4442
+ }
4443
+ };
4435
4444
 
4436
4445
  // node_modules/viem/_esm/utils/typedData.js
4437
4446
  function validateTypedData(parameters) {
@@ -4440,6 +4449,9 @@ function validateTypedData(parameters) {
4440
4449
  for (const param of struct) {
4441
4450
  const { name, type } = param;
4442
4451
  const value = data[name];
4452
+ const baseType = type.replace(/(\[[0-9]*\])+$/, "");
4453
+ if (baseType === "int" || baseType === "uint")
4454
+ throw new InvalidTypedDataTypeError({ type });
4443
4455
  const integerMatch = type.match(integerRegex);
4444
4456
  if (integerMatch && (typeof value === "number" || typeof value === "bigint")) {
4445
4457
  const [_type, base, size_] = integerMatch;
@@ -4670,6 +4682,11 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
4670
4682
  const redemptionFile = redemptionFlag === void 0 ? "./.mtok-redemption.jsonl" : redemptionFlag;
4671
4683
  const denylistRaw = flag(argv, "--payer-denylist") ?? env.RELAY_PAYER_DENYLIST ?? "";
4672
4684
  const payerDenylist = String(denylistRaw).split(",").map((a) => a.trim().toLowerCase()).filter(Boolean);
4685
+ const maxOutputRaw = flag(argv, "--max-output-tokens") ?? env.RELAY_MAX_OUTPUT_TOKENS;
4686
+ const maxOutputTokens = maxOutputRaw != null ? Number(maxOutputRaw) : void 0;
4687
+ if (maxOutputTokens != null && (!Number.isFinite(maxOutputTokens) || maxOutputTokens < 1)) {
4688
+ throw new Error("--max-output-tokens must be a positive integer");
4689
+ }
4673
4690
  if (!offerId) throw new Error("--offer <id> is required");
4674
4691
  if (!model) throw new Error("--model <id> is required (the offer model you serve)");
4675
4692
  if (!upstream) throw new Error("--upstream <url> is required");
@@ -4708,7 +4725,8 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
4708
4725
  mtokApiKey,
4709
4726
  upstreamKey,
4710
4727
  settlementAddr,
4711
- payerDenylist
4728
+ payerDenylist,
4729
+ maxOutputTokens
4712
4730
  };
4713
4731
  }
4714
4732
 
@@ -4929,7 +4947,17 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
4929
4947
  if (map.has(key)) return false;
4930
4948
  if (!markClaimed(markerKey)) return false;
4931
4949
  const entry = { state: "pending", at: now() };
4932
- append(key, entry);
4950
+ try {
4951
+ append(key, entry);
4952
+ } catch (e) {
4953
+ if (durable) {
4954
+ try {
4955
+ fs.unlinkSync(markerFor(markerKey));
4956
+ } catch {
4957
+ }
4958
+ }
4959
+ throw e;
4960
+ }
4933
4961
  map.set(key, entry);
4934
4962
  return true;
4935
4963
  },
@@ -5300,8 +5328,21 @@ function validateRequest(request, model, { legacy = false } = {}) {
5300
5328
  }
5301
5329
  };
5302
5330
  }
5331
+ function normalizeModelId(m) {
5332
+ return String(m ?? "").toLowerCase().split("/").pop().replace(/^@/, "");
5333
+ }
5334
+ function modelsCompatible(upstreamModel, offerModel) {
5335
+ const a = normalizeModelId(upstreamModel);
5336
+ const b = normalizeModelId(offerModel);
5337
+ if (!a || !b) return false;
5338
+ if (a === b) return true;
5339
+ const [longer, shorter] = a.length >= b.length ? [a, b] : [b, a];
5340
+ if (!longer.startsWith(shorter)) return false;
5341
+ const rest = longer.slice(shorter.length);
5342
+ return /^([-._]\d+)+$/.test(rest);
5343
+ }
5303
5344
  function enforceModelEcho(upstreamModel, offerModel) {
5304
- if (String(upstreamModel) !== String(offerModel))
5345
+ if (!modelsCompatible(upstreamModel, offerModel))
5305
5346
  throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
5306
5347
  }
5307
5348
  function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
@@ -5322,7 +5363,8 @@ function estimateInputTokens(messages) {
5322
5363
  }
5323
5364
  return envelope + Math.ceil(bytes / BYTES_PER_TOKEN_EST);
5324
5365
  }
5325
- function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
5366
+ var DEFAULT_MAX_OUTPUT_TOKENS = 32768;
5367
+ function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = DEFAULT_MAX_OUTPUT_TOKENS }) {
5326
5368
  const estIn = estimateInputTokens(messages);
5327
5369
  const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
5328
5370
  if (estInCostUsd >= budgetUsd) return { refuse: true, reason: "input", estIn, estInCostUsd };
@@ -5350,9 +5392,17 @@ function createServeCore({
5350
5392
  dripContractAddress,
5351
5393
  feeRecipient,
5352
5394
  feeBps,
5353
- screenPayer
5395
+ screenPayer,
5396
+ // #654: the output-token sanity ceiling for boundServe. The PAID budget already
5397
+ // bounds output (and metering is on real usage), so this is a defensive cap on a
5398
+ // single generation, not a money guard. It is a per-relay knob: the reference host
5399
+ // passes MTOK_MAX_OUTPUT_TOKENS / a config value; operators serving large-context
5400
+ // models raise it. Falls back to boundServe's own generous default when unset.
5401
+ maxOutputTokens
5354
5402
  }) {
5355
5403
  const serve = async (body) => {
5404
+ const currentFeeBps = typeof feeBps === "function" ? feeBps() : feeBps;
5405
+ const currentFeeRecipient = typeof feeRecipient === "function" ? feeRecipient() : feeRecipient;
5356
5406
  const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
5357
5407
  const hasRequestNonce = Object.hasOwn(body, "requestNonce");
5358
5408
  if (!bookingId) return { status: 400, body: { error: "bad_request", detail: "DRAW needs bookingId" } };
@@ -5386,7 +5436,7 @@ function createServeCore({
5386
5436
  n,
5387
5437
  requestHash,
5388
5438
  sellerWallet,
5389
- feeRecipient,
5439
+ feeRecipient: currentFeeRecipient,
5390
5440
  // #580: refuse a payment older than the redemption window. The JSONL
5391
5441
  // payload cache AND the claim markers are both aged out at boot (#600),
5392
5442
  // so past retention this age bound is the sole replay defense (its
@@ -5400,8 +5450,8 @@ function createServeCore({
5400
5450
  if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
5401
5451
  const expectedFee = configuredFeeAtomic({
5402
5452
  sellerUsdAtomic: paid.event.sellerUsdAtomic,
5403
- feeAddress: feeRecipient,
5404
- feeBps
5453
+ feeAddress: currentFeeRecipient,
5454
+ feeBps: currentFeeBps
5405
5455
  });
5406
5456
  if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
5407
5457
  return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
@@ -5439,7 +5489,7 @@ function createServeCore({
5439
5489
  const eventOutPriceUsd = Number(paidEvent.outputPricePerMTokAtomic || 0) / 1e6;
5440
5490
  const boundInPrice = Math.max(Number(inPrice) || 0, eventInPriceUsd);
5441
5491
  const boundOutPrice = Math.max(Number(outPrice) || 0, eventOutPriceUsd);
5442
- const bound = boundServe({ messages: checked.safeRequest.messages, budgetUsd: remainingUsd, inPrice: boundInPrice, outPrice: boundOutPrice, reqMax: checked.safeRequest.max_tokens });
5492
+ const bound = boundServe({ messages: checked.safeRequest.messages, budgetUsd: remainingUsd, inPrice: boundInPrice, outPrice: boundOutPrice, reqMax: checked.safeRequest.max_tokens, ...Number(maxOutputTokens) > 0 ? { contextCeil: Math.floor(Number(maxOutputTokens)) } : {} });
5443
5493
  if (bound.refuse) {
5444
5494
  const error = bound.reason === "input" ? "input_too_large" : "output_unfunded";
5445
5495
  return { status: 402, body: { error, detail: `estimated input (~${bound.estIn} tokens, $${bound.estInCostUsd.toFixed(6)}) leaves no safely funded output in the paid amount ($${remainingUsd})`, _bookingId: bookingId, remainingUsd } };
@@ -5463,6 +5513,7 @@ function createServeCore({
5463
5513
  } catch (e) {
5464
5514
  return { status: 502, body: { error: "model_mismatch", detail: e.message } };
5465
5515
  }
5516
+ if (completion && typeof completion === "object") completion.model = model;
5466
5517
  const usage = completion.usage ?? {};
5467
5518
  const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
5468
5519
  const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
@@ -5520,6 +5571,24 @@ async function createRelayRuntime(config) {
5520
5571
  const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
5521
5572
  const drawLocks = /* @__PURE__ */ new Map();
5522
5573
  const platform = await fetchPlatformConfig(config);
5574
+ const bootFeeBps = platform.feeBps;
5575
+ const FEE_REFRESH_MS = 6e4;
5576
+ let lastConfigFetch = Date.now();
5577
+ const refreshPlatformFee = async () => {
5578
+ lastConfigFetch = Date.now();
5579
+ try {
5580
+ const fresh = await fetchPlatformConfig(config);
5581
+ platform.feeBps = fresh.feeBps;
5582
+ return true;
5583
+ } catch (e) {
5584
+ (config.log ?? console).error?.(`mtok relay: platform config refresh failed (${e.message}); keeping last-known fee rate`);
5585
+ return false;
5586
+ }
5587
+ };
5588
+ const refreshPlatformFeeIfStale = async () => {
5589
+ if (Date.now() - lastConfigFetch < FEE_REFRESH_MS) return;
5590
+ await refreshPlatformFee();
5591
+ };
5523
5592
  const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress, expectedChainId: platform.chainId });
5524
5593
  if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
5525
5594
  const payerDenylist = new Set((config.payerDenylist ?? []).map((a) => String(a).trim().toLowerCase()).filter(Boolean));
@@ -5543,8 +5612,13 @@ async function createRelayRuntime(config) {
5543
5612
  sellerAgentId: config.sellerAgentId,
5544
5613
  sellerWallet: config.settlementAddr,
5545
5614
  dripContractAddress: platform.dripContractAddress,
5615
+ // #654: the fee floor is min(boot, current) so neither a fee increase nor a
5616
+ // decrease can over-demand and strand an honest already-paid draw; the recipient
5617
+ // stays pinned to the boot address (exact-match verify, see the note above).
5546
5618
  feeRecipient: platform.feeAddress,
5547
- feeBps: platform.feeBps,
5619
+ feeBps: () => Math.min(bootFeeBps, platform.feeBps),
5620
+ // #654: per-relay output sanity ceiling (unset => the shared generous default).
5621
+ maxOutputTokens: config.maxOutputTokens,
5548
5622
  screenPayer: payerDenied
5549
5623
  });
5550
5624
  const withBookingLock = async (bookingId, fn) => {
@@ -5566,7 +5640,11 @@ async function createRelayRuntime(config) {
5566
5640
  }
5567
5641
  };
5568
5642
  const handleDraw = (body, res) => withBookingLock(String(body?.bookingId ?? ""), async () => {
5569
- const out = await core.serve(body);
5643
+ await refreshPlatformFeeIfStale();
5644
+ let out = await core.serve(body);
5645
+ if (out.status === 402 && out.body?.detail === "fee_amount_too_low" && await refreshPlatformFee()) {
5646
+ out = await core.serve(body);
5647
+ }
5570
5648
  return send(res, out.status, out.body);
5571
5649
  });
5572
5650
  return { handleDraw };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-relay",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Reference seller relay for mtok.market — accepts on-chain-prepaid chunk draws and serves inference from an upstream you control.",
5
5
  "type": "module",
6
6
  "bin": {