mycomesh-consumer 0.9.7 → 0.9.9
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/package.json +1 -1
- package/src/consumer.mjs +154 -43
- package/src/protocol.mjs +39 -0
- package/src/reputation.mjs +6 -5
package/package.json
CHANGED
package/src/consumer.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname, resolve } from "node:path";
|
|
|
6
6
|
import { rootCertificates } from "node:tls";
|
|
7
7
|
import { addressOf, encodeCall } from "./eip712.mjs";
|
|
8
8
|
import { rpcCall } from "./chain.mjs";
|
|
9
|
-
import { openDelta, openResponse, outputText, prepareRequest, verifiedProvider } from "./protocol.mjs";
|
|
9
|
+
import { openDelta, openResponse, outputText, planRequest, prepareRequest, verifiedProvider } from "./protocol.mjs";
|
|
10
10
|
import { recordRequest } from "./disputes.mjs";
|
|
11
11
|
import { chatStream, responseStream } from "./sse.mjs";
|
|
12
12
|
import { consoleRoutes, localRequestAllowed } from "./console.mjs";
|
|
@@ -16,9 +16,35 @@ import { hashApiKey } from "./tenants.mjs";
|
|
|
16
16
|
import { anthropicError, anthropicStream, fromAnthropic, fromGemini, geminiError, geminiRoute, geminiStream,
|
|
17
17
|
toAnthropic, toGemini } from "./dialects.mjs";
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
19
|
+
// Discovery is served from cache and refreshed in the background once it is older than its fresh period; only
|
|
20
|
+
// data older than its usable period (or none yet) makes a request wait for it.
|
|
21
|
+
const PROVIDER_CACHE_MS = [30_000, 300_000];
|
|
22
|
+
const RELAY_CACHE_MS = [300_000, 3_600_000];
|
|
23
|
+
const REPUTATION_CACHE_MS = [600_000, 3_600_000];
|
|
24
|
+
const REPUTATION_WAIT_MS = 3_000; // a first request ranks without reputation rather than wait longer for it
|
|
25
|
+
const WARM_EVERY_MS = 20_000; // serve: keep discovery fresh and a connection to every Relay open
|
|
26
|
+
|
|
27
|
+
/** A value loaded once at a time, served stale while it is refreshed. */
|
|
28
|
+
class Cached {
|
|
29
|
+
constructor(load, [freshMs, usableMs]) { Object.assign(this, { load, freshMs, usableMs, at: 0, value: undefined, pending: null }); }
|
|
30
|
+
|
|
31
|
+
get() {
|
|
32
|
+
const age = Date.now() - this.at;
|
|
33
|
+
if (this.value !== undefined && age < this.freshMs) return Promise.resolve(this.value);
|
|
34
|
+
if (this.value !== undefined && age < this.usableMs) { this.refresh().catch(() => {}); return Promise.resolve(this.value); }
|
|
35
|
+
return this.refresh();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
refresh() {
|
|
39
|
+
this.pending ||= this.load().then((value) => { Object.assign(this, { value, at: Date.now() }); return value; })
|
|
40
|
+
.finally(() => { this.pending = null; });
|
|
41
|
+
return this.pending;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Consumers of one network (a node and its tenants) share what they discover.
|
|
46
|
+
const DISCOVERY = new WeakMap();
|
|
47
|
+
const VERIFIED = new WeakMap(); // descriptor -> the times its verification holds
|
|
22
48
|
const MAX_BODY = 16 * 1024 * 1024;
|
|
23
49
|
|
|
24
50
|
export function loadNetwork(path) {
|
|
@@ -29,30 +55,63 @@ export function loadNetwork(path) {
|
|
|
29
55
|
}
|
|
30
56
|
|
|
31
57
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
58
|
+
* Kept-alive connections, one agent per Relay origin (and pin): a request to a Relay this Consumer talked to in
|
|
59
|
+
* the last half minute skips the TCP and TLS handshakes (several round trips across the world). Idle sockets
|
|
60
|
+
* are dropped after `keepAlive.idleMs`, well inside the Relay edge's own keep-alive (nginx: 75 s), and a request
|
|
61
|
+
* whose reused socket turns out to have been closed by the Relay before any reply is sent again on a fresh
|
|
62
|
+
* one: a closed idle connection never carried it, and the Relay and Provider refuse a settlement key twice
|
|
63
|
+
* anyway. (Only an agent's own createConnection is honoured, hence the pinned one here.)
|
|
35
64
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
export const keepAlive = { idleMs: 30_000 };
|
|
66
|
+
const AGENTS = new Map();
|
|
67
|
+
|
|
68
|
+
function tlsOptions(target, ca, pin) {
|
|
69
|
+
const key = `${target.protocol}//${target.host}#${pin || ""}#${ca ? "ca" : ""}`;
|
|
70
|
+
let options = AGENTS.get(key);
|
|
71
|
+
if (!options) {
|
|
72
|
+
const settings = { keepAlive: true, timeout: keepAlive.idleMs, scheduling: "lifo" };
|
|
73
|
+
if (target.protocol !== "https:") options = { agent: new HttpAgent(settings) };
|
|
74
|
+
else {
|
|
75
|
+
const agent = new HttpsAgent(settings);
|
|
76
|
+
if (pin) agent.createConnection = pinnedOptions(target.href, pin).createConnection;
|
|
77
|
+
options = pin || !ca ? { agent } : { agent, ca: [...rootCertificates, ca] };
|
|
78
|
+
}
|
|
79
|
+
AGENTS.set(key, options);
|
|
42
80
|
}
|
|
43
|
-
return
|
|
81
|
+
return options;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Drop every kept-alive connection (tests, shutdown). */
|
|
85
|
+
export function closeConnections() {
|
|
86
|
+
for (const { agent } of AGENTS.values()) agent.destroy();
|
|
87
|
+
AGENTS.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const STALE = new Set(["ECONNRESET", "EPIPE", "ECONNABORTED"]);
|
|
91
|
+
|
|
92
|
+
/** Send once; again if a reused connection was already closed by the Relay before anything came back. */
|
|
93
|
+
function withFreshRetry(send) {
|
|
94
|
+
return new Promise((resolvePromise, reject) => {
|
|
95
|
+
const attempt = (tries) => send((error, req, answered) => {
|
|
96
|
+
if (error && req?.reusedSocket && !answered && STALE.has(error.code) && tries < 3) return attempt(tries + 1);
|
|
97
|
+
return error ? reject(error) : undefined;
|
|
98
|
+
}, resolvePromise);
|
|
99
|
+
attempt(0);
|
|
100
|
+
});
|
|
44
101
|
}
|
|
45
102
|
|
|
46
103
|
export async function httpJson(url, { method = "GET", body, ca, pin, timeoutMs = 330_000 } = {}) {
|
|
47
104
|
const target = new URL(url);
|
|
48
105
|
const data = body === undefined ? undefined : Buffer.from(JSON.stringify(body));
|
|
49
106
|
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
50
|
-
const tls =
|
|
51
|
-
return
|
|
107
|
+
const tls = tlsOptions(target, ca, pin);
|
|
108
|
+
return withFreshRetry((fail, resolvePromise) => {
|
|
109
|
+
let answered = false;
|
|
52
110
|
const req = send(target, {
|
|
53
111
|
method, headers: { accept: "application/json", ...(data ? { "content-type": "application/json", "content-length": data.length } : {}) },
|
|
54
112
|
...tls,
|
|
55
113
|
}, (res) => {
|
|
114
|
+
answered = true;
|
|
56
115
|
const chunks = [];
|
|
57
116
|
let size = 0;
|
|
58
117
|
res.on("data", (chunk) => {
|
|
@@ -62,14 +121,14 @@ export async function httpJson(url, { method = "GET", body, ca, pin, timeoutMs =
|
|
|
62
121
|
});
|
|
63
122
|
res.on("end", () => {
|
|
64
123
|
try { resolvePromise({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}") }); }
|
|
65
|
-
catch (error) {
|
|
124
|
+
catch (error) { fail(error, req, true); }
|
|
66
125
|
});
|
|
67
126
|
});
|
|
68
127
|
// A wall-clock deadline, not a socket idle timeout: keep-alive sockets are reused, and an idle timeout
|
|
69
128
|
// left on one by an earlier short request (a 10 s /providers call) would later cut a long answer.
|
|
70
129
|
const timer = setTimeout(() => req.destroy(new Error(`request to ${target.host}${target.pathname} timed out after ${timeoutMs / 1000} s`)), timeoutMs);
|
|
71
130
|
req.on("close", () => clearTimeout(timer));
|
|
72
|
-
req.on("error",
|
|
131
|
+
req.on("error", (error) => fail(error, req, answered));
|
|
73
132
|
if (data) req.write(data);
|
|
74
133
|
req.end();
|
|
75
134
|
});
|
|
@@ -80,13 +139,15 @@ export async function httpNdjson(url, { body, ca, pin, timeoutMs = 340_000, onLi
|
|
|
80
139
|
const target = new URL(url);
|
|
81
140
|
const data = Buffer.from(JSON.stringify(body));
|
|
82
141
|
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
83
|
-
const tls =
|
|
84
|
-
return
|
|
142
|
+
const tls = tlsOptions(target, ca, pin);
|
|
143
|
+
return withFreshRetry((fail, resolvePromise) => {
|
|
144
|
+
let answered = false;
|
|
85
145
|
const req = send(target, {
|
|
86
146
|
method: "POST",
|
|
87
147
|
headers: { accept: "application/x-ndjson", "content-type": "application/json", "content-length": data.length },
|
|
88
148
|
...tls,
|
|
89
149
|
}, (res) => {
|
|
150
|
+
answered = true;
|
|
90
151
|
const streaming = res.statusCode === 200 && String(res.headers["content-type"] || "").includes("ndjson");
|
|
91
152
|
let buffered = "";
|
|
92
153
|
const chunks = [];
|
|
@@ -105,14 +166,14 @@ export async function httpNdjson(url, { body, ca, pin, timeoutMs = 340_000, onLi
|
|
|
105
166
|
});
|
|
106
167
|
res.on("end", () => {
|
|
107
168
|
if (streaming) return resolvePromise({ status: 200, streamed: true });
|
|
108
|
-
try { resolvePromise({ status: res.statusCode, body: JSON.parse(chunks.join("") || "{}") }); } catch (error) {
|
|
169
|
+
try { resolvePromise({ status: res.statusCode, body: JSON.parse(chunks.join("") || "{}") }); } catch (error) { fail(error, req, true); }
|
|
109
170
|
});
|
|
110
171
|
});
|
|
111
172
|
// A wall-clock deadline, not a socket idle timeout: keep-alive sockets are reused, and an idle timeout
|
|
112
173
|
// left on one by an earlier short request (a 10 s /providers call) would later cut a long answer.
|
|
113
174
|
const timer = setTimeout(() => req.destroy(new Error(`request to ${target.host}${target.pathname} timed out after ${timeoutMs / 1000} s`)), timeoutMs);
|
|
114
175
|
req.on("close", () => clearTimeout(timer));
|
|
115
|
-
req.on("error",
|
|
176
|
+
req.on("error", (error) => fail(error, req, answered));
|
|
116
177
|
req.end(data);
|
|
117
178
|
});
|
|
118
179
|
}
|
|
@@ -152,32 +213,55 @@ export class Consumer {
|
|
|
152
213
|
this.fetchJson = fetchJson;
|
|
153
214
|
this.now = now;
|
|
154
215
|
this.deployment = { chainId: network.chain_id, settlement: network.settlement.toLowerCase() };
|
|
155
|
-
|
|
156
|
-
|
|
216
|
+
const shared = DISCOVERY.get(network) || new Map();
|
|
217
|
+
DISCOVERY.set(network, shared);
|
|
218
|
+
if (!shared.has(fetchJson)) {
|
|
219
|
+
shared.set(fetchJson, { relays: new Cached(() => this.loadRelays(), RELAY_CACHE_MS), providers: new Map(),
|
|
220
|
+
reputations: new Cached(() => this.loadReputations(), REPUTATION_CACHE_MS) });
|
|
221
|
+
}
|
|
222
|
+
this.discovery = shared.get(fetchJson);
|
|
157
223
|
}
|
|
158
224
|
|
|
159
225
|
/** On-chain reputation and verified probe verdicts for every Provider in view, cached. */
|
|
160
|
-
|
|
161
|
-
|
|
226
|
+
reputations({ waitMs = Infinity } = {}) {
|
|
227
|
+
const value = this.discovery.reputations.get();
|
|
228
|
+
if (!Number.isFinite(waitMs)) return value;
|
|
229
|
+
return Promise.race([value, new Promise((resolve) => setTimeout(resolve, waitMs, {}).unref())]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async loadReputations() {
|
|
162
233
|
const descriptors = new Map();
|
|
163
234
|
const relaysByOwner = {};
|
|
164
|
-
|
|
235
|
+
await Promise.all((await this.relays()).map(async (relay) => {
|
|
165
236
|
try {
|
|
166
237
|
const owner = relay.owner || `0x${(await rpcCall(this.network.rpc_urls, "eth_call", [{ to: this.network.settlement,
|
|
167
238
|
data: encodeCall("relaySignerOwner(address)", [["address", relay.signer]]) }, "latest"])).slice(26)}`;
|
|
168
239
|
relaysByOwner[owner.toLowerCase()] = relay;
|
|
169
240
|
} catch {}
|
|
170
241
|
try { for (const descriptor of await this.providers(relay)) descriptors.set(descriptor.provider_signer, descriptor); } catch {}
|
|
171
|
-
}
|
|
242
|
+
}));
|
|
172
243
|
let value = {};
|
|
173
244
|
try { value = await reputation(this, [...descriptors.values()], relaysByOwner); } catch {}
|
|
174
|
-
this.reputationCache = { at: Date.now(), value };
|
|
175
245
|
return value;
|
|
176
246
|
}
|
|
177
247
|
|
|
248
|
+
/** Discover now what the first request would otherwise wait for; with `everyMs`, keep it fresh (serve). */
|
|
249
|
+
warm({ everyMs } = {}) {
|
|
250
|
+
const once = async () => {
|
|
251
|
+
try { await Promise.all((await this.relays()).map((relay) => this.providers(relay).catch(() => {}))); } catch {}
|
|
252
|
+
await this.reputations().catch(() => {});
|
|
253
|
+
};
|
|
254
|
+
const first = once();
|
|
255
|
+
if (everyMs) setInterval(once, everyMs).unref();
|
|
256
|
+
return first;
|
|
257
|
+
}
|
|
258
|
+
|
|
178
259
|
/** Manifest Relays first, then directory Relays whose /health proves the announced signer. */
|
|
179
|
-
|
|
180
|
-
|
|
260
|
+
relays() {
|
|
261
|
+
return this.discovery.relays.get();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async loadRelays() {
|
|
181
265
|
const relays = this.network.relays.map((relay) => ({ ...relay }));
|
|
182
266
|
const known = new Set(relays.map((relay) => relay.signer.toLowerCase()));
|
|
183
267
|
try {
|
|
@@ -199,21 +283,30 @@ export class Consumer {
|
|
|
199
283
|
} catch {}
|
|
200
284
|
}
|
|
201
285
|
} catch {}
|
|
202
|
-
this.relayCache = { at: Date.now(), relays };
|
|
203
286
|
return relays;
|
|
204
287
|
}
|
|
205
288
|
|
|
289
|
+
/** A Relay's Providers whose signed descriptors verify now (a cached list is checked again on every use). */
|
|
206
290
|
async providers(relay) {
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
291
|
+
let entry = this.discovery.providers.get(relay.url);
|
|
292
|
+
if (!entry) {
|
|
293
|
+
entry = new Cached(async () => {
|
|
294
|
+
const { status, body } = await this.fetchJson(`${relay.url}/providers`, { ca: this.network.tls_ca, pin: relay.pin, timeoutMs: 10_000 });
|
|
295
|
+
if (status !== 200) throw new Error(`Relay ${relay.url} /providers returned ${status}`);
|
|
296
|
+
return body.providers || [];
|
|
297
|
+
}, PROVIDER_CACHE_MS);
|
|
298
|
+
this.discovery.providers.set(relay.url, entry);
|
|
299
|
+
}
|
|
211
300
|
const now = this.now();
|
|
212
|
-
|
|
213
|
-
|
|
301
|
+
return (await entry.get()).filter((descriptor) => {
|
|
302
|
+
// Signatures do not change: once verified, a descriptor stays good until its key or attestation expires.
|
|
303
|
+
const known = VERIFIED.get(descriptor);
|
|
304
|
+
if (known && known.from <= now && now < known.until) return true;
|
|
305
|
+
try { verifiedProvider(descriptor, this.deployment, now); } catch { return false; }
|
|
306
|
+
VERIFIED.set(descriptor, { from: now, until: Math.min(Number(descriptor.transport_key?.expires_at),
|
|
307
|
+
Number(descriptor.transport_attestation?.expires_at)) });
|
|
308
|
+
return true;
|
|
214
309
|
});
|
|
215
|
-
this.cache.set(relay.url, { at: Date.now(), providers });
|
|
216
|
-
return providers;
|
|
217
310
|
}
|
|
218
311
|
|
|
219
312
|
/** The network's model catalog (manifest "models"): vendor, tier, capabilities. */
|
|
@@ -248,6 +341,7 @@ export class Consumer {
|
|
|
248
341
|
|
|
249
342
|
async attempt({ endpoint, model, content, maxOutputTokens = 4096, options = {}, provider, onDelta }) {
|
|
250
343
|
let lastError;
|
|
344
|
+
let raisedFee = 0;
|
|
251
345
|
for (const relay of await this.relays()) {
|
|
252
346
|
let candidates;
|
|
253
347
|
try {
|
|
@@ -258,7 +352,7 @@ export class Consumer {
|
|
|
258
352
|
// What the chain proves ranks first (recent fraud, a suspected downgrade, verified probe failures); then
|
|
259
353
|
// Providers this Consumer is not already keeping busy; then reputation in coarse bands, shuffled within a
|
|
260
354
|
// band so that many Consumers spread over the network instead of all queueing at its single best Provider.
|
|
261
|
-
const records = await this.reputations().catch(() => ({}));
|
|
355
|
+
const records = await this.reputations({ waitMs: REPUTATION_WAIT_MS }).catch(() => ({}));
|
|
262
356
|
const key = (descriptor) => {
|
|
263
357
|
const [fraud, downgrade, failure, volume] = rankKey(descriptor, records[descriptor.provider_signer]);
|
|
264
358
|
const busy = (this.inFlight.get(descriptor.provider_signer) || 0) >= Math.max(1, Number(descriptor.capacity) || 1) ? 1 : 0;
|
|
@@ -267,10 +361,15 @@ export class Consumer {
|
|
|
267
361
|
const keyed = candidates.map((descriptor) => [key(descriptor), descriptor]);
|
|
268
362
|
keyed.sort((a, b) => compareRank(a[0], b[0]));
|
|
269
363
|
candidates = keyed.map(([, descriptor]) => descriptor);
|
|
270
|
-
|
|
364
|
+
let retried = false;
|
|
365
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
366
|
+
const descriptor = candidates[index];
|
|
367
|
+
// Authorize what this request can really cost, not the Consumer's whole per-request cap.
|
|
368
|
+
const plan = planRequest(descriptor, endpoint, content, maxOutputTokens, this.maxFee);
|
|
369
|
+
const maxFee = Math.min(Number(this.maxFee), Math.max(raisedFee || 0, plan.maxFee));
|
|
271
370
|
const prepared = prepareRequest({
|
|
272
371
|
descriptor, deployment: this.deployment, keyPrivate: this.keyPrivate, relaySigner: relay.signer,
|
|
273
|
-
endpoint, model, content, maxOutputTokens
|
|
372
|
+
endpoint, model, content, maxOutputTokens: plan.maxOutputTokens, maxFee, options, now: this.now(),
|
|
274
373
|
});
|
|
275
374
|
let reply;
|
|
276
375
|
const streamed = [];
|
|
@@ -312,6 +411,17 @@ export class Consumer {
|
|
|
312
411
|
}
|
|
313
412
|
lastError = Object.assign(new Error(reply.body?.error || `Relay returned ${reply.status}`),
|
|
314
413
|
{ status: reply.status, dispatched: reply.body?.dispatched });
|
|
414
|
+
// A Provider that found the max fee too low for this request says what it needs; ask again with that.
|
|
415
|
+
const needed = /worst-case price (\d+)/.exec(String(reply.body?.error || ""));
|
|
416
|
+
if (needed && reply.body?.dispatched === false) {
|
|
417
|
+
const required = Number(needed[1]);
|
|
418
|
+
if (required > Number(this.maxFee)) {
|
|
419
|
+
throw Object.assign(new Error(`this request may cost up to ${required}, above the per-request limit ${this.maxFee}`), { status: 402 });
|
|
420
|
+
}
|
|
421
|
+
raisedFee = Math.ceil(required * 1.2);
|
|
422
|
+
if (!retried) { retried = true; index -= 1; } // the same Provider, once, with the fee it asked for
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
315
425
|
if (reply.body?.dispatched !== false || ![502, 503].includes(reply.status)) throw lastError;
|
|
316
426
|
}
|
|
317
427
|
}
|
|
@@ -362,6 +472,7 @@ async function dialectRequest(req, res, active, path, setWriter) {
|
|
|
362
472
|
|
|
363
473
|
/** Local OpenAI-compatible endpoint (plus the Anthropic and Gemini dialects); nothing leaves this machine unsealed except pricing and routing. */
|
|
364
474
|
export function serveConsumer(consumer, { host = "127.0.0.1", port = 8110, apiKey, dataDir, tenants = () => ({}) } = {}) {
|
|
475
|
+
consumer.warm({ everyMs: WARM_EVERY_MS }).catch(() => {}); // tenants share it
|
|
365
476
|
const routes = dataDir ? consoleRoutes({ consumer, dataDir }) : {};
|
|
366
477
|
// Every dialect's way of presenting a key: OpenAI Bearer, Anthropic x-api-key, Gemini x-goog-api-key or ?key=.
|
|
367
478
|
const bearer = (req) => /^Bearer (.+)$/.exec(String(req.headers.authorization || ""))?.[1]
|
package/src/protocol.mjs
CHANGED
|
@@ -111,3 +111,42 @@ export function openResponse(prepared, result, deployment, now = Math.floor(Date
|
|
|
111
111
|
}
|
|
112
112
|
return { response, receipt: signed, responsePlaintext: opened.payload };
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* (input, output) tokens a request can cost at most, estimated exactly as the Provider does
|
|
117
|
+
* (mycomesh/protocol.py worst_case_tokens): content UTF-8 length / 3, plus the backend's own input, plus
|
|
118
|
+
* the full max_output_tokens.
|
|
119
|
+
*/
|
|
120
|
+
export function worstCaseTokens(endpoint, content, maxOutputTokens, inputOverheadTokens = 0) {
|
|
121
|
+
const bytes = Buffer.byteLength(JSON.stringify(content ?? null), "utf8");
|
|
122
|
+
return [Math.ceil(bytes / 3) + Number(inputOverheadTokens || 0), Number(maxOutputTokens)];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The max fee to authorize for one request, and the output it may use: its worst-case price at the Provider's
|
|
127
|
+
* advertised network price with 50% headroom (a day's price step, estimation error), never above the
|
|
128
|
+
* Consumer's own cap. Relays reserve this amount while the request runs, so a realistic figure is what lets
|
|
129
|
+
* many requests share one deposit and one Provider's daily capacity at once. When even the worst case does
|
|
130
|
+
* not fit the cap, the output is trimmed to what the cap pays for, rather than having the Provider refuse.
|
|
131
|
+
*/
|
|
132
|
+
export function planRequest(descriptor, endpoint, content, maxOutputTokens, cap) {
|
|
133
|
+
const prices = descriptor.prices || {};
|
|
134
|
+
const inPrice = Number(prices.input_per_1k || 0);
|
|
135
|
+
const outPrice = Number(prices.output_per_1k || 0);
|
|
136
|
+
const floor = Math.max(Number(prices.minimum_fee || 0), 1);
|
|
137
|
+
const [input] = worstCaseTokens(endpoint, content, 0, descriptor.input_overhead_tokens);
|
|
138
|
+
const worst = (output) => Math.max(Math.ceil((input * inPrice + output * outPrice) / 1000), floor);
|
|
139
|
+
let output = Number(maxOutputTokens);
|
|
140
|
+
if (worst(output) > Number(cap) && outPrice > 0) {
|
|
141
|
+
// What the cap pays for, keeping 10% for a price step between authorization and settlement.
|
|
142
|
+
output = Math.floor(((Number(cap) / 1.1) * 1000 - input * inPrice) / outPrice);
|
|
143
|
+
if (output < 16) {
|
|
144
|
+
throw Object.assign(new Error(`the per-request limit ${cap} cannot pay for this request at today's price`), { status: 402 });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { maxFee: Math.min(Number(cap), Math.ceil(worst(output) * 1.5)), maxOutputTokens: output };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function pricedMaxFee(descriptor, endpoint, content, maxOutputTokens, cap) {
|
|
151
|
+
return planRequest(descriptor, endpoint, content, maxOutputTokens, cap).maxFee;
|
|
152
|
+
}
|
package/src/reputation.mjs
CHANGED
|
@@ -48,11 +48,12 @@ async function ledgerEvents(network) {
|
|
|
48
48
|
export async function reputation(consumer, descriptors, relaysByOwner) {
|
|
49
49
|
const network = consumer.network;
|
|
50
50
|
const records = {};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
const [, events] = await Promise.all([
|
|
52
|
+
Promise.all(descriptors.map(async (descriptor) => {
|
|
53
|
+
try { records[descriptor.provider_signer] = await providerRecord(network, descriptor.provider_signer); } catch {}
|
|
54
|
+
})),
|
|
55
|
+
ledgerEvents(network).catch(() => []),
|
|
56
|
+
]);
|
|
56
57
|
const byOwner = {};
|
|
57
58
|
for (const record of Object.values(records)) {
|
|
58
59
|
byOwner[record.owner] = { ...record, probes_passed: 0, probes_failed_verified: 0, capability_passed: 0, capability_failed_verified: 0 };
|