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 +64 -28
- package/dist/inference.js +32 -17
- package/package.json +1 -1
package/dist/add.js
CHANGED
|
@@ -978,7 +978,9 @@ type Turn = {
|
|
|
978
978
|
jobCompletedTx?: \`0x\${string}\` | null;
|
|
979
979
|
};
|
|
980
980
|
|
|
981
|
-
|
|
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],
|
|
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
|
|
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
|
-
<
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
|
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],
|
|
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
|
|
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
|
-
<
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
|
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],
|
|
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
|
|
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
|
-
<
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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",
|