lightnode-sdk 0.9.1 → 0.10.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
@@ -1085,17 +1085,17 @@ export default function ChatWeb3() {
1085
1085
  setBusyStage("");
1086
1086
  patchLastAssistant({ text: totalSoFar });
1087
1087
  };
1088
+ const onStage = (s: string) => setBusyStage(s);
1089
+ const sendOpts = { onChunk, onStage };
1088
1090
 
1089
1091
  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
+ const result = await chat.send(prompt, sendOpts).catch(async () => {
1092
1093
  // Session expired or the worker stopped serving - reopen once and retry.
1093
1094
  sessionRef.current = null;
1094
1095
  patchLastAssistant({ text: "" });
1095
1096
  setBusyStage("Re-opening session...");
1096
1097
  const fresh = await ensureSession();
1097
- setBusyStage("Approve the per-turn transaction in your wallet...");
1098
- return fresh.send(prompt, { onChunk });
1098
+ return fresh.send(prompt, sendOpts);
1099
1099
  });
1100
1100
 
1101
1101
  patchLastAssistant({
@@ -1181,7 +1181,7 @@ export default function ChatWeb3() {
1181
1181
  )
1182
1182
  ) : (
1183
1183
  <div className="animate-pulse-dot pt-1 text-sm text-muted-foreground">
1184
- {busyStage || "Writing on chain..."}
1184
+ {busyStage || "Thinking..."}
1185
1185
  </div>
1186
1186
  )}
