@opennodes/registry 0.1.13 → 0.1.15

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/stageb.js +48 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennodes/registry",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "OpenNodes (ONP) reference registry: node registration with proof-of-resource qualification (Stage A/B/C), transparent ranking, enforceable estimates, catalog importers (OpenRouter, Hugging Face, models.dev, Ollama), MCP surface, and the web app (catalog, chat, advisor, Node Builder)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/stageb.js CHANGED
@@ -62,41 +62,63 @@ async function probeIdentity(safeFetch, card, offering, refs) {
62
62
  return { ok: matches === refs.length, matches, total: refs.length, ...(stale ? { stale: true } : {}) };
63
63
  }
64
64
 
65
- /** A needle counts as retrieved if the reply carries the full token or just its digits (small models drop the prefix). */
66
- export function needleFound(text, needle) {
65
+ /** The needle counts as retrieved if its number appears in the reply as a whole number. */
66
+ export function needleFound(text, digits) {
67
67
  if (typeof text !== 'string') return false;
68
- if (text.includes(needle)) return true;
69
- const digits = needle.split(':')[1];
70
- return new RegExp(`(^|[^0-9])${digits}([^0-9]|$)`).test(text);
68
+ const number = String(digits).split(':').pop();
69
+ return new RegExp(`(^|[^0-9])${number}([^0-9]|$)`).test(text);
71
70
  }
72
71
 
73
- /** DS-ADM-03: needle retrieval at 25/50/90% of the claimed window; returns the verified cap. */
72
+ const FILLER = 'lorem ipsum context filler segment. ';
73
+ const fill = (chars) => FILLER.repeat(Math.ceil(Math.max(0, chars) / FILLER.length)).slice(0, Math.max(0, chars));
74
+ const QUESTION = '\n\nWhat is the magic number mentioned in the text above? Reply with the number only.';
75
+
76
+ /** Characters per token for this model's tokenizer, measured from a short prompt's reported usage. */
77
+ async function calibrate(safeFetch, card, offering) {
78
+ const chars = 4000;
79
+ try {
80
+ const { status, usage } = await chatOnce(safeFetch, card, offering, `${fill(chars)}\n\nReply with OK.`, 4);
81
+ const reported = usage?.prompt_tokens;
82
+ if (status === 200 && reported > 0) return { charsPerToken: chars / reported, measured: true };
83
+ } catch { /* fall through to the default */ }
84
+ return { charsPerToken: 4, measured: false };
85
+ }
86
+
87
+ /**
88
+ * DS-ADM-03: retrieval at 25/50/90% of the claimed window; returns the verified cap.
89
+ * - Prompts are sized in the model's own tokens (calibrated from reported usage), not a chars/4 guess.
90
+ * - The fact sits near the start: engines that silently drop the front of an over-long prompt lose it,
91
+ * and a reported prompt_tokens well short of the target is failed as a cut prompt.
92
+ * - The question is plain ("the magic number"): small models refuse or mangle odd tokens like NEEDLE.
93
+ * - The cap is the largest size with every smaller size passing, so one lucky answer can't vouch for a window.
94
+ */
74
95
  async function probeContext(safeFetch, card, offering) {
75
96
  const claimed = offering.serving?.context_window ?? 4096;
97
+ const { charsPerToken, measured } = await calibrate(safeFetch, card, offering);
76
98
  const results = [];
77
- let cap = 0;
99
+ let cap = 0, unbroken = true;
78
100
  for (const fraction of [0.25, 0.5, 0.9]) {
79
101
  const tokens = Math.floor(claimed * fraction);
80
- const needle = `NEEDLE:${Math.floor(tokens * 7919)}`; // deterministic per size
81
- const fillerUnit = 'lorem ipsum context filler segment. ';
82
- const beforeNeedle = Math.floor(tokens * 0.9) * 4; // needle at ~90% depth of this size
83
- const afterNeedle = Math.max(0, tokens * 4 - beforeNeedle - needle.length - 60);
84
- const prompt = fillerUnit.repeat(Math.ceil(beforeNeedle / fillerUnit.length)).slice(0, beforeNeedle)
85
- + ` ${needle} `
86
- + fillerUnit.repeat(Math.ceil(afterNeedle / fillerUnit.length)).slice(0, afterNeedle)
87
- + ' Return the NEEDLE token from this text.';
88
- let found = false, why = null;
102
+ const digits = String(Math.floor(tokens * 7919)); // deterministic per size
103
+ const fact = ` The magic number is ${digits}. `;
104
+ const body = Math.floor(tokens * charsPerToken) - fact.length - QUESTION.length - 40; // 40: chat template slack
105
+ const before = Math.floor(body * 0.1);
106
+ const prompt = fill(before) + fact + fill(body - before) + QUESTION;
107
+ let found = false, why = null, promptTokens = null;
89
108
  try {
90
109
  // Prefill on modest hardware is slow: allow ~30 ms per claimed token on top of the base timeout.
91
- const { status, text } = await chatOnce(safeFetch, card, offering, prompt, 64, 60_000 + tokens * 30);
92
- found = status === 200 && needleFound(text, needle);
93
- if (!found) why = status !== 200 ? `status ${status}` : 'needle not in reply';
94
- } catch (err) { found = false; why = /abort|timeout/i.test(String(err?.message)) ? 'timeout' : String(err?.message ?? err).slice(0, 80); }
95
- results.push({ fraction, tokens, found, ...(why ? { why } : {}) });
96
- if (found) cap = Math.max(cap, tokens);
110
+ const { status, text, usage } = await chatOnce(safeFetch, card, offering, prompt, 64, 60_000 + tokens * 30);
111
+ promptTokens = usage?.prompt_tokens ?? null;
112
+ if (status !== 200) why = `status ${status}`;
113
+ else if (measured && promptTokens != null && promptTokens < tokens * 0.85) why = `prompt cut to ${promptTokens} tokens`;
114
+ else if (!needleFound(text, digits)) why = 'number not in reply';
115
+ else found = true;
116
+ } catch (err) { why = /abort|timeout/i.test(String(err?.message)) ? 'timeout' : String(err?.message ?? err).slice(0, 80); }
117
+ results.push({ fraction, tokens, found, ...(promptTokens != null ? { prompt_tokens: promptTokens } : {}), ...(why ? { why } : {}) });
118
+ if (found && unbroken) cap = tokens; else unbroken = false;
97
119
  }
98
120
  const allPassed = results.every((r) => r.found);
99
- return { ok: allPassed, claimed, verified_cap: allPassed ? claimed : cap, results };
121
+ return { ok: allPassed, claimed, verified_cap: allPassed ? claimed : cap, chars_per_token: Math.round(charsPerToken * 100) / 100, results };
100
122
  }
