mtok-relay 0.1.10 → 0.2.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/README.md +1 -1
- package/dist/mtok-relay.mjs +416 -147
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/mtok-relay.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// node_modules/@noble/hashes/esm/cryptoNode.js
|
|
5
5
|
import * as nc from "node:crypto";
|
|
6
|
-
var
|
|
6
|
+
var crypto2 = nc && typeof nc === "object" && "webcrypto" in nc ? nc.webcrypto : nc && typeof nc === "object" && "randomBytes" in nc ? nc : void 0;
|
|
7
7
|
|
|
8
8
|
// node_modules/@noble/hashes/esm/utils.js
|
|
9
9
|
function isBytes(a) {
|
|
@@ -100,11 +100,11 @@ function createHasher(hashCons) {
|
|
|
100
100
|
return hashC;
|
|
101
101
|
}
|
|
102
102
|
function randomBytes(bytesLength = 32) {
|
|
103
|
-
if (
|
|
104
|
-
return
|
|
103
|
+
if (crypto2 && typeof crypto2.getRandomValues === "function") {
|
|
104
|
+
return crypto2.getRandomValues(new Uint8Array(bytesLength));
|
|
105
105
|
}
|
|
106
|
-
if (
|
|
107
|
-
return Uint8Array.from(
|
|
106
|
+
if (crypto2 && typeof crypto2.randomBytes === "function") {
|
|
107
|
+
return Uint8Array.from(crypto2.randomBytes(bytesLength));
|
|
108
108
|
}
|
|
109
109
|
throw new Error("crypto.getRandomValues must be defined");
|
|
110
110
|
}
|
|
@@ -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.1";
|
|
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;
|
|
@@ -4771,66 +4778,125 @@ function startRelayServer({ config, handleDraw }) {
|
|
|
4771
4778
|
return server;
|
|
4772
4779
|
}
|
|
4773
4780
|
|
|
4774
|
-
// lib.mjs
|
|
4775
|
-
function enforceModelEcho(upstreamModel, offerModel) {
|
|
4776
|
-
if (String(upstreamModel) !== String(offerModel))
|
|
4777
|
-
throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
|
|
4778
|
-
}
|
|
4779
|
-
function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
|
|
4780
|
-
const bps = BigInt(Math.trunc(Math.max(0, Number(feeBps) || 0)));
|
|
4781
|
-
if (!feeAddress || bps === 0n) return 0n;
|
|
4782
|
-
return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
|
|
4783
|
-
}
|
|
4784
|
-
var CHARS_PER_TOKEN_EST = 3.2;
|
|
4785
|
-
function estimateInputTokens(messages) {
|
|
4786
|
-
let chars = 0;
|
|
4787
|
-
for (const m of messages ?? []) {
|
|
4788
|
-
const c = m?.content;
|
|
4789
|
-
if (typeof c === "string") chars += c.length;
|
|
4790
|
-
else if (Array.isArray(c)) for (const part of c) chars += String(part?.text ?? "").length;
|
|
4791
|
-
}
|
|
4792
|
-
return Math.ceil(chars / CHARS_PER_TOKEN_EST);
|
|
4793
|
-
}
|
|
4794
|
-
function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
|
|
4795
|
-
const estIn = estimateInputTokens(messages);
|
|
4796
|
-
const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
|
|
4797
|
-
if (estInCostUsd >= budgetUsd) return { refuse: true, estIn, estInCostUsd };
|
|
4798
|
-
const outBudgetUsd = budgetUsd - estInCostUsd;
|
|
4799
|
-
let maxTok = contextCeil;
|
|
4800
|
-
if (Number(reqMax) > 0) maxTok = Math.min(maxTok, Math.floor(Number(reqMax)));
|
|
4801
|
-
if (Number(outPrice) > 0) maxTok = Math.min(maxTok, Math.floor(outBudgetUsd / Number(outPrice) * 1e6));
|
|
4802
|
-
return { refuse: false, maxTok: Math.max(1, maxTok), estIn, estInCostUsd };
|
|
4803
|
-
}
|
|
4804
|
-
|
|
4805
4781
|
// src/redemption.mjs
|
|
4806
4782
|
import fs from "node:fs";
|
|
4783
|
+
import crypto3 from "node:crypto";
|
|
4807
4784
|
var DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4808
4785
|
function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS, now = () => Date.now(), log = console } = {}) {
|
|
4809
4786
|
const map = /* @__PURE__ */ new Map();
|
|
4787
|
+
const claimsDir = file ? `${file}.claims` : null;
|
|
4810
4788
|
let durable = false;
|
|
4789
|
+
let seenVersion = null;
|
|
4790
|
+
const recordFor = (key, entry) => ({
|
|
4791
|
+
k: key,
|
|
4792
|
+
at: entry.at,
|
|
4793
|
+
state: entry.state,
|
|
4794
|
+
...entry.state === "complete" ? { payload: entry.payload } : {}
|
|
4795
|
+
});
|
|
4796
|
+
const syncWrite = (target, flags, data) => {
|
|
4797
|
+
const fd = fs.openSync(target, flags);
|
|
4798
|
+
try {
|
|
4799
|
+
fs.writeFileSync(fd, data);
|
|
4800
|
+
fs.fsyncSync(fd);
|
|
4801
|
+
} finally {
|
|
4802
|
+
fs.closeSync(fd);
|
|
4803
|
+
}
|
|
4804
|
+
};
|
|
4805
|
+
const readRecords = () => {
|
|
4806
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
4807
|
+
if (!fs.existsSync(file)) return loaded;
|
|
4808
|
+
const cutoff = now() - retentionMs;
|
|
4809
|
+
let lineNumber = 0;
|
|
4810
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
4811
|
+
lineNumber += 1;
|
|
4812
|
+
if (!line.trim()) continue;
|
|
4813
|
+
let parsed;
|
|
4814
|
+
try {
|
|
4815
|
+
parsed = JSON.parse(line);
|
|
4816
|
+
} catch {
|
|
4817
|
+
throw new Error(`malformed redemption record at line ${lineNumber}`);
|
|
4818
|
+
}
|
|
4819
|
+
const { k, at } = parsed;
|
|
4820
|
+
if (typeof k !== "string" || !k || !Number.isFinite(Number(at))) {
|
|
4821
|
+
throw new Error(`malformed redemption record at line ${lineNumber}`);
|
|
4822
|
+
}
|
|
4823
|
+
if (Number(at) < cutoff) continue;
|
|
4824
|
+
const state2 = parsed.state === "pending" ? "pending" : "complete";
|
|
4825
|
+
loaded.set(k, { state: state2, at: Number(at), ...state2 === "complete" ? { payload: parsed.payload } : {} });
|
|
4826
|
+
}
|
|
4827
|
+
return loaded;
|
|
4828
|
+
};
|
|
4829
|
+
const fileVersion = () => {
|
|
4830
|
+
try {
|
|
4831
|
+
const stat = fs.statSync(file);
|
|
4832
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
4833
|
+
} catch {
|
|
4834
|
+
return null;
|
|
4835
|
+
}
|
|
4836
|
+
};
|
|
4811
4837
|
if (file) {
|
|
4812
4838
|
try {
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4839
|
+
for (const [key, entry] of readRecords()) map.set(key, entry);
|
|
4840
|
+
const compact = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
4841
|
+
try {
|
|
4842
|
+
syncWrite(compact, "w", [...map].map(([k, e]) => JSON.stringify(recordFor(k, e))).join("\n") + (map.size ? "\n" : ""));
|
|
4843
|
+
fs.renameSync(compact, file);
|
|
4844
|
+
} finally {
|
|
4845
|
+
fs.rmSync(compact, { force: true });
|
|
4846
|
+
}
|
|
4847
|
+
fs.mkdirSync(claimsDir, { recursive: true });
|
|
4848
|
+
const markerCutoff = Date.now() - retentionMs;
|
|
4849
|
+
for (const name of fs.readdirSync(claimsDir)) {
|
|
4850
|
+
try {
|
|
4851
|
+
const marker = `${claimsDir}/${name}`;
|
|
4852
|
+
if (fs.statSync(marker).mtimeMs < markerCutoff) fs.unlinkSync(marker);
|
|
4853
|
+
} catch {
|
|
4822
4854
|
}
|
|
4823
4855
|
}
|
|
4824
|
-
fs.writeFileSync(file, [...map].map(([k, e]) => JSON.stringify({ k, at: e.at, payload: e.payload })).join("\n") + (map.size ? "\n" : ""));
|
|
4825
4856
|
durable = true;
|
|
4857
|
+
seenVersion = fileVersion();
|
|
4826
4858
|
log.log?.(`mtok-relay: durable redemption at ${file} (${map.size} entries loaded)`);
|
|
4827
4859
|
} catch (e) {
|
|
4828
|
-
|
|
4860
|
+
map.clear();
|
|
4861
|
+
log.warn?.(`mtok-relay: redemption file ${file} not writable (${e.message}); paid serves will fail closed. Point --redemption-file at a durable path.`);
|
|
4829
4862
|
durable = false;
|
|
4830
4863
|
}
|
|
4831
4864
|
} else {
|
|
4832
|
-
log.warn?.("mtok-relay: no --redemption-file
|
|
4865
|
+
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
4866
|
}
|
|
4867
|
+
const append = (key, entry) => {
|
|
4868
|
+
if (!durable) throw new Error("durable redemption unavailable");
|
|
4869
|
+
syncWrite(file, "a", JSON.stringify(recordFor(key, entry)) + "\n");
|
|
4870
|
+
seenVersion = fileVersion();
|
|
4871
|
+
};
|
|
4872
|
+
const markerFor = (key) => `${claimsDir}/${crypto3.createHash("sha256").update(String(key)).digest("hex")}`;
|
|
4873
|
+
const markClaimed = (key) => {
|
|
4874
|
+
if (!durable) throw new Error("durable redemption unavailable");
|
|
4875
|
+
const marker = markerFor(key);
|
|
4876
|
+
try {
|
|
4877
|
+
syncWrite(marker, "wx", String(key) + "\n");
|
|
4878
|
+
return true;
|
|
4879
|
+
} catch (e) {
|
|
4880
|
+
if (e?.code === "EEXIST") return false;
|
|
4881
|
+
throw e;
|
|
4882
|
+
}
|
|
4883
|
+
};
|
|
4884
|
+
const refresh = () => {
|
|
4885
|
+
if (!durable) return;
|
|
4886
|
+
const version2 = fileVersion();
|
|
4887
|
+
if (!version2 || version2 === seenVersion) return;
|
|
4888
|
+
try {
|
|
4889
|
+
for (const [key, entry] of readRecords()) map.set(key, entry);
|
|
4890
|
+
seenVersion = version2;
|
|
4891
|
+
} catch (e) {
|
|
4892
|
+
log.warn?.(`mtok-relay: could not refresh redemption file ${file} (${e.message}); keeping the existing fail-closed state`);
|
|
4893
|
+
}
|
|
4894
|
+
};
|
|
4895
|
+
const state = (key) => {
|
|
4896
|
+
const current = map.get(key)?.state;
|
|
4897
|
+
if (durable && (!current || current === "pending")) refresh();
|
|
4898
|
+
return map.get(key)?.state ?? null;
|
|
4899
|
+
};
|
|
4834
4900
|
return {
|
|
4835
4901
|
durable,
|
|
4836
4902
|
retentionMs,
|
|
@@ -4838,18 +4904,30 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
|
|
|
4838
4904
|
has(key) {
|
|
4839
4905
|
return map.has(key);
|
|
4840
4906
|
},
|
|
4907
|
+
state,
|
|
4841
4908
|
get(key) {
|
|
4842
|
-
return map.get(key)
|
|
4909
|
+
return state(key) === "complete" ? map.get(key).payload : void 0;
|
|
4910
|
+
},
|
|
4911
|
+
claim(key, markerKey = key) {
|
|
4912
|
+
if (map.has(key)) return false;
|
|
4913
|
+
if (!markClaimed(markerKey)) return false;
|
|
4914
|
+
const entry = { state: "pending", at: now() };
|
|
4915
|
+
append(key, entry);
|
|
4916
|
+
map.set(key, entry);
|
|
4917
|
+
return true;
|
|
4843
4918
|
},
|
|
4919
|
+
complete(key, payload) {
|
|
4920
|
+
if (map.get(key)?.state !== "pending") throw new Error("redemption is not pending");
|
|
4921
|
+
const entry = { state: "complete", payload, at: now() };
|
|
4922
|
+
append(key, entry);
|
|
4923
|
+
map.set(key, entry);
|
|
4924
|
+
},
|
|
4925
|
+
// Backward-compatible store API for callers which only persist completed
|
|
4926
|
+
// payloads. The relay runtime itself always uses claim() then complete().
|
|
4844
4927
|
set(key, payload) {
|
|
4845
|
-
const
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
try {
|
|
4849
|
-
fs.appendFileSync(file, JSON.stringify({ k: key, at, payload }) + "\n");
|
|
4850
|
-
} catch {
|
|
4851
|
-
}
|
|
4852
|
-
}
|
|
4928
|
+
const entry = { state: "complete", payload, at: now() };
|
|
4929
|
+
if (durable) append(key, entry);
|
|
4930
|
+
map.set(key, entry);
|
|
4853
4931
|
}
|
|
4854
4932
|
};
|
|
4855
4933
|
}
|
|
@@ -5124,6 +5202,263 @@ function createOnchainVerifier({
|
|
|
5124
5202
|
};
|
|
5125
5203
|
}
|
|
5126
5204
|
|
|
5205
|
+
// bridge/serve-core.mjs
|
|
5206
|
+
var BALANCE_EPSILON = 1e-6;
|
|
5207
|
+
var REQUEST_NONCE_RE = /^0x[0-9a-fA-F]{32}$/;
|
|
5208
|
+
var CHAT_ROLES = /* @__PURE__ */ new Set(["developer", "system", "user", "assistant"]);
|
|
5209
|
+
var REQUEST_KEYS = /* @__PURE__ */ new Set(["model", "messages", "max_tokens", "temperature", "response_format", "stream", "n"]);
|
|
5210
|
+
async function hash32(v) {
|
|
5211
|
+
const bytes = new TextEncoder().encode(typeof v === "string" ? v : JSON.stringify(v ?? null));
|
|
5212
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
5213
|
+
return "0x" + [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
5214
|
+
}
|
|
5215
|
+
function legacyContentText(content) {
|
|
5216
|
+
const render = (part) => {
|
|
5217
|
+
if (typeof part === "string") return part;
|
|
5218
|
+
if (Array.isArray(part)) return part.map(render).filter(Boolean).join("\n");
|
|
5219
|
+
if (part == null) return "";
|
|
5220
|
+
if (typeof part !== "object") return String(part);
|
|
5221
|
+
if (typeof part.text === "string") return part.text;
|
|
5222
|
+
if (typeof part.content === "string") return part.content;
|
|
5223
|
+
const type = typeof part.type === "string" && /^[a-z0-9_-]{1,32}$/i.test(part.type) ? part.type : "non-text";
|
|
5224
|
+
return `[${type} omitted]`;
|
|
5225
|
+
};
|
|
5226
|
+
return render(content) || "[empty legacy content]";
|
|
5227
|
+
}
|
|
5228
|
+
function sanitizeLegacyMessage(message) {
|
|
5229
|
+
const source = message && typeof message === "object" && !Array.isArray(message) ? message : { content: message };
|
|
5230
|
+
if (CHAT_ROLES.has(source.role)) return { role: source.role, content: legacyContentText(source.content) };
|
|
5231
|
+
const label = source.role === "tool" || source.role === "function" ? source.role : "legacy";
|
|
5232
|
+
return { role: "user", content: `[${label} message]
|
|
5233
|
+
${legacyContentText(source.content)}` };
|
|
5234
|
+
}
|
|
5235
|
+
function validateRequest(request, model, { legacy = false } = {}) {
|
|
5236
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
5237
|
+
return { error: "request must be an object" };
|
|
5238
|
+
}
|
|
5239
|
+
const unknown = Object.keys(request).find((key) => !REQUEST_KEYS.has(key));
|
|
5240
|
+
if (unknown && !legacy) return { error: `unsupported request field: ${unknown}` };
|
|
5241
|
+
if (!legacy && request.model != null && String(request.model) !== String(model)) {
|
|
5242
|
+
return { error: `request model ${request.model} is not served here` };
|
|
5243
|
+
}
|
|
5244
|
+
if (!Array.isArray(request.messages) || request.messages.length === 0) {
|
|
5245
|
+
return { error: "request.messages must be a nonempty array" };
|
|
5246
|
+
}
|
|
5247
|
+
if (!legacy) {
|
|
5248
|
+
for (const message of request.messages) {
|
|
5249
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
5250
|
+
return { error: "each message must be an object" };
|
|
5251
|
+
}
|
|
5252
|
+
const extra = Object.keys(message).find((key) => key !== "role" && key !== "content");
|
|
5253
|
+
if (extra) return { error: `unsupported message field: ${extra}` };
|
|
5254
|
+
if (!CHAT_ROLES.has(message.role)) return { error: `unsupported message role: ${message.role}` };
|
|
5255
|
+
if (typeof message.content !== "string") return { error: "message content must be plain text" };
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
if (!legacy && request.stream != null && request.stream !== false) return { error: "streaming is not supported" };
|
|
5259
|
+
if (!legacy && request.n != null && request.n !== 1) return { error: "request.n must be 1" };
|
|
5260
|
+
const validMaxTokens = Number.isInteger(request.max_tokens) && request.max_tokens > 0;
|
|
5261
|
+
if (!legacy && request.max_tokens != null && !validMaxTokens) {
|
|
5262
|
+
return { error: "max_tokens must be a positive integer" };
|
|
5263
|
+
}
|
|
5264
|
+
const validTemperature = Number.isFinite(request.temperature) && request.temperature >= 0 && request.temperature <= 2;
|
|
5265
|
+
if (!legacy && request.temperature != null && !validTemperature) {
|
|
5266
|
+
return { error: "temperature must be between 0 and 2" };
|
|
5267
|
+
}
|
|
5268
|
+
let validResponseFormat = false;
|
|
5269
|
+
if (request.response_format != null) {
|
|
5270
|
+
const format = request.response_format;
|
|
5271
|
+
validResponseFormat = !!format && typeof format === "object" && !Array.isArray(format) && Object.keys(format).length === 1 && ["json_object", "text"].includes(format.type);
|
|
5272
|
+
if (!legacy && !validResponseFormat) {
|
|
5273
|
+
return { error: 'response_format must be exactly { type: "json_object" } or { type: "text" }' };
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
return {
|
|
5277
|
+
safeRequest: {
|
|
5278
|
+
model,
|
|
5279
|
+
messages: legacy ? request.messages.map(sanitizeLegacyMessage) : request.messages.map(({ role, content }) => ({ role, content })),
|
|
5280
|
+
...validMaxTokens ? { max_tokens: request.max_tokens } : {},
|
|
5281
|
+
...validTemperature ? { temperature: request.temperature } : {},
|
|
5282
|
+
...validResponseFormat ? { response_format: { type: request.response_format.type } } : {}
|
|
5283
|
+
}
|
|
5284
|
+
};
|
|
5285
|
+
}
|
|
5286
|
+
function enforceModelEcho(upstreamModel, offerModel) {
|
|
5287
|
+
if (String(upstreamModel) !== String(offerModel))
|
|
5288
|
+
throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
|
|
5289
|
+
}
|
|
5290
|
+
function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
|
|
5291
|
+
const bps = BigInt(Math.trunc(Math.max(0, Number(feeBps) || 0)));
|
|
5292
|
+
if (!feeAddress || bps === 0n) return 0n;
|
|
5293
|
+
return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
|
|
5294
|
+
}
|
|
5295
|
+
var MESSAGE_OVERHEAD_TOKENS = 4;
|
|
5296
|
+
function estimateInputTokens(messages) {
|
|
5297
|
+
const utf8 = new TextEncoder();
|
|
5298
|
+
let tokens = 3;
|
|
5299
|
+
for (const m of messages ?? []) {
|
|
5300
|
+
tokens += MESSAGE_OVERHEAD_TOKENS;
|
|
5301
|
+
tokens += utf8.encode(String(m?.role ?? "")).length;
|
|
5302
|
+
tokens += utf8.encode(typeof m?.content === "string" ? m.content : JSON.stringify(m?.content ?? null)).length;
|
|
5303
|
+
}
|
|
5304
|
+
return tokens;
|
|
5305
|
+
}
|
|
5306
|
+
function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
|
|
5307
|
+
const estIn = estimateInputTokens(messages);
|
|
5308
|
+
const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
|
|
5309
|
+
if (estInCostUsd >= budgetUsd) return { refuse: true, reason: "input", estIn, estInCostUsd };
|
|
5310
|
+
const outBudgetUsd = budgetUsd - estInCostUsd;
|
|
5311
|
+
let maxTok = contextCeil;
|
|
5312
|
+
if (Number(reqMax) > 0) maxTok = Math.min(maxTok, Math.floor(Number(reqMax)));
|
|
5313
|
+
if (!Number.isFinite(Number(outPrice)) || Number(outPrice) <= 0) {
|
|
5314
|
+
return { refuse: true, reason: "output_price", estIn, estInCostUsd };
|
|
5315
|
+
}
|
|
5316
|
+
maxTok = Math.min(maxTok, Math.floor(outBudgetUsd / Number(outPrice) * 1e6));
|
|
5317
|
+
if (maxTok < 1) return { refuse: true, reason: "output", estIn, estInCostUsd };
|
|
5318
|
+
return { refuse: false, maxTok, estIn, estInCostUsd };
|
|
5319
|
+
}
|
|
5320
|
+
function createServeCore({
|
|
5321
|
+
model,
|
|
5322
|
+
inPrice,
|
|
5323
|
+
outPrice,
|
|
5324
|
+
verifier,
|
|
5325
|
+
redemption,
|
|
5326
|
+
upstream,
|
|
5327
|
+
log,
|
|
5328
|
+
offerId,
|
|
5329
|
+
sellerAgentId,
|
|
5330
|
+
sellerWallet,
|
|
5331
|
+
dripContractAddress,
|
|
5332
|
+
feeRecipient,
|
|
5333
|
+
feeBps,
|
|
5334
|
+
screenPayer
|
|
5335
|
+
}) {
|
|
5336
|
+
const serve = async (body) => {
|
|
5337
|
+
const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
|
|
5338
|
+
const hasRequestNonce = Object.hasOwn(body, "requestNonce");
|
|
5339
|
+
if (!bookingId) return { status: 400, body: { error: "bad_request", detail: "DRAW needs bookingId" } };
|
|
5340
|
+
if (n == null) return { status: 400, body: { error: "bad_request", detail: "DRAW needs a delivery index n (per-booking idempotency key)" } };
|
|
5341
|
+
if (!Number.isSafeInteger(n) || n < 0 || n > 4294967295) {
|
|
5342
|
+
return { status: 400, body: { error: "bad_request", detail: "DRAW delivery index n must be a nonnegative uint32 integer" } };
|
|
5343
|
+
}
|
|
5344
|
+
if (hasRequestNonce && !REQUEST_NONCE_RE.test(requestNonce)) {
|
|
5345
|
+
return { status: 400, body: { error: "bad_request", detail: "DRAW needs requestNonce as 16 random bytes encoded as 0x-prefixed hex" } };
|
|
5346
|
+
}
|
|
5347
|
+
const checked = validateRequest(request, model, { legacy: !hasRequestNonce });
|
|
5348
|
+
if (checked.error) return { status: 400, body: { error: "bad_request", detail: checked.error } };
|
|
5349
|
+
const requestHashScheme = hasRequestNonce ? "nonce-v1" : "legacy-v0";
|
|
5350
|
+
const requestHash = hasRequestNonce ? await hash32({ request, requestNonce }) : await hash32(request);
|
|
5351
|
+
const cacheKey = `${requestHashScheme}:${bookingId}:${n}:${requestHash}`;
|
|
5352
|
+
const oldLegacyKey = hasRequestNonce ? null : `${bookingId}:${n}:${requestHash}`;
|
|
5353
|
+
if (!dripContractAddress) {
|
|
5354
|
+
return { status: 402, body: { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" } };
|
|
5355
|
+
}
|
|
5356
|
+
if (!drawPaidTxHash) return { status: 402, body: { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" } };
|
|
5357
|
+
let paid;
|
|
5358
|
+
try {
|
|
5359
|
+
paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
|
|
5360
|
+
contractAddress: dripContractAddress,
|
|
5361
|
+
buyerAgentId: buyerId,
|
|
5362
|
+
sellerAgentId,
|
|
5363
|
+
// when set, enforces the offer-owner match (#codex review)
|
|
5364
|
+
bookingId,
|
|
5365
|
+
offerId,
|
|
5366
|
+
model,
|
|
5367
|
+
n,
|
|
5368
|
+
requestHash,
|
|
5369
|
+
sellerWallet,
|
|
5370
|
+
feeRecipient,
|
|
5371
|
+
// #580: refuse a payment older than the redemption window. The JSONL
|
|
5372
|
+
// payload cache AND the claim markers are both aged out at boot (#600),
|
|
5373
|
+
// so past retention this age bound is the sole replay defense (its
|
|
5374
|
+
// skip-on-unreadable-block residual is named in redemption.mjs). An
|
|
5375
|
+
// honest retry is seconds-to-minutes old, never days.
|
|
5376
|
+
maxPaidAgeMs: redemption.retentionMs
|
|
5377
|
+
});
|
|
5378
|
+
} catch (e) {
|
|
5379
|
+
return { status: 402, body: { error: "payment_unverified", detail: e.message } };
|
|
5380
|
+
}
|
|
5381
|
+
if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
|
|
5382
|
+
const expectedFee = configuredFeeAtomic({
|
|
5383
|
+
sellerUsdAtomic: paid.event.sellerUsdAtomic,
|
|
5384
|
+
feeAddress: feeRecipient,
|
|
5385
|
+
feeBps
|
|
5386
|
+
});
|
|
5387
|
+
if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
|
|
5388
|
+
return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
|
|
5389
|
+
}
|
|
5390
|
+
if (screenPayer) {
|
|
5391
|
+
try {
|
|
5392
|
+
if (await screenPayer(String(paid.from || "").toLowerCase())) {
|
|
5393
|
+
return { status: 403, body: { error: "payer_denied", detail: "the verified payer wallet is denylisted by this relay" } };
|
|
5394
|
+
}
|
|
5395
|
+
} catch (e) {
|
|
5396
|
+
return { status: 403, body: { error: "payer_denied", detail: "payer screening failed: " + e.message } };
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
let storedKey = cacheKey;
|
|
5400
|
+
let redemptionState = await redemption.state(storedKey);
|
|
5401
|
+
if (!redemptionState && oldLegacyKey) {
|
|
5402
|
+
const oldLegacyState = await redemption.state(oldLegacyKey);
|
|
5403
|
+
if (oldLegacyState) {
|
|
5404
|
+
storedKey = oldLegacyKey;
|
|
5405
|
+
redemptionState = oldLegacyState;
|
|
5406
|
+
} else {
|
|
5407
|
+
redemptionState = await redemption.state(cacheKey);
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
if (redemptionState === "complete") return { status: 200, body: await redemption.get(storedKey) };
|
|
5411
|
+
if (redemptionState === "pending") {
|
|
5412
|
+
return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
|
|
5413
|
+
}
|
|
5414
|
+
const paidEvent = paid.event;
|
|
5415
|
+
const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
|
|
5416
|
+
if (remainingUsd < BALANCE_EPSILON) {
|
|
5417
|
+
return { status: 402, body: { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd } };
|
|
5418
|
+
}
|
|
5419
|
+
const eventInPriceUsd = Number(paidEvent.inputPricePerMTokAtomic || 0) / 1e6;
|
|
5420
|
+
const eventOutPriceUsd = Number(paidEvent.outputPricePerMTokAtomic || 0) / 1e6;
|
|
5421
|
+
const boundInPrice = Math.max(Number(inPrice) || 0, eventInPriceUsd);
|
|
5422
|
+
const boundOutPrice = Math.max(Number(outPrice) || 0, eventOutPriceUsd);
|
|
5423
|
+
const bound = boundServe({ messages: checked.safeRequest.messages, budgetUsd: remainingUsd, inPrice: boundInPrice, outPrice: boundOutPrice, reqMax: checked.safeRequest.max_tokens });
|
|
5424
|
+
if (bound.refuse) {
|
|
5425
|
+
const error = bound.reason === "input" ? "input_too_large" : "output_unfunded";
|
|
5426
|
+
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 } };
|
|
5427
|
+
}
|
|
5428
|
+
const safeRequest = { ...checked.safeRequest, model, max_tokens: bound.maxTok };
|
|
5429
|
+
try {
|
|
5430
|
+
if (!await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey)) {
|
|
5431
|
+
return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
|
|
5432
|
+
}
|
|
5433
|
+
} catch (e) {
|
|
5434
|
+
return { status: 503, body: { error: "redemption_unavailable", detail: `could not durably claim the paid draw: ${e.message}`, _bookingId: bookingId } };
|
|
5435
|
+
}
|
|
5436
|
+
let completion;
|
|
5437
|
+
try {
|
|
5438
|
+
completion = await upstream(safeRequest);
|
|
5439
|
+
} catch (e) {
|
|
5440
|
+
return { status: 502, body: { error: "upstream_error", detail: e.message } };
|
|
5441
|
+
}
|
|
5442
|
+
try {
|
|
5443
|
+
enforceModelEcho(completion.model, model);
|
|
5444
|
+
} catch (e) {
|
|
5445
|
+
return { status: 502, body: { error: "model_mismatch", detail: e.message } };
|
|
5446
|
+
}
|
|
5447
|
+
const usage = completion.usage ?? {};
|
|
5448
|
+
const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
|
|
5449
|
+
const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
|
|
5450
|
+
const usedUsd = (inTok * Number(paidEvent.inputPricePerMTokAtomic || 0) + outTok * Number(paidEvent.outputPricePerMTokAtomic || 0)) / 1e12;
|
|
5451
|
+
const payload = { ...completion, _bookingId: bookingId, remainingUsd: Math.max(0, Math.round((remainingUsd - usedUsd) * 1e6) / 1e6) };
|
|
5452
|
+
try {
|
|
5453
|
+
await redemption.complete(cacheKey, payload);
|
|
5454
|
+
} catch (e) {
|
|
5455
|
+
(log ?? console).error?.(`mtok serve core: completion for ${cacheKey} could not be persisted (${e.message}); retries will remain pending`);
|
|
5456
|
+
}
|
|
5457
|
+
return { status: 200, body: payload };
|
|
5458
|
+
};
|
|
5459
|
+
return { serve };
|
|
5460
|
+
}
|
|
5461
|
+
|
|
5127
5462
|
// bridge/bridge.mjs
|
|
5128
5463
|
function httpUpstream({ baseUrl, key }) {
|
|
5129
5464
|
const url = String(baseUrl || "").replace(/\/$/, "") + "/chat/completions";
|
|
@@ -5162,11 +5497,8 @@ var BASE_SEPOLIA_RPCS = [
|
|
|
5162
5497
|
var rpcUrlsFor = (chainId, override) => override ? [override] : Number(chainId) === 8453 ? BASE_MAINNET_RPCS : BASE_SEPOLIA_RPCS;
|
|
5163
5498
|
|
|
5164
5499
|
// src/runtime.mjs
|
|
5165
|
-
import crypto2 from "node:crypto";
|
|
5166
|
-
var BALANCE_EPSILON = 1e-6;
|
|
5167
|
-
var hash32 = (v) => "0x" + crypto2.createHash("sha256").update(typeof v === "string" ? v : JSON.stringify(v ?? null)).digest("hex");
|
|
5168
5500
|
async function createRelayRuntime(config) {
|
|
5169
|
-
const served = createRedemptionStore({ file: config.redemptionFile });
|
|
5501
|
+
const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
|
|
5170
5502
|
const drawLocks = /* @__PURE__ */ new Map();
|
|
5171
5503
|
const platform = await fetchPlatformConfig(config);
|
|
5172
5504
|
const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress, expectedChainId: platform.chainId });
|
|
@@ -5180,6 +5512,22 @@ async function createRelayRuntime(config) {
|
|
|
5180
5512
|
if (screenPayer && await screenPayer(payer)) return true;
|
|
5181
5513
|
return false;
|
|
5182
5514
|
};
|
|
5515
|
+
const core = createServeCore({
|
|
5516
|
+
model: config.model,
|
|
5517
|
+
inPrice: config.inPrice,
|
|
5518
|
+
outPrice: config.outPrice,
|
|
5519
|
+
verifier,
|
|
5520
|
+
redemption: served,
|
|
5521
|
+
upstream,
|
|
5522
|
+
log: config.log,
|
|
5523
|
+
offerId: config.offerId,
|
|
5524
|
+
sellerAgentId: config.sellerAgentId,
|
|
5525
|
+
sellerWallet: config.settlementAddr,
|
|
5526
|
+
dripContractAddress: platform.dripContractAddress,
|
|
5527
|
+
feeRecipient: platform.feeAddress,
|
|
5528
|
+
feeBps: platform.feeBps,
|
|
5529
|
+
screenPayer: payerDenied
|
|
5530
|
+
});
|
|
5183
5531
|
const withBookingLock = async (bookingId, fn) => {
|
|
5184
5532
|
const previous = drawLocks.get(bookingId) || Promise.resolve();
|
|
5185
5533
|
let release;
|
|
@@ -5198,89 +5546,10 @@ async function createRelayRuntime(config) {
|
|
|
5198
5546
|
if (drawLocks.get(bookingId) === tail) drawLocks.delete(bookingId);
|
|
5199
5547
|
}
|
|
5200
5548
|
};
|
|
5201
|
-
const handleDraw =
|
|
5202
|
-
const
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
return withBookingLock(bookingId, async () => {
|
|
5206
|
-
const requestHash = hash32(request);
|
|
5207
|
-
const cacheKey = `${bookingId}:${n}:${requestHash}`;
|
|
5208
|
-
if (!platform.dripContractAddress) {
|
|
5209
|
-
return send(res, 402, { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" });
|
|
5210
|
-
}
|
|
5211
|
-
if (!drawPaidTxHash) return send(res, 402, { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" });
|
|
5212
|
-
let paid;
|
|
5213
|
-
try {
|
|
5214
|
-
paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
|
|
5215
|
-
contractAddress: platform.dripContractAddress,
|
|
5216
|
-
buyerAgentId: buyerId,
|
|
5217
|
-
sellerAgentId: config.sellerAgentId,
|
|
5218
|
-
// when set, enforces the offer-owner match (#codex review)
|
|
5219
|
-
bookingId,
|
|
5220
|
-
offerId: config.offerId,
|
|
5221
|
-
model: config.model,
|
|
5222
|
-
n,
|
|
5223
|
-
requestHash,
|
|
5224
|
-
sellerWallet: config.settlementAddr,
|
|
5225
|
-
feeRecipient: platform.feeAddress,
|
|
5226
|
-
// #580: refuse a payment older than the redemption window. served.has() is the
|
|
5227
|
-
// primary one-serve guard, but it's pruned by age and lost on an in-memory restart;
|
|
5228
|
-
// this on-chain age bound closes the re-serve hole those cases open (a stale replay
|
|
5229
|
-
// buying a fresh inference). An honest retry is seconds-to-minutes old, never days.
|
|
5230
|
-
maxPaidAgeMs: served.retentionMs
|
|
5231
|
-
});
|
|
5232
|
-
} catch (e) {
|
|
5233
|
-
return send(res, 402, { error: "payment_unverified", detail: e.message });
|
|
5234
|
-
}
|
|
5235
|
-
if (!paid?.ok) return send(res, 402, { error: "payment_unverified", detail: paid?.reason || "unknown" });
|
|
5236
|
-
const expectedFee = configuredFeeAtomic({
|
|
5237
|
-
sellerUsdAtomic: paid.event.sellerUsdAtomic,
|
|
5238
|
-
feeAddress: platform.feeAddress,
|
|
5239
|
-
feeBps: platform.feeBps
|
|
5240
|
-
});
|
|
5241
|
-
if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
|
|
5242
|
-
return send(res, 402, { error: "payment_unverified", detail: "fee_amount_too_low" });
|
|
5243
|
-
}
|
|
5244
|
-
try {
|
|
5245
|
-
if (await payerDenied(String(paid.from || "").toLowerCase())) {
|
|
5246
|
-
return send(res, 403, { error: "payer_denied", detail: "the verified payer wallet is denylisted by this relay" });
|
|
5247
|
-
}
|
|
5248
|
-
} catch (e) {
|
|
5249
|
-
return send(res, 403, { error: "payer_denied", detail: "payer screening failed: " + e.message });
|
|
5250
|
-
}
|
|
5251
|
-
if (served.has(cacheKey)) return send(res, 200, served.get(cacheKey));
|
|
5252
|
-
const paidEvent = paid.event;
|
|
5253
|
-
const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
|
|
5254
|
-
if (remainingUsd <= BALANCE_EPSILON) {
|
|
5255
|
-
return send(res, 402, { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd });
|
|
5256
|
-
}
|
|
5257
|
-
const eventInPriceUsd = Number(paidEvent.inputPricePerMTokAtomic || 0) / 1e6;
|
|
5258
|
-
const inPrice = Math.max(Number(config.inPrice) || 0, eventInPriceUsd);
|
|
5259
|
-
const bound = boundServe({ messages: request?.messages, budgetUsd: remainingUsd, inPrice, outPrice: config.outPrice, reqMax: Number(request?.max_tokens) });
|
|
5260
|
-
if (bound.refuse) {
|
|
5261
|
-
return send(res, 402, { error: "input_too_large", detail: `estimated input (~${bound.estIn} tokens, $${bound.estInCostUsd.toFixed(6)}) meets or exceeds the paid amount ($${remainingUsd}); send a shorter prompt or pay for more`, _bookingId: bookingId, remainingUsd });
|
|
5262
|
-
}
|
|
5263
|
-
const safeRequest = { ...request, max_tokens: bound.maxTok };
|
|
5264
|
-
let completion;
|
|
5265
|
-
try {
|
|
5266
|
-
completion = await upstream(safeRequest);
|
|
5267
|
-
} catch (e) {
|
|
5268
|
-
return send(res, 502, { error: "upstream_error", detail: e.message });
|
|
5269
|
-
}
|
|
5270
|
-
try {
|
|
5271
|
-
enforceModelEcho(completion.model, config.model);
|
|
5272
|
-
} catch (e) {
|
|
5273
|
-
return send(res, 502, { error: "model_mismatch", detail: e.message });
|
|
5274
|
-
}
|
|
5275
|
-
const usage = completion.usage ?? {};
|
|
5276
|
-
const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
|
|
5277
|
-
const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
|
|
5278
|
-
const usedUsd = (inTok * Number(paidEvent.inputPricePerMTokAtomic || 0) + outTok * Number(paidEvent.outputPricePerMTokAtomic || 0)) / 1e12;
|
|
5279
|
-
const payload = { ...completion, _bookingId: bookingId, remainingUsd: Math.max(0, Math.round((remainingUsd - usedUsd) * 1e6) / 1e6) };
|
|
5280
|
-
served.set(cacheKey, payload);
|
|
5281
|
-
return send(res, 200, payload);
|
|
5282
|
-
});
|
|
5283
|
-
};
|
|
5549
|
+
const handleDraw = (body, res) => withBookingLock(String(body?.bookingId ?? ""), async () => {
|
|
5550
|
+
const out = await core.serve(body);
|
|
5551
|
+
return send(res, out.status, out.body);
|
|
5552
|
+
});
|
|
5284
5553
|
return { handleDraw };
|
|
5285
5554
|
}
|
|
5286
5555
|
async function fetchPlatformConfig(config) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtok-relay",
|
|
3
|
-
"version": "0.1
|
|
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.1",
|
|
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"
|