lightnode-sdk 0.4.1 → 0.4.3

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/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, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference } from "./inference.js";
4
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey } 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";
@@ -60,7 +60,7 @@ export declare class LightNode {
60
60
  baseUrl?: string;
61
61
  }): GatewayClient;
62
62
  }
63
- export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
63
+ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
64
64
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
65
- export type { SessionPreparation, RunInferenceArgs, RunInferenceResult } from "./inference.js";
65
+ export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs } from "./inference.js";
66
66
  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, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, } from "./inference.js";
5
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, } 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";
@@ -94,4 +94,6 @@ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggreg
94
94
  // v0.3 inference-submit surface (BETA - see README "Submitting inference").
95
95
  GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto,
96
96
  // v0.4 high-level orchestrator: one call, full flow.
97
- runInference, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
97
+ runInference,
98
+ // v0.4.3 key-in-answer-out shortcut: same flow, no viem/SIWE wiring.
99
+ runInferenceWithKey, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
@@ -159,7 +159,7 @@ export interface RunInferenceArgs {
159
159
  onChunk?: (chunk: string, totalSoFar: string) => void;
160
160
  /** Retry count if a worker stalls. Default 2 (so up to 3 paid attempts). */
161
161
  maxRetries?: number;
162
- /** How long to wait for JobCompleted before declaring the worker stalled. Default 90s. */
162
+ /** How long to wait for JobCompleted before declaring the worker stalled. Default 120s. */
163
163
  jobCompletedTimeoutMs?: number;
164
164
  /**
165
165
  * WebSocket constructor. In a browser, omit and `globalThis.WebSocket` is
@@ -179,7 +179,12 @@ export interface RunInferenceResult {
179
179
  txs: {
180
180
  createSession: `0x${string}`;
181
181
  submitJob: `0x${string}`;
182
- jobCompleted: `0x${string}`;
182
+ /**
183
+ * Worker's commit-result tx. Null if the on-chain event hasn't landed by the
184
+ * deadline but the WS-delivered, session-key-decrypted answer DID arrive -
185
+ * in that case the answer is still authentic; this is just the explorer link.
186
+ */
187
+ jobCompleted: `0x${string}` | null;
183
188
  };
184
189
  /** The dispatcher-assigned worker that produced this response. */
185
190
  worker: `0x${string}`;