101
123
 
102
124
  /** DS-ADM-04: stepped-concurrency ramp; measures TTFT p50/p95 and rough TPS. */
@@ -160,10 +182,9 @@ export async function runStageB(store, nodeId, card, { safeFetch, capacity = {}
160
182
  await store.recordProbe(nodeId, offering.offering_id, 'B', 'context', verdict.context.ok,
161
183
  `claimed ${verdict.context.claimed}, verified ${verdict.context.verified_cap}`,
162
184
  verdict.context);
163
- if (!verdict.context.ok) {
164
- // 0 = nothing verified: search projects the window as unverified rather than trusting the claim.
165
- await store.setObservation(nodeId, offering.offering_id, 'context_cap', verdict.context.verified_cap);
166
- }
185
+ // Every run replaces the last result: a pass stores the full claimed window, which lifts an
186
+ // earlier cap; 0 = nothing verified, shown as unverified rather than trusting the claim.
187
+ await store.setObservation(nodeId, offering.offering_id, 'context_cap', verdict.context.verified_cap);
167
188
 
168
189
  verdict.capacity = await probeCapacity(safeFetch, card, offering, capacity);
169
190
  await store.recordProbe(nodeId, offering.offering_id, 'B', 'capacity', verdict.capacity.ok, null, verdict.capacity);