lightnode-sdk 0.8.8 → 0.8.10

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/dist/add.js CHANGED
@@ -978,7 +978,9 @@ type Turn = {
978
978
  jobCompletedTx?: \`0x\${string}\` | null;
979
979
  };
980
980
 
981
- const MODEL = "llama3-8b";
981
+ // Models live on LightChain mainnet. The visitor picks one per the dropdown.
982
+ const MODELS = ["llama3-8b", "llama3-70b"] as const;
983
+ type ModelId = (typeof MODELS)[number];
982
984
 
983
985
  export default function ChatWeb3() {
984
986
  const { address, chain } = useAccount();
@@ -987,6 +989,7 @@ export default function ChatWeb3() {
987
989
  const { data: walletClient } = useWalletClient({ chainId: chain?.id });
988
990
  const publicClient = usePublicClient({ chainId: chain?.id });
989
991
 
992
+ const [model, setModel] = useState<ModelId>("llama3-8b");
990
993
  const [turns, setTurns] = useState<Turn[]>([]);
991
994
  const [input, setInput] = useState("");
992
995
  const [busy, setBusy] = useState(false);
@@ -1000,12 +1003,12 @@ export default function ChatWeb3() {
1000
1003
  useEffect(() => {
1001
1004
  if (!network) { setFeeLcai(null); return; }
1002
1005
  let cancelled = false;
1003
- estimateJobFee(NETWORKS[network], MODEL).then(
1006
+ estimateJobFee(NETWORKS[network], model).then(
1004
1007
  (fee) => { if (!cancelled) setFeeLcai(fee); },
1005
1008
  () => { if (!cancelled) setFeeLcai(null); },
1006
1009
  );
1007
1010
  return () => { cancelled = true; };
1008
- }, [network]);
1011
+ }, [network, model]);
1009
1012
 
1010
1013
  // Keep the latest turn in view. Instant while streaming (smooth scrolling on
1011
1014
  // every chunk competes for the main thread); smooth once idle.
@@ -1062,7 +1065,7 @@ export default function ChatWeb3() {
1062
1065
  wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1063
1066
  publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1064
1067
  network: NETWORKS[network],
1065
- model: MODEL,
1068
+ model,
1066
1069
  jobCompletedTimeoutMs: 120_000,
1067
1070
  maxRetries: 1,
1068
1071
  // Stream each decrypted chunk into the assistant bubble as it arrives.
@@ -1207,14 +1210,17 @@ export default function ChatWeb3() {
1207
1210
  />
1208
1211
  </div>
1209
1212
  <div className="mt-1 flex items-center justify-between gap-2">
1210
- <span className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs font-medium text-muted-foreground">
1211
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1212
- <rect x="4" y="4" width="16" height="16" rx="2" />
1213
- <rect x="9" y="9" width="6" height="6" />
1214
- <path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" />
1215
- </svg>
1216
- {MODEL}
1217
- </span>
1213
+ <select
1214
+ value={model}
1215
+ onChange={(e) => setModel(e.target.value as ModelId)}
1216
+ disabled={busy}
1217
+ title="Model (both live on LightChain mainnet)"
1218
+ className="rounded-lg border border-border bg-background px-2 py-1 text-xs font-medium text-muted-foreground outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
1219
+ >
1220
+ {MODELS.map((m) => (
1221
+ <option key={m} value={m}>{m}</option>
1222
+ ))}
1223
+ </select>
1218
1224
  <button
1219
1225
  type="button"
1220
1226
  onClick={() => send()}
@@ -1267,7 +1273,8 @@ type Result = {
1267
1273
  elapsedMs: number;
1268
1274
  };
1269
1275
 
1270
- const MODEL = "llama3-8b";
1276
+ const MODELS = ["llama3-8b", "llama3-70b"] as const;
1277
+ type ModelId = (typeof MODELS)[number];
1271
1278
 
1272
1279
  export default function InferenceWeb3() {
1273
1280
  const { address, chain } = useAccount();
@@ -1276,6 +1283,7 @@ export default function InferenceWeb3() {
1276
1283
  const { data: walletClient } = useWalletClient({ chainId: chain?.id });
1277
1284
  const publicClient = usePublicClient({ chainId: chain?.id });
1278
1285
 
1286
+ const [model, setModel] = useState<ModelId>("llama3-8b");
1279
1287
  const [system, setSystem] = useState("You are a concise assistant. Reply in one or two short sentences.");
1280
1288
  const [prompt, setPrompt] = useState("Reply with the single word OK.");
1281
1289
  const [busy, setBusy] = useState(false);
@@ -1288,12 +1296,12 @@ export default function InferenceWeb3() {
1288
1296
  useEffect(() => {
1289
1297
  if (!network) { setFeeLcai(null); return; }
1290
1298
  let cancelled = false;
1291
- estimateJobFee(NETWORKS[network], MODEL).then(
1299
+ estimateJobFee(NETWORKS[network], model).then(
1292
1300
  (fee) => { if (!cancelled) setFeeLcai(fee); },
1293
1301
  () => { if (!cancelled) setFeeLcai(null); },
1294
1302
  );
1295
1303
  return () => { cancelled = true; };
1296
- }, [network]);
1304
+ }, [network, model]);
1297
1305
 
1298
1306
  async function run() {
1299
1307
  if (!walletClient || !publicClient || !address || !network) {
@@ -1319,7 +1327,7 @@ export default function InferenceWeb3() {
1319
1327
  wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1320
1328
  publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1321
1329
  network: NETWORKS[network],
1322
- model: MODEL,
1330
+ model,
1323
1331
  jobCompletedTimeoutMs: 120_000,
1324
1332
  maxRetries: 1,
1325
1333
  // Stream the answer live as decrypted chunks arrive.
@@ -1382,10 +1390,23 @@ export default function InferenceWeb3() {
1382
1390
  className="resize-none rounded-xl border border-border bg-card px-3 py-2 font-mono text-xs text-foreground outline-none focus:ring-2 focus:ring-primary" />
1383
1391
  </label>
1384
1392
 
1385
- <button type="button" onClick={() => run()} disabled={busy || !prompt.trim() || !address || !network}
1386
- className="self-start rounded-xl bg-gradient-primary px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40">
1387
- {busy ? (busyStage || "Running...") : "Run inference"}
1388
- </button>
1393
+ <div className="flex items-center gap-3">
1394
+ <select
1395
+ value={model}
1396
+ onChange={(e) => setModel(e.target.value as ModelId)}
1397
+ disabled={busy}
1398
+ title="Model (both live on LightChain mainnet)"
1399
+ className="rounded-xl border border-border bg-card px-2 py-2 text-xs font-medium text-muted-foreground outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
1400
+ >
1401
+ {MODELS.map((m) => (
1402
+ <option key={m} value={m}>{m}</option>
1403
+ ))}
1404
+ </select>
1405
+ <button type="button" onClick={() => run()} disabled={busy || !prompt.trim() || !address || !network}
1406
+ className="rounded-xl bg-gradient-primary px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40">
1407
+ {busy ? (busyStage || "Running...") : "Run inference"}
1408
+ </button>
1409
+ </div>
1389
1410
 
1390
1411
  {err && (
1391
1412
  <p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
@@ -1464,7 +1485,8 @@ type Result = {
1464
1485
  jobCompleted: \`0x\${string}\` | null;
1465
1486
  };
1466
1487
 
1467
- const MODEL = "llama3-8b";
1488
+ const MODELS = ["llama3-8b", "llama3-70b"] as const;
1489
+ type ModelId = (typeof MODELS)[number];
1468
1490
 
1469
1491
  export default function JudgeWeb3() {
1470
1492
  const { address, chain } = useAccount();
@@ -1473,6 +1495,7 @@ export default function JudgeWeb3() {
1473
1495
  const { data: walletClient } = useWalletClient({ chainId: chain?.id });
1474
1496
  const publicClient = usePublicClient({ chainId: chain?.id });
1475
1497
 
1498
+ const [model, setModel] = useState<ModelId>("llama3-8b");
1476
1499
  const [criteria, setCriteria] = useState("Run a mile under 8 minutes");
1477
1500
  const [evidence, setEvidence] = useState('{"distance_km": 1.61, "time_minutes": 7.4}');
1478
1501
  const [busy, setBusy] = useState(false);
@@ -1485,12 +1508,12 @@ export default function JudgeWeb3() {
1485
1508
  useEffect(() => {
1486
1509
  if (!network) { setFeeLcai(null); return; }
1487
1510
  let cancelled = false;
1488
- estimateJobFee(NETWORKS[network], MODEL).then(
1511
+ estimateJobFee(NETWORKS[network], model).then(
1489
1512
  (fee) => { if (!cancelled) setFeeLcai(fee); },
1490
1513
  () => { if (!cancelled) setFeeLcai(null); },
1491
1514
  );
1492
1515
  return () => { cancelled = true; };
1493
- }, [network]);
1516
+ }, [network, model]);
1494
1517
 
1495
1518
  /** Parse the verdict defensively; fall back to the first {...} block. */
1496
1519
  function parseVerdict(answer: string): Verdict | null {
@@ -1535,7 +1558,7 @@ Reply with STRICT JSON only, matching: { "passed": boolean, "confidence": 0-1, "
1535
1558
  wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1536
1559
  publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1537
1560
  network: NETWORKS[network],
1538
- model: MODEL,
1561
+ model,
1539
1562
  jobCompletedTimeoutMs: 120_000,
1540
1563
  maxRetries: 1,
1541
1564
  // Show the model's raw output streaming in while it generates the verdict.
@@ -1598,10 +1621,23 @@ Reply with STRICT JSON only, matching: { "passed": boolean, "confidence": 0-1, "
1598
1621
  className="resize-none rounded-xl border border-border bg-card px-3 py-2 font-mono text-xs text-foreground outline-none focus:ring-2 focus:ring-primary" />
1599
1622
  </label>
1600
1623
 
1601
- <button type="button" onClick={() => run()} disabled={busy || !criteria.trim() || !evidence.trim() || !address || !network}
1602
- className="self-start rounded-xl bg-gradient-primary px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40">
1603
- {busy ? (busyStage || "Judging...") : "Get AI verdict"}
1604
- </button>
1624
+ <div className="flex items-center gap-3">
1625
+ <select
1626
+ value={model}
1627
+ onChange={(e) => setModel(e.target.value as ModelId)}
1628
+ disabled={busy}
1629
+ title="Model (both live on LightChain mainnet)"
1630
+ className="rounded-xl border border-border bg-card px-2 py-2 text-xs font-medium text-muted-foreground outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
1631
+ >
1632
+ {MODELS.map((m) => (
1633
+ <option key={m} value={m}>{m}</option>
1634
+ ))}
1635
+ </select>
1636
+ <button type="button" onClick={() => run()} disabled={busy || !criteria.trim() || !evidence.trim() || !address || !network}
1637
+ className="rounded-xl bg-gradient-primary px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40">
1638
+ {busy ? (busyStage || "Judging...") : "Get AI verdict"}
1639
+ </button>
1640
+ </div>
1605
1641
 
1606
1642
  {err && (
1607
1643
  <p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
package/dist/inference.js CHANGED
@@ -293,6 +293,8 @@ async function runOneAttempt(args, attempt) {
293
293
  setTimeout(() => reject(new Error("relay WebSocket open timeout")), 20000);
294
294
  });
295
295
  const chunks = [];
296
+ let streamDone = false;
297
+ let streamDoneAt = null;
296
298
  const handleMessage = async (rawData) => {
297
299
  const raw = typeof rawData === "string"
298
300
  ? rawData
@@ -308,20 +310,26 @@ async function runOneAttempt(args, attempt) {
308
310
  catch {
309
311
  return;
310
312
  }
311
- if (!frame?.payload)
312
- return;
313
- if (frame.type === "chunk") {
314
- try {
315
- const piece = await decryptResponse(prepared.sessionKey, frame.payload);
316
- chunks.push(piece);
317
- if (onChunk)
318
- onChunk(piece, chunks.join(""));
319
- }
320
- catch {
321
- /* control frame */
313
+ // "complete" marks end-of-stream (it may or may not carry a payload).
314
+ // Record it so the JobCompleted wait can stop promptly instead of polling
315
+ // the full grace window after the answer is already in hand.
316
+ if (frame.type === "complete") {
317
+ streamDone = true;
318
+ streamDoneAt = Date.now();
319
+ if (chunks.length === 0 && frame.payload) {
320
+ try {
321
+ const piece = await decryptResponse(prepared.sessionKey, frame.payload);
322
+ chunks.push(piece);
323
+ if (onChunk)
324
+ onChunk(piece, chunks.join(""));
325
+ }
326
+ catch {
327
+ /* ignore */
328
+ }
322
329
  }
330
+ return;
323
331
  }
324
- else if (frame.type === "complete" && chunks.length === 0) {
332
+ if (frame.type === "chunk" && frame.payload) {
325
333
  try {
326
334
  const piece = await decryptResponse(prepared.sessionKey, frame.payload);
327
335
  chunks.push(piece);
@@ -329,7 +337,7 @@ async function runOneAttempt(args, attempt) {
329
337
  onChunk(piece, chunks.join(""));
330
338
  }
331
339
  catch {
332
- /* ignore */
340
+ /* control frame */
333
341
  }
334
342
  }
335
343
  };
@@ -376,7 +384,8 @@ async function runOneAttempt(args, attempt) {
376
384
  // answer with txs.jobCompleted=null (the answer is still session-key
377
385
  // authentic; the on-chain proof can be polled for separately by callers).
378
386
  const deadline = Date.now() + jobCompletedTimeoutMs;
379
- const POST_CHUNKS_GRACE_MS = 45000;
387
+ const POST_CHUNKS_GRACE_MS = 45000; // fallback if the relay never sends a 'complete' frame
388
+ const POST_DONE_GRACE_MS = 8000; // once the answer is fully in, the worker commits JobCompleted within ~seconds
380
389
  const waitStart = Date.now();
381
390
  let firstChunkAt = chunks.length > 0 ? waitStart : null;
382
391
  const jobIdTopic = (`0x${jobId.toString(16).padStart(64, "0")}`);
@@ -385,9 +394,14 @@ async function runOneAttempt(args, attempt) {
385
394
  const now = Date.now();
386
395
  if (now >= deadline)
387
396
  break;
397
+ // Answer fully received (relay sent 'complete'): wait only briefly for the
398
+ // on-chain proof, then return. The answer is already session-key authentic;
399
+ // callers get txs.jobCompleted=null and can poll the proof later if needed.
400
+ if (streamDone && now - (streamDoneAt ?? now) >= POST_DONE_GRACE_MS)
401
+ break;
388
402
  if (firstChunkAt != null && now - firstChunkAt >= POST_CHUNKS_GRACE_MS)
389
403
  break;
390
- await new Promise((res) => setTimeout(res, 3000));
404
+ await new Promise((res) => setTimeout(res, 1500));
391
405
  if (firstChunkAt == null && chunks.length > 0)
392
406
  firstChunkAt = Date.now();
393
407
  const logs = await publicClient.getLogs({
@@ -414,8 +428,9 @@ async function runOneAttempt(args, attempt) {
414
428
  feeLcai: fee,
415
429
  });
416
430
  }
417
- // 7. grace period for the last relay frame, then close
418
- await new Promise((res) => setTimeout(res, 4000));
431
+ // 7. grace period for the last relay frame, then close. If the stream already
432
+ // signaled 'complete', no more frames are coming - skip the wait.
433
+ await new Promise((res) => setTimeout(res, streamDone ? 300 : 4000));
419
434
  try {
420
435
  ws.close();
421
436
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",