lightnode-sdk 0.4.2 → 0.4.4

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, };
@@ -229,3 +229,61 @@ export interface RunInferenceResult {
229
229
  export declare function runInference(args: RunInferenceArgs): Promise<RunInferenceResult>;
230
230
  /** Re-export the typed errors at this layer so a single import covers everything. */
231
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
@@ -326,16 +326,31 @@ 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
+ // The actual *result* is the WS-delivered, session-key-decrypted ciphertext.
330
+ // JobCompleted is an explorer pointer (the worker's commit-result tx).
331
+ // Polling rules:
332
+ // - No chunks yet: poll for the full deadline (default 120s). Still nothing
333
+ // -> throw stalled so the outer loop can retry with a different worker.
334
+ // - Chunks arrived: keep polling for a 45s grace window after the FIRST
335
+ // chunk. Workers usually commit JobCompleted within ~10s of broadcasting
336
+ // the answer, so 45s is generous. If it still doesn't land, surface the
337
+ // answer with txs.jobCompleted=null (the answer is still session-key
338
+ // authentic; the on-chain proof can be polled for separately by callers).
334
339
  const deadline = Date.now() + jobCompletedTimeoutMs;
340
+ const POST_CHUNKS_GRACE_MS = 45000;
341
+ const waitStart = Date.now();
342
+ let firstChunkAt = chunks.length > 0 ? waitStart : null;
335
343
  const jobIdTopic = (`0x${jobId.toString(16).padStart(64, "0")}`);
336
344
  let completed = null;
337
- while (!completed && Date.now() < deadline) {
345
+ while (!completed) {
346
+ const now = Date.now();
347
+ if (now >= deadline)
348
+ break;
349
+ if (firstChunkAt != null && now - firstChunkAt >= POST_CHUNKS_GRACE_MS)
350
+ break;
338
351
  await new Promise((res) => setTimeout(res, 3000));
352
+ if (firstChunkAt == null && chunks.length > 0)
353
+ firstChunkAt = Date.now();
339
354
  const logs = await publicClient.getLogs({
340
355
  address: network.jobRegistry,
341
356
  fromBlock: submitReceipt.blockNumber,
@@ -345,10 +360,6 @@ async function runOneAttempt(args, attempt) {
345
360
  logs.find((l) => l.topics[0] === JOB_COMPLETED_TOPIC && l.topics[1] === jobIdTopic) ?? null;
346
361
  if (completed)
347
362
  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;
352
363
  }
353
364
  if (!completed && chunks.length === 0) {
354
365
  try {
@@ -434,3 +445,100 @@ export async function runInference(args) {
434
445
  }
435
446
  /** Re-export the typed errors at this layer so a single import covers everything. */
436
447
  export { StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker } from "./errors.js";
448
+ import { NETWORKS } from "./networks.js";
449
+ import { GatewayClient as GatewayClientCtor, consumerGatewayUrl as consumerGatewayUrlFn } from "./gateway.js";
450
+ import { GatewayAuthError } from "./errors.js";
451
+ import { createPublicClient as viemCreatePublicClient, createWalletClient as viemCreateWalletClient, http as viemHttp } from "viem";
452
+ import { privateKeyToAccount as viemPrivateKeyToAccount } from "viem/accounts";
453
+ /**
454
+ * One call, key-in / answer-out encrypted inference. Builds viem clients,
455
+ * runs the SIWE handshake, opens the encrypted session, submits + decrypts,
456
+ * and returns. Same proof chain (`createSession`, `submitJob`, `jobCompleted`)
457
+ * as the lower-level `runInference`.
458
+ *
459
+ * @example
460
+ * ```ts
461
+ * import { runInferenceWithKey } from "lightnode-sdk";
462
+ * import WS from "ws";
463
+ *
464
+ * const { answer, txs } = await runInferenceWithKey({
465
+ * network: "testnet",
466
+ * privateKey: process.env.PRIVATE_KEY!,
467
+ * prompt: "Reply with a one-sentence fun fact about the ocean.",
468
+ * WebSocket: WS, // omit in the browser
469
+ * });
470
+ *
471
+ * console.log(answer);
472
+ * ```
473
+ */
474
+ export async function runInferenceWithKey(args) {
475
+ // Resolve the network config and validate the key shape up front so a
476
+ // mistyped key fails BEFORE we touch the RPC or the gateway.
477
+ const network = typeof args.network === "string" ? NETWORKS[args.network] : args.network;
478
+ if (!network)
479
+ throw new Error(`unknown network: ${String(args.network)}`);
480
+ const networkId = (typeof args.network === "string" ? args.network : "mainnet");
481
+ const key = args.privateKey?.trim();
482
+ if (!key || !key.startsWith("0x") || key.length !== 66) {
483
+ throw new Error("runInferenceWithKey: privateKey must be a 0x-prefixed 32-byte hex string");
484
+ }
485
+ const account = viemPrivateKeyToAccount(key);
486
+ const chain = {
487
+ id: network.chainId,
488
+ name: network.label,
489
+ nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 },
490
+ rpcUrls: { default: { http: [network.rpc] } },
491
+ };
492
+ // Keep viem's real types here so signMessage / etc. are typed. The MinimalX
493
+ // casts only happen at the runInference() call site below.
494
+ const publicClient = viemCreatePublicClient({ transport: viemHttp(network.rpc), chain });
495
+ const wallet = viemCreateWalletClient({ account, transport: viemHttp(network.rpc), chain });
496
+ // One-shot SIWE handshake. We do this inline (rather than re-export it) so
497
+ // the caller doesn't need a second import; in browsers + Node it works the
498
+ // same against the consumer-api gateway.
499
+ const gwBase = args.gatewayUrl ?? consumerGatewayUrlFn(networkId);
500
+ const chRes = await fetch(`${gwBase}/api/auth/challenge?address=${account.address}`, {
501
+ headers: { Accept: "application/json" },
502
+ });
503
+ if (!chRes.ok)
504
+ throw new GatewayAuthError(chRes.status, await chRes.text());
505
+ const ch = (await chRes.json());
506
+ if (!ch.message)
507
+ throw new GatewayAuthError(chRes.status, "auth challenge returned no message");
508
+ const signature = await wallet.signMessage({ account, message: ch.message });
509
+ const verifyRes = await fetch(`${gwBase}/api/auth/verify`, {
510
+ method: "POST",
511
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
512
+ body: JSON.stringify({ message: ch.message, signature }),
513
+ });
514
+ if (!verifyRes.ok)
515
+ throw new GatewayAuthError(verifyRes.status, await verifyRes.text());
516
+ const verify = (await verifyRes.json());
517
+ if (!verify.token)
518
+ throw new GatewayAuthError(verifyRes.status, "auth verify returned no token");
519
+ const gateway = new GatewayClientCtor({ network: networkId, bearer: verify.token, baseUrl: args.gatewayUrl });
520
+ // Pick a WebSocket: the browser global if present, otherwise the caller-
521
+ // supplied ctor. We deliberately do NOT try to dynamic-import "ws" - it
522
+ // isn't a hard dep, and a bundler trying to resolve it would fail noisily.
523
+ const wsCtor = args.WebSocket ??
524
+ (typeof globalThis !== "undefined" && globalThis.WebSocket
525
+ ? globalThis.WebSocket
526
+ : undefined);
527
+ if (!wsCtor) {
528
+ throw new Error("runInferenceWithKey: no WebSocket constructor available. In Node, install `ws` and pass it: " +
529
+ "`import WS from 'ws'; runInferenceWithKey({ WebSocket: WS, ... })`");
530
+ }
531
+ return runInference({
532
+ prompt: args.prompt,
533
+ gateway,
534
+ wallet: wallet,
535
+ publicClient: publicClient,
536
+ network,
537
+ model: args.model,
538
+ onChunk: args.onChunk,
539
+ maxRetries: args.maxRetries,
540
+ jobCompletedTimeoutMs: args.jobCompletedTimeoutMs,
541
+ WebSocket: wsCtor,
542
+ relayUrl: args.relayUrl,
543
+ });
544
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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",