lightnode-sdk 0.10.0 → 0.10.2

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, LightChatSession, estimateJobFee, NETWORKS } from "lightnode-sdk";
966
+ import { siweSignIn, GatewayClient, LightChatSession, estimateJobFee, modelId, 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";
@@ -976,6 +976,7 @@ type Turn = {
976
976
  jobId?: string | null;
977
977
  submitTx?: \`0x\${string}\` | null;
978
978
  jobCompletedTx?: \`0x\${string}\` | null;
979
+ sources?: { position: number; title: string; url: string; description: string }[];
979
980
  };
980
981
 
981
982
  // Models live on LightChain mainnet. The visitor picks one per the dropdown.
@@ -1000,6 +1001,10 @@ export default function ChatWeb3() {
1000
1001
  // Reused across turns so follow-ups skip SIWE + createSession.
1001
1002
  const sessionRef = useRef<LightChatSession | null>(null);
1002
1003
  const sessionKeyRef = useRef<string>("");
1004
+ const [searchEnabled, setSearchEnabled] = useState(false);
1005
+ const [searchCapable, setSearchCapable] = useState(false);
1006
+ const searchEnabledRef = useRef(false);
1007
+ searchEnabledRef.current = searchEnabled && searchCapable;
1003
1008
 
1004
1009
  // Read the on-chain fee for the connected network so we can show the
1005
1010
  // visitor the real cost per turn before they click Send.
@@ -1013,6 +1018,23 @@ export default function ChatWeb3() {
1013
1018
  return () => { cancelled = true; };
1014
1019
  }, [network, model]);
1015
1020
 
1021
+ // Gate the Web Search toggle on the model advertising the "search" capability.
1022
+ // (On networks where the capabilities endpoint isn't deployed this 404s and the
1023
+ // toggle stays locked - the honest state until a search-capable worker is up.)
1024
+ useEffect(() => {
1025
+ let cancelled = false;
1026
+ (async () => {
1027
+ try {
1028
+ const gw = new GatewayClient({ network: network ?? "mainnet" });
1029
+ const caps = await gw.getModelCapabilities(modelId(model));
1030
+ if (!cancelled) setSearchCapable(Array.isArray(caps?.capabilities) && caps.capabilities.includes("search"));
1031
+ } catch {
1032
+ if (!cancelled) setSearchCapable(false);
1033
+ }
1034
+ })();
1035
+ return () => { cancelled = true; };
1036
+ }, [network, model]);
1037
+
1016
1038
  // Keep the latest turn in view. Instant while streaming (smooth scrolling on
1017
1039
  // every chunk competes for the main thread); smooth once idle.
1018
1040
  useEffect(() => {
@@ -1046,7 +1068,9 @@ export default function ChatWeb3() {
1046
1068
  */
1047
1069
  async function ensureSession(): Promise<LightChatSession> {
1048
1070
  if (!walletClient || !publicClient || !address || !network) throw new Error("connect a wallet first");
1049
- const key = \`\${address}:\${network}:\${model}\`;
1071
+ // A search session must bind to a search-capable worker, so it keys separately.
1072
+ const wantSearch = searchEnabledRef.current;
1073
+ const key = \`\${address}:\${network}:\${model}:\${wantSearch}\`;
1050
1074
  const existing = sessionRef.current;
1051
1075
  if (existing && !existing.expired && sessionKeyRef.current === key) return existing;
1052
1076
  setBusyStage("Sign in with your wallet (SIWE)...");
@@ -1059,6 +1083,7 @@ export default function ChatWeb3() {
1059
1083
  publicClient: publicClient as unknown as Parameters<typeof LightChatSession.open>[0]["publicClient"],
1060
1084
  network: NETWORKS[network],
1061
1085
  model,
1086
+ ...(wantSearch ? { requiredCapabilities: ["search"] } : {}),
1062
1087
  });
1063
1088
  sessionRef.current = chat;
1064
1089
  sessionKeyRef.current = key;
@@ -1086,7 +1111,7 @@ export default function ChatWeb3() {
1086
1111
  patchLastAssistant({ text: totalSoFar });
1087
1112
  };
1088
1113
  const onStage = (s: string) => setBusyStage(s);
1089
- const sendOpts = { onChunk, onStage };
1114
+ const sendOpts = { onChunk, onStage, searchEnabled: searchEnabledRef.current };
1090
1115
 
1091
1116
  const chat = await ensureSession();
1092
1117
  const result = await chat.send(prompt, sendOpts).catch(async () => {
@@ -1105,6 +1130,7 @@ export default function ChatWeb3() {
1105
1130
  jobId: result.jobId?.toString() ?? null,
1106
1131
  submitTx: result.txs?.submitJob ?? null,
1107
1132
  jobCompletedTx: result.txs?.jobCompleted ?? null,
1133
+ sources: result.sources,
1108
1134
  });
1109
1135
  } catch (e) {
1110
1136
  // Roll back the optimistic user bubble so the visitor can retry.
@@ -1184,6 +1210,22 @@ export default function ChatWeb3() {
1184
1210
  {busyStage || "Thinking..."}
1185
1211
  </div>
1186
1212
  )}
1213
+ {t.sources && t.sources.length > 0 && (
1214
+ <div className="mt-1 border-t border-border pt-3">
1215
+ <div className="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">Sources</div>
1216
+ <div className="grid gap-2">
1217
+ {t.sources.map((s) => (
1218
+ <a key={s.position + "-" + s.url} href={s.url} target="_blank" rel="noopener noreferrer" className="grid grid-cols-[1.5rem_1fr] gap-2 rounded-lg bg-surface-base-faint px-2.5 py-2 transition-colors hover:bg-card">
1219
+ <span className="flex size-5 items-center justify-center rounded-md bg-card text-[11px] font-medium text-muted-foreground">{s.position}</span>
1220
+ <span className="min-w-0">
1221
+ <span className="block truncate text-sm font-medium text-foreground hover:underline">{s.title || s.url}</span>
1222
+ <span className="block truncate text-xs text-muted-foreground">{s.description || s.url}</span>
1223
+ </span>
1224
+ </a>
1225
+ ))}
1226
+ </div>
1227
+ </div>
1228
+ )}
1187
1229
  {t.submitTx && (
1188
1230
  <div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100">
1189
1231
  <button
@@ -1233,17 +1275,35 @@ export default function ChatWeb3() {
1233
1275
  />
1234
1276
  </div>
1235
1277
  <div className="mt-1 flex items-center justify-between gap-2">
1236
- <select
1237
- value={model}
1238
- onChange={(e) => setModel(e.target.value as ModelId)}
1239
- disabled={busy}
1240
- title="Model (both live on LightChain mainnet)"
1241
- className="rounded-lg border border-border bg-background px-2 py-1 text-xs font-medium text-muted-foreground outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
1242
- >
1243
- {MODELS.map((m) => (
1244
- <option key={m} value={m}>{m}</option>
1245
- ))}
1246
- </select>
1278
+ <div className="flex items-center gap-3">
1279
+ <select
1280
+ value={model}
1281
+ onChange={(e) => setModel(e.target.value as ModelId)}
1282
+ disabled={busy}
1283
+ title="Model (both live on LightChain mainnet)"
1284
+ className="rounded-lg border border-border bg-background px-2 py-1 text-xs font-medium text-muted-foreground outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
1285
+ >
1286
+ {MODELS.map((m) => (
1287
+ <option key={m} value={m}>{m}</option>
1288
+ ))}
1289
+ </select>
1290
+ <div
1291
+ className="flex items-center gap-2"
1292
+ title={searchCapable ? "Let the worker search the web for this turn" : "No web-search-capable worker is online for this model right now."}
1293
+ >
1294
+ <button
1295
+ type="button"
1296
+ role="switch"
1297
+ aria-checked={searchEnabled && searchCapable}
1298
+ disabled={!searchCapable || busy}
1299
+ onClick={() => setSearchEnabled((v) => !v)}
1300
+ className={"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50 " + (searchEnabled && searchCapable ? "bg-primary" : "bg-muted-foreground/30")}
1301
+ >
1302
+ <span className={"inline-block size-4 rounded-full bg-white shadow transition-transform " + (searchEnabled && searchCapable ? "translate-x-4" : "translate-x-0.5")} />
1303
+ </button>
1304
+ <span className="text-xs text-muted-foreground">Web Search</span>
1305
+ </div>
1306
+ </div>
1247
1307
  <button
1248
1308
  type="button"
1249
1309
  onClick={() => send()}
@@ -158,6 +158,8 @@ export interface RunInferenceArgs {
158
158
  network: NetworkConfig;
159
159
  /** Inference model tag. Default: `"llama3-8b"`. */
160
160
  model?: string;
161
+ /** Opt into worker-side web search (needs a search-capable worker). */
162
+ searchEnabled?: boolean;
161
163
  /**
162
164
  * Streaming callback invoked once per decrypted relay chunk. Use for live
163
165
  * stdout / UI updates. Optional - the final `answer` is returned either way.
@@ -339,6 +341,8 @@ export interface RunInferenceWithKeyArgs {
339
341
  prompt: string;
340
342
  /** Inference model tag. Default: `"llama3-8b"`. */
341
343
  model?: string;
344
+ /** Opt into worker-side web search (needs a search-capable worker). */
345
+ searchEnabled?: boolean;
342
346
  /**
343
347
  * Streaming callback invoked once per decrypted relay chunk. Use for live
344
348
  * stdout / UI updates. Optional - the final `answer` is returned either way.
package/dist/inference.js CHANGED
@@ -544,9 +544,11 @@ async function runOneAttempt(args, attempt) {
544
544
  publicClient: args.publicClient,
545
545
  network: args.network,
546
546
  model: args.model,
547
+ ...(args.searchEnabled ? { requiredCapabilities: ["search"] } : {}),
547
548
  });
548
549
  return runJobOnSession(session, args.prompt, {
549
550
  onChunk: args.onChunk,
551
+ searchEnabled: args.searchEnabled,
550
552
  jobCompletedTimeoutMs: args.jobCompletedTimeoutMs,
551
553
  WebSocket: args.WebSocket,
552
554
  relayUrl: args.relayUrl,
@@ -762,6 +764,7 @@ export async function runInferenceWithKey(args) {
762
764
  publicClient: publicClient,
763
765
  network,
764
766
  model: args.model,
767
+ searchEnabled: args.searchEnabled,
765
768
  onChunk: args.onChunk,
766
769
  maxRetries: args.maxRetries,
767
770
  jobCompletedTimeoutMs: args.jobCompletedTimeoutMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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",