lightnode-sdk 0.7.14 → 0.7.16
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.d.ts +28 -0
- package/dist/add.js +583 -8
- package/dist/cli.js +52 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/add.d.ts
CHANGED
|
@@ -48,6 +48,34 @@ export declare function addChat(opts?: AddOpts): {
|
|
|
48
48
|
template: Template;
|
|
49
49
|
network: Network;
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* `lightnode add chat-web3` - the user-pays counterpart to addChat.
|
|
53
|
+
*
|
|
54
|
+
* Architecture:
|
|
55
|
+
* - Each visitor's own wallet signs SIWE + createSession per turn.
|
|
56
|
+
* - The dev's app holds zero funds; cost is borne by each user (0.02 LCAI
|
|
57
|
+
* per llama3-8b turn on mainnet).
|
|
58
|
+
* - No backend, no PRIVATE_KEY, no server-side hot wallet.
|
|
59
|
+
*
|
|
60
|
+
* Fit:
|
|
61
|
+
* - Web3 dApps where users already have a wallet (NFT, meme coin, on-chain
|
|
62
|
+
* games, LightChallenge-style challenge platforms).
|
|
63
|
+
* - For SaaS chatbots where users do NOT have a wallet, use `add chat`
|
|
64
|
+
* instead (dev pays, server-side route).
|
|
65
|
+
*/
|
|
66
|
+
export declare function addChatWeb3(opts?: AddOpts): {
|
|
67
|
+
written: WrittenFile[];
|
|
68
|
+
install: string;
|
|
69
|
+
template: Template;
|
|
70
|
+
network: Network;
|
|
71
|
+
needsWagmi: boolean;
|
|
72
|
+
};
|
|
73
|
+
export declare function addWagmiSetup(opts?: AddOpts): {
|
|
74
|
+
written: WrittenFile[];
|
|
75
|
+
install: string;
|
|
76
|
+
template: Template;
|
|
77
|
+
network: Network;
|
|
78
|
+
};
|
|
51
79
|
export declare function addJudge(opts?: AddOpts): {
|
|
52
80
|
written: WrittenFile[];
|
|
53
81
|
install: string;
|
package/dist/add.js
CHANGED
|
@@ -31,6 +31,75 @@ function detectTemplate(cwd) {
|
|
|
31
31
|
return "hono";
|
|
32
32
|
return "node";
|
|
33
33
|
}
|
|
34
|
+
const HOSTING_GUIDE = `# Hosting LightChain AI inference - your options
|
|
35
|
+
|
|
36
|
+
A single LightChain mainnet inference takes **60-90 seconds** under normal
|
|
37
|
+
load (the workers do the model run, attest the result on-chain, and return
|
|
38
|
+
the answer). If your server-side route has a function timeout shorter than
|
|
39
|
+
that, every call will fail with a generic timeout error.
|
|
40
|
+
|
|
41
|
+
This file is a quick reference for picking a host that actually finishes
|
|
42
|
+
the request. If you used \`lightnode add chat-web3\`, ignore everything
|
|
43
|
+
below - that path has NO server-side route to worry about.
|
|
44
|
+
|
|
45
|
+
## TL;DR
|
|
46
|
+
|
|
47
|
+
| Need | Use |
|
|
48
|
+
|---------------------------------------|--------------------------------|
|
|
49
|
+
| Zero infra, users pay (Web3 dApp) | \`lightnode add chat-web3\` |
|
|
50
|
+
| Cheapest server-side host that works | Railway / Fly.io / Render |
|
|
51
|
+
| Already on Vercel and need it to fit | Vercel Pro (60s) + streaming |
|
|
52
|
+
| You self-host already (Docker, VPS) | Anywhere - no timeout to fight |
|
|
53
|
+
|
|
54
|
+
## The detailed table
|
|
55
|
+
|
|
56
|
+
| Host | Free / Hobby timeout | Paid timeout | Verdict |
|
|
57
|
+
|---------------------|----------------------------|-----------------------------|---------|
|
|
58
|
+
| Vercel Hobby (free) | **10 seconds** | - | **DOES NOT WORK** for mainnet inference. Every call times out. |
|
|
59
|
+
| Vercel Pro ($20/mo) | - | 60s default, up to 800s with \`maxDuration\` config | Works if calls finish under 60s; 70-80s calls cut it close. Stream tokens to keep the connection warm. |
|
|
60
|
+
| Cloudflare Workers | 30s CPU | unbounded with Durable Objects | Free tier 30s is too tight; Workers + DO works but is complex. |
|
|
61
|
+
| Railway | none | none | **Great fit.** $5/mo for the smallest container, no timeout. Deploy a plain Node server or Next.js. |
|
|
62
|
+
| Fly.io | none | none | **Great fit.** Free tier covers small apps, scales to paid. |
|
|
63
|
+
| Render | none | none | **Great fit.** $7/mo for the smallest web service. |
|
|
64
|
+
| Netlify Functions | 26s sync / 15min background | 26s sync / 15min background | Use background functions for inference; sync functions time out. |
|
|
65
|
+
| AWS Lambda | 15 min | 15 min | Works, but WebSocket setup for streaming is involved. |
|
|
66
|
+
| Self-host (Docker) | no limit | no limit | **Works anywhere.** ECS, Cloud Run, your own VPS, fine. |
|
|
67
|
+
|
|
68
|
+
## What I recommend
|
|
69
|
+
|
|
70
|
+
1. **Building a Web3 app where users have wallets?** Switch to \`lightnode add
|
|
71
|
+
chat-web3\`. Each user signs and pays from their own wallet. You host the
|
|
72
|
+
static page (Vercel/Netlify/Cloudflare Pages free tier all work). No timeout
|
|
73
|
+
to fight because there is no server-side inference route.
|
|
74
|
+
|
|
75
|
+
2. **Building a SaaS chatbot where users do NOT have wallets?** Deploy your
|
|
76
|
+
inference route on Railway or Fly. Both have no function timeout, scale
|
|
77
|
+
to traffic, and cost ~$5/mo for small apps. Cheaper than Vercel Pro.
|
|
78
|
+
|
|
79
|
+
3. **Already committed to Vercel?** Upgrade to Pro and add streaming. The
|
|
80
|
+
chat template generated by \`lightnode add chat\` already uses streaming
|
|
81
|
+
tokens, so the connection stays warm while the model runs.
|
|
82
|
+
|
|
83
|
+
4. **Have a Docker setup already?** Run the inference route in your existing
|
|
84
|
+
container. No timeout, no per-call cost beyond your existing infra.
|
|
85
|
+
|
|
86
|
+
## Why the request is slow at all
|
|
87
|
+
|
|
88
|
+
LightChain inference is not a synchronous LLM call. Each request:
|
|
89
|
+
1. Negotiates an ECDH-encrypted session with a worker (off-chain).
|
|
90
|
+
2. Sends a \`createSession\` tx on-chain (one confirmation).
|
|
91
|
+
3. Uploads the encrypted prompt blob to the gateway.
|
|
92
|
+
4. Sends a \`submitJob\` tx on-chain (one confirmation).
|
|
93
|
+
5. Waits for the worker to run inference + post the encrypted result.
|
|
94
|
+
6. Decrypts the result client-side.
|
|
95
|
+
|
|
96
|
+
Steps 2, 4, and 5 are the slow part - each waits for a block confirmation
|
|
97
|
+
and worker pickup. The protocol's verifiable-AI guarantee comes from doing
|
|
98
|
+
all of this on-chain instead of just hitting an OpenAI-style API, which is
|
|
99
|
+
the reason the call takes 60-90s instead of 1-2s. There is no way to
|
|
100
|
+
shortcut this on the SDK side; the host just has to allow long-running
|
|
101
|
+
functions.
|
|
102
|
+
`;
|
|
34
103
|
const ENV_EXAMPLE = (net) => `# Funded private key. Testnet works free (faucet at https://lightfaucet.ai).
|
|
35
104
|
PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
|
|
36
105
|
|
|
@@ -182,6 +251,7 @@ export function addInference(opts = {}) {
|
|
|
182
251
|
const written = [];
|
|
183
252
|
if (template === "nextjs-api") {
|
|
184
253
|
written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_ROUTE, force));
|
|
254
|
+
written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
|
|
185
255
|
}
|
|
186
256
|
else if (template === "hono") {
|
|
187
257
|
written.push(writeFile(path.join(cwd, "lightchain-inference.ts"), HONO_HANDLER, force));
|
|
@@ -590,20 +660,47 @@ export default function Chat() {
|
|
|
590
660
|
setTurns(next);
|
|
591
661
|
setDraft("");
|
|
592
662
|
setBusy(true);
|
|
663
|
+
// Reserve the assistant bubble immediately so tokens can stream into it.
|
|
664
|
+
setTurns([...next, { role: "assistant", text: "" }]);
|
|
593
665
|
try {
|
|
594
|
-
// Concatenate prior turns so the model has the conversation context.
|
|
595
666
|
const prompt =
|
|
596
667
|
next.map((t) => (t.role === "user" ? \`User: \${t.text}\` : \`Assistant: \${t.text}\`)).join("\\n") +
|
|
597
668
|
"\\nAssistant:";
|
|
598
669
|
const r = await fetch("/api/inference", {
|
|
599
670
|
method: "POST",
|
|
600
671
|
headers: { "Content-Type": "application/json" },
|
|
601
|
-
body: JSON.stringify({ prompt }),
|
|
602
|
-
})
|
|
603
|
-
if (!r.
|
|
604
|
-
|
|
672
|
+
body: JSON.stringify({ prompt, stream: true }),
|
|
673
|
+
});
|
|
674
|
+
if (!r.ok || !r.body) {
|
|
675
|
+
const err = await r.text().catch(() => "");
|
|
676
|
+
throw new Error(\`route returned \${r.status}: \${err.slice(0, 200)}\`);
|
|
677
|
+
}
|
|
678
|
+
const reader = r.body.getReader();
|
|
679
|
+
const decoder = new TextDecoder();
|
|
680
|
+
let assembled = "";
|
|
681
|
+
let txs: Record<string, string> | undefined;
|
|
682
|
+
while (true) {
|
|
683
|
+
const { done, value } = await reader.read();
|
|
684
|
+
if (done) break;
|
|
685
|
+
const text = decoder.decode(value, { stream: true });
|
|
686
|
+
// The route ends the stream with one line of JSON on a new \\u0000
|
|
687
|
+
// delimiter (a NUL byte we put between the streamed prose and the
|
|
688
|
+
// metadata). Anything before is body; anything after is metadata.
|
|
689
|
+
const nulIdx = text.indexOf("\\u0000");
|
|
690
|
+
if (nulIdx === -1) {
|
|
691
|
+
assembled += text;
|
|
692
|
+
setTurns((prev) => prev.map((t, i) => i === prev.length - 1 ? { ...t, text: assembled } : t));
|
|
693
|
+
} else {
|
|
694
|
+
assembled += text.slice(0, nulIdx);
|
|
695
|
+
try { txs = JSON.parse(text.slice(nulIdx + 1)) as Record<string, string>; } catch { /* ignore */ }
|
|
696
|
+
setTurns((prev) => prev.map((t, i) => i === prev.length - 1 ? { ...t, text: assembled, txs } : t));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
605
699
|
} catch (e) {
|
|
606
|
-
|
|
700
|
+
// Replace the empty assistant bubble with the error.
|
|
701
|
+
setTurns((prev) => prev.map((t, i) =>
|
|
702
|
+
i === prev.length - 1 ? { ...t, text: \`(error: \${(e as Error).message})\` } : t
|
|
703
|
+
));
|
|
607
704
|
} finally {
|
|
608
705
|
setBusy(false);
|
|
609
706
|
}
|
|
@@ -664,13 +761,300 @@ export default function Chat() {
|
|
|
664
761
|
</button>
|
|
665
762
|
</div>
|
|
666
763
|
<p style={{ marginTop: 16, fontSize: 12, color: "#888" }}>
|
|
667
|
-
|
|
668
|
-
|
|
764
|
+
Your server signs every call with the PRIVATE_KEY in <code>.env</code>. Cost per turn is paid from that wallet.
|
|
765
|
+
See <code>LIGHTNODE-HOSTING.md</code> for picking a host that handles 60-90s function calls.
|
|
669
766
|
</p>
|
|
670
767
|
</main>
|
|
671
768
|
);
|
|
672
769
|
}
|
|
673
770
|
`;
|
|
771
|
+
/**
|
|
772
|
+
* Streaming inference route, paired with NEXTJS_CHAT_PAGE.
|
|
773
|
+
*
|
|
774
|
+
* Why a second route: the plain NEXTJS_ROUTE returns JSON in one shot,
|
|
775
|
+
* which is fine for one-off calls but means the chat UI shows nothing for
|
|
776
|
+
* 60-90s. This streams decrypted chunks as they arrive (via
|
|
777
|
+
* runInferenceStream), so the user sees tokens land live and the host
|
|
778
|
+
* keeps the connection warm. After the last chunk we append a NUL byte
|
|
779
|
+
* (\\u0000) and a JSON line with the tx hashes so the client can render
|
|
780
|
+
* the on-chain receipt under the assistant bubble.
|
|
781
|
+
*/
|
|
782
|
+
const NEXTJS_INFERENCE_STREAM_ROUTE = `// app/api/inference/route.ts
|
|
783
|
+
// Generated by 'lightnode add chat'. Streaming inference route - tokens
|
|
784
|
+
// arrive live as the model produces them, then a NUL byte separates them
|
|
785
|
+
// from a final JSON line with the on-chain tx hashes.
|
|
786
|
+
//
|
|
787
|
+
// Pass { stream: true } in the body to get streaming. Without that flag
|
|
788
|
+
// the same route still returns one-shot JSON, which is fine for non-chat
|
|
789
|
+
// integrations.
|
|
790
|
+
//
|
|
791
|
+
// Mainnet 8b inference takes 60-90s. Vercel Hobby caps function execution
|
|
792
|
+
// at 10s and will time out. See LIGHTNODE-HOSTING.md for hosts that work.
|
|
793
|
+
import { NextResponse } from "next/server";
|
|
794
|
+
import { runInferenceWithKey, runInferenceStream } from "lightnode-sdk";
|
|
795
|
+
|
|
796
|
+
export const runtime = "nodejs";
|
|
797
|
+
export const dynamic = "force-dynamic";
|
|
798
|
+
export const maxDuration = 120;
|
|
799
|
+
|
|
800
|
+
const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
|
|
801
|
+
const MODEL = process.env.MODEL ?? "llama3-8b";
|
|
802
|
+
|
|
803
|
+
export async function POST(req: Request) {
|
|
804
|
+
if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
|
|
805
|
+
return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
|
|
806
|
+
}
|
|
807
|
+
const body = (await req.json().catch(() => ({}))) as { prompt?: string; system?: string; stream?: boolean };
|
|
808
|
+
const prompt = body.prompt?.trim();
|
|
809
|
+
if (!prompt) return NextResponse.json({ error: "prompt is required" }, { status: 400 });
|
|
810
|
+
|
|
811
|
+
const args = {
|
|
812
|
+
network: NETWORK,
|
|
813
|
+
privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
|
|
814
|
+
model: MODEL,
|
|
815
|
+
system: body.system?.trim() || undefined,
|
|
816
|
+
prompt,
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
// One-shot JSON path: same shape as the original 'add inference' route.
|
|
820
|
+
if (!body.stream) {
|
|
821
|
+
try {
|
|
822
|
+
const { answer, worker, txs, jobId } = await runInferenceWithKey(args);
|
|
823
|
+
return NextResponse.json({
|
|
824
|
+
answer, worker, jobId: jobId.toString(),
|
|
825
|
+
txs: { createSession: txs.createSession, submitJob: txs.submitJob, jobCompleted: txs.jobCompleted },
|
|
826
|
+
});
|
|
827
|
+
} catch (e) {
|
|
828
|
+
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Streaming path: tokens, NUL byte, then tx metadata as JSON.
|
|
833
|
+
const stream = runInferenceStream(args);
|
|
834
|
+
const encoder = new TextEncoder();
|
|
835
|
+
const body$ = new ReadableStream({
|
|
836
|
+
async start(controller) {
|
|
837
|
+
try {
|
|
838
|
+
for await (const chunk of stream) controller.enqueue(encoder.encode(chunk));
|
|
839
|
+
const { worker, txs, jobId } = await stream.done;
|
|
840
|
+
const meta = JSON.stringify({
|
|
841
|
+
worker, jobId: jobId.toString(),
|
|
842
|
+
createSession: txs.createSession, submitJob: txs.submitJob, jobCompleted: txs.jobCompleted,
|
|
843
|
+
});
|
|
844
|
+
controller.enqueue(encoder.encode("\\u0000" + meta));
|
|
845
|
+
controller.close();
|
|
846
|
+
} catch (e) {
|
|
847
|
+
controller.enqueue(encoder.encode(\`\\u0000{"error":\${JSON.stringify((e as Error).message)}}\`));
|
|
848
|
+
controller.close();
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
return new Response(body$, {
|
|
853
|
+
headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
`;
|
|
857
|
+
const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
|
|
858
|
+
// Generated by 'lightnode add chat-web3'. User-pays chat: each visitor's own
|
|
859
|
+
// wallet signs SIWE + createSession per turn. Your app holds zero funds.
|
|
860
|
+
//
|
|
861
|
+
// Prereqs:
|
|
862
|
+
// - wagmi configured in your app (https://wagmi.sh/react/getting-started)
|
|
863
|
+
// - the connected wallet has LCAI on the LightChain network it is on
|
|
864
|
+
// (mainnet 9200 or testnet 8200). Mainnet llama3-8b costs 0.02 LCAI per
|
|
865
|
+
// turn plus a small gas amount.
|
|
866
|
+
"use client";
|
|
867
|
+
|
|
868
|
+
import { useEffect, useState } from "react";
|
|
869
|
+
import { useAccount, useWalletClient, usePublicClient } from "wagmi";
|
|
870
|
+
import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
|
|
871
|
+
|
|
872
|
+
type Turn = {
|
|
873
|
+
role: "user" | "assistant";
|
|
874
|
+
text: string;
|
|
875
|
+
worker?: string | null;
|
|
876
|
+
jobId?: string | null;
|
|
877
|
+
submitTx?: \`0x\${string}\` | null;
|
|
878
|
+
jobCompletedTx?: \`0x\${string}\` | null;
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const MODEL = "llama3-8b";
|
|
882
|
+
|
|
883
|
+
export default function ChatWeb3() {
|
|
884
|
+
const { address, chain } = useAccount();
|
|
885
|
+
const network: "mainnet" | "testnet" | null =
|
|
886
|
+
chain?.id === 9200 ? "mainnet" : chain?.id === 8200 ? "testnet" : null;
|
|
887
|
+
const { data: walletClient } = useWalletClient({ chainId: chain?.id });
|
|
888
|
+
const publicClient = usePublicClient({ chainId: chain?.id });
|
|
889
|
+
|
|
890
|
+
const [turns, setTurns] = useState<Turn[]>([]);
|
|
891
|
+
const [input, setInput] = useState("");
|
|
892
|
+
const [busy, setBusy] = useState(false);
|
|
893
|
+
const [busyStage, setBusyStage] = useState("");
|
|
894
|
+
const [err, setErr] = useState<string | null>(null);
|
|
895
|
+
const [feeLcai, setFeeLcai] = useState<number | null>(null);
|
|
896
|
+
|
|
897
|
+
// Read the on-chain fee for the connected network so we can show the
|
|
898
|
+
// visitor the real cost per turn before they click Send.
|
|
899
|
+
useEffect(() => {
|
|
900
|
+
if (!network) { setFeeLcai(null); return; }
|
|
901
|
+
let cancelled = false;
|
|
902
|
+
estimateJobFee(NETWORKS[network], MODEL).then(
|
|
903
|
+
(fee) => { if (!cancelled) setFeeLcai(fee); },
|
|
904
|
+
() => { if (!cancelled) setFeeLcai(null); },
|
|
905
|
+
);
|
|
906
|
+
return () => { cancelled = true; };
|
|
907
|
+
}, [network]);
|
|
908
|
+
|
|
909
|
+
/** Build a single prompt from history + new user input. */
|
|
910
|
+
function composePrompt(history: Turn[], next: string, system: string): string {
|
|
911
|
+
const lines: string[] = [];
|
|
912
|
+
for (const t of history) {
|
|
913
|
+
lines.push(\`\${t.role === "user" ? "User" : "Assistant"}: \${t.text}\`);
|
|
914
|
+
}
|
|
915
|
+
lines.push(\`User: \${next}\`);
|
|
916
|
+
lines.push("Assistant:");
|
|
917
|
+
return system ? \`\${system}\\n\\n\${lines.join("\\n\\n")}\` : lines.join("\\n\\n");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function send() {
|
|
921
|
+
if (!walletClient || !publicClient || !address || !network) {
|
|
922
|
+
setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const next = input.trim();
|
|
926
|
+
if (!next) return;
|
|
927
|
+
setBusy(true);
|
|
928
|
+
setErr(null);
|
|
929
|
+
const history = [...turns];
|
|
930
|
+
// Optimistic user bubble so it appears immediately.
|
|
931
|
+
setTurns([...history, { role: "user", text: next }]);
|
|
932
|
+
setInput("");
|
|
933
|
+
try {
|
|
934
|
+
const system = "You are a concise assistant. Reply in one or two short sentences.";
|
|
935
|
+
const prompt = composePrompt(history, next, system);
|
|
936
|
+
|
|
937
|
+
setBusyStage("Sign in with your wallet (SIWE)...");
|
|
938
|
+
const session = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
|
|
939
|
+
|
|
940
|
+
setBusyStage("Approve the createSession transaction in your wallet...");
|
|
941
|
+
const gateway = new GatewayClient({ network, bearer: session.bearer });
|
|
942
|
+
const result = await runInference({
|
|
943
|
+
prompt,
|
|
944
|
+
gateway,
|
|
945
|
+
wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
|
|
946
|
+
publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
|
|
947
|
+
network: NETWORKS[network],
|
|
948
|
+
model: MODEL,
|
|
949
|
+
jobCompletedTimeoutMs: 120_000,
|
|
950
|
+
maxRetries: 1,
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
setTurns([...history, { role: "user", text: next }, {
|
|
954
|
+
role: "assistant",
|
|
955
|
+
text: result.answer,
|
|
956
|
+
worker: result.worker,
|
|
957
|
+
jobId: result.jobId?.toString() ?? null,
|
|
958
|
+
submitTx: result.txs?.submitJob ?? null,
|
|
959
|
+
jobCompletedTx: result.txs?.jobCompleted ?? null,
|
|
960
|
+
}]);
|
|
961
|
+
} catch (e) {
|
|
962
|
+
// Roll back the optimistic user bubble so the visitor can retry.
|
|
963
|
+
setTurns(history);
|
|
964
|
+
setInput(next);
|
|
965
|
+
const msg = (e as Error).message ?? "chat failed";
|
|
966
|
+
setErr(
|
|
967
|
+
/user rejected|user denied|reject/i.test(msg)
|
|
968
|
+
? "You rejected the wallet popup. Try again."
|
|
969
|
+
: /insufficient funds|insufficient balance/i.test(msg)
|
|
970
|
+
? \`Your wallet has no \${network === "mainnet" ? "LCAI" : "testnet LCAI"}. Top it up before sending.\`
|
|
971
|
+
: msg.split("\\n")[0],
|
|
972
|
+
);
|
|
973
|
+
} finally {
|
|
974
|
+
setBusy(false);
|
|
975
|
+
setBusyStage("");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return (
|
|
980
|
+
<main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
|
|
981
|
+
<h1>Chat (user-pays)</h1>
|
|
982
|
+
<p style={{ color: "#666" }}>
|
|
983
|
+
Each turn signs one createSession transaction from your wallet on{" "}
|
|
984
|
+
<code>{network ?? "(connect a wallet)"}</code>. Fee:{" "}
|
|
985
|
+
<code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> per turn plus a small gas amount.
|
|
986
|
+
</p>
|
|
987
|
+
{!address && (
|
|
988
|
+
<div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
|
|
989
|
+
Connect a wallet to start chatting. (Use whichever connector your app exposes - e.g. RainbowKit,
|
|
990
|
+
ConnectKit, Reown AppKit, or wagmi's useConnect directly.)
|
|
991
|
+
</div>
|
|
992
|
+
)}
|
|
993
|
+
|
|
994
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8, margin: "16px 0" }}>
|
|
995
|
+
{turns.map((t, i) => (
|
|
996
|
+
<div
|
|
997
|
+
key={i}
|
|
998
|
+
style={{
|
|
999
|
+
alignSelf: t.role === "user" ? "flex-end" : "flex-start",
|
|
1000
|
+
maxWidth: "85%",
|
|
1001
|
+
borderRadius: 12,
|
|
1002
|
+
padding: "8px 12px",
|
|
1003
|
+
background: t.role === "user" ? "#e9e7ff" : "#f5f5f7",
|
|
1004
|
+
}}
|
|
1005
|
+
>
|
|
1006
|
+
<div style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>{t.text}</div>
|
|
1007
|
+
{t.role === "assistant" && t.submitTx ? (
|
|
1008
|
+
<div style={{ marginTop: 6, fontSize: 11, color: "#666", display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
1009
|
+
{t.worker && (
|
|
1010
|
+
<a href={\`https://\${network}.lightscan.app/address/\${t.worker}\`} target="_blank" rel="noopener noreferrer">
|
|
1011
|
+
worker
|
|
1012
|
+
</a>
|
|
1013
|
+
)}
|
|
1014
|
+
{t.jobId && <span>job #{t.jobId}</span>}
|
|
1015
|
+
{t.submitTx && (
|
|
1016
|
+
<a href={\`https://\${network}.lightscan.app/tx/\${t.submitTx}\`} target="_blank" rel="noopener noreferrer">
|
|
1017
|
+
submitJob
|
|
1018
|
+
</a>
|
|
1019
|
+
)}
|
|
1020
|
+
{t.jobCompletedTx && (
|
|
1021
|
+
<a href={\`https://\${network}.lightscan.app/tx/\${t.jobCompletedTx}\`} target="_blank" rel="noopener noreferrer">
|
|
1022
|
+
completed
|
|
1023
|
+
</a>
|
|
1024
|
+
)}
|
|
1025
|
+
</div>
|
|
1026
|
+
) : null}
|
|
1027
|
+
</div>
|
|
1028
|
+
))}
|
|
1029
|
+
</div>
|
|
1030
|
+
|
|
1031
|
+
<textarea
|
|
1032
|
+
value={input}
|
|
1033
|
+
onChange={(e) => setInput(e.target.value)}
|
|
1034
|
+
onKeyDown={(e) => {
|
|
1035
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); if (!busy && input.trim()) send(); }
|
|
1036
|
+
}}
|
|
1037
|
+
rows={2}
|
|
1038
|
+
placeholder={turns.length === 0 ? "Say hello (cmd+enter to send)" : "Reply..."}
|
|
1039
|
+
style={{ width: "100%", padding: 8, fontFamily: "inherit" }}
|
|
1040
|
+
/>
|
|
1041
|
+
<button
|
|
1042
|
+
type="button"
|
|
1043
|
+
onClick={() => send()}
|
|
1044
|
+
disabled={busy || !input.trim() || !address || !network}
|
|
1045
|
+
style={{ marginTop: 8, padding: "8px 16px" }}
|
|
1046
|
+
>
|
|
1047
|
+
{busy ? (busyStage || "Sending...") : (turns.length === 0 ? "Send first message" : "Send")}
|
|
1048
|
+
</button>
|
|
1049
|
+
{err && (
|
|
1050
|
+
<p style={{ marginTop: 8, padding: "8px 12px", border: "1px solid #f5c2c7", background: "#f8d7da", color: "#842029", borderRadius: 6 }}>
|
|
1051
|
+
{err}
|
|
1052
|
+
</p>
|
|
1053
|
+
)}
|
|
1054
|
+
</main>
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
`;
|
|
674
1058
|
const NODE_CHAT_REPL = `// chat-repl.ts
|
|
675
1059
|
// Generated by 'lightnode add chat'. Interactive chat REPL in your terminal.
|
|
676
1060
|
// npm install lightnode-sdk viem ws
|
|
@@ -900,7 +1284,12 @@ export function addChat(opts = {}) {
|
|
|
900
1284
|
const force = !!opts.force;
|
|
901
1285
|
const written = [];
|
|
902
1286
|
if (template === "nextjs-api") {
|
|
1287
|
+
// 'add chat' is now self-contained: it writes BOTH the chat page AND
|
|
1288
|
+
// the streaming inference route. Previously the user had to remember
|
|
1289
|
+
// to also run 'add inference' separately, which was easy to miss.
|
|
903
1290
|
written.push(writeFile(path.join(cwd, "app/chat/page.tsx"), NEXTJS_CHAT_PAGE, force));
|
|
1291
|
+
written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_INFERENCE_STREAM_ROUTE, force));
|
|
1292
|
+
written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
|
|
904
1293
|
}
|
|
905
1294
|
else {
|
|
906
1295
|
written.push(writeFile(path.join(cwd, "chat-repl.ts"), NODE_CHAT_REPL, force));
|
|
@@ -908,6 +1297,191 @@ export function addChat(opts = {}) {
|
|
|
908
1297
|
written.push(writeFile(path.join(cwd, ".env.example"), ENV_EXAMPLE(network), force));
|
|
909
1298
|
return { written, install: `npm install ${depsNeeded(template).join(" ")}`, template, network };
|
|
910
1299
|
}
|
|
1300
|
+
/**
|
|
1301
|
+
* `lightnode add chat-web3` - the user-pays counterpart to addChat.
|
|
1302
|
+
*
|
|
1303
|
+
* Architecture:
|
|
1304
|
+
* - Each visitor's own wallet signs SIWE + createSession per turn.
|
|
1305
|
+
* - The dev's app holds zero funds; cost is borne by each user (0.02 LCAI
|
|
1306
|
+
* per llama3-8b turn on mainnet).
|
|
1307
|
+
* - No backend, no PRIVATE_KEY, no server-side hot wallet.
|
|
1308
|
+
*
|
|
1309
|
+
* Fit:
|
|
1310
|
+
* - Web3 dApps where users already have a wallet (NFT, meme coin, on-chain
|
|
1311
|
+
* games, LightChallenge-style challenge platforms).
|
|
1312
|
+
* - For SaaS chatbots where users do NOT have a wallet, use `add chat`
|
|
1313
|
+
* instead (dev pays, server-side route).
|
|
1314
|
+
*/
|
|
1315
|
+
export function addChatWeb3(opts = {}) {
|
|
1316
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1317
|
+
const network = opts.network ?? "mainnet";
|
|
1318
|
+
// chat-web3 is browser-only. If the project is not Next.js (or another
|
|
1319
|
+
// React framework we'd detect), fall back to nextjs-api anyway and warn
|
|
1320
|
+
// the user in the CLI's next-steps that the file expects a Next.js
|
|
1321
|
+
// setup with wagmi.
|
|
1322
|
+
const detected = detectTemplate(cwd);
|
|
1323
|
+
const template = opts.template && opts.template !== "auto" ? opts.template : detected;
|
|
1324
|
+
const force = !!opts.force;
|
|
1325
|
+
const written = [];
|
|
1326
|
+
// Detect whether the project already has wagmi; if not, the next-steps
|
|
1327
|
+
// output prints the install line.
|
|
1328
|
+
const pkg = readPackageJson(cwd);
|
|
1329
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
1330
|
+
const hasWagmi = Boolean(deps["wagmi"]);
|
|
1331
|
+
written.push(writeFile(path.join(cwd, "app/chat-web3/page.tsx"), NEXTJS_CHAT_WEB3_PAGE, force));
|
|
1332
|
+
return {
|
|
1333
|
+
written,
|
|
1334
|
+
install: `npm install lightnode-sdk viem` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
|
|
1335
|
+
template,
|
|
1336
|
+
network,
|
|
1337
|
+
needsWagmi: !hasWagmi,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const WAGMI_CONFIG_FILE = `// lib/wagmi.ts
|
|
1341
|
+
// Generated by 'lightnode add wagmi-setup'. Minimal wagmi setup for
|
|
1342
|
+
// LightChain mainnet (9200) + testnet (8200). Use this as a starting
|
|
1343
|
+
// point; swap in RainbowKit / Reown AppKit / ConnectKit if you want a
|
|
1344
|
+
// richer connect UI.
|
|
1345
|
+
import { createConfig, http } from "wagmi";
|
|
1346
|
+
import { injected } from "wagmi/connectors";
|
|
1347
|
+
import type { Chain } from "viem";
|
|
1348
|
+
|
|
1349
|
+
export const lightchainMainnet: Chain = {
|
|
1350
|
+
id: 9200,
|
|
1351
|
+
name: "LightChain",
|
|
1352
|
+
nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 },
|
|
1353
|
+
rpcUrls: { default: { http: ["https://rpc.mainnet.lightchain.ai"] } },
|
|
1354
|
+
blockExplorers: { default: { name: "Lightscan", url: "https://mainnet.lightscan.app" } },
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
export const lightchainTestnet: Chain = {
|
|
1358
|
+
id: 8200,
|
|
1359
|
+
name: "LightChain Testnet",
|
|
1360
|
+
nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 },
|
|
1361
|
+
rpcUrls: { default: { http: ["https://rpc.testnet.lightchain.ai"] } },
|
|
1362
|
+
blockExplorers: { default: { name: "Lightscan", url: "https://testnet.lightscan.app" } },
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
export const wagmiConfig = createConfig({
|
|
1366
|
+
chains: [lightchainMainnet, lightchainTestnet],
|
|
1367
|
+
// \`injected\` covers MetaMask, Rabby, OKX, Phantom EVM, and any browser
|
|
1368
|
+
// wallet that follows EIP-1193. Add walletConnect() / coinbaseWallet()
|
|
1369
|
+
// here if you want explicit support for those.
|
|
1370
|
+
connectors: [injected()],
|
|
1371
|
+
transports: {
|
|
1372
|
+
[lightchainMainnet.id]: http(),
|
|
1373
|
+
[lightchainTestnet.id]: http(),
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1376
|
+
`;
|
|
1377
|
+
const WAGMI_PROVIDERS_FILE = `// app/providers.tsx
|
|
1378
|
+
// Generated by 'lightnode add wagmi-setup'. Wraps the app with the
|
|
1379
|
+
// wagmi + react-query providers needed for any wagmi hook to work.
|
|
1380
|
+
//
|
|
1381
|
+
// Import this from your root layout:
|
|
1382
|
+
//
|
|
1383
|
+
// // app/layout.tsx
|
|
1384
|
+
// import { Providers } from "./providers";
|
|
1385
|
+
// export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
1386
|
+
// return (
|
|
1387
|
+
// <html lang="en">
|
|
1388
|
+
// <body><Providers>{children}</Providers></body>
|
|
1389
|
+
// </html>
|
|
1390
|
+
// );
|
|
1391
|
+
// }
|
|
1392
|
+
"use client";
|
|
1393
|
+
|
|
1394
|
+
import { WagmiProvider } from "wagmi";
|
|
1395
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
1396
|
+
import { wagmiConfig } from "@/lib/wagmi";
|
|
1397
|
+
import type { ReactNode } from "react";
|
|
1398
|
+
|
|
1399
|
+
const queryClient = new QueryClient();
|
|
1400
|
+
|
|
1401
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
1402
|
+
return (
|
|
1403
|
+
<WagmiProvider config={wagmiConfig}>
|
|
1404
|
+
<QueryClientProvider client={queryClient}>
|
|
1405
|
+
{children}
|
|
1406
|
+
</QueryClientProvider>
|
|
1407
|
+
</WagmiProvider>
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
`;
|
|
1411
|
+
const WAGMI_CONNECT_BUTTON = `// components/connect-button.tsx
|
|
1412
|
+
// Generated by 'lightnode add wagmi-setup'. Minimal Connect/Disconnect
|
|
1413
|
+
// button using wagmi's useConnect / useAccount hooks. Swap in
|
|
1414
|
+
// RainbowKit's ConnectButton or Reown's <w3m-button /> for a richer UI.
|
|
1415
|
+
"use client";
|
|
1416
|
+
|
|
1417
|
+
import { useAccount, useConnect, useDisconnect, useSwitchChain } from "wagmi";
|
|
1418
|
+
|
|
1419
|
+
const LIGHTCHAIN_IDS = new Set<number>([9200, 8200]);
|
|
1420
|
+
|
|
1421
|
+
function shortAddress(addr: string): string {
|
|
1422
|
+
return \`\${addr.slice(0, 6)}...\${addr.slice(-4)}\`;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
export function ConnectButton() {
|
|
1426
|
+
const { address, chain, isConnected } = useAccount();
|
|
1427
|
+
const { connect, connectors, isPending } = useConnect();
|
|
1428
|
+
const { disconnect } = useDisconnect();
|
|
1429
|
+
const { switchChain, isPending: switching } = useSwitchChain();
|
|
1430
|
+
|
|
1431
|
+
if (!isConnected) {
|
|
1432
|
+
const connector = connectors[0];
|
|
1433
|
+
return (
|
|
1434
|
+
<button
|
|
1435
|
+
type="button"
|
|
1436
|
+
onClick={() => connect({ connector })}
|
|
1437
|
+
disabled={isPending}
|
|
1438
|
+
style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer" }}
|
|
1439
|
+
>
|
|
1440
|
+
{isPending ? "Connecting..." : "Connect wallet"}
|
|
1441
|
+
</button>
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (chain && !LIGHTCHAIN_IDS.has(chain.id)) {
|
|
1446
|
+
return (
|
|
1447
|
+
<button
|
|
1448
|
+
type="button"
|
|
1449
|
+
onClick={() => switchChain({ chainId: 9200 })}
|
|
1450
|
+
disabled={switching}
|
|
1451
|
+
style={{ padding: "8px 16px", borderRadius: 8, background: "#fee", cursor: "pointer" }}
|
|
1452
|
+
>
|
|
1453
|
+
{switching ? "Switching..." : "Switch to LightChain"}
|
|
1454
|
+
</button>
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
return (
|
|
1459
|
+
<button
|
|
1460
|
+
type="button"
|
|
1461
|
+
onClick={() => disconnect()}
|
|
1462
|
+
style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer", fontFamily: "monospace" }}
|
|
1463
|
+
>
|
|
1464
|
+
{address ? shortAddress(address) : "(unknown)"} ({chain?.name}) - disconnect
|
|
1465
|
+
</button>
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
`;
|
|
1469
|
+
export function addWagmiSetup(opts = {}) {
|
|
1470
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1471
|
+
const network = opts.network ?? "mainnet";
|
|
1472
|
+
const template = opts.template && opts.template !== "auto" ? opts.template : detectTemplate(cwd);
|
|
1473
|
+
const force = !!opts.force;
|
|
1474
|
+
const written = [];
|
|
1475
|
+
written.push(writeFile(path.join(cwd, "lib/wagmi.ts"), WAGMI_CONFIG_FILE, force));
|
|
1476
|
+
written.push(writeFile(path.join(cwd, "app/providers.tsx"), WAGMI_PROVIDERS_FILE, force));
|
|
1477
|
+
written.push(writeFile(path.join(cwd, "components/connect-button.tsx"), WAGMI_CONNECT_BUTTON, force));
|
|
1478
|
+
return {
|
|
1479
|
+
written,
|
|
1480
|
+
install: `npm install wagmi viem @tanstack/react-query`,
|
|
1481
|
+
template,
|
|
1482
|
+
network,
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
911
1485
|
const NEXTJS_JUDGE_ROUTE = `// app/api/judge/route.ts
|
|
912
1486
|
// Generated by 'lightnode add judge'. See https://lightnode.app/build
|
|
913
1487
|
// The LightChallenge-style evaluator: post evidence + criteria, get a
|
|
@@ -1041,6 +1615,7 @@ export function addJudge(opts = {}) {
|
|
|
1041
1615
|
const written = [];
|
|
1042
1616
|
if (template === "nextjs-api") {
|
|
1043
1617
|
written.push(writeFile(path.join(cwd, "app/api/judge/route.ts"), NEXTJS_JUDGE_ROUTE, force));
|
|
1618
|
+
written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
|
|
1044
1619
|
}
|
|
1045
1620
|
else {
|
|
1046
1621
|
written.push(writeFile(path.join(cwd, "judge.ts"), NODE_JUDGE_SCRIPT, force));
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv, runInferenceWithKey, runInferenceBatch, Agent, isStalledWorker, workerPreflight, workerWatch, WorkerOperator, isWorkerOpError, BRIDGE_ROUTE, DAO, DAO_ADDRESSES } from "./index.js";
|
|
3
|
-
import { addInference, addAnalyticsDashboard, addNftMint, addChat, addAgent, addJudge } from "./add.js";
|
|
3
|
+
import { addInference, addAnalyticsDashboard, addNftMint, addChat, addChatWeb3, addAgent, addJudge, addWagmiSetup } from "./add.js";
|
|
4
4
|
import { createPublicClient, createWalletClient, http, parseEther } from "viem";
|
|
5
5
|
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
|
|
6
6
|
function flag(name) {
|
|
@@ -462,7 +462,7 @@ async function main() {
|
|
|
462
462
|
const template = flag("--template") ?? "auto";
|
|
463
463
|
const force = process.argv.includes("--force");
|
|
464
464
|
const network = (net === "mainnet" ? "mainnet" : "testnet");
|
|
465
|
-
const known = ["inference", "chat", "agent", "judge", "analytics-dashboard", "nft-mint-with-inference"];
|
|
465
|
+
const known = ["inference", "chat", "chat-web3", "wagmi-setup", "agent", "judge", "analytics-dashboard", "nft-mint-with-inference"];
|
|
466
466
|
if (!known.includes(sub ?? "")) {
|
|
467
467
|
die(`usage: lightnode add <${known.join("|")}> [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]`);
|
|
468
468
|
}
|
|
@@ -470,13 +470,17 @@ async function main() {
|
|
|
470
470
|
? addAnalyticsDashboard({ template, network, force })
|
|
471
471
|
: sub === "nft-mint-with-inference"
|
|
472
472
|
? addNftMint({ template, network, force })
|
|
473
|
-
: sub === "chat"
|
|
474
|
-
?
|
|
475
|
-
: sub === "
|
|
476
|
-
?
|
|
477
|
-
: sub === "
|
|
478
|
-
?
|
|
479
|
-
:
|
|
473
|
+
: sub === "chat-web3"
|
|
474
|
+
? addChatWeb3({ template, network, force })
|
|
475
|
+
: sub === "wagmi-setup"
|
|
476
|
+
? addWagmiSetup({ template, network, force })
|
|
477
|
+
: sub === "chat"
|
|
478
|
+
? addChat({ template, network, force })
|
|
479
|
+
: sub === "agent"
|
|
480
|
+
? addAgent({ template, network, force })
|
|
481
|
+
: sub === "judge"
|
|
482
|
+
? addJudge({ template, network, force })
|
|
483
|
+
: addInference({ template, network, force });
|
|
480
484
|
console.log(`▶ add ${sub} (${result.template} template, default network ${result.network})`);
|
|
481
485
|
for (const f of result.written) {
|
|
482
486
|
if (f.skipped)
|
|
@@ -491,7 +495,36 @@ async function main() {
|
|
|
491
495
|
else {
|
|
492
496
|
console.log(`\nNext steps (these files were added to your CURRENT folder, not a new project):`);
|
|
493
497
|
console.log(` 1. ${result.install}`);
|
|
494
|
-
if (sub === "
|
|
498
|
+
if (sub === "wagmi-setup") {
|
|
499
|
+
console.log(` 2. Import Providers in app/layout.tsx and wrap children:`);
|
|
500
|
+
console.log(` import { Providers } from "./providers";`);
|
|
501
|
+
console.log(` // <body><Providers>{children}</Providers></body>`);
|
|
502
|
+
console.log(` 3. Drop <ConnectButton /> anywhere you want a connect UI:`);
|
|
503
|
+
console.log(` import { ConnectButton } from "@/components/connect-button";`);
|
|
504
|
+
console.log(` 4. You can now use any wagmi hook (useAccount, useWalletClient, ...).`);
|
|
505
|
+
console.log(` Wallets on chains other than 9200/8200 will be prompted to switch.`);
|
|
506
|
+
}
|
|
507
|
+
else if (sub === "chat-web3") {
|
|
508
|
+
// chat-web3 has no PRIVATE_KEY (each visitor pays their own way).
|
|
509
|
+
const needsWagmi = result.needsWagmi;
|
|
510
|
+
if (needsWagmi) {
|
|
511
|
+
console.log(` 2. Get wagmi wired up with one command:`);
|
|
512
|
+
console.log(` npx lightnode add wagmi-setup`);
|
|
513
|
+
console.log(` (drops lib/wagmi.ts + app/providers.tsx + components/connect-button.tsx)`);
|
|
514
|
+
console.log(` 3. Wrap your layout with <Providers> (see step 2 output) and drop`);
|
|
515
|
+
console.log(` <ConnectButton /> somewhere on the page.`);
|
|
516
|
+
console.log(` 4. npm run dev, open /chat-web3, connect on chainId ${result.network === "mainnet" ? "9200" : "8200"}.`);
|
|
517
|
+
console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
console.log(` 2. npm run dev, open /chat-web3`);
|
|
521
|
+
console.log(` 3. Connect a wallet on LightChain ${result.network === "mainnet" ? "mainnet (chainId 9200)" : "testnet (chainId 8200)"}.`);
|
|
522
|
+
console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
|
|
523
|
+
}
|
|
524
|
+
console.log(`\n Note: chat-web3 has NO server-side route, so it scales infinitely on`);
|
|
525
|
+
console.log(` static hosting (Vercel/Netlify/Cloudflare Pages free tier all work).`);
|
|
526
|
+
}
|
|
527
|
+
else if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent" || sub === "judge") {
|
|
495
528
|
console.log(` 2. cp .env.example .env (and put a funded ${result.network} PRIVATE_KEY in it)`);
|
|
496
529
|
if (sub === "agent" && result.template === "nextjs-api") {
|
|
497
530
|
console.log(` 3. Set CRON_SECRET in your Vercel env vars + edit AGENT_TASK in .env`);
|
|
@@ -501,8 +534,8 @@ async function main() {
|
|
|
501
534
|
console.log(` 3. AGENT_INTERVAL_MS=3600000 npx tsx agent.ts # or run under pm2/systemd`);
|
|
502
535
|
}
|
|
503
536
|
else if (sub === "chat" && result.template === "nextjs-api") {
|
|
504
|
-
console.log(` 3.
|
|
505
|
-
console.log(`
|
|
537
|
+
console.log(` 3. npm run dev, open /chat`);
|
|
538
|
+
console.log(` (the chat page + /api/inference streaming route are both already wired up)`);
|
|
506
539
|
}
|
|
507
540
|
else if (sub === "chat") {
|
|
508
541
|
console.log(` 3. npx tsx chat-repl.ts (interactive terminal chat)`);
|
|
@@ -541,6 +574,13 @@ async function main() {
|
|
|
541
574
|
console.log(` 2. npx tsx lightnode-analytics.ts`);
|
|
542
575
|
}
|
|
543
576
|
}
|
|
577
|
+
// Hosting warning for server-side commands that ship LIGHTNODE-HOSTING.md.
|
|
578
|
+
if (result.template === "nextjs-api"
|
|
579
|
+
&& (sub === "inference" || sub === "chat" || sub === "judge")) {
|
|
580
|
+
console.log(`\n Hosting: a mainnet inference takes 60-90s. Vercel Hobby (free) times`);
|
|
581
|
+
console.log(` out at 10s; Vercel Pro at 60s. See LIGHTNODE-HOSTING.md (in this folder)`);
|
|
582
|
+
console.log(` for hosts that work - Railway / Fly / Render handle long calls fine.`);
|
|
583
|
+
}
|
|
544
584
|
if (result.network === "testnet") {
|
|
545
585
|
console.log(`\nNo wallet yet? Make one: npx lightnode wallet new then fund it free below.`);
|
|
546
586
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -134,7 +134,7 @@ export declare class LightNode {
|
|
|
134
134
|
* (especially in registry-proxy environments like StackBlitz where lockfiles
|
|
135
135
|
* may pin an older minor than the local install command suggests).
|
|
136
136
|
*/
|
|
137
|
-
export declare const SDK_VERSION = "0.7.
|
|
137
|
+
export declare const SDK_VERSION = "0.7.16";
|
|
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, 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";
|
package/dist/index.js
CHANGED
|
@@ -213,7 +213,7 @@ export class LightNode {
|
|
|
213
213
|
* (especially in registry-proxy environments like StackBlitz where lockfiles
|
|
214
214
|
* may pin an older minor than the local install command suggests).
|
|
215
215
|
*/
|
|
216
|
-
export const SDK_VERSION = "0.7.
|
|
216
|
+
export const SDK_VERSION = "0.7.16";
|
|
217
217
|
export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei,
|
|
218
218
|
// v0.7.3 per-job transaction-hash resolver (lifts the upstream
|
|
219
219
|
// subgraph's "block-only" Job entity to a deep-linkable Job + tx pair).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lightnode-sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.16",
|
|
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",
|