mtok-relay 0.1.9 → 0.2.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/README.md +1 -1
- package/dist/mtok-relay.mjs +282 -58
- package/package.json +2 -2
package/README.md
CHANGED
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.55.0";
|
|
2161
2161
|
|
|
2162
2162
|
// node_modules/viem/_esm/errors/base.js
|
|
2163
2163
|
var errorConfig = {
|
|
@@ -4646,15 +4646,22 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
|
|
|
4646
4646
|
const rpcFlag = flag(argv, "--rpc");
|
|
4647
4647
|
const settlementPubkeyFlag = flag(argv, "--settlement-pubkey");
|
|
4648
4648
|
const sellerAgentId = flag(argv, "--seller-agent");
|
|
4649
|
-
const outPrice = Number(flag(argv, "--out-price")
|
|
4649
|
+
const outPrice = Number(flag(argv, "--out-price"));
|
|
4650
4650
|
const inPrice = Number(flag(argv, "--in-price") ?? outPrice);
|
|
4651
4651
|
const redemptionFlag = flag(argv, "--redemption-file") ?? env.RELAY_REDEMPTION_FILE;
|
|
4652
|
-
|
|
4652
|
+
if (redemptionFlag === "") throw new Error("--redemption-file cannot be empty; paid serves require durable redemption");
|
|
4653
|
+
const redemptionFile = redemptionFlag === void 0 ? "./.mtok-redemption.jsonl" : redemptionFlag;
|
|
4653
4654
|
const denylistRaw = flag(argv, "--payer-denylist") ?? env.RELAY_PAYER_DENYLIST ?? "";
|
|
4654
4655
|
const payerDenylist = String(denylistRaw).split(",").map((a) => a.trim().toLowerCase()).filter(Boolean);
|
|
4655
4656
|
if (!offerId) throw new Error("--offer <id> is required");
|
|
4656
4657
|
if (!model) throw new Error("--model <id> is required (the offer model you serve)");
|
|
4657
4658
|
if (!upstream) throw new Error("--upstream <url> is required");
|
|
4659
|
+
if (!Number.isFinite(outPrice) || outPrice <= 0) {
|
|
4660
|
+
throw new Error("--out-price <usd/MTok> is required and must be finite and positive");
|
|
4661
|
+
}
|
|
4662
|
+
if (!Number.isFinite(inPrice) || inPrice <= 0) {
|
|
4663
|
+
throw new Error("--in-price <usd/MTok> must be finite and positive");
|
|
4664
|
+
}
|
|
4658
4665
|
const mtokApiKey = env.MTOK_API_KEY;
|
|
4659
4666
|
const upstreamKey = env.UPSTREAM_KEY;
|
|
4660
4667
|
const relayWalletKey = env.RELAY_WALLET_KEY;
|
|
@@ -4781,56 +4788,143 @@ function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
|
|
|
4781
4788
|
if (!feeAddress || bps === 0n) return 0n;
|
|
4782
4789
|
return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
|
|
4783
4790
|
}
|
|
4784
|
-
var
|
|
4791
|
+
var MESSAGE_OVERHEAD_TOKENS = 4;
|
|
4785
4792
|
function estimateInputTokens(messages) {
|
|
4786
|
-
|
|
4793
|
+
const utf8 = new TextEncoder();
|
|
4794
|
+
let tokens = 3;
|
|
4787
4795
|
for (const m of messages ?? []) {
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4796
|
+
tokens += MESSAGE_OVERHEAD_TOKENS;
|
|
4797
|
+
tokens += utf8.encode(String(m?.role ?? "")).length;
|
|
4798
|
+
tokens += utf8.encode(typeof m?.content === "string" ? m.content : JSON.stringify(m?.content ?? null)).length;
|
|
4791
4799
|
}
|
|
4792
|
-
return
|
|
4800
|
+
return tokens;
|
|
4793
4801
|
}
|
|
4794
4802
|
function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
|
|
4795
4803
|
const estIn = estimateInputTokens(messages);
|
|
4796
4804
|
const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
|
|
4797
|
-
if (estInCostUsd >= budgetUsd) return { refuse: true, estIn, estInCostUsd };
|
|
4805
|
+
if (estInCostUsd >= budgetUsd) return { refuse: true, reason: "input", estIn, estInCostUsd };
|
|
4798
4806
|
const outBudgetUsd = budgetUsd - estInCostUsd;
|
|
4799
4807
|
let maxTok = contextCeil;
|
|
4800
4808
|
if (Number(reqMax) > 0) maxTok = Math.min(maxTok, Math.floor(Number(reqMax)));
|
|
4801
|
-
if (Number(outPrice)
|
|
4802
|
-
|
|
4809
|
+
if (!Number.isFinite(Number(outPrice)) || Number(outPrice) <= 0) {
|
|
4810
|
+
return { refuse: true, reason: "output_price", estIn, estInCostUsd };
|
|
4811
|
+
}
|
|
4812
|
+
maxTok = Math.min(maxTok, Math.floor(outBudgetUsd / Number(outPrice) * 1e6));
|
|
4813
|
+
if (maxTok < 1) return { refuse: true, reason: "output", estIn, estInCostUsd };
|
|
4814
|
+
return { refuse: false, maxTok, estIn, estInCostUsd };
|
|
4803
4815
|
}
|
|
4804
4816
|
|
|
4805
4817
|
// src/redemption.mjs
|
|
4806
4818
|
import fs from "node:fs";
|
|
4819
|
+
import crypto2 from "node:crypto";
|
|
4807
4820
|
var DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4808
4821
|
function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS, now = () => Date.now(), log = console } = {}) {
|
|
4809
4822
|
const map = /* @__PURE__ */ new Map();
|
|
4823
|
+
const claimsDir = file ? `${file}.claims` : null;
|
|
4810
4824
|
let durable = false;
|
|
4825
|
+
let seenVersion = null;
|
|
4826
|
+
const recordFor = (key, entry) => ({
|
|
4827
|
+
k: key,
|
|
4828
|
+
at: entry.at,
|
|
4829
|
+
state: entry.state,
|
|
4830
|
+
...entry.state === "complete" ? { payload: entry.payload } : {}
|
|
4831
|
+
});
|
|
4832
|
+
const syncWrite = (target, flags, data) => {
|
|
4833
|
+
const fd = fs.openSync(target, flags);
|
|
4834
|
+
try {
|
|
4835
|
+
fs.writeFileSync(fd, data);
|
|
4836
|
+
fs.fsyncSync(fd);
|
|
4837
|
+
} finally {
|
|
4838
|
+
fs.closeSync(fd);
|
|
4839
|
+
}
|
|
4840
|
+
};
|
|
4841
|
+
const readRecords = () => {
|
|
4842
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
4843
|
+
if (!fs.existsSync(file)) return loaded;
|
|
4844
|
+
const cutoff = now() - retentionMs;
|
|
4845
|
+
let lineNumber = 0;
|
|
4846
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
4847
|
+
lineNumber += 1;
|
|
4848
|
+
if (!line.trim()) continue;
|
|
4849
|
+
let parsed;
|
|
4850
|
+
try {
|
|
4851
|
+
parsed = JSON.parse(line);
|
|
4852
|
+
} catch {
|
|
4853
|
+
throw new Error(`malformed redemption record at line ${lineNumber}`);
|
|
4854
|
+
}
|
|
4855
|
+
const { k, at } = parsed;
|
|
4856
|
+
if (typeof k !== "string" || !k || !Number.isFinite(Number(at))) {
|
|
4857
|
+
throw new Error(`malformed redemption record at line ${lineNumber}`);
|
|
4858
|
+
}
|
|
4859
|
+
if (Number(at) < cutoff) continue;
|
|
4860
|
+
const state2 = parsed.state === "pending" ? "pending" : "complete";
|
|
4861
|
+
loaded.set(k, { state: state2, at: Number(at), ...state2 === "complete" ? { payload: parsed.payload } : {} });
|
|
4862
|
+
}
|
|
4863
|
+
return loaded;
|
|
4864
|
+
};
|
|
4865
|
+
const fileVersion = () => {
|
|
4866
|
+
try {
|
|
4867
|
+
const stat = fs.statSync(file);
|
|
4868
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
4869
|
+
} catch {
|
|
4870
|
+
return null;
|
|
4871
|
+
}
|
|
4872
|
+
};
|
|
4811
4873
|
if (file) {
|
|
4812
4874
|
try {
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
} catch {
|
|
4821
|
-
}
|
|
4822
|
-
}
|
|
4875
|
+
for (const [key, entry] of readRecords()) map.set(key, entry);
|
|
4876
|
+
const compact = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
4877
|
+
try {
|
|
4878
|
+
syncWrite(compact, "w", [...map].map(([k, e]) => JSON.stringify(recordFor(k, e))).join("\n") + (map.size ? "\n" : ""));
|
|
4879
|
+
fs.renameSync(compact, file);
|
|
4880
|
+
} finally {
|
|
4881
|
+
fs.rmSync(compact, { force: true });
|
|
4823
4882
|
}
|
|
4824
|
-
fs.
|
|
4883
|
+
fs.mkdirSync(claimsDir, { recursive: true });
|
|
4825
4884
|
durable = true;
|
|
4885
|
+
seenVersion = fileVersion();
|
|
4826
4886
|
log.log?.(`mtok-relay: durable redemption at ${file} (${map.size} entries loaded)`);
|
|
4827
4887
|
} catch (e) {
|
|
4828
|
-
|
|
4888
|
+
map.clear();
|
|
4889
|
+
log.warn?.(`mtok-relay: redemption file ${file} not writable (${e.message}); paid serves will fail closed. Point --redemption-file at a durable path.`);
|
|
4829
4890
|
durable = false;
|
|
4830
4891
|
}
|
|
4831
4892
|
} else {
|
|
4832
|
-
log.warn?.("mtok-relay: no --redemption-file
|
|
4893
|
+
log.warn?.("mtok-relay: no --redemption-file; paid serves will fail closed because a claim cannot be persisted. Set --redemption-file to a durable path.");
|
|
4833
4894
|
}
|
|
4895
|
+
const append = (key, entry) => {
|
|
4896
|
+
if (!durable) throw new Error("durable redemption unavailable");
|
|
4897
|
+
syncWrite(file, "a", JSON.stringify(recordFor(key, entry)) + "\n");
|
|
4898
|
+
seenVersion = fileVersion();
|
|
4899
|
+
};
|
|
4900
|
+
const markerFor = (key) => `${claimsDir}/${crypto2.createHash("sha256").update(String(key)).digest("hex")}`;
|
|
4901
|
+
const markClaimed = (key) => {
|
|
4902
|
+
if (!durable) throw new Error("durable redemption unavailable");
|
|
4903
|
+
const marker = markerFor(key);
|
|
4904
|
+
try {
|
|
4905
|
+
syncWrite(marker, "wx", String(key) + "\n");
|
|
4906
|
+
return true;
|
|
4907
|
+
} catch (e) {
|
|
4908
|
+
if (e?.code === "EEXIST") return false;
|
|
4909
|
+
throw e;
|
|
4910
|
+
}
|
|
4911
|
+
};
|
|
4912
|
+
const refresh = () => {
|
|
4913
|
+
if (!durable) return;
|
|
4914
|
+
const version2 = fileVersion();
|
|
4915
|
+
if (!version2 || version2 === seenVersion) return;
|
|
4916
|
+
try {
|
|
4917
|
+
for (const [key, entry] of readRecords()) map.set(key, entry);
|
|
4918
|
+
seenVersion = version2;
|
|
4919
|
+
} catch (e) {
|
|
4920
|
+
log.warn?.(`mtok-relay: could not refresh redemption file ${file} (${e.message}); keeping the existing fail-closed state`);
|
|
4921
|
+
}
|
|
4922
|
+
};
|
|
4923
|
+
const state = (key) => {
|
|
4924
|
+
const current = map.get(key)?.state;
|
|
4925
|
+
if (durable && (!current || current === "pending")) refresh();
|
|
4926
|
+
return map.get(key)?.state ?? null;
|
|
4927
|
+
};
|
|
4834
4928
|
return {
|
|
4835
4929
|
durable,
|
|
4836
4930
|
retentionMs,
|
|
@@ -4838,18 +4932,30 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
|
|
|
4838
4932
|
has(key) {
|
|
4839
4933
|
return map.has(key);
|
|
4840
4934
|
},
|
|
4935
|
+
state,
|
|
4841
4936
|
get(key) {
|
|
4842
|
-
return map.get(key)
|
|
4937
|
+
return state(key) === "complete" ? map.get(key).payload : void 0;
|
|
4843
4938
|
},
|
|
4939
|
+
claim(key, markerKey = key) {
|
|
4940
|
+
if (map.has(key)) return false;
|
|
4941
|
+
if (!markClaimed(markerKey)) return false;
|
|
4942
|
+
const entry = { state: "pending", at: now() };
|
|
4943
|
+
append(key, entry);
|
|
4944
|
+
map.set(key, entry);
|
|
4945
|
+
return true;
|
|
4946
|
+
},
|
|
4947
|
+
complete(key, payload) {
|
|
4948
|
+
if (map.get(key)?.state !== "pending") throw new Error("redemption is not pending");
|
|
4949
|
+
const entry = { state: "complete", payload, at: now() };
|
|
4950
|
+
append(key, entry);
|
|
4951
|
+
map.set(key, entry);
|
|
4952
|
+
},
|
|
4953
|
+
// Backward-compatible store API for callers which only persist completed
|
|
4954
|
+
// payloads. The relay runtime itself always uses claim() then complete().
|
|
4844
4955
|
set(key, payload) {
|
|
4845
|
-
const
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
try {
|
|
4849
|
-
fs.appendFileSync(file, JSON.stringify({ k: key, at, payload }) + "\n");
|
|
4850
|
-
} catch {
|
|
4851
|
-
}
|
|
4852
|
-
}
|
|
4956
|
+
const entry = { state: "complete", payload, at: now() };
|
|
4957
|
+
if (durable) append(key, entry);
|
|
4958
|
+
map.set(key, entry);
|
|
4853
4959
|
}
|
|
4854
4960
|
};
|
|
4855
4961
|
}
|
|
@@ -5090,16 +5196,22 @@ function createOnchainVerifier({
|
|
|
5090
5196
|
const maxAge = Number(maxPaidAgeMs);
|
|
5091
5197
|
if (Number.isFinite(maxAge) && maxAge > 0) {
|
|
5092
5198
|
const bn = matchedLog?.blockNumber;
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
try {
|
|
5199
|
+
let paidAtMs = null;
|
|
5200
|
+
if (bn != null) {
|
|
5096
5201
|
const blockTag = typeof bn === "string" && bn.startsWith("0x") ? bn : "0x" + BigInt(bn).toString(16);
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5202
|
+
for (let i = 0; i <= receiptRetries; i++) {
|
|
5203
|
+
try {
|
|
5204
|
+
const block = await rpc("eth_getBlockByNumber", [blockTag, false]);
|
|
5205
|
+
if (block?.timestamp != null) {
|
|
5206
|
+
paidAtMs = Number(BigInt(block.timestamp)) * 1e3;
|
|
5207
|
+
break;
|
|
5208
|
+
}
|
|
5209
|
+
} catch {
|
|
5210
|
+
}
|
|
5211
|
+
if (i < receiptRetries) await sleepImpl(receiptRetryMs);
|
|
5212
|
+
}
|
|
5101
5213
|
}
|
|
5102
|
-
if (nowMs() - paidAtMs > maxAge) return { ok: false, reason: "payment_too_old" };
|
|
5214
|
+
if (paidAtMs != null && nowMs() - paidAtMs > maxAge) return { ok: false, reason: "payment_too_old" };
|
|
5103
5215
|
}
|
|
5104
5216
|
const consumed = /* @__PURE__ */ new Set();
|
|
5105
5217
|
let sellerTransfer = null;
|
|
@@ -5156,11 +5268,85 @@ var BASE_SEPOLIA_RPCS = [
|
|
|
5156
5268
|
var rpcUrlsFor = (chainId, override) => override ? [override] : Number(chainId) === 8453 ? BASE_MAINNET_RPCS : BASE_SEPOLIA_RPCS;
|
|
5157
5269
|
|
|
5158
5270
|
// src/runtime.mjs
|
|
5159
|
-
import
|
|
5271
|
+
import crypto3 from "node:crypto";
|
|
5160
5272
|
var BALANCE_EPSILON = 1e-6;
|
|
5161
|
-
var hash32 = (v) => "0x" +
|
|
5273
|
+
var hash32 = (v) => "0x" + crypto3.createHash("sha256").update(typeof v === "string" ? v : JSON.stringify(v ?? null)).digest("hex");
|
|
5274
|
+
var REQUEST_NONCE_RE = /^0x[0-9a-fA-F]{32}$/;
|
|
5275
|
+
var CHAT_ROLES = /* @__PURE__ */ new Set(["developer", "system", "user", "assistant"]);
|
|
5276
|
+
var REQUEST_KEYS = /* @__PURE__ */ new Set(["model", "messages", "max_tokens", "temperature", "response_format", "stream", "n"]);
|
|
5277
|
+
function legacyContentText(content) {
|
|
5278
|
+
const render = (part) => {
|
|
5279
|
+
if (typeof part === "string") return part;
|
|
5280
|
+
if (Array.isArray(part)) return part.map(render).filter(Boolean).join("\n");
|
|
5281
|
+
if (part == null) return "";
|
|
5282
|
+
if (typeof part !== "object") return String(part);
|
|
5283
|
+
if (typeof part.text === "string") return part.text;
|
|
5284
|
+
if (typeof part.content === "string") return part.content;
|
|
5285
|
+
const type = typeof part.type === "string" && /^[a-z0-9_-]{1,32}$/i.test(part.type) ? part.type : "non-text";
|
|
5286
|
+
return `[${type} omitted]`;
|
|
5287
|
+
};
|
|
5288
|
+
return render(content) || "[empty legacy content]";
|
|
5289
|
+
}
|
|
5290
|
+
function sanitizeLegacyMessage(message) {
|
|
5291
|
+
const source = message && typeof message === "object" && !Array.isArray(message) ? message : { content: message };
|
|
5292
|
+
if (CHAT_ROLES.has(source.role)) return { role: source.role, content: legacyContentText(source.content) };
|
|
5293
|
+
const label = source.role === "tool" || source.role === "function" ? source.role : "legacy";
|
|
5294
|
+
return { role: "user", content: `[${label} message]
|
|
5295
|
+
${legacyContentText(source.content)}` };
|
|
5296
|
+
}
|
|
5297
|
+
function validateRequest(request, model, { legacy = false } = {}) {
|
|
5298
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
5299
|
+
return { error: "request must be an object" };
|
|
5300
|
+
}
|
|
5301
|
+
const unknown = Object.keys(request).find((key) => !REQUEST_KEYS.has(key));
|
|
5302
|
+
if (unknown && !legacy) return { error: `unsupported request field: ${unknown}` };
|
|
5303
|
+
if (!legacy && request.model != null && String(request.model) !== String(model)) {
|
|
5304
|
+
return { error: `request model ${request.model} is not served here` };
|
|
5305
|
+
}
|
|
5306
|
+
if (!Array.isArray(request.messages) || request.messages.length === 0) {
|
|
5307
|
+
return { error: "request.messages must be a nonempty array" };
|
|
5308
|
+
}
|
|
5309
|
+
if (!legacy) {
|
|
5310
|
+
for (const message of request.messages) {
|
|
5311
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
5312
|
+
return { error: "each message must be an object" };
|
|
5313
|
+
}
|
|
5314
|
+
const extra = Object.keys(message).find((key) => key !== "role" && key !== "content");
|
|
5315
|
+
if (extra) return { error: `unsupported message field: ${extra}` };
|
|
5316
|
+
if (!CHAT_ROLES.has(message.role)) return { error: `unsupported message role: ${message.role}` };
|
|
5317
|
+
if (typeof message.content !== "string") return { error: "message content must be plain text" };
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
if (!legacy && request.stream != null && request.stream !== false) return { error: "streaming is not supported" };
|
|
5321
|
+
if (!legacy && request.n != null && request.n !== 1) return { error: "request.n must be 1" };
|
|
5322
|
+
const validMaxTokens = Number.isInteger(request.max_tokens) && request.max_tokens > 0;
|
|
5323
|
+
if (!legacy && request.max_tokens != null && !validMaxTokens) {
|
|
5324
|
+
return { error: "max_tokens must be a positive integer" };
|
|
5325
|
+
}
|
|
5326
|
+
const validTemperature = Number.isFinite(request.temperature) && request.temperature >= 0 && request.temperature <= 2;
|
|
5327
|
+
if (!legacy && request.temperature != null && !validTemperature) {
|
|
5328
|
+
return { error: "temperature must be between 0 and 2" };
|
|
5329
|
+
}
|
|
5330
|
+
let validResponseFormat = false;
|
|
5331
|
+
if (request.response_format != null) {
|
|
5332
|
+
const format = request.response_format;
|
|
5333
|
+
validResponseFormat = !!format && typeof format === "object" && !Array.isArray(format) && Object.keys(format).length === 1 && ["json_object", "text"].includes(format.type);
|
|
5334
|
+
if (!legacy && !validResponseFormat) {
|
|
5335
|
+
return { error: 'response_format must be exactly { type: "json_object" } or { type: "text" }' };
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
return {
|
|
5339
|
+
safeRequest: {
|
|
5340
|
+
model,
|
|
5341
|
+
messages: legacy ? request.messages.map(sanitizeLegacyMessage) : request.messages.map(({ role, content }) => ({ role, content })),
|
|
5342
|
+
...validMaxTokens ? { max_tokens: request.max_tokens } : {},
|
|
5343
|
+
...validTemperature ? { temperature: request.temperature } : {},
|
|
5344
|
+
...validResponseFormat ? { response_format: { type: request.response_format.type } } : {}
|
|
5345
|
+
}
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5162
5348
|
async function createRelayRuntime(config) {
|
|
5163
|
-
const served = createRedemptionStore({ file: config.redemptionFile });
|
|
5349
|
+
const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
|
|
5164
5350
|
const drawLocks = /* @__PURE__ */ new Map();
|
|
5165
5351
|
const platform = await fetchPlatformConfig(config);
|
|
5166
5352
|
const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress, expectedChainId: platform.chainId });
|
|
@@ -5193,12 +5379,23 @@ async function createRelayRuntime(config) {
|
|
|
5193
5379
|
}
|
|
5194
5380
|
};
|
|
5195
5381
|
const handleDraw = async (body, res) => {
|
|
5196
|
-
const { bookingId, n, buyerId, request, drawPaidTxHash } = body;
|
|
5382
|
+
const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
|
|
5383
|
+
const hasRequestNonce = Object.hasOwn(body, "requestNonce");
|
|
5197
5384
|
if (!bookingId) return send(res, 400, { error: "bad_request", detail: "DRAW needs bookingId" });
|
|
5198
5385
|
if (n == null) return send(res, 400, { error: "bad_request", detail: "DRAW needs a delivery index n (per-booking idempotency key)" });
|
|
5386
|
+
if (!Number.isSafeInteger(n) || n < 0 || n > 4294967295) {
|
|
5387
|
+
return send(res, 400, { error: "bad_request", detail: "DRAW delivery index n must be a nonnegative uint32 integer" });
|
|
5388
|
+
}
|
|
5389
|
+
if (hasRequestNonce && !REQUEST_NONCE_RE.test(requestNonce)) {
|
|
5390
|
+
return send(res, 400, { error: "bad_request", detail: "DRAW needs requestNonce as 16 random bytes encoded as 0x-prefixed hex" });
|
|
5391
|
+
}
|
|
5392
|
+
const checked = validateRequest(request, config.model, { legacy: !hasRequestNonce });
|
|
5393
|
+
if (checked.error) return send(res, 400, { error: "bad_request", detail: checked.error });
|
|
5199
5394
|
return withBookingLock(bookingId, async () => {
|
|
5200
|
-
const
|
|
5201
|
-
const
|
|
5395
|
+
const requestHashScheme = hasRequestNonce ? "nonce-v1" : "legacy-v0";
|
|
5396
|
+
const requestHash = hasRequestNonce ? hash32({ request, requestNonce }) : hash32(request);
|
|
5397
|
+
const cacheKey = `${requestHashScheme}:${bookingId}:${n}:${requestHash}`;
|
|
5398
|
+
const oldLegacyKey = hasRequestNonce ? null : `${bookingId}:${n}:${requestHash}`;
|
|
5202
5399
|
if (!platform.dripContractAddress) {
|
|
5203
5400
|
return send(res, 402, { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" });
|
|
5204
5401
|
}
|
|
@@ -5217,10 +5414,9 @@ async function createRelayRuntime(config) {
|
|
|
5217
5414
|
requestHash,
|
|
5218
5415
|
sellerWallet: config.settlementAddr,
|
|
5219
5416
|
feeRecipient: platform.feeAddress,
|
|
5220
|
-
// #580: refuse a payment older than the redemption window.
|
|
5221
|
-
//
|
|
5222
|
-
//
|
|
5223
|
-
// buying a fresh inference). An honest retry is seconds-to-minutes old, never days.
|
|
5417
|
+
// #580: refuse a payment older than the redemption window. The JSONL
|
|
5418
|
+
// payload cache is compacted by age (exclusive claim markers remain
|
|
5419
|
+
// fail-closed), and an honest retry is seconds-to-minutes old, never days.
|
|
5224
5420
|
maxPaidAgeMs: served.retentionMs
|
|
5225
5421
|
});
|
|
5226
5422
|
} catch (e) {
|
|
@@ -5242,19 +5438,43 @@ async function createRelayRuntime(config) {
|
|
|
5242
5438
|
} catch (e) {
|
|
5243
5439
|
return send(res, 403, { error: "payer_denied", detail: "payer screening failed: " + e.message });
|
|
5244
5440
|
}
|
|
5245
|
-
|
|
5441
|
+
let storedKey = cacheKey;
|
|
5442
|
+
let redemptionState = served.state(storedKey);
|
|
5443
|
+
if (!redemptionState && oldLegacyKey) {
|
|
5444
|
+
const oldLegacyState = served.state(oldLegacyKey);
|
|
5445
|
+
if (oldLegacyState) {
|
|
5446
|
+
storedKey = oldLegacyKey;
|
|
5447
|
+
redemptionState = oldLegacyState;
|
|
5448
|
+
} else {
|
|
5449
|
+
redemptionState = served.state(cacheKey);
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
if (redemptionState === "complete") return send(res, 200, served.get(storedKey));
|
|
5453
|
+
if (redemptionState === "pending") {
|
|
5454
|
+
return send(res, 409, { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId });
|
|
5455
|
+
}
|
|
5246
5456
|
const paidEvent = paid.event;
|
|
5247
5457
|
const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
|
|
5248
|
-
if (remainingUsd
|
|
5458
|
+
if (remainingUsd < BALANCE_EPSILON) {
|
|
5249
5459
|
return send(res, 402, { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd });
|
|
5250
5460
|
}
|
|
5251
5461
|
const eventInPriceUsd = Number(paidEvent.inputPricePerMTokAtomic || 0) / 1e6;
|
|
5462
|
+
const eventOutPriceUsd = Number(paidEvent.outputPricePerMTokAtomic || 0) / 1e6;
|
|
5252
5463
|
const inPrice = Math.max(Number(config.inPrice) || 0, eventInPriceUsd);
|
|
5253
|
-
const
|
|
5464
|
+
const outPrice = Math.max(Number(config.outPrice) || 0, eventOutPriceUsd);
|
|
5465
|
+
const bound = boundServe({ messages: checked.safeRequest.messages, budgetUsd: remainingUsd, inPrice, outPrice, reqMax: checked.safeRequest.max_tokens });
|
|
5254
5466
|
if (bound.refuse) {
|
|
5255
|
-
|
|
5467
|
+
const error = bound.reason === "input" ? "input_too_large" : "output_unfunded";
|
|
5468
|
+
return send(res, 402, { error, detail: `estimated input (~${bound.estIn} tokens, $${bound.estInCostUsd.toFixed(6)}) leaves no safely funded output in the paid amount ($${remainingUsd})`, _bookingId: bookingId, remainingUsd });
|
|
5469
|
+
}
|
|
5470
|
+
const safeRequest = { ...checked.safeRequest, model: config.model, max_tokens: bound.maxTok };
|
|
5471
|
+
try {
|
|
5472
|
+
if (!served.claim(cacheKey, oldLegacyKey ?? cacheKey)) {
|
|
5473
|
+
return send(res, 409, { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId });
|
|
5474
|
+
}
|
|
5475
|
+
} catch (e) {
|
|
5476
|
+
return send(res, 503, { error: "redemption_unavailable", detail: `could not durably claim the paid draw: ${e.message}`, _bookingId: bookingId });
|
|
5256
5477
|
}
|
|
5257
|
-
const safeRequest = { ...request, max_tokens: bound.maxTok };
|
|
5258
5478
|
let completion;
|
|
5259
5479
|
try {
|
|
5260
5480
|
completion = await upstream(safeRequest);
|
|
@@ -5271,7 +5491,11 @@ async function createRelayRuntime(config) {
|
|
|
5271
5491
|
const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
|
|
5272
5492
|
const usedUsd = (inTok * Number(paidEvent.inputPricePerMTokAtomic || 0) + outTok * Number(paidEvent.outputPricePerMTokAtomic || 0)) / 1e12;
|
|
5273
5493
|
const payload = { ...completion, _bookingId: bookingId, remainingUsd: Math.max(0, Math.round((remainingUsd - usedUsd) * 1e6) / 1e6) };
|
|
5274
|
-
|
|
5494
|
+
try {
|
|
5495
|
+
served.complete(cacheKey, payload);
|
|
5496
|
+
} catch (e) {
|
|
5497
|
+
(config.log ?? console).error?.(`mtok-relay: completion for ${cacheKey} could not be persisted (${e.message}); retries will remain pending`);
|
|
5498
|
+
}
|
|
5275
5499
|
return send(res, 200, payload);
|
|
5276
5500
|
});
|
|
5277
5501
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtok-relay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Reference seller relay for mtok.market — accepts on-chain-prepaid chunk draws and serves inference from an upstream you control.
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|
|
7
7
|
"mtok-relay": "dist/mtok-relay.mjs"
|