mycomesh-consumer 0.7.0 → 0.8.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/networks/mycomesh-v11-sepolia.json +3 -2
- package/package.json +1 -1
- package/src/probes.mjs +102 -2
- package/src/reputation.mjs +29 -10
- package/src/web/console.html +11 -3
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"stablecoin": "0xbce27efad4191277167fe76819581a97959048cf",
|
|
7
7
|
"registry": "0x0ff3210e8e8abf5f5bfa73a39e60db7416b6c8c7",
|
|
8
8
|
"relay_directory": "0xa0aaad9393f51cb5786c69f152133a174588bdab",
|
|
9
|
-
"probe_ledger": "
|
|
9
|
+
"probe_ledger": "0x3bbc6fc971a6c4e528af9228b95d4b83ef188795",
|
|
10
10
|
"emission": "0x85ee0e8f63400041f0c76010c590c330bc135a8d",
|
|
11
11
|
"token": "0x620d026ad854fb17c7b16534a0f28ef5499e2d4a",
|
|
12
12
|
"emission_block": 11815544,
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"name": "OpenAI frontier",
|
|
16
16
|
"models": [
|
|
17
17
|
"gpt-5.5"
|
|
18
|
-
]
|
|
18
|
+
],
|
|
19
|
+
"capability_floor": 0.85
|
|
19
20
|
},
|
|
20
21
|
"2": {
|
|
21
22
|
"name": "Anthropic Claude",
|
package/package.json
CHANGED
package/src/probes.mjs
CHANGED
|
@@ -4,9 +4,108 @@ import { verifySignedReceipt } from "./eip712.mjs";
|
|
|
4
4
|
|
|
5
5
|
export const PROBE_EVIDENCE_SCHEMA = "mycomesh.v11.probe-evidence.v1";
|
|
6
6
|
const WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
7
|
-
const numbers = (text) => (text.match(
|
|
7
|
+
const numbers = (text) => (text.match(/[0-9]{1,3}(?:[,\s_][0-9]{3})+(?![0-9])|[0-9]+/g) || []).map((match) => match.replace(/[,\s_]/g, ""));
|
|
8
|
+
|
|
9
|
+
// ---------------- capability probes (mirrors mycomesh/capability.py) ----------------
|
|
10
|
+
|
|
11
|
+
export const CAPABILITY_KINDS = ["trace", "crt", "long_multiply", "path", "kth", "letters", "calendar"];
|
|
12
|
+
/** Default per-tier pass-rate floors; a manifest tier's capability_floor overrides them. */
|
|
13
|
+
export const CAPABILITY_FLOORS = { 1: 0.85 }; // tier 2 (Claude) has none until calibrated
|
|
14
|
+
|
|
15
|
+
export function ordinal(n) {
|
|
16
|
+
const suffix = n % 100 >= 10 && n % 100 <= 20 ? "th" : ({ 1: "st", 2: "nd", 3: "rd" })[n % 10] || "th";
|
|
17
|
+
return `${n}${suffix}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LINE_BREAKS = /\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/;
|
|
21
|
+
const weekdays = (text) => WEEKDAYS.filter((day) => text.toLowerCase().includes(day.toLowerCase()));
|
|
22
|
+
|
|
23
|
+
function weekdayAfter(start, offset) {
|
|
24
|
+
const day = new Date(`${start}T00:00:00Z`);
|
|
25
|
+
day.setUTCDate(day.getUTCDate() + Number(offset));
|
|
26
|
+
return WEEKDAYS[(day.getUTCDay() + 6) % 7];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function capabilityTask(kind, p) {
|
|
30
|
+
if (kind === "trace") {
|
|
31
|
+
let [x, y] = [Number(p.x), Number(p.y)];
|
|
32
|
+
const [a, m, n] = [Number(p.a), Number(p.m), Number(p.n)];
|
|
33
|
+
const question = `Start with x = ${x} and y = ${y}. Repeat the following ${n} times: first set x to (x * ${a} + y) mod ${m}, `
|
|
34
|
+
+ `then set y to (y + 2 * x) mod ${m}. What is x + y at the end?`;
|
|
35
|
+
for (let i = 0; i < n; i += 1) { x = (x * a + y) % m; y = (y + 2 * x) % m; }
|
|
36
|
+
return { question, reference: String(x + y) };
|
|
37
|
+
}
|
|
38
|
+
if (kind === "crt") {
|
|
39
|
+
const moduli = p.moduli.map(Number); const residues = p.residues.map(Number);
|
|
40
|
+
const parts = moduli.map((m, i) => `remainder ${residues[i]} when divided by ${m}`);
|
|
41
|
+
const question = `What is the smallest positive integer that leaves ${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}?`;
|
|
42
|
+
// Constructive CRT (the moduli are distinct primes): the least positive solution.
|
|
43
|
+
const big = moduli.map(BigInt);
|
|
44
|
+
const product = big.reduce((a, b) => a * b, 1n);
|
|
45
|
+
const power = (base, exponent, m) => { let r = 1n; base %= m; for (; exponent > 0n; exponent >>= 1n, base = base * base % m) if (exponent & 1n) r = r * base % m; return r; };
|
|
46
|
+
const x = big.reduce((sum, m, i) => sum + BigInt(residues[i]) * (product / m) * power(product / m, m - 2n, m), 0n) % product;
|
|
47
|
+
return { question, reference: String(x || product) };
|
|
48
|
+
}
|
|
49
|
+
if (kind === "long_multiply") return { question: `What is ${p.a} times ${p.b}?`, reference: String(BigInt(p.a) * BigInt(p.b)) };
|
|
50
|
+
if (kind === "path") {
|
|
51
|
+
const edges = p.edges.map(([u, v, w]) => [String(u), String(v), Number(w)]);
|
|
52
|
+
const question = `In a road network the two-way roads and their lengths are: ${edges.map(([u, v, w]) => `${u}-${v} ${w}`).join(", ")}. `
|
|
53
|
+
+ `What is the length of the shortest route from ${p.start} to ${p.end}?`;
|
|
54
|
+
const distance = new Map([[String(p.start), 0]]); const done = new Set();
|
|
55
|
+
for (;;) {
|
|
56
|
+
const open = [...distance].filter(([node]) => !done.has(node)).sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1));
|
|
57
|
+
if (!open.length) break;
|
|
58
|
+
const [node, d] = open[0];
|
|
59
|
+
done.add(node);
|
|
60
|
+
for (const [u, v, w] of edges) {
|
|
61
|
+
for (const [here, there] of [[u, v], [v, u]]) {
|
|
62
|
+
if (here === node && d + w < (distance.get(there) ?? Infinity)) distance.set(there, d + w);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { question, reference: String(distance.get(String(p.end))) };
|
|
67
|
+
}
|
|
68
|
+
if (kind === "kth") {
|
|
69
|
+
const values = p.values.map(Number); const k = Number(p.k);
|
|
70
|
+
return { question: `What is the ${ordinal(k)} smallest number in this list: ${values.join(", ")}?`,
|
|
71
|
+
reference: String([...values].sort((a, b) => a - b)[k - 1]) };
|
|
72
|
+
}
|
|
73
|
+
if (kind === "letters") {
|
|
74
|
+
return { question: `How many times does the letter "${p.letter}" appear in the following text: "${p.text}"?`,
|
|
75
|
+
reference: String(String(p.text).split(String(p.letter)).length - 1) };
|
|
76
|
+
}
|
|
77
|
+
if (kind === "calendar") {
|
|
78
|
+
return { question: `What day of the week will it be ${p.offset} days after ${p.start}?`, reference: weekdayAfter(p.start, p.offset) };
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`unknown capability kind ${kind}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function gradeCapability(task, answer) {
|
|
84
|
+
// The answer finally stated decides: the last \boxed{...}, else the last line that states a value.
|
|
85
|
+
// A handful of values at most: listing candidates is not answering.
|
|
86
|
+
const text = String(answer);
|
|
87
|
+
const pick = task.kind === "calendar" ? weekdays : numbers;
|
|
88
|
+
const boxed = [...text.matchAll(/\\boxed\{([^{}]*)\}/g)];
|
|
89
|
+
let stated = [];
|
|
90
|
+
if (boxed.length) stated = pick(boxed[boxed.length - 1][1]);
|
|
91
|
+
else for (const line of text.split(LINE_BREAKS).reverse()) { const found = pick(line); if (found.length) { stated = found; break; } }
|
|
92
|
+
return stated.length <= (task.kind === "calendar" ? 1 : 3) && stated.includes(task.reference) ? "pass" : "wrong";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Upper bound of a pass rate at 99% one-sided confidence (Wilson). */
|
|
96
|
+
export function wilsonUpper(passes, total, z = 2.326) {
|
|
97
|
+
if (!total) return 1;
|
|
98
|
+
const p = passes / total;
|
|
99
|
+
const centre = p + (z * z) / (2 * total);
|
|
100
|
+
const spread = z * Math.sqrt((p * (1 - p)) / total + (z * z) / (4 * total * total));
|
|
101
|
+
return Math.min(1, (centre + spread) / (1 + (z * z) / total));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 99% confident the pass rate is below the floor, after enough probes. */
|
|
105
|
+
export const capabilityFlagged = (passes, total, floor, minimum = 20) => total >= minimum && wilsonUpper(passes, total) < floor;
|
|
8
106
|
|
|
9
107
|
export function buildTask(kind, params) {
|
|
108
|
+
if (CAPABILITY_KINDS.includes(kind)) return { kind, capability: true, ...capabilityTask(kind, params) };
|
|
10
109
|
if (kind === "multiply") return { kind, question: `What is ${params.a} multiplied by ${params.b}?`, reference: String(BigInt(params.a) * BigInt(params.b)), numeric: true };
|
|
11
110
|
if (kind === "sum") {
|
|
12
111
|
return { kind, question: `What is the sum of ${params.values.join(", ")}?`, reference: String(params.values.reduce((a, b) => a + Number(b), 0)), numeric: true };
|
|
@@ -26,6 +125,7 @@ export function buildTask(kind, params) {
|
|
|
26
125
|
}
|
|
27
126
|
|
|
28
127
|
export function grade(task, answer) {
|
|
128
|
+
if (task.capability) return gradeCapability(task, answer);
|
|
29
129
|
const text = String(answer).trim();
|
|
30
130
|
if (!text) return "unrelated";
|
|
31
131
|
if (task.numeric) {
|
|
@@ -66,5 +166,5 @@ export function verifyProbeEvidence(evidence, deployment) {
|
|
|
66
166
|
}
|
|
67
167
|
const verdict = grade(task, outputText(JSON.parse(response.toString("utf8")).output));
|
|
68
168
|
if (verdict !== evidence.verdict || verdict === "unrelated") throw new Error(`re-grading gives ${verdict}`);
|
|
69
|
-
return { provider: signed.authorization.provider_signer, verdict };
|
|
169
|
+
return { provider: signed.authorization.provider_signer, verdict, capability: Boolean(task.capability) };
|
|
70
170
|
}
|
package/src/reputation.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { encodeCall, keccak, hex } from "./eip712.mjs";
|
|
3
3
|
import { rpcCall } from "./chain.mjs";
|
|
4
4
|
import { evidenceHash } from "./disputes.mjs";
|
|
5
|
-
import { verifyProbeEvidence } from "./probes.mjs";
|
|
5
|
+
import { CAPABILITY_FLOORS, capabilityFlagged, verifyProbeEvidence } from "./probes.mjs";
|
|
6
6
|
|
|
7
7
|
const PROBE_RECORDED = hex(keccak(Buffer.from("ProbeRecorded(address,address,bytes32,bytes32,uint8)")));
|
|
8
8
|
const LOOKBACK_BLOCKS = 50_400n; // about 7 days of 12-second blocks
|
|
@@ -10,6 +10,9 @@ const LOG_CHUNK = 10_000n;
|
|
|
10
10
|
const FRAUD_COOLDOWN = 30 * 86_400;
|
|
11
11
|
const MAX_VERIFIED = 20; // evidence downloads per refresh
|
|
12
12
|
|
|
13
|
+
// Ledger codes: basic probes 1/2, capability probes (is the advertised model really served?) 3/4.
|
|
14
|
+
const VERDICT_CODES = { 1: { verdict: "pass", capability: false }, 2: { verdict: "wrong", capability: false },
|
|
15
|
+
3: { verdict: "pass", capability: true }, 4: { verdict: "wrong", capability: true } };
|
|
13
16
|
const word = (raw, index) => BigInt(`0x${raw.slice(2 + index * 64, 2 + (index + 1) * 64)}`);
|
|
14
17
|
const address = (topic) => `0x${topic.slice(26)}`.toLowerCase();
|
|
15
18
|
|
|
@@ -31,7 +34,7 @@ async function ledgerEvents(network) {
|
|
|
31
34
|
for (const log of await rpcCall(network.rpc_urls, "eth_getLogs", [{ address: network.probe_ledger, topics: [PROBE_RECORDED],
|
|
32
35
|
fromBlock: `0x${from.toString(16)}`, toBlock: `0x${to.toString(16)}` }])) {
|
|
33
36
|
events.push({ provider: address(log.topics[1]), relay: address(log.topics[2]), key: log.topics[3],
|
|
34
|
-
evidence: `0x${log.data.slice(2, 66)}`,
|
|
37
|
+
evidence: `0x${log.data.slice(2, 66)}`, ...VERDICT_CODES[Number(BigInt(`0x${log.data.slice(66, 130)}`))] });
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
return events;
|
|
@@ -50,34 +53,50 @@ export async function reputation(consumer, descriptors, relaysByOwner) {
|
|
|
50
53
|
let events = [];
|
|
51
54
|
try { events = await ledgerEvents(network); } catch {}
|
|
52
55
|
const byOwner = {};
|
|
53
|
-
for (const record of Object.values(records))
|
|
56
|
+
for (const record of Object.values(records)) {
|
|
57
|
+
byOwner[record.owner] = { ...record, probes_passed: 0, probes_failed_verified: 0, capability_passed: 0, capability_failed_verified: 0 };
|
|
58
|
+
}
|
|
54
59
|
let verified = 0;
|
|
55
60
|
for (const event of events) {
|
|
56
61
|
const entry = byOwner[event.provider];
|
|
57
|
-
if (!entry) continue;
|
|
58
|
-
if (event.verdict === "pass") { entry.probes_passed += 1; continue; }
|
|
62
|
+
if (!entry || !event.verdict) continue;
|
|
63
|
+
if (event.verdict === "pass") { entry[event.capability ? "capability_passed" : "probes_passed"] += 1; continue; }
|
|
59
64
|
const relay = relaysByOwner[event.relay];
|
|
60
65
|
if (!relay || verified >= MAX_VERIFIED) continue;
|
|
61
66
|
verified += 1;
|
|
62
67
|
try {
|
|
63
68
|
const { status, body } = await consumer.fetchJson(`${relay.url}/v11/evidence/${event.evidence}`,
|
|
64
69
|
{ ca: network.tls_ca, pin: relay.pin, timeoutMs: 8_000 });
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
const checked = status === 200 && evidenceHash(body) === event.evidence ? verifyProbeEvidence(body, consumer.deployment) : null;
|
|
71
|
+
if (checked?.verdict === "wrong" && checked.capability === event.capability) {
|
|
72
|
+
entry[event.capability ? "capability_failed_verified" : "probes_failed_verified"] += 1;
|
|
67
73
|
}
|
|
68
74
|
} catch {}
|
|
69
75
|
}
|
|
70
|
-
return Object.fromEntries(
|
|
76
|
+
return Object.fromEntries(descriptors.filter((d) => records[d.provider_signer]).map((descriptor) => {
|
|
77
|
+
const entry = { ...byOwner[records[descriptor.provider_signer].owner] };
|
|
78
|
+
const tier = Number(descriptor.tier || 0);
|
|
79
|
+
const floor = network.tiers?.[tier]?.capability_floor ?? CAPABILITY_FLOORS[tier];
|
|
80
|
+
const graded = entry.capability_passed + entry.capability_failed_verified;
|
|
81
|
+
// Passes are counted as reported; failures only once re-graded here, so no Relay can frame a Provider.
|
|
82
|
+
entry.downgrade_suspected = floor !== undefined && capabilityFlagged(entry.capability_passed, graded, floor);
|
|
83
|
+
entry.capability_floor = floor ?? null;
|
|
84
|
+
return [descriptor.provider_signer, entry];
|
|
85
|
+
}));
|
|
71
86
|
}
|
|
72
87
|
|
|
73
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Lower is better: recent confirmed fraud last, then a suspected model downgrade, then the verified probe
|
|
90
|
+
* failure rate, then reputation, then price.
|
|
91
|
+
*/
|
|
74
92
|
export function rankKey(descriptor, record, now = Math.floor(Date.now() / 1000)) {
|
|
75
93
|
const fraud = record && record.last_fraud_at && now < record.last_fraud_at + FRAUD_COOLDOWN ? 1 : 0;
|
|
94
|
+
const downgrade = record?.downgrade_suspected ? 1 : 0;
|
|
76
95
|
const probes = record ? record.probes_passed + record.probes_failed_verified : 0;
|
|
77
96
|
const failure = record && probes ? Math.round((10 * record.probes_failed_verified) / probes) : 0;
|
|
78
97
|
const volume = record ? -Number(BigInt(record.counted_volume) / 1000n) : 0;
|
|
79
98
|
const price = descriptor.prices.output_per_1k * 1e6 + descriptor.prices.input_per_1k;
|
|
80
|
-
return [fraud, failure, volume, price];
|
|
99
|
+
return [fraud, downgrade, failure, volume, price];
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
export function compareRank(a, b) {
|
package/src/web/console.html
CHANGED
|
@@ -381,13 +381,21 @@ async function refreshNetwork() {
|
|
|
381
381
|
const card = document.createElement("div"); card.className = "card"; card.style.marginBottom = "12px";
|
|
382
382
|
const reputation = (r) => r ? `${fmt(r.counted_volume)} · ${r.counterparties} 个付款方` : "—";
|
|
383
383
|
const probes = (r) => r ? `通过 ${r.probes_passed} · 验证失败 ${r.probes_failed_verified}` : "—";
|
|
384
|
+
// Capability probes: hard tasks the advertised model passes and a cheaper substitute tends to miss.
|
|
385
|
+
const capability = (r) => {
|
|
386
|
+
if (!r) return "—";
|
|
387
|
+
const graded = r.capability_passed + r.capability_failed_verified;
|
|
388
|
+
if (!graded) return `<span class="muted">暂无</span>`;
|
|
389
|
+
const text = `${r.capability_passed}/${graded}`;
|
|
390
|
+
return r.downgrade_suspected ? `<span class="chip disputed" title="通过率在 99% 置信度下低于该档位下限 ${Math.round(r.capability_floor * 100)}%">疑似降级 ${text}</span>` : text;
|
|
391
|
+
};
|
|
384
392
|
const fraud = (r) => r && r.last_fraud_at && Date.now() / 1000 < r.last_fraud_at + 30 * 86400 ? `<span class="chip disputed">近期欺诈</span>` : "无";
|
|
385
393
|
card.innerHTML = `<div class="row" style="margin:0"><strong>${relay.url}</strong><span class="chip ${relay.healthy ? "released" : "disputed"}">${relay.healthy ? "在线" : "离线"}</span>
|
|
386
394
|
<span class="muted mono">签名 ${short(relay.signer)}</span>${relay.pinned ? `<span class="chip">证书指纹已上链</span>` : ""}</div>
|
|
387
|
-
<table style="margin-top:8px"><thead><tr><th>Provider</th><th>模型</th><th>输入</th><th>输出</th><th>最低</th><th>信誉(计入成交)</th><th>探针</th><th>欺诈</th></tr></thead><tbody>${relay.providers.map((p) =>
|
|
395
|
+
<table style="margin-top:8px"><thead><tr><th>Provider</th><th>模型</th><th>输入</th><th>输出</th><th>最低</th><th>信誉(计入成交)</th><th>探针</th><th>能力探针</th><th>欺诈</th></tr></thead><tbody>${relay.providers.map((p) =>
|
|
388
396
|
`<tr><td class="mono">${short(p.signer)}</td><td>${p.models.join(", ")}</td><td>${fmt(p.prices.input_per_1k)}</td><td>${fmt(p.prices.output_per_1k)}</td><td>${fmt(p.prices.minimum_fee)}</td>
|
|
389
|
-
<td>${reputation(p.reputation)}</td><td>${probes(p.reputation)}</td><td>${fraud(p.reputation)}</td></tr>`).join("")
|
|
390
|
-
|| `<tr><td colspan="
|
|
397
|
+
<td>${reputation(p.reputation)}</td><td>${probes(p.reputation)}</td><td>${capability(p.reputation)}</td><td>${fraud(p.reputation)}</td></tr>`).join("")
|
|
398
|
+
|| `<tr><td colspan="9" class="muted">没有已连接的 Provider</td></tr>`}</tbody></table>`;
|
|
391
399
|
$("network").append(card);
|
|
392
400
|
}
|
|
393
401
|
}
|