lightnode-sdk 0.8.10 → 0.9.0

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
@@ -963,7 +963,7 @@ const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
963
963
 
964
964
  import { useEffect, useRef, useState } from "react";
965
965
  import { useAccount, useWalletClient, usePublicClient } from "wagmi";
966
- import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
966
+ import { siweSignIn, GatewayClient, LightChatSession, estimateJobFee, NETWORKS } from "lightnode-sdk";
967
967
  import { Streamdown } from "streamdown";
968
968
  import { ConnectButton } from "@/components/connect-button";
969
969
  import { LcaiMark } from "@/components/lcai-mark";
@@ -997,6 +997,9 @@ export default function ChatWeb3() {
997
997
  const [err, setErr] = useState<string | null>(null);
998
998
  const [feeLcai, setFeeLcai] = useState<number | null>(null);
999
999
  const endRef = useRef<HTMLDivElement>(null);
1000
+ // Reused across turns so follow-ups skip SIWE + createSession.
1001
+ const sessionRef = useRef<LightChatSession | null>(null);
1002
+ const sessionKeyRef = useRef<string>("");
1000
1003
 
1001
1004
  // Read the on-chain fee for the connected network so we can show the
1002
1005
  // visitor the real cost per turn before they click Send.
@@ -1037,6 +1040,31 @@ export default function ChatWeb3() {
1037
1040
  });
1038
1041
  }
1039
1042
 
