lightnode-sdk 0.7.16 → 0.7.18
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 +33 -0
- package/dist/add.js +548 -55
- package/dist/cli.js +42 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/add.d.ts
CHANGED
|
@@ -70,6 +70,39 @@ export declare function addChatWeb3(opts?: AddOpts): {
|
|
|
70
70
|
network: Network;
|
|
71
71
|
needsWagmi: boolean;
|
|
72
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* `lightnode add inference-web3` - the user-pays counterpart to addInference.
|
|
75
|
+
*
|
|
76
|
+
* Browser-only React component. Each visitor signs SIWE + createSession from
|
|
77
|
+
* their own wallet. No backend, no PRIVATE_KEY, no per-call cost for the dev.
|
|
78
|
+
*
|
|
79
|
+
* Fits any one-shot inference UI: classifier, NFT trait generator, content
|
|
80
|
+
* moderation, evaluator. For multi-turn chat use addChatWeb3; for the
|
|
81
|
+
* dedicated judge pattern use addJudgeWeb3.
|
|
82
|
+
*/
|
|
83
|
+
export declare function addInferenceWeb3(opts?: AddOpts): {
|
|
84
|
+
written: WrittenFile[];
|
|
85
|
+
install: string;
|
|
86
|
+
template: Template;
|
|
87
|
+
network: Network;
|
|
88
|
+
needsWagmi: boolean;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* `lightnode add judge-web3` - the user-pays counterpart to addJudge.
|
|
92
|
+
*
|
|
93
|
+
* The LightChallenge pattern with the user paying: visitor submits criteria
|
|
94
|
+
* + evidence, signs from their own wallet, the structured pass/fail/confidence
|
|
95
|
+
* verdict comes back with on-chain proof. Fit: challenge completion grading,
|
|
96
|
+
* NFT trait verification, content moderation, automated scoring - any flow
|
|
97
|
+
* where the END USER wants a verifiable AI verdict on their own submission.
|
|
98
|
+
*/
|
|
99
|
+
export declare function addJudgeWeb3(opts?: AddOpts): {
|
|
100
|
+
written: WrittenFile[];
|
|
101
|
+
install: string;
|
|
102
|
+
template: Template;
|
|
103
|
+
network: Network;
|
|
104
|
+
needsWagmi: boolean;
|
|
105
|
+
};
|
|
73
106
|
export declare function addWagmiSetup(opts?: AddOpts): {
|
|
74
107
|
written: WrittenFile[];
|
|
75
108
|
install: string;
|
package/dist/add.js
CHANGED
|
@@ -31,57 +31,72 @@ function detectTemplate(cwd) {
|
|
|
31
31
|
return "hono";
|
|
32
32
|
return "node";
|
|
33
33
|
}
|
|
34
|
-
const HOSTING_GUIDE = `# Hosting LightChain AI
|
|
35
|
-
|
|
36
|
-
A single LightChain mainnet inference takes **60-90 seconds**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
##
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
34
|
+
const HOSTING_GUIDE = `# Hosting your LightChain AI app
|
|
35
|
+
|
|
36
|
+
A single LightChain mainnet inference takes **60-90 seconds** (the workers
|
|
37
|
+
run the model, attest the result on-chain, and return the answer). Anywhere
|
|
38
|
+
that puts a short timeout on your request will fail with a generic timeout
|
|
39
|
+
error. So the question is just: how do you give your route enough time to
|
|
40
|
+
finish?
|
|
41
|
+
|
|
42
|
+
If you used \`lightnode add chat-web3\`, skip this file - that path has NO
|
|
43
|
+
server-side route, the visitor's own browser does the wait.
|
|
44
|
+
|
|
45
|
+
## The clean answer: run it yourself
|
|
46
|
+
|
|
47
|
+
This template ships with a **Dockerfile + docker-compose.yml** so you can
|
|
48
|
+
run the entire stack on your own machine, your own VPS, or anywhere Docker
|
|
49
|
+
runs. Long-running Node processes have no timeout. The result:
|
|
50
|
+
|
|
51
|
+
\`\`\`bash
|
|
52
|
+
docker compose up --build
|
|
53
|
+
# → http://localhost:3000 ready, no signup, no per-call cost beyond your
|
|
54
|
+
# LCAI fee, no platform vendor lock-in.
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
That's the recommended path. You own the box, you own the keys, you own
|
|
58
|
+
the uptime. Costs as much as the VPS does - $5/mo on Hetzner gets you a
|
|
59
|
+
2-core machine that handles plenty of traffic.
|
|
60
|
+
|
|
61
|
+
### Where to run that container
|
|
62
|
+
|
|
63
|
+
| Where | Cost | Notes |
|
|
64
|
+
|--------------------------|-----------------------|-------|
|
|
65
|
+
| Your laptop / home server | free | Perfect for dev + small personal projects. Expose via Cloudflare Tunnel or Tailscale Funnel if you want a public URL. |
|
|
66
|
+
| Hetzner CX22 | ~€4/mo | 2 CPU, 4 GB RAM. Generous bandwidth. EU-based. |
|
|
67
|
+
| DigitalOcean droplet | $4/mo | 1 CPU, 512 MB. Bumps to $6 for 1 GB. |
|
|
68
|
+
| OVH VPS | ~€3/mo | Cheap, EU. |
|
|
69
|
+
| AWS Lightsail | $5/mo | 1 CPU, 1 GB. AWS billing if you want it. |
|
|
70
|
+
| Your existing k8s | $0 marginal | Just \`docker push\` and \`kubectl apply\`. |
|
|
71
|
+
| Fly.io | free tier + $0-5/mo | Their Docker-native platform, cleanest UX of the paid options. |
|
|
72
|
+
| Railway | $5/mo | Same idea. No timeout, easy deploys. |
|
|
73
|
+
| Render | $7/mo | Same idea. |
|
|
74
|
+
| Google Cloud Run | pay-per-request | Scales to zero. Watch the 60-minute request limit. |
|
|
75
|
+
|
|
76
|
+
## When you'd pick a managed platform instead
|
|
77
|
+
|
|
78
|
+
| Platform | Trade-off |
|
|
79
|
+
|---------------------|-----------|
|
|
80
|
+
| Vercel Pro ($20/mo) | If you're already deploying your Next.js app on Vercel and don't want to split infra. The 60s function cap is tight for mainnet (70-80s calls cut it close); rely on streaming to keep the connection warm. **Hobby tier (free) does NOT work** - 10s cap, every call times out. |
|
|
81
|
+
| Netlify | 26s sync function cap is too tight. Use Netlify's "background functions" (15min) and adapt the route to write the result to KV / a webhook. More work. |
|
|
82
|
+
| Cloudflare Workers | 30s on free, unbounded with Durable Objects. The WebSocket relay setup is more involved than a plain Node server. |
|
|
83
|
+
|
|
84
|
+
The free-tier serverless platforms (Vercel Hobby, Netlify free, Cloudflare
|
|
85
|
+
free) **all fail** at the 60-90s mark. There's no way around that on those
|
|
86
|
+
plans short of upgrading. If you're not paying anyway, self-host - it's
|
|
87
|
+
strictly cheaper and faster than a $20/mo plan.
|
|
88
|
+
|
|
89
|
+
## What I'd actually pick
|
|
90
|
+
|
|
91
|
+
- **First time trying this out**: \`docker compose up\` on your laptop. Free,
|
|
92
|
+
works in 30 seconds, real end-to-end test of your code.
|
|
93
|
+
- **Going to production with users**: same Dockerfile on a $5/mo Hetzner or
|
|
94
|
+
Fly VM. You're done; it'll handle plenty of traffic.
|
|
95
|
+
- **You already have a Next.js app on Vercel**: upgrade to Pro and keep your
|
|
96
|
+
build pipeline. The streaming route works under their 60s cap for most
|
|
97
|
+
mainnet calls.
|
|
98
|
+
- **You're building a Web3 dApp**: re-run \`lightnode add chat-web3\`. No
|
|
99
|
+
backend, no LCAI cost for you - each user pays their own way.
|
|
85
100
|
|
|
86
101
|
## Why the request is slow at all
|
|
87
102
|
|
|
@@ -98,7 +113,74 @@ and worker pickup. The protocol's verifiable-AI guarantee comes from doing
|
|
|
98
113
|
all of this on-chain instead of just hitting an OpenAI-style API, which is
|
|
99
114
|
the reason the call takes 60-90s instead of 1-2s. There is no way to
|
|
100
115
|
shortcut this on the SDK side; the host just has to allow long-running
|
|
101
|
-
|
|
116
|
+
processes - which is exactly what a plain Node server (or Docker container)
|
|
117
|
+
already does for free.
|
|
118
|
+
`;
|
|
119
|
+
/**
|
|
120
|
+
* Dockerfile that builds your Next.js app and runs it as a long-running
|
|
121
|
+
* Node server. There is no function timeout on a plain server, so a 60-90s
|
|
122
|
+
* mainnet inference just works. Multi-stage build keeps the runtime image
|
|
123
|
+
* around 200 MB.
|
|
124
|
+
*/
|
|
125
|
+
const NEXTJS_DOCKERFILE = `# Generated by 'lightnode add chat' (or 'add inference' / 'add judge').
|
|
126
|
+
# Build a Next.js production image; run with 'docker compose up --build'.
|
|
127
|
+
FROM node:20-alpine AS deps
|
|
128
|
+
WORKDIR /app
|
|
129
|
+
COPY package.json package-lock.json* ./
|
|
130
|
+
RUN npm ci --omit=optional
|
|
131
|
+
|
|
132
|
+
FROM node:20-alpine AS builder
|
|
133
|
+
WORKDIR /app
|
|
134
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
135
|
+
COPY . .
|
|
136
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
137
|
+
RUN npm run build
|
|
138
|
+
|
|
139
|
+
FROM node:20-alpine AS runner
|
|
140
|
+
WORKDIR /app
|
|
141
|
+
ENV NODE_ENV=production
|
|
142
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
143
|
+
COPY --from=builder /app/public ./public
|
|
144
|
+
COPY --from=builder /app/.next ./.next
|
|
145
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
146
|
+
COPY --from=builder /app/package.json ./package.json
|
|
147
|
+
EXPOSE 3000
|
|
148
|
+
# Long-running Node process - no function timeout to fight. Mainnet inference
|
|
149
|
+
# calls (60-90s) complete normally because the process just stays up.
|
|
150
|
+
CMD ["npm", "start"]
|
|
151
|
+
`;
|
|
152
|
+
/**
|
|
153
|
+
* docker-compose.yml. The 'env_file' line wires .env from the project root
|
|
154
|
+
* into the container at runtime, so the same PRIVATE_KEY a 'npm run dev'
|
|
155
|
+
* session uses also flows to the container build.
|
|
156
|
+
*/
|
|
157
|
+
const NEXTJS_DOCKER_COMPOSE = `# Generated by 'lightnode add chat' (or 'add inference' / 'add judge').
|
|
158
|
+
# Quick start: docker compose up --build
|
|
159
|
+
# (then visit http://localhost:3000)
|
|
160
|
+
services:
|
|
161
|
+
app:
|
|
162
|
+
build: .
|
|
163
|
+
image: lightnode-app
|
|
164
|
+
container_name: lightnode-app
|
|
165
|
+
ports:
|
|
166
|
+
- "3000:3000"
|
|
167
|
+
# PRIVATE_KEY, NETWORK, MODEL are read from .env at runtime.
|
|
168
|
+
# Make sure .env exists in the same folder as this file (cp .env.example .env).
|
|
169
|
+
env_file:
|
|
170
|
+
- .env
|
|
171
|
+
restart: unless-stopped
|
|
172
|
+
`;
|
|
173
|
+
const DOCKERIGNORE = `# Generated by 'lightnode add chat' (or 'add inference' / 'add judge').
|
|
174
|
+
.git
|
|
175
|
+
.gitignore
|
|
176
|
+
node_modules
|
|
177
|
+
.next
|
|
178
|
+
.env
|
|
179
|
+
.env.local
|
|
180
|
+
.env.*.local
|
|
181
|
+
LIGHTNODE-HOSTING.md
|
|
182
|
+
README.md
|
|
183
|
+
*.log
|
|
102
184
|
`;
|
|
103
185
|
const ENV_EXAMPLE = (net) => `# Funded private key. Testnet works free (faucet at https://lightfaucet.ai).
|
|
104
186
|
PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
|
|
@@ -252,6 +334,9 @@ export function addInference(opts = {}) {
|
|
|
252
334
|
if (template === "nextjs-api") {
|
|
253
335
|
written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_ROUTE, force));
|
|
254
336
|
written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
|
|
337
|
+
written.push(writeFile(path.join(cwd, "Dockerfile"), NEXTJS_DOCKERFILE, force));
|
|
338
|
+
written.push(writeFile(path.join(cwd, "docker-compose.yml"), NEXTJS_DOCKER_COMPOSE, force));
|
|
339
|
+
written.push(writeFile(path.join(cwd, ".dockerignore"), DOCKERIGNORE, force));
|
|
255
340
|
}
|
|
256
341
|
else if (template === "hono") {
|
|
257
342
|
written.push(writeFile(path.join(cwd, "lightchain-inference.ts"), HONO_HANDLER, force));
|
|
@@ -1055,6 +1140,354 @@ export default function ChatWeb3() {
|
|
|
1055
1140
|
);
|
|
1056
1141
|
}
|
|
1057
1142
|
`;
|
|
1143
|
+
const NEXTJS_INFERENCE_WEB3_PAGE = `// app/inference-web3/page.tsx
|
|
1144
|
+
// Generated by 'lightnode add inference-web3'. User-pays one-shot inference:
|
|
1145
|
+
// each visitor's wallet signs SIWE + createSession + submitJob. Your app
|
|
1146
|
+
// holds zero funds and the answer comes back with on-chain proof anyone
|
|
1147
|
+
// can verify.
|
|
1148
|
+
//
|
|
1149
|
+
// Prereqs:
|
|
1150
|
+
// - wagmi configured (npx lightnode add wagmi-setup if you don't have it)
|
|
1151
|
+
// - the connected wallet has LCAI on the chain it's on
|
|
1152
|
+
// (mainnet 9200 or testnet 8200). Mainnet llama3-8b is 0.02 LCAI per call.
|
|
1153
|
+
"use client";
|
|
1154
|
+
|
|
1155
|
+
import { useEffect, useState } from "react";
|
|
1156
|
+
import { useAccount, useWalletClient, usePublicClient } from "wagmi";
|
|
1157
|
+
import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
|
|
1158
|
+
|
|
1159
|
+
type Result = {
|
|
1160
|
+
answer: string;
|
|
1161
|
+
worker: \`0x\${string}\`;
|
|
1162
|
+
jobId: string;
|
|
1163
|
+
submitJob: \`0x\${string}\`;
|
|
1164
|
+
jobCompleted: \`0x\${string}\` | null;
|
|
1165
|
+
elapsedMs: number;
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const MODEL = "llama3-8b";
|
|
1169
|
+
|
|
1170
|
+
export default function InferenceWeb3() {
|
|
1171
|
+
const { address, chain } = useAccount();
|
|
1172
|
+
const network: "mainnet" | "testnet" | null =
|
|
1173
|
+
chain?.id === 9200 ? "mainnet" : chain?.id === 8200 ? "testnet" : null;
|
|
1174
|
+
const { data: walletClient } = useWalletClient({ chainId: chain?.id });
|
|
1175
|
+
const publicClient = usePublicClient({ chainId: chain?.id });
|
|
1176
|
+
|
|
1177
|
+
const [system, setSystem] = useState("You are a concise assistant. Reply in one or two short sentences.");
|
|
1178
|
+
const [prompt, setPrompt] = useState("Reply with the single word OK.");
|
|
1179
|
+
const [busy, setBusy] = useState(false);
|
|
1180
|
+
const [busyStage, setBusyStage] = useState("");
|
|
1181
|
+
const [result, setResult] = useState<Result | null>(null);
|
|
1182
|
+
const [err, setErr] = useState<string | null>(null);
|
|
1183
|
+
const [feeLcai, setFeeLcai] = useState<number | null>(null);
|
|
1184
|
+
|
|
1185
|
+
useEffect(() => {
|
|
1186
|
+
if (!network) { setFeeLcai(null); return; }
|
|
1187
|
+
let cancelled = false;
|
|
1188
|
+
estimateJobFee(NETWORKS[network], MODEL).then(
|
|
1189
|
+
(fee) => { if (!cancelled) setFeeLcai(fee); },
|
|
1190
|
+
() => { if (!cancelled) setFeeLcai(null); },
|
|
1191
|
+
);
|
|
1192
|
+
return () => { cancelled = true; };
|
|
1193
|
+
}, [network]);
|
|
1194
|
+
|
|
1195
|
+
async function run() {
|
|
1196
|
+
if (!walletClient || !publicClient || !address || !network) {
|
|
1197
|
+
setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (!prompt.trim()) return;
|
|
1201
|
+
setBusy(true);
|
|
1202
|
+
setErr(null);
|
|
1203
|
+
setResult(null);
|
|
1204
|
+
const t0 = Date.now();
|
|
1205
|
+
try {
|
|
1206
|
+
setBusyStage("Sign in with your wallet (SIWE)...");
|
|
1207
|
+
const session = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
|
|
1208
|
+
|
|
1209
|
+
setBusyStage("Approve the createSession transaction in your wallet...");
|
|
1210
|
+
const gateway = new GatewayClient({ network, bearer: session.bearer });
|
|
1211
|
+
const composed = system.trim() ? \`\${system.trim()}\\n\\n\${prompt}\` : prompt;
|
|
1212
|
+
const r = await runInference({
|
|
1213
|
+
prompt: composed,
|
|
1214
|
+
gateway,
|
|
1215
|
+
wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
|
|
1216
|
+
publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
|
|
1217
|
+
network: NETWORKS[network],
|
|
1218
|
+
model: MODEL,
|
|
1219
|
+
jobCompletedTimeoutMs: 120_000,
|
|
1220
|
+
maxRetries: 1,
|
|
1221
|
+
});
|
|
1222
|
+
setResult({
|
|
1223
|
+
answer: r.answer,
|
|
1224
|
+
worker: r.worker,
|
|
1225
|
+
jobId: r.jobId?.toString() ?? "",
|
|
1226
|
+
submitJob: r.txs?.submitJob,
|
|
1227
|
+
jobCompleted: r.txs?.jobCompleted ?? null,
|
|
1228
|
+
elapsedMs: Date.now() - t0,
|
|
1229
|
+
});
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
const msg = (e as Error).message ?? "inference failed";
|
|
1232
|
+
setErr(
|
|
1233
|
+
/user rejected|user denied|reject/i.test(msg) ? "You rejected the wallet popup. Try again."
|
|
1234
|
+
: /insufficient funds|insufficient balance/i.test(msg)
|
|
1235
|
+
? \`Your wallet has no \${network === "mainnet" ? "LCAI" : "testnet LCAI"}. Top it up before sending.\`
|
|
1236
|
+
: msg.split("\\n")[0]
|
|
1237
|
+
);
|
|
1238
|
+
} finally {
|
|
1239
|
+
setBusy(false);
|
|
1240
|
+
setBusyStage("");
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return (
|
|
1245
|
+
<main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
|
|
1246
|
+
<h1>Inference (user-pays)</h1>
|
|
1247
|
+
<p style={{ color: "#666" }}>
|
|
1248
|
+
Signs one encrypted inference from your wallet on{" "}
|
|
1249
|
+
<code>{network ?? "(connect a wallet)"}</code>. Fee:{" "}
|
|
1250
|
+
<code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> per call plus a small gas amount.
|
|
1251
|
+
</p>
|
|
1252
|
+
{!address && (
|
|
1253
|
+
<div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
|
|
1254
|
+
Connect a wallet to run inference. Drop <ConnectButton /> here or wherever your app exposes one.
|
|
1255
|
+
</div>
|
|
1256
|
+
)}
|
|
1257
|
+
|
|
1258
|
+
<label style={{ display: "block", margin: "12px 0" }}>
|
|
1259
|
+
<div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>System prompt</div>
|
|
1260
|
+
<textarea value={system} onChange={(e) => setSystem(e.target.value)} rows={2}
|
|
1261
|
+
style={{ width: "100%", padding: 8, fontFamily: "monospace", fontSize: 12 }} />
|
|
1262
|
+
</label>
|
|
1263
|
+
<label style={{ display: "block", margin: "12px 0" }}>
|
|
1264
|
+
<div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>Prompt</div>
|
|
1265
|
+
<textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} rows={5}
|
|
1266
|
+
style={{ width: "100%", padding: 8, fontFamily: "monospace", fontSize: 12 }} />
|
|
1267
|
+
</label>
|
|
1268
|
+
<button type="button" onClick={() => run()} disabled={busy || !prompt.trim() || !address || !network}
|
|
1269
|
+
style={{ padding: "8px 16px", borderRadius: 8, cursor: busy ? "wait" : "pointer" }}>
|
|
1270
|
+
{busy ? (busyStage || "Running...") : "Run inference"}
|
|
1271
|
+
</button>
|
|
1272
|
+
|
|
1273
|
+
{err && (
|
|
1274
|
+
<p style={{ marginTop: 12, padding: "8px 12px", border: "1px solid #f5c2c7", background: "#f8d7da", color: "#842029", borderRadius: 6 }}>
|
|
1275
|
+
{err}
|
|
1276
|
+
</p>
|
|
1277
|
+
)}
|
|
1278
|
+
|
|
1279
|
+
{result && (
|
|
1280
|
+
<div style={{ marginTop: 16, padding: 16, border: "1px solid #ddd", borderRadius: 8 }}>
|
|
1281
|
+
<div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 8 }}>Answer</div>
|
|
1282
|
+
<pre style={{ whiteSpace: "pre-wrap", margin: 0, fontFamily: "inherit" }}>{result.answer}</pre>
|
|
1283
|
+
<div style={{ marginTop: 12, fontSize: 12, color: "#666", display: "flex", gap: 12, flexWrap: "wrap" }}>
|
|
1284
|
+
<span>elapsed {Math.round(result.elapsedMs / 1000)}s</span>
|
|
1285
|
+
<span>job #{result.jobId}</span>
|
|
1286
|
+
<a href={\`https://\${network}.lightscan.app/address/\${result.worker}\`} target="_blank" rel="noopener noreferrer">worker</a>
|
|
1287
|
+
<a href={\`https://\${network}.lightscan.app/tx/\${result.submitJob}\`} target="_blank" rel="noopener noreferrer">submitJob</a>
|
|
1288
|
+
{result.jobCompleted && (
|
|
1289
|
+
<a href={\`https://\${network}.lightscan.app/tx/\${result.jobCompleted}\`} target="_blank" rel="noopener noreferrer">jobCompleted</a>
|
|
1290
|
+
)}
|
|
1291
|
+
</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
)}
|
|
1294
|
+
</main>
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
`;
|
|
1298
|
+
const NEXTJS_JUDGE_WEB3_PAGE = `// app/judge-web3/page.tsx
|
|
1299
|
+
// Generated by 'lightnode add judge-web3'. The LightChallenge pattern with
|
|
1300
|
+
// the user paying: visitor submits criteria + evidence, signs from their own
|
|
1301
|
+
// wallet, the verdict comes back with on-chain proof. Pattern fits any
|
|
1302
|
+
// platform where users want a verifiable AI verdict on their own submission
|
|
1303
|
+
// (challenge completion, NFT trait grading, content moderation, etc.).
|
|
1304
|
+
//
|
|
1305
|
+
// Prereqs:
|
|
1306
|
+
// - wagmi configured (npx lightnode add wagmi-setup if you don't have it)
|
|
1307
|
+
// - the connected wallet has LCAI on the chain it's on
|
|
1308
|
+
"use client";
|
|
1309
|
+
|
|
1310
|
+
import { useEffect, useState } from "react";
|
|
1311
|
+
import { useAccount, useWalletClient, usePublicClient } from "wagmi";
|
|
1312
|
+
import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
|
|
1313
|
+
|
|
1314
|
+
type Verdict = {
|
|
1315
|
+
passed: boolean;
|
|
1316
|
+
confidence: number;
|
|
1317
|
+
reason: string;
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
type Result = {
|
|
1321
|
+
verdict: Verdict | null;
|
|
1322
|
+
raw: string;
|
|
1323
|
+
worker: \`0x\${string}\`;
|
|
1324
|
+
jobId: string;
|
|
1325
|
+
submitJob: \`0x\${string}\`;
|
|
1326
|
+
jobCompleted: \`0x\${string}\` | null;
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
const MODEL = "llama3-8b";
|
|
1330
|
+
|
|
1331
|
+
export default function JudgeWeb3() {
|
|
1332
|
+
const { address, chain } = useAccount();
|
|
1333
|
+
const network: "mainnet" | "testnet" | null =
|
|
1334
|
+
chain?.id === 9200 ? "mainnet" : chain?.id === 8200 ? "testnet" : null;
|
|
1335
|
+
const { data: walletClient } = useWalletClient({ chainId: chain?.id });
|
|
1336
|
+
const publicClient = usePublicClient({ chainId: chain?.id });
|
|
1337
|
+
|
|
1338
|
+
const [criteria, setCriteria] = useState("Run a mile under 8 minutes");
|
|
1339
|
+
const [evidence, setEvidence] = useState('{"distance_km": 1.61, "time_minutes": 7.4}');
|
|
1340
|
+
const [busy, setBusy] = useState(false);
|
|
1341
|
+
const [busyStage, setBusyStage] = useState("");
|
|
1342
|
+
const [result, setResult] = useState<Result | null>(null);
|
|
1343
|
+
const [err, setErr] = useState<string | null>(null);
|
|
1344
|
+
const [feeLcai, setFeeLcai] = useState<number | null>(null);
|
|
1345
|
+
|
|
1346
|
+
useEffect(() => {
|
|
1347
|
+
if (!network) { setFeeLcai(null); return; }
|
|
1348
|
+
let cancelled = false;
|
|
1349
|
+
estimateJobFee(NETWORKS[network], MODEL).then(
|
|
1350
|
+
(fee) => { if (!cancelled) setFeeLcai(fee); },
|
|
1351
|
+
() => { if (!cancelled) setFeeLcai(null); },
|
|
1352
|
+
);
|
|
1353
|
+
return () => { cancelled = true; };
|
|
1354
|
+
}, [network]);
|
|
1355
|
+
|
|
1356
|
+
/** Parse the verdict defensively; fall back to the first {...} block. */
|
|
1357
|
+
function parseVerdict(answer: string): Verdict | null {
|
|
1358
|
+
try { return JSON.parse(answer) as Verdict; } catch { /* try regex */ }
|
|
1359
|
+
const m = answer.match(/\\{[\\s\\S]*\\}/);
|
|
1360
|
+
if (m) {
|
|
1361
|
+
try { return JSON.parse(m[0]) as Verdict; } catch { /* keep null */ }
|
|
1362
|
+
}
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function run() {
|
|
1367
|
+
if (!walletClient || !publicClient || !address || !network) {
|
|
1368
|
+
setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (!criteria.trim() || !evidence.trim()) return;
|
|
1372
|
+
// Validate evidence JSON before paying.
|
|
1373
|
+
try { JSON.parse(evidence); } catch {
|
|
1374
|
+
setErr("Evidence is not valid JSON. Fix it and try again.");
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
setBusy(true);
|
|
1378
|
+
setErr(null);
|
|
1379
|
+
setResult(null);
|
|
1380
|
+
try {
|
|
1381
|
+
setBusyStage("Sign in with your wallet (SIWE)...");
|
|
1382
|
+
const session = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
|
|
1383
|
+
|
|
1384
|
+
setBusyStage("Approve the createSession transaction in your wallet...");
|
|
1385
|
+
const gateway = new GatewayClient({ network, bearer: session.bearer });
|
|
1386
|
+
const prompt = \`Criteria: \${criteria.trim()}
|
|
1387
|
+
|
|
1388
|
+
Evidence: \${evidence.trim()}
|
|
1389
|
+
|
|
1390
|
+
Reply with STRICT JSON only, matching: { "passed": boolean, "confidence": 0-1, "reason": string }\`;
|
|
1391
|
+
|
|
1392
|
+
const r = await runInference({
|
|
1393
|
+
prompt: \`You are a careful judge. Reply with STRICT JSON only, no prose.\\n\\n\${prompt}\`,
|
|
1394
|
+
gateway,
|
|
1395
|
+
wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
|
|
1396
|
+
publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
|
|
1397
|
+
network: NETWORKS[network],
|
|
1398
|
+
model: MODEL,
|
|
1399
|
+
jobCompletedTimeoutMs: 120_000,
|
|
1400
|
+
maxRetries: 1,
|
|
1401
|
+
});
|
|
1402
|
+
setResult({
|
|
1403
|
+
verdict: parseVerdict(r.answer),
|
|
1404
|
+
raw: r.answer,
|
|
1405
|
+
worker: r.worker,
|
|
1406
|
+
jobId: r.jobId?.toString() ?? "",
|
|
1407
|
+
submitJob: r.txs?.submitJob,
|
|
1408
|
+
jobCompleted: r.txs?.jobCompleted ?? null,
|
|
1409
|
+
});
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
const msg = (e as Error).message ?? "judge failed";
|
|
1412
|
+
setErr(
|
|
1413
|
+
/user rejected|user denied|reject/i.test(msg) ? "You rejected the wallet popup. Try again."
|
|
1414
|
+
: /insufficient funds|insufficient balance/i.test(msg)
|
|
1415
|
+
? \`Your wallet has no \${network === "mainnet" ? "LCAI" : "testnet LCAI"}. Top it up before submitting.\`
|
|
1416
|
+
: msg.split("\\n")[0]
|
|
1417
|
+
);
|
|
1418
|
+
} finally {
|
|
1419
|
+
setBusy(false);
|
|
1420
|
+
setBusyStage("");
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
return (
|
|
1425
|
+
<main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
|
|
1426
|
+
<h1>AI Judge (user-pays)</h1>
|
|
1427
|
+
<p style={{ color: "#666" }}>
|
|
1428
|
+
Each submission signs one inference from your wallet on{" "}
|
|
1429
|
+
<code>{network ?? "(connect a wallet)"}</code>. Cost:{" "}
|
|
1430
|
+
<code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> plus gas. Verdict comes back with on-chain proof.
|
|
1431
|
+
</p>
|
|
1432
|
+
{!address && (
|
|
1433
|
+
<div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
|
|
1434
|
+
Connect a wallet to submit. Drop <ConnectButton /> here or wherever your app exposes one.
|
|
1435
|
+
</div>
|
|
1436
|
+
)}
|
|
1437
|
+
|
|
1438
|
+
<label style={{ display: "block", margin: "12px 0" }}>
|
|
1439
|
+
<div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>Criteria</div>
|
|
1440
|
+
<textarea value={criteria} onChange={(e) => setCriteria(e.target.value)} rows={2}
|
|
1441
|
+
style={{ width: "100%", padding: 8, fontFamily: "monospace", fontSize: 12 }} />
|
|
1442
|
+
</label>
|
|
1443
|
+
<label style={{ display: "block", margin: "12px 0" }}>
|
|
1444
|
+
<div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>Evidence (JSON)</div>
|
|
1445
|
+
<textarea value={evidence} onChange={(e) => setEvidence(e.target.value)} rows={5}
|
|
1446
|
+
style={{ width: "100%", padding: 8, fontFamily: "monospace", fontSize: 12 }} />
|
|
1447
|
+
</label>
|
|
1448
|
+
<button type="button" onClick={() => run()} disabled={busy || !criteria.trim() || !evidence.trim() || !address || !network}
|
|
1449
|
+
style={{ padding: "8px 16px", borderRadius: 8, cursor: busy ? "wait" : "pointer" }}>
|
|
1450
|
+
{busy ? (busyStage || "Judging...") : "Get AI verdict"}
|
|
1451
|
+
</button>
|
|
1452
|
+
|
|
1453
|
+
{err && (
|
|
1454
|
+
<p style={{ marginTop: 12, padding: "8px 12px", border: "1px solid #f5c2c7", background: "#f8d7da", color: "#842029", borderRadius: 6 }}>
|
|
1455
|
+
{err}
|
|
1456
|
+
</p>
|
|
1457
|
+
)}
|
|
1458
|
+
|
|
1459
|
+
{result && (
|
|
1460
|
+
<div style={{ marginTop: 16, padding: 16, border: "1px solid #ddd", borderRadius: 8 }}>
|
|
1461
|
+
<div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 8 }}>Verdict</div>
|
|
1462
|
+
{result.verdict ? (
|
|
1463
|
+
<div>
|
|
1464
|
+
<div style={{ fontSize: 24, fontWeight: 600, color: result.verdict.passed ? "#2e7d32" : "#c62828" }}>
|
|
1465
|
+
{result.verdict.passed ? "PASSED" : "FAILED"}
|
|
1466
|
+
<span style={{ marginLeft: 12, fontSize: 14, color: "#666" }}>
|
|
1467
|
+
confidence {Math.round(result.verdict.confidence * 100)}%
|
|
1468
|
+
</span>
|
|
1469
|
+
</div>
|
|
1470
|
+
<p style={{ marginTop: 8, color: "#444" }}>{result.verdict.reason}</p>
|
|
1471
|
+
</div>
|
|
1472
|
+
) : (
|
|
1473
|
+
<pre style={{ whiteSpace: "pre-wrap", margin: 0, fontFamily: "monospace", fontSize: 12, color: "#666" }}>
|
|
1474
|
+
{result.raw}
|
|
1475
|
+
</pre>
|
|
1476
|
+
)}
|
|
1477
|
+
<div style={{ marginTop: 12, fontSize: 12, color: "#666", display: "flex", gap: 12, flexWrap: "wrap" }}>
|
|
1478
|
+
<span>job #{result.jobId}</span>
|
|
1479
|
+
<a href={\`https://\${network}.lightscan.app/address/\${result.worker}\`} target="_blank" rel="noopener noreferrer">worker</a>
|
|
1480
|
+
<a href={\`https://\${network}.lightscan.app/tx/\${result.submitJob}\`} target="_blank" rel="noopener noreferrer">submitJob</a>
|
|
1481
|
+
{result.jobCompleted && (
|
|
1482
|
+
<a href={\`https://\${network}.lightscan.app/tx/\${result.jobCompleted}\`} target="_blank" rel="noopener noreferrer">jobCompleted</a>
|
|
1483
|
+
)}
|
|
1484
|
+
</div>
|
|
1485
|
+
</div>
|
|
1486
|
+
)}
|
|
1487
|
+
</main>
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
`;
|
|
1058
1491
|
const NODE_CHAT_REPL = `// chat-repl.ts
|
|
1059
1492
|
// Generated by 'lightnode add chat'. Interactive chat REPL in your terminal.
|
|
1060
1493
|
// npm install lightnode-sdk viem ws
|
|
@@ -1284,12 +1717,16 @@ export function addChat(opts = {}) {
|
|
|
1284
1717
|
const force = !!opts.force;
|
|
1285
1718
|
const written = [];
|
|
1286
1719
|
if (template === "nextjs-api") {
|
|
1287
|
-
// 'add chat' is
|
|
1288
|
-
// the
|
|
1289
|
-
//
|
|
1720
|
+
// 'add chat' is self-contained: it writes the chat page, the streaming
|
|
1721
|
+
// inference route, the hosting guide, AND the Docker setup so the dev
|
|
1722
|
+
// can run the whole stack locally (or anywhere Docker runs) with one
|
|
1723
|
+
// command. No external host signup, no function-timeout fights.
|
|
1290
1724
|
written.push(writeFile(path.join(cwd, "app/chat/page.tsx"), NEXTJS_CHAT_PAGE, force));
|
|
1291
1725
|
written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_INFERENCE_STREAM_ROUTE, force));
|
|
1292
1726
|
written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
|
|
1727
|
+
written.push(writeFile(path.join(cwd, "Dockerfile"), NEXTJS_DOCKERFILE, force));
|
|
1728
|
+
written.push(writeFile(path.join(cwd, "docker-compose.yml"), NEXTJS_DOCKER_COMPOSE, force));
|
|
1729
|
+
written.push(writeFile(path.join(cwd, ".dockerignore"), DOCKERIGNORE, force));
|
|
1293
1730
|
}
|
|
1294
1731
|
else {
|
|
1295
1732
|
written.push(writeFile(path.join(cwd, "chat-repl.ts"), NODE_CHAT_REPL, force));
|
|
@@ -1337,6 +1774,59 @@ export function addChatWeb3(opts = {}) {
|
|
|
1337
1774
|
needsWagmi: !hasWagmi,
|
|
1338
1775
|
};
|
|
1339
1776
|
}
|
|
1777
|
+
/**
|
|
1778
|
+
* `lightnode add inference-web3` - the user-pays counterpart to addInference.
|
|
1779
|
+
*
|
|
1780
|
+
* Browser-only React component. Each visitor signs SIWE + createSession from
|
|
1781
|
+
* their own wallet. No backend, no PRIVATE_KEY, no per-call cost for the dev.
|
|
1782
|
+
*
|
|
1783
|
+
* Fits any one-shot inference UI: classifier, NFT trait generator, content
|
|
1784
|
+
* moderation, evaluator. For multi-turn chat use addChatWeb3; for the
|
|
1785
|
+
* dedicated judge pattern use addJudgeWeb3.
|
|
1786
|
+
*/
|
|
1787
|
+
export function addInferenceWeb3(opts = {}) {
|
|
1788
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1789
|
+
const network = opts.network ?? "mainnet";
|
|
1790
|
+
const detected = detectTemplate(cwd);
|
|
1791
|
+
const template = opts.template && opts.template !== "auto" ? opts.template : detected;
|
|
1792
|
+
const force = !!opts.force;
|
|
1793
|
+
const written = [];
|
|
1794
|
+
const pkg = readPackageJson(cwd);
|
|
1795
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
1796
|
+
const hasWagmi = Boolean(deps["wagmi"]);
|
|
1797
|
+
written.push(writeFile(path.join(cwd, "app/inference-web3/page.tsx"), NEXTJS_INFERENCE_WEB3_PAGE, force));
|
|
1798
|
+
return {
|
|
1799
|
+
written,
|
|
1800
|
+
install: `npm install lightnode-sdk viem` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
|
|
1801
|
+
template, network, needsWagmi: !hasWagmi,
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* `lightnode add judge-web3` - the user-pays counterpart to addJudge.
|
|
1806
|
+
*
|
|
1807
|
+
* The LightChallenge pattern with the user paying: visitor submits criteria
|
|
1808
|
+
* + evidence, signs from their own wallet, the structured pass/fail/confidence
|
|
1809
|
+
* verdict comes back with on-chain proof. Fit: challenge completion grading,
|
|
1810
|
+
* NFT trait verification, content moderation, automated scoring - any flow
|
|
1811
|
+
* where the END USER wants a verifiable AI verdict on their own submission.
|
|
1812
|
+
*/
|
|
1813
|
+
export function addJudgeWeb3(opts = {}) {
|
|
1814
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1815
|
+
const network = opts.network ?? "mainnet";
|
|
1816
|
+
const detected = detectTemplate(cwd);
|
|
1817
|
+
const template = opts.template && opts.template !== "auto" ? opts.template : detected;
|
|
1818
|
+
const force = !!opts.force;
|
|
1819
|
+
const written = [];
|
|
1820
|
+
const pkg = readPackageJson(cwd);
|
|
1821
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
1822
|
+
const hasWagmi = Boolean(deps["wagmi"]);
|
|
1823
|
+
written.push(writeFile(path.join(cwd, "app/judge-web3/page.tsx"), NEXTJS_JUDGE_WEB3_PAGE, force));
|
|
1824
|
+
return {
|
|
1825
|
+
written,
|
|
1826
|
+
install: `npm install lightnode-sdk viem` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
|
|
1827
|
+
template, network, needsWagmi: !hasWagmi,
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1340
1830
|
const WAGMI_CONFIG_FILE = `// lib/wagmi.ts
|
|
1341
1831
|
// Generated by 'lightnode add wagmi-setup'. Minimal wagmi setup for
|
|
1342
1832
|
// LightChain mainnet (9200) + testnet (8200). Use this as a starting
|
|
@@ -1616,6 +2106,9 @@ export function addJudge(opts = {}) {
|
|
|
1616
2106
|
if (template === "nextjs-api") {
|
|
1617
2107
|
written.push(writeFile(path.join(cwd, "app/api/judge/route.ts"), NEXTJS_JUDGE_ROUTE, force));
|
|
1618
2108
|
written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
|
|
2109
|
+
written.push(writeFile(path.join(cwd, "Dockerfile"), NEXTJS_DOCKERFILE, force));
|
|
2110
|
+
written.push(writeFile(path.join(cwd, "docker-compose.yml"), NEXTJS_DOCKER_COMPOSE, force));
|
|
2111
|
+
written.push(writeFile(path.join(cwd, ".dockerignore"), DOCKERIGNORE, force));
|
|
1619
2112
|
}
|
|
1620
2113
|
else {
|
|
1621
2114
|
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, addChatWeb3, addAgent, addJudge, addWagmiSetup } from "./add.js";
|
|
3
|
+
import { addInference, addInferenceWeb3, addJudgeWeb3, 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,25 +462,20 @@ 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", "chat-web3", "
|
|
465
|
+
const known = ["inference", "inference-web3", "chat", "chat-web3", "judge", "judge-web3", "wagmi-setup", "agent", "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
|
}
|
|
469
|
-
const result = sub === "analytics-dashboard"
|
|
470
|
-
?
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
: sub === "agent"
|
|
480
|
-
? addAgent({ template, network, force })
|
|
481
|
-
: sub === "judge"
|
|
482
|
-
? addJudge({ template, network, force })
|
|
483
|
-
: addInference({ template, network, force });
|
|
469
|
+
const result = sub === "analytics-dashboard" ? addAnalyticsDashboard({ template, network, force })
|
|
470
|
+
: sub === "nft-mint-with-inference" ? addNftMint({ template, network, force })
|
|
471
|
+
: sub === "chat-web3" ? addChatWeb3({ template, network, force })
|
|
472
|
+
: sub === "inference-web3" ? addInferenceWeb3({ template, network, force })
|
|
473
|
+
: sub === "judge-web3" ? addJudgeWeb3({ template, network, force })
|
|
474
|
+
: sub === "wagmi-setup" ? addWagmiSetup({ template, network, force })
|
|
475
|
+
: sub === "chat" ? addChat({ template, network, force })
|
|
476
|
+
: sub === "agent" ? addAgent({ template, network, force })
|
|
477
|
+
: sub === "judge" ? addJudge({ template, network, force })
|
|
478
|
+
: addInference({ template, network, force });
|
|
484
479
|
console.log(`▶ add ${sub} (${result.template} template, default network ${result.network})`);
|
|
485
480
|
for (const f of result.written) {
|
|
486
481
|
if (f.skipped)
|
|
@@ -504,24 +499,26 @@ async function main() {
|
|
|
504
499
|
console.log(` 4. You can now use any wagmi hook (useAccount, useWalletClient, ...).`);
|
|
505
500
|
console.log(` Wallets on chains other than 9200/8200 will be prompted to switch.`);
|
|
506
501
|
}
|
|
507
|
-
else if (sub === "chat-web3") {
|
|
508
|
-
//
|
|
502
|
+
else if (sub === "chat-web3" || sub === "inference-web3" || sub === "judge-web3") {
|
|
503
|
+
// *-web3 variants have no PRIVATE_KEY (each visitor pays their own way).
|
|
509
504
|
const needsWagmi = result.needsWagmi;
|
|
505
|
+
const route = sub === "chat-web3" ? "/chat-web3"
|
|
506
|
+
: sub === "inference-web3" ? "/inference-web3"
|
|
507
|
+
: "/judge-web3";
|
|
510
508
|
if (needsWagmi) {
|
|
511
509
|
console.log(` 2. Get wagmi wired up with one command:`);
|
|
512
510
|
console.log(` npx lightnode add wagmi-setup`);
|
|
513
511
|
console.log(` (drops lib/wagmi.ts + app/providers.tsx + components/connect-button.tsx)`);
|
|
514
|
-
console.log(` 3. Wrap your layout with <Providers>
|
|
515
|
-
console.log(`
|
|
516
|
-
console.log(`
|
|
517
|
-
console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
|
|
512
|
+
console.log(` 3. Wrap your layout with <Providers> and drop <ConnectButton /> on the page.`);
|
|
513
|
+
console.log(` 4. npm run dev, open ${route}, connect on chainId ${result.network === "mainnet" ? "9200" : "8200"}.`);
|
|
514
|
+
console.log(` Mainnet llama3-8b is 0.02 LCAI per call; testnet is free from https://lightfaucet.ai`);
|
|
518
515
|
}
|
|
519
516
|
else {
|
|
520
|
-
console.log(` 2. npm run dev, open
|
|
517
|
+
console.log(` 2. npm run dev, open ${route}`);
|
|
521
518
|
console.log(` 3. Connect a wallet on LightChain ${result.network === "mainnet" ? "mainnet (chainId 9200)" : "testnet (chainId 8200)"}.`);
|
|
522
|
-
console.log(` Mainnet llama3-8b
|
|
519
|
+
console.log(` Mainnet llama3-8b is 0.02 LCAI per call; testnet is free from https://lightfaucet.ai`);
|
|
523
520
|
}
|
|
524
|
-
console.log(`\n Note:
|
|
521
|
+
console.log(`\n Note: ${sub} has NO server-side route, so it scales infinitely on`);
|
|
525
522
|
console.log(` static hosting (Vercel/Netlify/Cloudflare Pages free tier all work).`);
|
|
526
523
|
}
|
|
527
524
|
else if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent" || sub === "judge") {
|
|
@@ -534,14 +531,18 @@ async function main() {
|
|
|
534
531
|
console.log(` 3. AGENT_INTERVAL_MS=3600000 npx tsx agent.ts # or run under pm2/systemd`);
|
|
535
532
|
}
|
|
536
533
|
else if (sub === "chat" && result.template === "nextjs-api") {
|
|
537
|
-
console.log(` 3.
|
|
538
|
-
console.log(`
|
|
534
|
+
console.log(` 3. Pick one:`);
|
|
535
|
+
console.log(` a) docker compose up --build # run the whole stack yourself, no timeout`);
|
|
536
|
+
console.log(` b) npm run dev # local dev only`);
|
|
537
|
+
console.log(` Open http://localhost:3000/chat`);
|
|
539
538
|
}
|
|
540
539
|
else if (sub === "chat") {
|
|
541
540
|
console.log(` 3. npx tsx chat-repl.ts (interactive terminal chat)`);
|
|
542
541
|
}
|
|
543
542
|
else if (sub === "judge" && result.template === "nextjs-api") {
|
|
544
|
-
console.log(` 3.
|
|
543
|
+
console.log(` 3. Pick one:`);
|
|
544
|
+
console.log(` a) docker compose up --build # run the whole stack yourself, no timeout`);
|
|
545
|
+
console.log(` b) npm run dev # local dev only`);
|
|
545
546
|
console.log(` 4. curl -X POST localhost:3000/api/judge -H 'content-type: application/json' \\\\`);
|
|
546
547
|
console.log(` -d '{"criteria":"Run a mile under 8 minutes","evidence":{"time_minutes":7.4,"distance_km":1.61}}'`);
|
|
547
548
|
}
|
|
@@ -553,7 +554,10 @@ async function main() {
|
|
|
553
554
|
console.log(` 4. npm run dev, open /nft-mint`);
|
|
554
555
|
}
|
|
555
556
|
else if (result.template === "nextjs-api") {
|
|
556
|
-
console.log(` 3.
|
|
557
|
+
console.log(` 3. Pick one:`);
|
|
558
|
+
console.log(` a) docker compose up --build # run the whole stack yourself, no timeout`);
|
|
559
|
+
console.log(` b) npm run dev # local dev only`);
|
|
560
|
+
console.log(` POST http://localhost:3000/api/inference {"prompt":"hello"}`);
|
|
557
561
|
}
|
|
558
562
|
else if (result.template === "hono") {
|
|
559
563
|
console.log(` 3. wire inferenceHandler into your Hono app, then start it`);
|
|
@@ -574,12 +578,16 @@ async function main() {
|
|
|
574
578
|
console.log(` 2. npx tsx lightnode-analytics.ts`);
|
|
575
579
|
}
|
|
576
580
|
}
|
|
577
|
-
// Hosting
|
|
581
|
+
// Hosting note: the Docker setup we shipped is the recommended path.
|
|
582
|
+
// The managed platforms (Vercel etc.) are the fallback if a builder is
|
|
583
|
+
// already committed to one.
|
|
578
584
|
if (result.template === "nextjs-api"
|
|
579
585
|
&& (sub === "inference" || sub === "chat" || sub === "judge")) {
|
|
580
|
-
console.log(`\n Hosting: a mainnet inference takes 60-90s.
|
|
581
|
-
console.log(`
|
|
582
|
-
console.log(`
|
|
586
|
+
console.log(`\n Hosting: a mainnet inference takes 60-90s. The Dockerfile + docker-compose.yml`);
|
|
587
|
+
console.log(` we just dropped run a long-running Node server with no timeout - that's the`);
|
|
588
|
+
console.log(` recommended path (your laptop, a $5/mo VPS, anywhere Docker runs).`);
|
|
589
|
+
console.log(` Don't use Vercel Hobby (10s cap, every call times out). Vercel Pro works at`);
|
|
590
|
+
console.log(` 60s if you'd rather stay on Vercel. See LIGHTNODE-HOSTING.md for the full table.`);
|
|
583
591
|
}
|
|
584
592
|
if (result.network === "testnet") {
|
|
585
593
|
console.log(`\nNo wallet yet? Make one: npx lightnode wallet new then fund it free below.`);
|
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.18";
|
|
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.18";
|
|
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.18",
|
|
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",
|