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.
- package/dist/mtok-relay.mjs +90 -12
- package/package.json +1 -1
package/dist/mtok-relay.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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