lightnode-sdk 0.9.2 → 0.10.1

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/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. */
@@ -152,6 +158,8 @@ export interface RunInferenceArgs {
152
158
  network: NetworkConfig;
153
159
  /** Inference model tag. Default: `"llama3-8b"`. */
154
160
  model?: string;
161
+ /** Opt into worker-side web search (needs a search-capable worker). */
162
+ searchEnabled?: boolean;
155
163
  /**
156
164
  * Streaming callback invoked once per decrypted relay chunk. Use for live
157
165
  * stdout / UI updates. Optional - the final `answer` is returned either way.
@@ -203,6 +211,8 @@ export interface RunInferenceResult {
203
211
  worker: `0x${string}`;
204
212
  submitTx: `0x${string}`;
205
213
  }>;
214
+ /** Web-search citations, when searchEnabled and the worker returned them. */
215
+ sources?: WebSearchSource[];
206
216
  }
207
217
  /** A live, on-chain session. Open once, then run many jobs through it - each
208
218
  * follow-up turn skips SIWE + createSession, leaving just the submitJob tx. */
@@ -219,6 +229,8 @@ export interface OpenSession {
219
229
  readonly createTx: `0x${string}`;
220
230
  /** Unix seconds when the on-chain session window closes. */
221
231
  readonly expirySec: number;
232
+ /** Capabilities the bound worker advertises (e.g. ["search"]). */
233
+ readonly capabilities: string[];
222
234
  }
223
235
  export interface OpenSessionArgs {
224
236
  gateway: GatewayClient;
@@ -226,6 +238,8 @@ export interface OpenSessionArgs {
226
238
  publicClient: MinimalPublicClient;
227
239
  network: NetworkConfig;
228
240
  model?: string;
241
+ /** Bind only to a worker advertising these (e.g. ["search"]). */
242
+ requiredCapabilities?: string[];
229
243
  }
230
244
  /**
231
245
  * prepareSession + the on-chain createSession tx. Do this once, then run many
@@ -233,8 +247,18 @@ export interface OpenSessionArgs {
233
247
  * passes or the chosen worker stops serving.
234
248
  */
235
249
  export declare function openSession(args: OpenSessionArgs): Promise<OpenSession>;
250
+ /** A web-search citation returned alongside an answer (from the worker's
251
+ * "metadata" relay frame when searchEnabled was set). */
252
+ export interface WebSearchSource {
253
+ position: number;
254
+ title: string;
255
+ url: string;
256
+ description: string;
257
+ }
236
258
  export interface RunJobOpts {
237
259
  onChunk?: (chunk: string, totalSoFar: string) => void;
260
+ /** Opt this job into worker-side web search (requires a search-capable worker). */
261
+ searchEnabled?: boolean;
238
262
  /** Human-readable progress, e.g. "Uploading prompt to chain..." then "Thinking...". */
239
263
  onStage?: (stage: string) => void;
240
264
  jobCompletedTimeoutMs?: number;
@@ -268,6 +292,8 @@ export declare class LightChatSession {
268
292
  get sessionId(): bigint;
269
293
  get worker(): `0x${string}`;
270
294
  get model(): string;
295
+ /** Capabilities the bound worker advertises (e.g. ["search"]). */
296
+ get capabilities(): string[];
271
297
  /** true once the on-chain session window has closed; re-open before sending. */
272
298
  get expired(): boolean;
273
299
  send(prompt: string, opts?: RunJobOpts): Promise<RunInferenceResult>;
@@ -315,6 +341,8 @@ export interface RunInferenceWithKeyArgs {
315
341
  prompt: string;
316
342
  /** Inference model tag. Default: `"llama3-8b"`. */
317
343
  model?: string;
344
+ /** Opt into worker-side web search (needs a search-capable worker). */
345
+ searchEnabled?: boolean;
318
346
  /**
319
347
  * Streaming callback invoked once per decrypted relay chunk. Use for live
320
348
  * stdout / UI updates. Optional - the final `answer` is returned either way.
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, onStage, 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,8 +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
- onStage?.("Uploading prompt to chain...");
386
- 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);
387
435
  // 5. submitJob on-chain
388
436
  const submitTx = await wallet.writeContract({
389
437
  address: network.jobRegistry,
@@ -485,6 +533,7 @@ export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
485
533
  jobId,
486
534
  attempts: attempt,
487
535
  stalled: [],
536
+ ...(sources.length > 0 ? { sources } : {}),
488
537
  };
489
538
  }
490
539
  /** One attempt = open a fresh session, then run one job through it. */
@@ -495,9 +544,11 @@ async function runOneAttempt(args, attempt) {
495
544
  publicClient: args.publicClient,
496
545
  network: args.network,
497
546
  model: args.model,
547
+ ...(args.searchEnabled ? { requiredCapabilities: ["search"] } : {}),
498
548
  });
499
549
  return runJobOnSession(session, args.prompt, {
500
550
  onChunk: args.onChunk,
551
+ searchEnabled: args.searchEnabled,
501
552
  jobCompletedTimeoutMs: args.jobCompletedTimeoutMs,
502
553
  WebSocket: args.WebSocket,
503
554
  relayUrl: args.relayUrl,
@@ -528,6 +579,8 @@ export class LightChatSession {
528
579
  get sessionId() { return this.session.sessionId; }
529
580
  get worker() { return this.session.worker; }
530
581
  get model() { return this.session.model; }
582
+ /** Capabilities the bound worker advertises (e.g. ["search"]). */
583
+ get capabilities() { return this.session.capabilities; }
531
584
  /** true once the on-chain session window has closed; re-open before sending. */
532
585
  get expired() { return Date.now() / 1000 >= this.session.expirySec; }
533
586
  send(prompt, opts = {}) {
@@ -711,6 +764,7 @@ export async function runInferenceWithKey(args) {
711
764
  publicClient: publicClient,
712
765
  network,
713
766
  model: args.model,
767
+ searchEnabled: args.searchEnabled,
714
768
  onChunk: args.onChunk,
715
769
  maxRetries: args.maxRetries,
716
770
  jobCompletedTimeoutMs: args.jobCompletedTimeoutMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
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",