mycomesh-consumer 0.9.6 → 0.9.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mycomesh-consumer",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "MycoMesh V11 Consumer: a custodied deposit, relay-blind encrypted requests to any Provider, and a local OpenAI-compatible endpoint.",
5
5
  "repository": {
6
6
  "type": "git",
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";
@@ -248,6 +248,7 @@ export class Consumer {
248
248
 
249
249
  async attempt({ endpoint, model, content, maxOutputTokens = 4096, options = {}, provider, onDelta }) {
250
250
  let lastError;
251
+ let raisedFee = 0;
251
252
  for (const relay of await this.relays()) {
252
253
  let candidates;
253
254
  try {
@@ -267,10 +268,15 @@ export class Consumer {
267
268
  const keyed = candidates.map((descriptor) => [key(descriptor), descriptor]);
268
269
  keyed.sort((a, b) => compareRank(a[0], b[0]));
269
270
  candidates = keyed.map(([, descriptor]) => descriptor);
270
- for (const descriptor of candidates) {
271
+ let retried = false;
272
+ for (let index = 0; index < candidates.length; index += 1) {
273
+ const descriptor = candidates[index];
274
+ // Authorize what this request can really cost, not the Consumer's whole per-request cap.
275
+ const plan = planRequest(descriptor, endpoint, content, maxOutputTokens, this.maxFee);
276
+ const maxFee = Math.min(Number(this.maxFee), Math.max(raisedFee || 0, plan.maxFee));
271
277
  const prepared = prepareRequest({
272
278
  descriptor, deployment: this.deployment, keyPrivate: this.keyPrivate, relaySigner: relay.signer,
273
- endpoint, model, content, maxOutputTokens, maxFee: this.maxFee, options, now: this.now(),
279
+ endpoint, model, content, maxOutputTokens: plan.maxOutputTokens, maxFee, options, now: this.now(),
274
280
  });
275
281
  let reply;
276
282
  const streamed = [];
@@ -312,6 +318,17 @@ export class Consumer {
312
318
  }
313
319
  lastError = Object.assign(new Error(reply.body?.error || `Relay returned ${reply.status}`),
314
320
  { status: reply.status, dispatched: reply.body?.dispatched });
321
+ // A Provider that found the max fee too low for this request says what it needs; ask again with that.
322
+ const needed = /worst-case price (\d+)/.exec(String(reply.body?.error || ""));
323
+ if (needed && reply.body?.dispatched === false) {
324
+ const required = Number(needed[1]);
325
+ if (required > Number(this.maxFee)) {
326
+ throw Object.assign(new Error(`this request may cost up to ${required}, above the per-request limit ${this.maxFee}`), { status: 402 });
327
+ }
328
+ raisedFee = Math.ceil(required * 1.2);
329
+ if (!retried) { retried = true; index -= 1; } // the same Provider, once, with the fee it asked for
330
+ continue;
331
+ }
315
332
  if (reply.body?.dispatched !== false || ![502, 503].includes(reply.status)) throw lastError;
316
333
  }
317
334
  }
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
+ }