1187
1187
  {t.submitTx && (
package/dist/gateway.d.ts CHANGED
@@ -92,8 +92,19 @@ export declare class GatewayClient {
92
92
  name: string;
93
93
  }[];
94
94
  }>;
95
- /** Protected: dispatcher picks a worker for a session and returns its pubkey. */
96
- selectSession(modelId: `0x${string}`): Promise<SelectSessionResult>;
95
+ /** Protected: dispatcher picks a worker for a session and returns its pubkey.
96
+ * Pass requiredCapabilities (e.g. ["search"]) to bind only to a worker that
97
+ * advertises them. */
98
+ selectSession(modelId: `0x${string}`, opts?: {
99
+ requiredCapabilities?: string[];
100
+ }): Promise<SelectSessionResult>;
101
+ /** Public: union of capabilities advertised by active workers for a model
102
+ * (e.g. ["search"]). Used to gate UI before a session binds. Note: this
103
+ * endpoint may 404 on networks where consumer-api hasn't deployed it yet. */
104
+ getModelCapabilities(modelIdHex: `0x${string}`): Promise<{
105
+ modelId: string;
106
+ capabilities: string[];
107
+ }>;
97
108
  /**
98
109
  * Protected: hand the dispatcher the encrypted session key it can give the
99
110
  * worker, get back the EIP-712 signature authorising on-chain createSession.
@@ -116,8 +127,11 @@ export declare class GatewayClient {
116
127
  selectionId?: string;
117
128
  requiredCapabilities?: string[];
118
129
  }): Promise<PrepareSessionResult>;
119
- /** Protected: upload an encrypted prompt blob; returns the EIP-4844 blob hash. */
120
- uploadBlob(base64Data: string): Promise<UploadBlobResult>;
130
+ /** Protected: upload an encrypted prompt blob; returns the EIP-4844 blob hash.
131
+ * Pass { searchEnabled: true } to opt this job into worker-side web search. */
132
+ uploadBlob(base64Data: string, opts?: {
133
+ searchEnabled?: boolean;
134
+ }): Promise<UploadBlobResult>;
121
135
  /** Protected: fetch the relay JWT for an active session (202 = pending). */
122
136
  getSessionToken(sessionId: number): Promise<SessionTokenResult>;
123
137
  private req;
package/dist/gateway.js CHANGED
@@ -75,9 +75,21 @@ export class GatewayClient {
75
75
  getModels() {
76
76
  return this.req("GET", "/api/models");
77
77
  }
78
- /** Protected: dispatcher picks a worker for a session and returns its pubkey. */
79
- selectSession(modelId) {
80
- return this.req("POST", "/api/sessions/select", { modelId });
78
+ /** Protected: dispatcher picks a worker for a session and returns its pubkey.
79
+ * Pass requiredCapabilities (e.g. ["search"]) to bind only to a worker that
80
+ * advertises them. */
81
+ selectSession(modelId, opts) {
82
+ const body = { modelId };
83
+ if (opts?.requiredCapabilities && opts.requiredCapabilities.length > 0) {
84
+ body.requiredCapabilities = opts.requiredCapabilities;
85
+ }
86
+ return this.req("POST", "/api/sessions/select", body);
87
+ }
88
+ /** Public: union of capabilities advertised by active workers for a model
89
+ * (e.g. ["search"]). Used to gate UI before a session binds. Note: this
90
+ * endpoint may 404 on networks where consumer-api hasn't deployed it yet. */
91
+ getModelCapabilities(modelIdHex) {
92
+ return this.req("GET", `/api/models/${modelIdHex}/capabilities`);
81
93
  }
82
94
  /**
83
95
  * Protected: hand the dispatcher the encrypted session key it can give the
@@ -92,9 +104,13 @@ export class GatewayClient {
92
104
  prepareSession(input) {
93
105
  return this.req("POST", "/api/sessions/prepare", input);
94
106
  }
95
- /** Protected: upload an encrypted prompt blob; returns the EIP-4844 blob hash. */
96
- uploadBlob(base64Data) {
97
- return this.req("POST", "/api/blobs", { data: base64Data });
107
+ /** Protected: upload an encrypted prompt blob; returns the EIP-4844 blob hash.
108
+ * Pass { searchEnabled: true } to opt this job into worker-side web search. */
109
+ uploadBlob(base64Data, opts) {
110
+ const body = { data: base64Data };
111
+ if (opts?.searchEnabled === true)
112
+ body.searchEnabled = true;
113
+ return this.req("POST", "/api/blobs", body);
98
114
  }
99
115
  /** Protected: fetch the relay JWT for an active session (202 = pending). */
100
116
  getSessionToken(sessionId) {
package/dist/index.d.ts CHANGED
@@ -137,7 +137,7 @@ export declare class LightNode {
137
137
  export declare const SDK_VERSION = "0.7.20";
138
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
- export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
140
+ export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult, OpenSession, OpenSessionArgs, RunJobOpts, WebSearchSource } from "./inference.js";
141
141
  export type { ChatRole, ChatMessage, ConversationOptions, ConversationSendResult } from "./chat.js";
142
142
  export type { BatchPrompt, BatchResult, RunInferenceBatchArgs } from "./batch.js";
143
143
  export type { AgentTool, AgentStep, AgentOptions, AgentRunResult } from "./agent.js";
@@ -73,6 +73,8 @@ export interface SessionPreparation {
73
73
  expiry: bigint;
74
74
  };
75
75
  nonce: number;
76
+ /** Capabilities the bound worker advertises (e.g. ["search"]). */
77
+ workerCapabilities?: string[];
76
78
  }
77
79
  /**
78
80
  * Step 1 + 2 of the protocol: ask the gateway which worker to use, generate a
@@ -82,12 +84,16 @@ export interface SessionPreparation {
82
84
  * After this returns, the caller submits the on-chain `createSession` tx with
83
85
  * `createSessionArgs` and remembers `sessionKey` for the rest of the session.
84
86
  */
85
- export declare function prepareSession(gateway: GatewayClient, modelTag: string): Promise<SessionPreparation>;
87
+ export declare function prepareSession(gateway: GatewayClient, modelTag: string, opts?: {
88
+ requiredCapabilities?: string[];
89
+ }): Promise<SessionPreparation>;
86
90
  /**
87
91
  * Encrypt a UTF-8 prompt with the session key, upload as a blob, and return
88
92
  * the EIP-4844 blob hash to pass to `submitJob(sessionId, blobHash)`.
89
93
  */
90
- export declare function submitPrompt(gateway: GatewayClient, sessionKey: Uint8Array, prompt: string): Promise<`0x${string}`>;
94
+ export declare function submitPrompt(gateway: GatewayClient, sessionKey: Uint8Array, prompt: string, opts?: {
95
+ searchEnabled?: boolean;
96
+ }): Promise<`0x${string}`>;
91
97
  /** Decrypt a worker response (raw bytes or base64 from the relay) with the session key. */
92
98
  export declare function decryptResponse(sessionKey: Uint8Array, ciphertext: Uint8Array | string): Promise<string>;
93
99
  /** Re-export so callers don't have to import from a second module just for the URL helper. */
@@ -203,6 +209,8 @@ export interface RunInferenceResult {
203
209
  worker: `0x${string}`;
204
210
  submitTx: `0x${string}`;
205
211
  }>;
212
+ /** Web-search citations, when searchEnabled and the worker returned them. */
213
+ sources?: WebSearchSource[];
206
214
  }
207
215
  /** A live, on-chain session. Open once, then run many jobs through it - each
208
216
  * follow-up turn skips SIWE + createSession, leaving just the submitJob tx. */
@@ -219,6 +227,8 @@ export interface OpenSession {
219
227
  readonly createTx: `0x${string}`;
220
228
  /** Unix seconds when the on-chain session window closes. */
221
229
  readonly expirySec: number;
230
+ /** Capabilities the bound worker advertises (e.g. ["search"]). */
231
+ readonly capabilities: string[];
222
232
  }
223
233
  export interface OpenSessionArgs {
224
234
  gateway: GatewayClient;
@@ -226,6 +236,8 @@ export interface OpenSessionArgs {
226
236
  publicClient: MinimalPublicClient;
227
237
  network: NetworkConfig;
228
238
  model?: string;
239
+ /** Bind only to a worker advertising these (e.g. ["search"]). */
240
+ requiredCapabilities?: string[];
229
241
  }
230
242
  /**
231
243
  * prepareSession + the on-chain createSession tx. Do this once, then run many
@@ -233,8 +245,20 @@ export interface OpenSessionArgs {
233
245
  * passes or the chosen worker stops serving.
234
246
  */
235
247
  export declare function openSession(args: OpenSessionArgs): Promise<OpenSession>;
248
+ /** A web-search citation returned alongside an answer (from the worker's
249
+ * "metadata" relay frame when searchEnabled was set). */
250
+ export interface WebSearchSource {
251
+ position: number;
252
+ title: string;
253
+ url: string;
254
+ description: string;
255
+ }
236
256
  export interface RunJobOpts {
237
257
  onChunk?: (chunk: string, totalSoFar: string) => void;
258
+ /** Opt this job into worker-side web search (requires a search-capable worker). */
259
+ searchEnabled?: boolean;
260
+ /** Human-readable progress, e.g. "Uploading prompt to chain..." then "Thinking...". */
261
+ onStage?: (stage: string) => void;
238
262
  jobCompletedTimeoutMs?: number;
239
263
  WebSocket?: WebSocketCtor;
240
264
  relayUrl?: string;
@@ -266,6 +290,8 @@ export declare class LightChatSession {
266
290
  get sessionId(): bigint;
267
291
  get worker(): `0x${string}`;
268
292
  get model(): string;
293
+ /** Capabilities the bound worker advertises (e.g. ["search"]). */
294
+ get capabilities(): string[];
269
295
  /** true once the on-chain session window has closed; re-open before sending. */
270
296
  get expired(): boolean;
271
297
  send(prompt: string, opts?: RunJobOpts): Promise<RunInferenceResult>;
package/dist/inference.js CHANGED
@@ -73,8 +73,9 @@ export const JOB_REGISTRY_CONSUMER_ABI = [
73
73
  * After this returns, the caller submits the on-chain `createSession` tx with
74
74
  * `createSessionArgs` and remembers `sessionKey` for the rest of the session.
75
75
  */
76
- export async function prepareSession(gateway, modelTag) {
76
+ export async function prepareSession(gateway, modelTag, opts) {
77
77
  const id = modelId(modelTag);
78
+ const requiredCapabilities = opts?.requiredCapabilities;
78
79
  // The gateway returns 409 selection_mismatch when a NEWER selectSession()
79
80
  // for the same wallet supersedes ours between the select and the prepare.
80
81
  // The error message is literally "re-run POST /api/sessions/select", so we
@@ -98,7 +99,7 @@ export async function prepareSession(gateway, modelTag) {
98
99
  // call has already superseded ours by the time the gateway processes
99
100
  // it. prepareSession 409s when the same happens between select and
100
101
  // prepare. The whole select -> prepare flow is one atomic unit.
101
- const selected = await gateway.selectSession(id);
102
+ const selected = await gateway.selectSession(id, requiredCapabilities ? { requiredCapabilities } : undefined);
102
103
  const sessionKey = await generateSessionKey();
103
104
  // Workers' pubkeys arrive as base64; disputer's as hex - decodePublicKey
104
105
  // accepts either.
@@ -119,10 +120,12 @@ export async function prepareSession(gateway, modelTag) {
119
120
  encWorkerKey: bytesToBase64(encWorker),
120
121
  encDisputerKey: bytesToBase64(encDisputer),
121
122
  ...(selected.selectionId ? { selectionId: selected.selectionId } : {}),
123
+ ...(requiredCapabilities ? { requiredCapabilities } : {}),
122
124
  });
123
125
  return {
124
126
  sessionKey,
125
127
  nonce: prepared.nonce,
128
+ workerCapabilities: selected.workerCapabilities ?? [],
126
129
  createSessionArgs: {
127
130
  paramsHash: id,
128
131
  worker: prepared.worker,
@@ -148,9 +151,9 @@ export async function prepareSession(gateway, modelTag) {
148
151
  * Encrypt a UTF-8 prompt with the session key, upload as a blob, and return
149
152
  * the EIP-4844 blob hash to pass to `submitJob(sessionId, blobHash)`.
150
153
  */
151
- export async function submitPrompt(gateway, sessionKey, prompt) {
154
+ export async function submitPrompt(gateway, sessionKey, prompt, opts) {
152
155
  const ct = await encrypt(sessionKey, utf8ToBytes(prompt));
153
- const res = await gateway.uploadBlob(bytesToBase64(ct));
156
+ const res = await gateway.uploadBlob(bytesToBase64(ct), opts?.searchEnabled ? { searchEnabled: true } : undefined);
154
157
  const first = res.blobHashes?.[0];
155
158
  if (!first)
156
159
  throw new Error("gateway returned no blob hashes");
@@ -228,6 +231,37 @@ function pickWebSocket(provided) {
228
231
  function topicAsUint(hex) {
229
232
  return BigInt(hex);
230
233
  }
234
+ /** Parse a decrypted "metadata" relay frame into web-search citations. Mirrors
235
+ * lcai-chat-v2's parseWebSearchSources: requires type === "webSearchSources"
236
+ * and an array of { position:number, url:string, title?, snippet? }. */
237
+ function parseWebSearchSources(payload) {
238
+ let parsed;
239
+ try {
240
+ parsed = JSON.parse(payload);
241
+ }
242
+ catch {
243
+ return [];
244
+ }
245
+ if (!parsed || typeof parsed !== "object")
246
+ return [];
247
+ const obj = parsed;
248
+ if (obj.type !== "webSearchSources" || !Array.isArray(obj.sources))
249
+ return [];
250
+ const out = [];
251
+ for (const s of obj.sources) {
252
+ if (!s || typeof s !== "object")
253
+ continue;
254
+ const src = s;
255
+ const position = typeof src.position === "number" ? src.position : undefined;
256
+ const url = typeof src.url === "string" ? src.url : "";
257
+ const title = typeof src.title === "string" ? src.title : "";
258
+ const snippet = typeof src.snippet === "string" ? src.snippet : "";
259
+ if (!position || !url)
260
+ continue;
261
+ out.push({ position, title: title || url, url, description: snippet });
262
+ }
263
+ return out;
264
+ }
231
265
  /**
232
266
  * prepareSession + the on-chain createSession tx. Do this once, then run many
233
267
  * jobs through the handle with `runJobOnSession`. Re-open when `expirySec`
@@ -235,7 +269,7 @@ function topicAsUint(hex) {
235
269
  */
236
270
  export async function openSession(args) {
237
271
  const { gateway, wallet, publicClient, network, model = "llama3-8b" } = args;
238
- const prepared = await prepareSession(gateway, model);
272
+ const prepared = await prepareSession(gateway, model, args.requiredCapabilities ? { requiredCapabilities: args.requiredCapabilities } : undefined);
239
273
  const fee = await estimateJobFee(network, model);
240
274
  const createTx = await wallet.writeContract({
241
275
  address: network.jobRegistry,
@@ -269,6 +303,7 @@ export async function openSession(args) {
269
303
  worker: prepared.createSessionArgs.worker,
270
304
  createTx,
271
305
  expirySec: Number(prepared.createSessionArgs.expiry),
306
+ capabilities: prepared.workerCapabilities ?? [],
272
307
  };
273
308
  }
274
309
  /**
@@ -277,7 +312,7 @@ export async function openSession(args) {
277
312
  */
278
313
  export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
279
314
  const { gateway, wallet, publicClient, network, sessionId, sessionKey, worker, fee, createTx } = session;
280
- const { onChunk, jobCompletedTimeoutMs = 120000 } = opts;
315
+ const { onChunk, onStage, searchEnabled, jobCompletedTimeoutMs = 120000 } = opts;
281
316
  const WS = pickWebSocket(opts.WebSocket);
282
317
  const relayUrl = opts.relayUrl ?? `wss://relay.${network.id}.lightchain.ai/ws`;
283
318
  // Shim so the job body below can keep referencing prepared.* unchanged.
@@ -327,6 +362,7 @@ export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
327
362
  setTimeout(() => reject(new Error("relay WebSocket open timeout")), 20000);
328
363
  });
329
364
  const chunks = [];
365
+ const sources = [];
330
366
  let streamDone = false;
331
367
  let streamDoneAt = null;
332
368
  const handleMessage = async (rawData) => {
@@ -344,6 +380,18 @@ export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
344
380
  catch {
345
381
  return;
346
382
  }
383
+ // "metadata" carries web-search citations (sent before the answer stream).
384
+ if (frame.type === "metadata" && frame.payload) {
385
+ try {
386
+ const decoded = await decryptResponse(prepared.sessionKey, frame.payload);
387
+ for (const s of parseWebSearchSources(decoded))
388
+ sources.push(s);
389
+ }
390
+ catch {
391
+ /* ignore malformed metadata */
392
+ }
393
+ return;
394
+ }
347
395
  // "complete" marks end-of-stream (it may or may not carry a payload).
348
396
  // Record it so the JobCompleted wait can stop promptly instead of polling
349
397
  // the full grace window after the answer is already in hand.
@@ -382,7 +430,8 @@ export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
382
430
  ws.addEventListener("message", (ev) => handleMessage(ev.data));
383
431
  }
384
432
  // 4. encrypt + upload prompt
385
- const promptHash = await submitPrompt(gateway, prepared.sessionKey, prompt);
433
+ onStage?.(searchEnabled ? "Searching the web + uploading prompt..." : "Uploading prompt to chain...");
434
+ const promptHash = await submitPrompt(gateway, prepared.sessionKey, prompt, searchEnabled ? { searchEnabled: true } : undefined);
386
435
  // 5. submitJob on-chain
387
436
  const submitTx = await wallet.writeContract({
388
437
  address: network.jobRegistry,
@@ -406,6 +455,7 @@ export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
406
455
  if (!jobLog)
407
456
  throw new Error("JobSubmitted log missing in submitJob receipt");
408
457
  const jobId = topicAsUint(jobLog.topics[1]);
458
+ onStage?.("Thinking...");
409
459
  // 6. wait for JobCompleted
410
460
  // The actual *result* is the WS-delivered, session-key-decrypted ciphertext.
411
461
  // JobCompleted is an explorer pointer (the worker's commit-result tx).
@@ -483,6 +533,7 @@ export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
483
533
  jobId,
484
534
  attempts: attempt,
485
535
  stalled: [],
536
+ ...(sources.length > 0 ? { sources } : {}),
486
537
  };
487
538
  }
488
539
  /** One attempt = open a fresh session, then run one job through it. */
@@ -526,6 +577,8 @@ export class LightChatSession {
526
577
  get sessionId() { return this.session.sessionId; }
527
578
  get worker() { return this.session.worker; }
528
579
  get model() { return this.session.model; }
580
+ /** Capabilities the bound worker advertises (e.g. ["search"]). */
581
+ get capabilities() { return this.session.capabilities; }
529
582
  /** true once the on-chain session window has closed; re-open before sending. */
530
583
  get expired() { return Date.now() / 1000 >= this.session.expirySec; }
531
584
  send(prompt, opts = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.9.1",
3
+ "version": "0.10.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",