lightnode-sdk 0.4.8 → 0.4.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/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv } from "./index.js";
2
+ import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv, runInferenceWithKey, isStalledWorker } from "./index.js";
3
3
  import { addInference, addAnalyticsDashboard, addNftMint, addChat, addAgent } from "./add.js";
4
+ import { createPublicClient, http, parseEther } from "viem";
5
+ import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
4
6
  function flag(name) {
5
7
  const i = process.argv.indexOf(name);
6
8
  return i >= 0 ? process.argv[i + 1] : undefined;
@@ -17,6 +19,16 @@ const lcai = (wei) => (wei ? Number(BigInt(wei)) / 1e18 : 0);
17
19
  const rate = (r) => (r == null ? "-" : `${Math.round(r * 100)}%`);
18
20
  const HELP = `lightnode <command> [--net mainnet|testnet]
19
21
 
22
+ Run one inference (needs PRIVATE_KEY in env):
23
+ chat <prompt> stream one encrypted inference answer to stdout
24
+ ([--model llama3-8b] [--key 0x...])
25
+
26
+ Wallet helpers:
27
+ wallet new generate a fresh testnet key, print it
28
+ wallet address print the address of PRIVATE_KEY
29
+ wallet balance [--net] print LCAI balance for PRIVATE_KEY's address
30
+
31
+ Read-only network commands (no key):
20
32
  network network summary (workers, jobs, models, earnings)
21
33
  models registered models + per-job fee
22
34
  worker <addr> a worker: on-chain registration + recent jobs
@@ -26,6 +38,7 @@ const HELP = `lightnode <command> [--net mainnet|testnet]
26
38
  analytics [--csv] per-model performance (completion, p50/p95, incomplete)
27
39
  reliability [--csv] per-worker reliability, busiest first
28
40
 
41
+ Scaffold templates into the current project:
29
42
  add inference end-to-end encrypted inference route/script
30
43
  add chat chat-style UI with conversation history
31
44
  add agent scheduled/loop inference (cron-style)
@@ -34,9 +47,93 @@ const HELP = `lightnode <command> [--net mainnet|testnet]
34
47
  (all add commands: [--template auto|nextjs-api|hono|node] [--force])
35
48
 
36
49
  To scaffold a new project instead, run: npm create lightnode-app my-app`;
50
+ function pickKey() {
51
+ const k = flag("--key") ?? process.env.PRIVATE_KEY;
52
+ if (!k || !k.startsWith("0x") || k.length !== 66) {
53
+ die("set PRIVATE_KEY=0x... in your env, or pass --key 0x... (need a funded EVM key)");
54
+ }
55
+ return k;
56
+ }
37
57
  async function main() {
38
58
  const ln = new LightNode(net);
39
59
  switch (cmd) {
60
+ case "chat": {
61
+ // One-shot encrypted inference straight from the CLI. Pipe the prompt as
62
+ // positional args (or read from stdin if there are none) so this composes
63
+ // with shell scripts: `cat doc.md | lightnode chat` works.
64
+ const inlinePrompt = positionals.slice(1).join(" ").trim();
65
+ const prompt = inlinePrompt ||
66
+ (await new Promise((resolve) => {
67
+ let buf = "";
68
+ process.stdin.setEncoding("utf8");
69
+ process.stdin.on("data", (d) => (buf += d));
70
+ process.stdin.on("end", () => resolve(buf.trim()));
71
+ }));
72
+ if (!prompt)
73
+ die("usage: lightnode chat <prompt> (or pipe the prompt to stdin)");
74
+ const model = flag("--model") ?? "llama3-8b";
75
+ const privateKey = pickKey();
76
+ try {
77
+ const { answer, txs, worker, jobId } = await runInferenceWithKey({
78
+ network: net,
79
+ privateKey,
80
+ prompt,
81
+ model,
82
+ onChunk: (chunk) => process.stdout.write(chunk),
83
+ });
84
+ process.stdout.write("\n");
85
+ // Tiny one-liner trailer so the receipt is reachable without burying
86
+ // the answer. JSON is grep-friendly for shell pipelines.
87
+ const explorer = ln.network.explorer;
88
+ process.stderr.write(JSON.stringify({
89
+ chars: answer.length,
90
+ worker,
91
+ jobId: jobId.toString(),
92
+ createSession: `${explorer}/tx/${txs.createSession}`,
93
+ submitJob: `${explorer}/tx/${txs.submitJob}`,
94
+ jobCompleted: txs.jobCompleted ? `${explorer}/tx/${txs.jobCompleted}` : null,
95
+ }) + "\n");
96
+ }
97
+ catch (e) {
98
+ if (isStalledWorker(e))
99
+ die("3 workers stalled in a row. Protocol refunds the fees; try again later.");
100
+ die("inference failed: " + e.message);
101
+ }
102
+ break;
103
+ }
104
+ case "wallet": {
105
+ const sub = positionals[1];
106
+ if (sub === "new") {
107
+ // Fresh testnet-shaped key. Plain stdout output so it's copy-pasteable
108
+ // out of a script: `lightnode wallet new --quiet | head -1` works.
109
+ const pk = generatePrivateKey();
110
+ const addr = privateKeyToAccount(pk).address;
111
+ console.log(`PRIVATE_KEY=${pk}`);
112
+ console.error(`# address: ${addr}`);
113
+ console.error(`# fund at https://lightfaucet.ai before running paid commands`);
114
+ }
115
+ else if (sub === "address") {
116
+ const pk = pickKey();
117
+ console.log(privateKeyToAccount(pk).address);
118
+ }
119
+ else if (sub === "balance") {
120
+ const pk = pickKey();
121
+ const addr = privateKeyToAccount(pk).address;
122
+ const pub = createPublicClient({ transport: http(ln.network.rpc) });
123
+ const bal = await pub.getBalance({ address: addr });
124
+ const lcaiVal = Number(bal) / 1e18;
125
+ console.log(`${lcaiVal} LCAI`);
126
+ if (bal < parseEther("0.05")) {
127
+ console.error(`# under 0.05 LCAI - too low to run one inference`);
128
+ if (net === "testnet")
129
+ console.error(`# get free testnet LCAI: https://lightfaucet.ai`);
130
+ }
131
+ }
132
+ else {
133
+ die("usage: lightnode wallet <new|address|balance> [--net testnet|mainnet]");
134
+ }
135
+ break;
136
+ }
40
137
  case "network": {
41
138
  console.log(JSON.stringify(await ln.getNetworkAnalytics(), null, 2));
42
139
  break;
package/dist/crypto.js CHANGED
@@ -44,7 +44,12 @@ async function getRng() {
44
44
  return bound;
45
45
  }
46
46
  try {
47
- const mod = (await import("node:crypto"));
47
+ // The /* webpackIgnore: true */ magic comment stops Next.js / webpack
48
+ // from trying to bundle node:crypto for the browser. In a real browser
49
+ // we never reach this line (globalThis.crypto is available), so the
50
+ // import is dead code there - but webpack analyzes it statically and
51
+ // errors on the `node:` URI scheme without the hint.
52
+ const mod = (await import(/* webpackIgnore: true */ "node:crypto"));
48
53
  const wc = mod.webcrypto;
49
54
  if (wc && typeof wc.getRandomValues === "function") {
50
55
  const bound = wc.getRandomValues.bind(wc);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
2
  import { fromWei } from "./subgraph.js";
3
3
  import { aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv } from "./analytics.js";
4
- import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey } from "./inference.js";
4
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream } from "./inference.js";
5
5
  import { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker } from "./errors.js";
6
6
  import { GatewayClient, GatewayHttpError } from "./gateway.js";
7
7
  import * as crypto from "./crypto.js";
@@ -66,8 +66,8 @@ export declare class LightNode {
66
66
  * (especially in registry-proxy environments like StackBlitz where lockfiles
67
67
  * may pin an older minor than the local install command suggests).
68
68
  */
69
- export declare const SDK_VERSION = "0.4.8";
70
- export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
69
+ export declare const SDK_VERSION = "0.4.9";
70
+ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
71
71
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
72
- export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs } from "./inference.js";
72
+ export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
73
73
  export type { NetworkId, NetworkConfig, Worker, Job, ModelInfo, NetworkStats, ModelStat, WorkerStat, NetworkAnalytics };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
2
  import { fetchWorker, fetchWorkerJobs, fetchRecentJobs, fetchModels, fetchWorkers, summarize, fromWei, } from "./subgraph.js";
3
3
  import { isRegistered } from "./onchain.js";
4
4
  import { aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, } from "./analytics.js";
5
- import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, } from "./inference.js";
5
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream, } from "./inference.js";
6
6
  import { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, } from "./errors.js";
7
7
  import { GatewayClient, GatewayHttpError } from "./gateway.js";
8
8
  import * as crypto from "./crypto.js";
@@ -96,11 +96,13 @@ export class LightNode {
96
96
  * (especially in registry-proxy environments like StackBlitz where lockfiles
97
97
  * may pin an older minor than the local install command suggests).
98
98
  */
99
- export const SDK_VERSION = "0.4.8";
99
+ export const SDK_VERSION = "0.4.9";
100
100
  export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost,
101
101
  // v0.3 inference-submit surface (BETA - see README "Submitting inference").
102
102
  GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto,
103
103
  // v0.4 high-level orchestrator: one call, full flow.
104
104
  runInference,
105
105
  // v0.4.3 key-in-answer-out shortcut: same flow, no viem/SIWE wiring.
106
- runInferenceWithKey, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
106
+ runInferenceWithKey,
107
+ // v0.4.9 AsyncIterable<string> wrapper around runInferenceWithKey.
108
+ runInferenceStream, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
@@ -287,3 +287,37 @@ export interface RunInferenceWithKeyArgs {
287
287
  * ```
288
288
  */
289
289
  export declare function runInferenceWithKey(args: RunInferenceWithKeyArgs): Promise<RunInferenceResult>;
290
+ export interface RunInferenceStreamResult {
291
+ /** Streamed chunks (decrypted, in arrival order). */
292
+ [Symbol.asyncIterator](): AsyncIterator<string>;
293
+ /**
294
+ * Resolves with the same shape `runInference` returns once the iterator
295
+ * has finished (i.e. you've consumed all chunks). `answer` is the full
296
+ * assembled string. Awaiting this before consuming the iterator hangs;
297
+ * always iterate first or in parallel with another consumer.
298
+ */
299
+ done: Promise<RunInferenceResult>;
300
+ }
301
+ /**
302
+ * Stream-shaped wrapper over `runInferenceWithKey`. Returns an async-iterable
303
+ * of decrypted chunks plus a `done` promise that resolves to the full result
304
+ * once the iteration completes.
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * import { runInferenceStream } from "lightnode-sdk";
309
+ *
310
+ * const stream = runInferenceStream({
311
+ * network: "testnet",
312
+ * privateKey: process.env.PRIVATE_KEY!,
313
+ * prompt: "Write a haiku about decentralized AI.",
314
+ * });
315
+ *
316
+ * for await (const chunk of stream) {
317
+ * process.stdout.write(chunk);
318
+ * }
319
+ * const { txs } = await stream.done;
320
+ * console.log("\n", txs);
321
+ * ```
322
+ */
323
+ export declare function runInferenceStream(args: RunInferenceWithKeyArgs): RunInferenceStreamResult;
package/dist/inference.js CHANGED
@@ -535,16 +535,32 @@ export async function runInferenceWithKey(args) {
535
535
  if (!verify.token)
536
536
  throw new GatewayAuthError(verifyRes.status, "auth verify returned no token");
537
537
  const gateway = new GatewayClientCtor({ network: networkId, bearer: verify.token, baseUrl: args.gatewayUrl ?? gwBase });
538
- // Pick a WebSocket: the browser global if present, otherwise the caller-
539
- // supplied ctor. We deliberately do NOT try to dynamic-import "ws" - it
540
- // isn't a hard dep, and a bundler trying to resolve it would fail noisily.
541
- const wsCtor = args.WebSocket ??
538
+ // Pick a WebSocket: caller-supplied wins, else the browser global, else try
539
+ // to lazy-import the `ws` package (Node). The webpackIgnore hint keeps
540
+ // bundlers from blowing up trying to resolve `ws` for browser bundles where
541
+ // we never reach this branch.
542
+ let wsCtor = args.WebSocket ??
542
543
  (typeof globalThis !== "undefined" && globalThis.WebSocket
543
544
  ? globalThis.WebSocket
544
545
  : undefined);
545
546
  if (!wsCtor) {
546
- throw new Error("runInferenceWithKey: no WebSocket constructor available. In Node, install `ws` and pass it: " +
547
- "`import WS from 'ws'; runInferenceWithKey({ WebSocket: WS, ... })`");
547
+ try {
548
+ // Hide the module name from TS's static resolver via a Function-built
549
+ // dynamic import - otherwise TS errors trying to find @types/ws (we do
550
+ // not want that as a SDK devDep). The webpackIgnore-style comment also
551
+ // keeps browser bundlers from trying to resolve `ws`.
552
+ const dynamicImport = Function("n", "return import(/* webpackIgnore: true */ n)");
553
+ const mod = await dynamicImport("ws");
554
+ wsCtor = mod.default ?? mod.WebSocket;
555
+ }
556
+ catch {
557
+ // `ws` not installed - keep wsCtor undefined and fall into the error below.
558
+ }
559
+ }
560
+ if (!wsCtor) {
561
+ throw new Error("runInferenceWithKey: no WebSocket constructor available. In Node, install `ws` " +
562
+ "(`npm i ws`) - the SDK will pick it up automatically. Or pass one explicitly: " +
563
+ "`import WS from 'ws'; runInferenceWithKey({ WebSocket: WS, ... })`.");
548
564
  }
549
565
  return runInference({
550
566
  prompt: args.prompt,
@@ -560,3 +576,88 @@ export async function runInferenceWithKey(args) {
560
576
  relayUrl: args.relayUrl,
561
577
  });
562
578
  }
579
+ /**
580
+ * Stream-shaped wrapper over `runInferenceWithKey`. Returns an async-iterable
581
+ * of decrypted chunks plus a `done` promise that resolves to the full result
582
+ * once the iteration completes.
583
+ *
584
+ * @example
585
+ * ```ts
586
+ * import { runInferenceStream } from "lightnode-sdk";
587
+ *
588
+ * const stream = runInferenceStream({
589
+ * network: "testnet",
590
+ * privateKey: process.env.PRIVATE_KEY!,
591
+ * prompt: "Write a haiku about decentralized AI.",
592
+ * });
593
+ *
594
+ * for await (const chunk of stream) {
595
+ * process.stdout.write(chunk);
596
+ * }
597
+ * const { txs } = await stream.done;
598
+ * console.log("\n", txs);
599
+ * ```
600
+ */
601
+ export function runInferenceStream(args) {
602
+ // Bounded queue of pending chunks; consumed in order by the iterator. We
603
+ // can't use an unbounded array because the inference may produce chunks
604
+ // faster than the consumer reads them - bounding at 1024 is enough to absorb
605
+ // model-output bursts without unbounded memory growth.
606
+ const queue = [];
607
+ const waiters = [];
608
+ let finished = false;
609
+ let error = null;
610
+ const push = (chunk) => {
611
+ if (waiters.length > 0) {
612
+ const resolve = waiters.shift();
613
+ if (resolve)
614
+ resolve({ value: chunk, done: false });
615
+ }
616
+ else {
617
+ queue.push(chunk);
618
+ }
619
+ };
620
+ const finish = (err = null) => {
621
+ finished = true;
622
+ error = err;
623
+ while (waiters.length > 0) {
624
+ const resolve = waiters.shift();
625
+ if (!resolve)
626
+ continue;
627
+ if (err)
628
+ resolve({ value: undefined, done: true });
629
+ else
630
+ resolve({ value: undefined, done: true });
631
+ }
632
+ };
633
+ const done = runInferenceWithKey({
634
+ ...args,
635
+ onChunk: (chunk) => push(chunk),
636
+ })
637
+ .then((res) => {
638
+ finish(null);
639
+ return res;
640
+ })
641
+ .catch((e) => {
642
+ finish(e);
643
+ throw e;
644
+ });
645
+ return {
646
+ [Symbol.asyncIterator]() {
647
+ return {
648
+ async next() {
649
+ if (queue.length > 0) {
650
+ return { value: queue.shift(), done: false };
651
+ }
652
+ if (finished) {
653
+ if (error)
654
+ throw error;
655
+ return { value: undefined, done: true };
656
+ }
657
+ return new Promise((resolve) => waiters.push(resolve));
658
+ },
659
+ };
660
+ },
661
+ done,
662
+ };
663
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
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",