@lightninglabs/pi-402 0.0.1 → 0.0.3
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/README.md +2 -0
- package/package.json +1 -1
- package/src/format.ts +4 -0
- package/src/index.ts +55 -3
- package/src/payer.ts +19 -2
- package/src/provider.ts +1 -0
- package/src/wallet.ts +72 -2
- package/wasm/wavewalletdk.wasm +2 -2
package/README.md
CHANGED
|
@@ -95,6 +95,7 @@ Select a paid model and prompt:
|
|
|
95
95
|
|
|
96
96
|
```
|
|
97
97
|
/model l402-baseten/kimi-k3 # 1M context, natively multimodal
|
|
98
|
+
/model l402-baseten/kimi-k3.1-pro # 1M context, cheaper than K3 upstream
|
|
98
99
|
/model l402-baseten/glm-4.7 # 200K context
|
|
99
100
|
/model l402-baseten/glm-5.2 # 200K context
|
|
100
101
|
/model l402-baseten/deepseek-v4-flash # 1M context, the cheap one
|
|
@@ -183,6 +184,7 @@ The WASM bundle is not in git. To work on the embedded wallet, build it with
|
|
|
183
184
|
|
|
184
185
|
## Docs
|
|
185
186
|
|
|
187
|
+
- [`docs/architecture.md`](docs/architecture.md): a map of the extension: components, request and payment pipelines, storage, and concurrency.
|
|
186
188
|
- [`docs/l402-walletdk-inference-payments.md`](docs/l402-walletdk-inference-payments.md): end-to-end design of the extension.
|
|
187
189
|
- [`docs/l402-protocol-extensions.md`](docs/l402-protocol-extensions.md): furthering L402 for metered / streaming inference: keep the macaroon + preimage atomic core, add the session economic model (verifiable receipts, HTLC-native refunds, a BOLT12 payment plane), informed by the Machine Payment Protocol.
|
|
188
190
|
|
package/package.json
CHANGED
package/src/format.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { registerReceiptCommands, registerWalletCommands } from "./commands.ts";
|
|
4
|
+
import { formatSat } from "./format.ts";
|
|
4
5
|
import { MockInvoicePayer, WalletInvoicePayer } from "./payer.ts";
|
|
5
6
|
import { buildBudgetPolicy } from "./policy.ts";
|
|
6
7
|
import { makeStreamSimple } from "./provider.ts";
|
|
@@ -23,10 +24,35 @@ export default function (pi: ExtensionAPI): void {
|
|
|
23
24
|
// The wallet holds the embedded walletdk instance for the whole session.
|
|
24
25
|
const wallet = new WalletService();
|
|
25
26
|
|
|
27
|
+
// The footer status shows the wallet balance, and a one-line notice marks
|
|
28
|
+
// each payment, so an agent that spends on its own never does it silently.
|
|
29
|
+
// The UI handle arrives with session_start; it is a convenience only, so a
|
|
30
|
+
// failure here (no UI, a stale session, a balance read that errors) is
|
|
31
|
+
// swallowed and never reaches a payment.
|
|
32
|
+
let ui: ExtensionUIContext | undefined;
|
|
33
|
+
const showBalance = async (paidSat?: number): Promise<void> => {
|
|
34
|
+
const target = ui;
|
|
35
|
+
if (!target) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const balance = await wallet.confirmedBalanceSat();
|
|
41
|
+
target.setStatus("pi-402", `⚡ ${formatSat(balance)} sat`);
|
|
42
|
+
if (paidSat !== undefined) {
|
|
43
|
+
target.notify(`pi-402 paid ${formatSat(paidSat)} sat (${formatSat(balance)} sat left)`, "info");
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Leave the last status in place.
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
26
50
|
// The mock payer exercises the buyer loop end to end against a mock proxy
|
|
27
51
|
// with no Lightning at all; the real payer settles through walletdk.
|
|
28
52
|
const mockMode = process.env.PI402_MOCK_PAYER === "1";
|
|
29
|
-
const payer = mockMode
|
|
53
|
+
const payer = mockMode
|
|
54
|
+
? new MockInvoicePayer()
|
|
55
|
+
: new WalletInvoicePayer(wallet, ({ amountPaidSat }) => showBalance(amountPaidSat));
|
|
30
56
|
|
|
31
57
|
// Durable state: paid tokens and the receipt ledger live under the data
|
|
32
58
|
// dir, so a paid bundle survives a restart instead of being re-purchased
|
|
@@ -44,8 +70,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
44
70
|
// Bootstrap the wallet once per session. In mock mode there is no wallet
|
|
45
71
|
// to load, so the session starts without touching the WASM at all.
|
|
46
72
|
if (!mockMode) {
|
|
47
|
-
pi.on("session_start", async () => {
|
|
73
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
48
74
|
await wallet.startAndUnlock();
|
|
75
|
+
ui = ctx.ui;
|
|
76
|
+
await showBalance();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Release the wallet when Pi tears the extension down. Without this
|
|
80
|
+
// the WASM runtime and its SQLite worker thread keep Node's event
|
|
81
|
+
// loop alive, and a non-interactive `pi -p` never exits.
|
|
82
|
+
//
|
|
83
|
+
// Only a quit stops it. Pi also emits session_shutdown when it replaces
|
|
84
|
+
// the session (reload, new, resume, fork) and keeps running, and the
|
|
85
|
+
// next session needs the wallet the SQLite bridge belongs to.
|
|
86
|
+
pi.on("session_shutdown", async (event) => {
|
|
87
|
+
if (event.reason === "quit") {
|
|
88
|
+
await wallet.stop();
|
|
89
|
+
}
|
|
49
90
|
});
|
|
50
91
|
}
|
|
51
92
|
|
|
@@ -103,6 +144,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
103
144
|
contextWindow: 1_048_576,
|
|
104
145
|
maxTokens: 32_768,
|
|
105
146
|
},
|
|
147
|
+
{
|
|
148
|
+
// Maps to moonshotai/Kimi-K3.1-Pro on the wire. 1M-token
|
|
149
|
+
// context, priced below Kimi K3 upstream.
|
|
150
|
+
id: "kimi-k3.1-pro",
|
|
151
|
+
name: "Kimi K3.1 Pro (L402)",
|
|
152
|
+
input: ["text"],
|
|
153
|
+
reasoning: true,
|
|
154
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
155
|
+
contextWindow: 1_000_000,
|
|
156
|
+
maxTokens: 32_768,
|
|
157
|
+
},
|
|
106
158
|
],
|
|
107
159
|
streamSimple: makeStreamSimple({ payer, policy, tokenStore, credentialStore, receiptLog }),
|
|
108
160
|
});
|
package/src/payer.ts
CHANGED
|
@@ -8,6 +8,9 @@ interface PayInvoiceInput {
|
|
|
8
8
|
challengeId: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// PaidHook is told about each payment the wallet settles.
|
|
12
|
+
export type PaidHook = (paid: { amountPaidSat: number }) => void | Promise<void>;
|
|
13
|
+
|
|
11
14
|
// MockInvoicePayer returns a fixed preimage without paying anything. It exists
|
|
12
15
|
// so the buyer loop can be exercised end to end with no wallet and no
|
|
13
16
|
// Lightning at all.
|
|
@@ -23,12 +26,26 @@ export class MockInvoicePayer implements InvoicePayer {
|
|
|
23
26
|
// is read off the settled activity entry once the swap completes.
|
|
24
27
|
export class WalletInvoicePayer implements InvoicePayer {
|
|
25
28
|
private readonly wallet: WalletService;
|
|
29
|
+
private readonly onPaid?: PaidHook;
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
// onPaid runs after each successful payment, for display only: it is not
|
|
32
|
+
// awaited and a failure in it is swallowed, so it can never delay a
|
|
33
|
+
// payment's preimage or turn a settled payment into an error.
|
|
34
|
+
constructor(wallet: WalletService, onPaid?: PaidHook) {
|
|
28
35
|
this.wallet = wallet;
|
|
36
|
+
this.onPaid = onPaid;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
async payInvoice(input: PayInvoiceInput): Promise<{ preimageHex: string; amountPaidSat: number }> {
|
|
32
|
-
|
|
40
|
+
const result = await this.wallet.payInvoice(input.invoice, input.amountSat);
|
|
41
|
+
|
|
42
|
+
if (this.onPaid) {
|
|
43
|
+
const hook = this.onPaid;
|
|
44
|
+
void Promise.resolve()
|
|
45
|
+
.then(() => hook({ amountPaidSat: result.amountPaidSat }))
|
|
46
|
+
.catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
33
50
|
}
|
|
34
51
|
}
|
package/src/provider.ts
CHANGED
|
@@ -57,6 +57,7 @@ const BASETEN_MODEL_SLUGS: Record<string, string> = {
|
|
|
57
57
|
"glm-5": "zai-org/GLM-5",
|
|
58
58
|
"glm-5.2": "zai-org/GLM-5.2",
|
|
59
59
|
"kimi-k3": "moonshotai/Kimi-K3",
|
|
60
|
+
"kimi-k3.1-pro": "moonshotai/Kimi-K3.1-Pro",
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
// wireModelId resolves the model id sent to the proxy/upstream.
|
package/src/wallet.ts
CHANGED
|
@@ -104,6 +104,9 @@ export class WalletService {
|
|
|
104
104
|
// any more, so calls have to fail fast rather than hang against it.
|
|
105
105
|
private runtimeExited = false;
|
|
106
106
|
|
|
107
|
+
// stopped records that stop() has run, so a second call does nothing.
|
|
108
|
+
private stopped = false;
|
|
109
|
+
|
|
107
110
|
// startAndUnlock brings up the selected backend and unlocks the wallet,
|
|
108
111
|
// creating it on the first run. It is bootstrapped once per session from
|
|
109
112
|
// the session_start hook.
|
|
@@ -274,6 +277,14 @@ export class WalletService {
|
|
|
274
277
|
}
|
|
275
278
|
}
|
|
276
279
|
|
|
280
|
+
// Credit caps: small payments (under about 1,000 sat) go over the swap
|
|
281
|
+
// server's credit rail, and the wallet only authorizes that rail when the
|
|
282
|
+
// caller sets an explicit cap. With no cap the quote is refused ("below
|
|
283
|
+
// the 1000 sat minimum for pay swaps"), which is how a 20 sat session
|
|
284
|
+
// deposit fails. We pass the per-request ceiling for both caps, so the
|
|
285
|
+
// wallet never reserves or tops up more credit than a single request may
|
|
286
|
+
// spend.
|
|
287
|
+
//
|
|
277
288
|
// payInvoice pays a BOLT11 invoice via a two-phase wavewalletdk send.
|
|
278
289
|
// prepareSend quotes the payment (including the swap fee), send
|
|
279
290
|
// dispatches it, and the preimage is read from the settled entry once
|
|
@@ -289,7 +300,11 @@ export class WalletService {
|
|
|
289
300
|
const ceiling = maxSatPerRequest();
|
|
290
301
|
|
|
291
302
|
if (this.gateway) {
|
|
292
|
-
const quote = await this.rest("/v1/wallet/prepare-send", {
|
|
303
|
+
const quote = await this.rest("/v1/wallet/prepare-send", {
|
|
304
|
+
invoice,
|
|
305
|
+
max_credit_sat: ceiling,
|
|
306
|
+
max_credit_topup_sat: ceiling,
|
|
307
|
+
});
|
|
293
308
|
const outflow = quotedOutflowSat(quote);
|
|
294
309
|
assertWithinCeiling(outflow, ceiling);
|
|
295
310
|
|
|
@@ -312,7 +327,11 @@ export class WalletService {
|
|
|
312
327
|
}
|
|
313
328
|
|
|
314
329
|
const call = this.requireCall();
|
|
315
|
-
const quote = await call("prepareSend", {
|
|
330
|
+
const quote = await call("prepareSend", {
|
|
331
|
+
Invoice: invoice,
|
|
332
|
+
MaxCreditSat: ceiling,
|
|
333
|
+
MaxCreditTopupSat: ceiling,
|
|
334
|
+
});
|
|
316
335
|
const outflow = quotedOutflowSat(quote);
|
|
317
336
|
assertWithinCeiling(outflow, ceiling);
|
|
318
337
|
|
|
@@ -502,6 +521,57 @@ export class WalletService {
|
|
|
502
521
|
return payload;
|
|
503
522
|
}
|
|
504
523
|
|
|
524
|
+
// stop shuts the embedded wallet down so the process can exit. The Go
|
|
525
|
+
// runtime parks on a channel and the SQLite bridge owns a worker thread;
|
|
526
|
+
// a worker thread is a live handle, so until both are released Node never
|
|
527
|
+
// drains its event loop and a non-interactive `pi -p` hangs after the
|
|
528
|
+
// reply is printed.
|
|
529
|
+
//
|
|
530
|
+
// It is idempotent, a no-op when the embedded runtime never started (mock
|
|
531
|
+
// mode, sidecar mode, or a failed bootstrap), and never throws: it runs on
|
|
532
|
+
// Pi's shutdown path, where an error would only mask the exit. Each step
|
|
533
|
+
// is bounded by timeoutMs so a wedged wallet cannot hang the exit it is
|
|
534
|
+
// meant to enable.
|
|
535
|
+
async stop(timeoutMs = 5_000): Promise<void> {
|
|
536
|
+
const call = this.call;
|
|
537
|
+
if (!call || this.stopped) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.stopped = true;
|
|
541
|
+
|
|
542
|
+
// Once stopping begins nothing may use the wallet again.
|
|
543
|
+
this.ready = false;
|
|
544
|
+
|
|
545
|
+
const bounded = async (what: string, work: () => Promise<unknown>) => {
|
|
546
|
+
let timer: NodeJS.Timeout | undefined;
|
|
547
|
+
try {
|
|
548
|
+
await Promise.race([
|
|
549
|
+
work(),
|
|
550
|
+
new Promise((_, reject) => {
|
|
551
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
552
|
+
}),
|
|
553
|
+
]);
|
|
554
|
+
} catch (err) {
|
|
555
|
+
console.error(`[pi-402] embedded wallet ${what} failed:`, err);
|
|
556
|
+
} finally {
|
|
557
|
+
clearTimeout(timer);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// If the runtime already died on its own there is nothing to stop,
|
|
562
|
+
// but the worker thread may still be holding the process open.
|
|
563
|
+
if (!this.runtimeExited) {
|
|
564
|
+
await bounded("stop", () => call("stop"));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// The bridge's shutdown terminates the SQLite worker. It runs even
|
|
568
|
+
// when stop failed, since a leaked worker is what blocks the exit.
|
|
569
|
+
const bridge = (globalThis as any).sqliteBridge;
|
|
570
|
+
if (typeof bridge?.shutdown === "function") {
|
|
571
|
+
await bounded("sqlite shutdown", () => bridge.shutdown());
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
505
575
|
// noteRuntimeExit records that the embedded Go runtime is gone. The
|
|
506
576
|
// program is supposed to run for the lifetime of the process, so any exit
|
|
507
577
|
// is the wallet dying, and the only useful things to do are say so once,
|
package/wasm/wavewalletdk.wasm
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/wasm/wavewalletdk.wasm
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 133563673 bytes
|
|
4
|
+
sha256: 6d945797ce051806ed880b80a22a3a79e2b303d229cea41dc6d5071a30139f84
|