@@ -224,3 +229,61 @@ export interface RunInferenceResult {
224
229
  export declare function runInference(args: RunInferenceArgs): Promise<RunInferenceResult>;
225
230
  /** Re-export the typed errors at this layer so a single import covers everything. */
226
231
  export { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker } from "./errors.js";
232
+ import type { NetworkId } from "./types.js";
233
+ export interface RunInferenceWithKeyArgs {
234
+ /** Network ID (`"testnet"` / `"mainnet"`) or a custom NetworkConfig. */
235
+ network: NetworkId | NetworkConfig;
236
+ /**
237
+ * A funded EVM private key, hex with `0x` prefix. Pays the job fee + gas and
238
+ * signs createSession + submitJob. NEVER hardcode this - load from env.
239
+ */
240
+ privateKey: string;
241
+ /** The plaintext prompt to send. UTF-8 encoded before encryption. */
242
+ prompt: string;
243
+ /** Inference model tag. Default: `"llama3-8b"`. */
244
+ model?: string;
245
+ /**
246
+ * Streaming callback invoked once per decrypted relay chunk. Use for live
247
+ * stdout / UI updates. Optional - the final `answer` is returned either way.
248
+ */
249
+ onChunk?: (chunk: string, totalSoFar: string) => void;
250
+ /** Retry count if a worker stalls. Default 2 (so up to 3 paid attempts). */
251
+ maxRetries?: number;
252
+ /** How long to wait for JobCompleted before declaring the worker stalled. Default 120s. */
253
+ jobCompletedTimeoutMs?: number;
254
+ /**
255
+ * WebSocket constructor. In a browser this is auto-detected from
256
+ * `globalThis.WebSocket`. In Node, pass `WS` from the `ws` package
257
+ * (`import WS from "ws"`) - `ws` is not a hard dep of this SDK.
258
+ */
259
+ WebSocket?: WebSocketCtor;
260
+ /** Override the relay URL (defaults to `wss://relay.<network>.lightchain.ai/ws`). */
261
+ relayUrl?: string;
262
+ /**
263
+ * Override the consumer-api gateway URL. Defaults to a network-derived URL.
264
+ * Useful for tests / mirrors / proxying through your own backend.
265
+ */
266
+ gatewayUrl?: string;
267
+ }
268
+ /**
269
+ * One call, key-in / answer-out encrypted inference. Builds viem clients,
270
+ * runs the SIWE handshake, opens the encrypted session, submits + decrypts,
271
+ * and returns. Same proof chain (`createSession`, `submitJob`, `jobCompleted`)
272
+ * as the lower-level `runInference`.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * import { runInferenceWithKey } from "lightnode-sdk";
277
+ * import WS from "ws";
278
+ *
279
+ * const { answer, txs } = await runInferenceWithKey({
280
+ * network: "testnet",
281
+ * privateKey: process.env.PRIVATE_KEY!,
282
+ * prompt: "Reply with a one-sentence fun fact about the ocean.",
283
+ * WebSocket: WS, // omit in the browser
284
+ * });
285
+ *
286
+ * console.log(answer);
287
+ * ```
288
+ */
289
+ export declare function runInferenceWithKey(args: RunInferenceWithKeyArgs): Promise<RunInferenceResult>;
package/dist/inference.js CHANGED
@@ -190,7 +190,7 @@ function topicAsUint(hex) {
190
190
  return BigInt(hex);
191
191
  }
192
192
  async function runOneAttempt(args, attempt) {
193
- const { prompt, gateway, wallet, publicClient, network, model = "llama3-8b", onChunk, jobCompletedTimeoutMs = 90000, } = args;
193
+ const { prompt, gateway, wallet, publicClient, network, model = "llama3-8b", onChunk, jobCompletedTimeoutMs = 120000, } = args;
194
194
  const WS = pickWebSocket(args.WebSocket);
195
195
  const relayUrl = args.relayUrl ?? `wss://relay.${network.id}.lightchain.ai/ws`;
196
196
  // 1. prepareSession
@@ -326,6 +326,11 @@ async function runOneAttempt(args, attempt) {
326
326
  throw new Error("JobSubmitted log missing in submitJob receipt");
327
327
  const jobId = topicAsUint(jobLog.topics[1]);
328
328
  // 6. wait for JobCompleted
329
+ // KEY INVARIANT: the real result is the WS-delivered, session-key-decrypted
330
+ // ciphertext. JobCompleted is an explorer pointer (the worker's commit-result
331
+ // tx). If the WS already produced an answer (chunks.length > 0) we MUST
332
+ // accept it even if the on-chain event hasn't landed - throwing stalled here
333
+ // wipes a delivered answer on retry, which reads as "the call returned nothing".
329
334
  const deadline = Date.now() + jobCompletedTimeoutMs;
330
335
  const jobIdTopic = (`0x${jobId.toString(16).padStart(64, "0")}`);
331
336
  let completed = null;
@@ -338,8 +343,14 @@ async function runOneAttempt(args, attempt) {
338
343
  });
339
344
  completed =
340
345
  logs.find((l) => l.topics[0] === JOB_COMPLETED_TOPIC && l.topics[1] === jobIdTopic) ?? null;
346
+ if (completed)
347
+ break;
348
+ // If the WS already delivered the answer, stop polling - no point burning
349
+ // more time on a confirmation that only serves as an explorer link.
350
+ if (chunks.length > 0)
351
+ break;
341
352
  }
342
- if (!completed) {
353
+ if (!completed && chunks.length === 0) {
343
354
  try {
344
355
  ws.close();
345
356
  }
@@ -363,7 +374,11 @@ async function runOneAttempt(args, attempt) {
363
374
  }
364
375
  return {
365
376
  answer: chunks.join(""),
366
- txs: { createSession: createTx, submitJob: submitTx, jobCompleted: completed.transactionHash },
377
+ // completed may be null when the answer arrived via the WS but JobCompleted
378
+ // hasn't landed on-chain yet. The decrypted answer is still authentic
379
+ // (session-key bound); callers can poll for the event later if they want
380
+ // the explorer-link form of the proof.
381
+ txs: { createSession: createTx, submitJob: submitTx, jobCompleted: completed?.transactionHash ?? null },
367
382
  worker: prepared.createSessionArgs.worker,
368
383
  sessionId,
369
384
  jobId,
@@ -419,3 +434,100 @@ export async function runInference(args) {
419
434
  }
420
435
  /** Re-export the typed errors at this layer so a single import covers everything. */
421
436
  export { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker } from "./errors.js";
437
+ import { NETWORKS } from "./networks.js";
438
+ import { GatewayClient as GatewayClientCtor, consumerGatewayUrl as consumerGatewayUrlFn } from "./gateway.js";
439
+ import { GatewayAuthError } from "./errors.js";
440
+ import { createPublicClient as viemCreatePublicClient, createWalletClient as viemCreateWalletClient, http as viemHttp } from "viem";
441
+ import { privateKeyToAccount as viemPrivateKeyToAccount } from "viem/accounts";
442
+ /**
443
+ * One call, key-in / answer-out encrypted inference. Builds viem clients,
444
+ * runs the SIWE handshake, opens the encrypted session, submits + decrypts,
445
+ * and returns. Same proof chain (`createSession`, `submitJob`, `jobCompleted`)
446
+ * as the lower-level `runInference`.
447
+ *
448
+ * @example
449
+ * ```ts
450
+ * import { runInferenceWithKey } from "lightnode-sdk";
451
+ * import WS from "ws";
452
+ *
453
+ * const { answer, txs } = await runInferenceWithKey({
454
+ * network: "testnet",
455
+ * privateKey: process.env.PRIVATE_KEY!,
456
+ * prompt: "Reply with a one-sentence fun fact about the ocean.",
457
+ * WebSocket: WS, // omit in the browser
458
+ * });
459
+ *
460
+ * console.log(answer);
461
+ * ```
462
+ */
463
+ export async function runInferenceWithKey(args) {
464
+ // Resolve the network config and validate the key shape up front so a
465
+ // mistyped key fails BEFORE we touch the RPC or the gateway.
466
+ const network = typeof args.network === "string" ? NETWORKS[args.network] : args.network;
467
+ if (!network)
468
+ throw new Error(`unknown network: ${String(args.network)}`);
469
+ const networkId = (typeof args.network === "string" ? args.network : "mainnet");
470
+ const key = args.privateKey?.trim();
471
+ if (!key || !key.startsWith("0x") || key.length !== 66) {
472
+ throw new Error("runInferenceWithKey: privateKey must be a 0x-prefixed 32-byte hex string");
473
+ }
474
+ const account = viemPrivateKeyToAccount(key);
475
+ const chain = {
476
+ id: network.chainId,
477
+ name: network.label,
478
+ nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 },
479
+ rpcUrls: { default: { http: [network.rpc] } },
480
+ };
481
+ // Keep viem's real types here so signMessage / etc. are typed. The MinimalX
482
+ // casts only happen at the runInference() call site below.
483
+ const publicClient = viemCreatePublicClient({ transport: viemHttp(network.rpc), chain });
484
+ const wallet = viemCreateWalletClient({ account, transport: viemHttp(network.rpc), chain });
485
+ // One-shot SIWE handshake. We do this inline (rather than re-export it) so
486
+ // the caller doesn't need a second import; in browsers + Node it works the
487
+ // same against the consumer-api gateway.
488
+ const gwBase = args.gatewayUrl ?? consumerGatewayUrlFn(networkId);
489
+ const chRes = await fetch(`${gwBase}/api/auth/challenge?address=${account.address}`, {
490
+ headers: { Accept: "application/json" },
491
+ });
492
+ if (!chRes.ok)
493
+ throw new GatewayAuthError(chRes.status, await chRes.text());
494
+ const ch = (await chRes.json());
495
+ if (!ch.message)
496
+ throw new GatewayAuthError(chRes.status, "auth challenge returned no message");
497
+ const signature = await wallet.signMessage({ account, message: ch.message });
498
+ const verifyRes = await fetch(`${gwBase}/api/auth/verify`, {
499
+ method: "POST",
500
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
501
+ body: JSON.stringify({ message: ch.message, signature }),
502
+ });
503
+ if (!verifyRes.ok)
504
+ throw new GatewayAuthError(verifyRes.status, await verifyRes.text());
505
+ const verify = (await verifyRes.json());
506
+ if (!verify.token)
507
+ throw new GatewayAuthError(verifyRes.status, "auth verify returned no token");
508
+ const gateway = new GatewayClientCtor({ network: networkId, bearer: verify.token, baseUrl: args.gatewayUrl });
509
+ // Pick a WebSocket: the browser global if present, otherwise the caller-
510
+ // supplied ctor. We deliberately do NOT try to dynamic-import "ws" - it
511
+ // isn't a hard dep, and a bundler trying to resolve it would fail noisily.
512
+ const wsCtor = args.WebSocket ??
513
+ (typeof globalThis !== "undefined" && globalThis.WebSocket
514
+ ? globalThis.WebSocket
515
+ : undefined);
516
+ if (!wsCtor) {
517
+ throw new Error("runInferenceWithKey: no WebSocket constructor available. In Node, install `ws` and pass it: " +
518
+ "`import WS from 'ws'; runInferenceWithKey({ WebSocket: WS, ... })`");
519
+ }
520
+ return runInference({
521
+ prompt: args.prompt,
522
+ gateway,
523
+ wallet: wallet,
524
+ publicClient: publicClient,
525
+ network,
526
+ model: args.model,
527
+ onChunk: args.onChunk,
528
+ maxRetries: args.maxRetries,
529
+ jobCompletedTimeoutMs: args.jobCompletedTimeoutMs,
530
+ WebSocket: wsCtor,
531
+ relayUrl: args.relayUrl,
532
+ });
533
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",