1043
+ /**
1044
+ * Open a session on the first turn (or after expiry / a model or wallet
1045
+ * change), then reuse it so every follow-up turn skips SIWE + createSession.
1046
+ */
1047
+ async function ensureSession(): Promise<LightChatSession> {
1048
+ if (!walletClient || !publicClient || !address || !network) throw new Error("connect a wallet first");
1049
+ const key = \`\${address}:\${network}:\${model}\`;
1050
+ const existing = sessionRef.current;
1051
+ if (existing && !existing.expired && sessionKeyRef.current === key) return existing;
1052
+ setBusyStage("Sign in with your wallet (SIWE)...");
1053
+ const siwe = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
1054
+ setBusyStage("Approve createSession in your wallet (one-time per session)...");
1055
+ const gateway = new GatewayClient({ network, bearer: siwe.bearer });
1056
+ const chat = await LightChatSession.open({
1057
+ gateway,
1058
+ wallet: walletClient as unknown as Parameters<typeof LightChatSession.open>[0]["wallet"],
1059
+ publicClient: publicClient as unknown as Parameters<typeof LightChatSession.open>[0]["publicClient"],
1060
+ network: NETWORKS[network],
1061
+ model,
1062
+ });
1063
+ sessionRef.current = chat;
1064
+ sessionKeyRef.current = key;
1065
+ return chat;
1066
+ }
1067
+
1040
1068
  async function send() {
1041
1069
  if (!walletClient || !publicClient || !address || !network) {
1042
1070
  setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
@@ -1053,26 +1081,21 @@ export default function ChatWeb3() {
1053
1081
  try {
1054
1082
  const system = "You are a concise assistant. Reply in one or two short sentences.";
1055
1083
  const prompt = composePrompt(history, next, system);
1056
-
1057
- setBusyStage("Sign in with your wallet (SIWE)...");
1058
- const session = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
1059
-
1060
- setBusyStage("Approve the createSession transaction in your wallet...");
1061
- const gateway = new GatewayClient({ network, bearer: session.bearer });
1062
- const result = await runInference({
1063
- prompt,
1064
- gateway,
1065
- wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1066
- publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1067
- network: NETWORKS[network],
1068
- model,
1069
- jobCompletedTimeoutMs: 120_000,
1070
- maxRetries: 1,
1071
- // Stream each decrypted chunk into the assistant bubble as it arrives.
1072
- onChunk: (_chunk, totalSoFar) => {
1073
- setBusyStage("");
1074
- patchLastAssistant({ text: totalSoFar });
1075
- },
1084
+ const onChunk = (_chunk: string, totalSoFar: string) => {
1085
+ setBusyStage("");
1086
+ patchLastAssistant({ text: totalSoFar });
1087
+ };
1088
+
1089
+ const chat = await ensureSession();
1090
+ setBusyStage("Approve the per-turn transaction in your wallet...");
1091
+ const result = await chat.send(prompt, { onChunk }).catch(async () => {
1092
+ // Session expired or the worker stopped serving - reopen once and retry.
1093
+ sessionRef.current = null;
1094
+ patchLastAssistant({ text: "" });
1095
+ setBusyStage("Re-opening session...");
1096
+ const fresh = await ensureSession();
1097
+ setBusyStage("Approve the per-turn transaction in your wallet...");
1098
+ return fresh.send(prompt, { onChunk });
1076
1099
  });
1077
1100
 
1078
1101
  patchLastAssistant({
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
2
  import { fetchWorkerModels, fromWei, resolveJobTransactions } 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, runInferenceStream } from "./inference.js";
4
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream, openSession, runJobOnSession, LightChatSession } from "./inference.js";
5
5
  import { Conversation, chat } from "./chat.js";
6
6
  import { runInferenceBatch } from "./batch.js";
7
7
  import { Agent, parseAgentOutput } from "./agent.js";
@@ -135,7 +135,7 @@ export declare class LightNode {
135
135
  * may pin an older minor than the local install command suggests).
136
136
  */
137
137
  export declare const SDK_VERSION = "0.7.20";
138
- export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, siweSignIn, siweChallenge, siweVerify, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
138
+ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, siweSignIn, siweChallenge, siweVerify, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, openSession, runJobOnSession, LightChatSession, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
139
139
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
140
140
  export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
141
141
  export type { ChatRole, ChatMessage, ConversationOptions, ConversationSendResult } from "./chat.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
2
  import { fetchWorker, fetchWorkerJobs, fetchWorkerModels, fetchRecentJobs, fetchJob, fetchModels, fetchWorkers, summarize, fromWei, resolveJobTransactions, } from "./subgraph.js";
3
3
  import { isRegistered, fetchOnchainEligibleModels } 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, runInferenceStream, } from "./inference.js";
5
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream, openSession, runJobOnSession, LightChatSession, } from "./inference.js";
6
6
  import { Conversation, chat } from "./chat.js";
7
7
  import { runInferenceBatch } from "./batch.js";
8
8
  import { Agent, parseAgentOutput } from "./agent.js";
@@ -233,6 +233,8 @@ runInference,
233
233
  runInferenceWithKey,
234
234
  // v0.4.9 AsyncIterable<string> wrapper around runInferenceWithKey.
235
235
  runInferenceStream,
236
+ // v0.9.0 session reuse: open once, run many jobs (follow-ups skip createSession).
237
+ openSession, runJobOnSession, LightChatSession,
236
238
  // v0.5.0 multi-turn conversation helper (history client-side; one inference per turn).
237
239
  Conversation, chat,
238
240
  // v0.6.0 batch runner: many prompts, capped parallelism, stable result order.
@@ -204,6 +204,72 @@ export interface RunInferenceResult {
204
204
  submitTx: `0x${string}`;
205
205
  }>;
206
206
  }
207
+ /** A live, on-chain session. Open once, then run many jobs through it - each
208
+ * follow-up turn skips SIWE + createSession, leaving just the submitJob tx. */
209
+ export interface OpenSession {
210
+ readonly gateway: GatewayClient;
211
+ readonly wallet: MinimalWalletClient;
212
+ readonly publicClient: MinimalPublicClient;
213
+ readonly network: NetworkConfig;
214
+ readonly model: string;
215
+ readonly fee: number;
216
+ readonly sessionId: bigint;
217
+ readonly sessionKey: Uint8Array;
218
+ readonly worker: `0x${string}`;
219
+ readonly createTx: `0x${string}`;
220
+ /** Unix seconds when the on-chain session window closes. */
221
+ readonly expirySec: number;
222
+ }
223
+ export interface OpenSessionArgs {
224
+ gateway: GatewayClient;
225
+ wallet: MinimalWalletClient;
226
+ publicClient: MinimalPublicClient;
227
+ network: NetworkConfig;
228
+ model?: string;
229
+ }
230
+ /**
231
+ * prepareSession + the on-chain createSession tx. Do this once, then run many
232
+ * jobs through the handle with `runJobOnSession`. Re-open when `expirySec`
233
+ * passes or the chosen worker stops serving.
234
+ */
235
+ export declare function openSession(args: OpenSessionArgs): Promise<OpenSession>;
236
+ export interface RunJobOpts {
237
+ onChunk?: (chunk: string, totalSoFar: string) => void;
238
+ jobCompletedTimeoutMs?: number;
239
+ WebSocket?: WebSocketCtor;
240
+ relayUrl?: string;
241
+ signal?: AbortSignal;
242
+ }
243
+ /**
244
+ * Run ONE job against an already-open session: submitPrompt + submitJob + relay
245
+ * stream + wait for JobCompleted. No SIWE, no createSession.
246
+ */
247
+ export declare function runJobOnSession(session: OpenSession, prompt: string, opts?: RunJobOpts, attempt?: number): Promise<RunInferenceResult>;
248
+ /**
249
+ * A reusable, wallet-signed inference session. Open it once (SIWE happens before
250
+ * this; createSession happens here), then call `.send()` per turn - each
251
+ * follow-up turn skips createSession, leaving just the submitJob tx. Re-open
252
+ * (call `LightChatSession.open(...)` again) when `expired` is true or a
253
+ * `.send()` throws because the worker stopped serving.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * const session = await LightChatSession.open({ gateway, wallet, publicClient, network, model: "llama3-8b" });
258
+ * const a = await session.send("Who wrote The Great Gatsby?", { onChunk });
259
+ * const b = await session.send("In what year?", { onChunk }); // no createSession
260
+ * ```
261
+ */
262
+ export declare class LightChatSession {
263
+ private readonly session;
264
+ private constructor();
265
+ static open(args: OpenSessionArgs): Promise<LightChatSession>;
266
+ get sessionId(): bigint;
267
+ get worker(): `0x${string}`;
268
+ get model(): string;
269
+ /** true once the on-chain session window has closed; re-open before sending. */
270
+ get expired(): boolean;
271
+ send(prompt: string, opts?: RunJobOpts): Promise<RunInferenceResult>;
272
+ }
207
273
  /**
208
274
  * One call, full encrypted inference. Same code path the live playground at
209
275
  * lightnode.app/playground drives, condensed into a single function.
package/dist/inference.js CHANGED
@@ -228,14 +228,15 @@ function pickWebSocket(provided) {
228
228
  function topicAsUint(hex) {
229
229
  return BigInt(hex);
230
230
  }
231
- async function runOneAttempt(args, attempt) {
232
- const { prompt, gateway, wallet, publicClient, network, model = "llama3-8b", onChunk, jobCompletedTimeoutMs = 120000, } = args;
233
- const WS = pickWebSocket(args.WebSocket);
234
- const relayUrl = args.relayUrl ?? `wss://relay.${network.id}.lightchain.ai/ws`;
235
- // 1. prepareSession
231
+ /**
232
+ * prepareSession + the on-chain createSession tx. Do this once, then run many
233
+ * jobs through the handle with `runJobOnSession`. Re-open when `expirySec`
234
+ * passes or the chosen worker stops serving.
235
+ */
236
+ export async function openSession(args) {
237
+ const { gateway, wallet, publicClient, network, model = "llama3-8b" } = args;
236
238
  const prepared = await prepareSession(gateway, model);
237
239
  const fee = await estimateJobFee(network, model);
238
- // 2. createSession on-chain
239
240
  const createTx = await wallet.writeContract({
240
241
  address: network.jobRegistry,
241
242
  abi: JOB_REGISTRY_ABI_PARSED,
@@ -256,7 +257,31 @@ async function runOneAttempt(args, attempt) {
256
257
  const createLog = (await publicClient.getLogs({ address: network.jobRegistry, blockHash: createReceipt.blockHash })).find((l) => l.transactionHash === createTx && l.topics[0] === SESSION_CREATED_TOPIC);
257
258
  if (!createLog)
258
259
  throw new Error("SessionCreated log missing in createSession receipt");
259
- const sessionId = topicAsUint(createLog.topics[1]);
260
+ return {
261
+ gateway,
262
+ wallet,
263
+ publicClient,
264
+ network,
265
+ model,
266
+ fee,
267
+ sessionId: topicAsUint(createLog.topics[1]),
268
+ sessionKey: prepared.sessionKey,
269
+ worker: prepared.createSessionArgs.worker,
270
+ createTx,
271
+ expirySec: Number(prepared.createSessionArgs.expiry),
272
+ };
273
+ }
274
+ /**
275
+ * Run ONE job against an already-open session: submitPrompt + submitJob + relay
276
+ * stream + wait for JobCompleted. No SIWE, no createSession.
277
+ */
278
+ export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
279
+ const { gateway, wallet, publicClient, network, sessionId, sessionKey, worker, fee, createTx } = session;
280
+ const { onChunk, jobCompletedTimeoutMs = 120000 } = opts;
281
+ const WS = pickWebSocket(opts.WebSocket);
282
+ const relayUrl = opts.relayUrl ?? `wss://relay.${network.id}.lightchain.ai/ws`;
283
+ // Shim so the job body below can keep referencing prepared.* unchanged.
284
+ const prepared = { sessionKey, createSessionArgs: { worker } };
260
285
  // 3. relay token + WebSocket
261
286
  let relayToken;
262
287
  for (let i = 0; i < 30 && !relayToken; i++) {
@@ -451,6 +476,53 @@ async function runOneAttempt(args, attempt) {
451
476
  stalled: [],
452
477
  };
453
478
  }
479
+ /** One attempt = open a fresh session, then run one job through it. */
480
+ async function runOneAttempt(args, attempt) {
481
+ const session = await openSession({
482
+ gateway: args.gateway,
483
+ wallet: args.wallet,
484
+ publicClient: args.publicClient,
485
+ network: args.network,
486
+ model: args.model,
487
+ });
488
+ return runJobOnSession(session, args.prompt, {
489
+ onChunk: args.onChunk,
490
+ jobCompletedTimeoutMs: args.jobCompletedTimeoutMs,
491
+ WebSocket: args.WebSocket,
492
+ relayUrl: args.relayUrl,
493
+ signal: args.signal,
494
+ }, attempt);
495
+ }
496
+ /**
497
+ * A reusable, wallet-signed inference session. Open it once (SIWE happens before
498
+ * this; createSession happens here), then call `.send()` per turn - each
499
+ * follow-up turn skips createSession, leaving just the submitJob tx. Re-open
500
+ * (call `LightChatSession.open(...)` again) when `expired` is true or a
501
+ * `.send()` throws because the worker stopped serving.
502
+ *
503
+ * @example
504
+ * ```ts
505
+ * const session = await LightChatSession.open({ gateway, wallet, publicClient, network, model: "llama3-8b" });
506
+ * const a = await session.send("Who wrote The Great Gatsby?", { onChunk });
507
+ * const b = await session.send("In what year?", { onChunk }); // no createSession
508
+ * ```
509
+ */
510
+ export class LightChatSession {
511
+ constructor(session) {
512
+ this.session = session;
513
+ }
514
+ static async open(args) {
515
+ return new LightChatSession(await openSession(args));
516
+ }
517
+ get sessionId() { return this.session.sessionId; }
518
+ get worker() { return this.session.worker; }
519
+ get model() { return this.session.model; }
520
+ /** true once the on-chain session window has closed; re-open before sending. */
521
+ get expired() { return Date.now() / 1000 >= this.session.expirySec; }
522
+ send(prompt, opts = {}) {
523
+ return runJobOnSession(this.session, prompt, opts);
524
+ }
525
+ }
454
526
  /**
455
527
  * One call, full encrypted inference. Same code path the live playground at
456
528
  * lightnode.app/playground drives, condensed into a single function.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.8.10",
3
+ "version": "0.9.0",
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",