pi-lilac-provider 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/index.ts +58 -21
- package/models.json +2 -1
- package/package.json +1 -1
- package/scripts/test-discounts.ts +160 -4
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ pi
|
|
|
77
77
|
| GLM 5.2 | 524K | ❌ | ✅ | $0.90 | $0.27 | $3.00 |
|
|
78
78
|
| Kimi K2.6 | 262K | ✅ | ✅ | $0.70 | $0.20 | $3.50 |
|
|
79
79
|
| MiniMax M2.7 | 205K | ❌ | ✅ | $0.30 | $0.06 | $1.20 |
|
|
80
|
-
| MiniMax M3 | 1.0M |
|
|
80
|
+
| MiniMax M3 | 1.0M | ✅ | ✅ | $0.28 | $0.05 | $1.10 |
|
|
81
81
|
|
|
82
82
|
*Costs are per million tokens. Prices subject to change — check [getlilac.com](https://getlilac.com/) for current pricing.*
|
|
83
83
|
|
package/index.ts
CHANGED
|
@@ -479,6 +479,41 @@ function dimStatus(ctx: any, text: string): string {
|
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Paint the footer status from the LIVE session model — the single source of
|
|
484
|
+
* truth for the display. ctx.model is a lazy getter (not a snapshot), so this
|
|
485
|
+
* reflects the currently-selected model even when called from a deferred
|
|
486
|
+
* post-await callback that captured a different (stale) model at hook start.
|
|
487
|
+
*
|
|
488
|
+
* Set for lilac models, cleared for everything else. Every status paint site
|
|
489
|
+
* calls this, so whichever handler runs last wins — including after a switch
|
|
490
|
+
* to a non-lilac model, fixing a race where deferred post-await setStatus()
|
|
491
|
+
* calls re-painted a stale captured lilac model's discount over the clear
|
|
492
|
+
* that model_select had just issued.
|
|
493
|
+
*
|
|
494
|
+
* Only the DISPLAY follows the live model. Cost mutation (applyDiscountInPlace)
|
|
495
|
+
* and registerProvider() in before_provider_request still target the turn's
|
|
496
|
+
* captured in-flight model — that's the object pi bound and whose .cost
|
|
497
|
+
* calculateCost() reads, and it may legitimately differ from the live model
|
|
498
|
+
* after a mid-turn /model switch.
|
|
499
|
+
*
|
|
500
|
+
* Wrapped in try/catch: ctx.model / ctx.ui assert the extension runner is
|
|
501
|
+
* still active and throw if the session ended mid-fetch, so a late deferred
|
|
502
|
+
* callback after session_shutdown no-ops instead of throwing.
|
|
503
|
+
*/
|
|
504
|
+
function syncStatus(ctx: any): void {
|
|
505
|
+
try {
|
|
506
|
+
const model = ctx.model;
|
|
507
|
+
if (model?.provider === "lilac") {
|
|
508
|
+
ctx.ui.setStatus("lilac", dimStatus(ctx, formatDiscountStatus(model.id)));
|
|
509
|
+
} else {
|
|
510
|
+
ctx.ui.setStatus("lilac", undefined);
|
|
511
|
+
}
|
|
512
|
+
} catch {
|
|
513
|
+
// Runner stale (session ended mid-fetch) — nothing to paint.
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
482
517
|
function discountsChanged(
|
|
483
518
|
a: Map<string, JsonDiscount> | null,
|
|
484
519
|
b: Map<string, JsonDiscount> | null,
|
|
@@ -572,11 +607,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
572
607
|
// Replay persisted discount state from session JSONL (synchronous, zero-latency)
|
|
573
608
|
replayDiscountEvents(ctx);
|
|
574
609
|
|
|
575
|
-
// Show status immediately with replayed/cached data — don't block pi startup
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
610
|
+
// Show status immediately with replayed/cached data — don't block pi startup.
|
|
611
|
+
// syncStatus reads the LIVE ctx.model so a switch away from lilac before the
|
|
612
|
+
// background fetch resolves never leaves a stale discount painted.
|
|
613
|
+
syncStatus(ctx);
|
|
580
614
|
|
|
581
615
|
// Fire-and-forget: resolve API key, then fetch live data in background.
|
|
582
616
|
// Provider and status are hot-swapped when results arrive.
|
|
@@ -613,9 +647,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
613
647
|
});
|
|
614
648
|
}
|
|
615
649
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
650
|
+
// Re-paint from the LIVE model: if the user switched to a non-lilac
|
|
651
|
+
// model (or a different lilac model) during the fetch, this reflects
|
|
652
|
+
// the new selection instead of re-showing the stale captured model.
|
|
653
|
+
syncStatus(ctx);
|
|
619
654
|
}).catch(() => { /* network errors are non-fatal */ });
|
|
620
655
|
});
|
|
621
656
|
});
|
|
@@ -652,7 +687,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
652
687
|
// returned unchanged) and recomputes from list price so it never compounds
|
|
653
688
|
// a previously-applied factor.
|
|
654
689
|
applyDiscountInPlace(inFlightModel, listModels, latestDiscounts);
|
|
655
|
-
|
|
690
|
+
// Display follows the LIVE model (clears if the user switched away during
|
|
691
|
+
// this turn); the cost mutation above still targets the captured in-flight
|
|
692
|
+
// model pi bound for this turn.
|
|
693
|
+
syncStatus(ctx);
|
|
656
694
|
|
|
657
695
|
if (!cachedApiKey) return;
|
|
658
696
|
|
|
@@ -667,7 +705,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
667
705
|
lastDiscountFetchTime = now;
|
|
668
706
|
|
|
669
707
|
if (!discountsChanged(latestDiscounts, discounts)) {
|
|
670
|
-
|
|
708
|
+
syncStatus(ctx);
|
|
671
709
|
return;
|
|
672
710
|
}
|
|
673
711
|
|
|
@@ -687,23 +725,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
687
725
|
api: "openai-completions",
|
|
688
726
|
models: applyDiscounts(freshList, discounts),
|
|
689
727
|
});
|
|
690
|
-
|
|
728
|
+
// Display reflects the LIVE model: post-await the user may have switched to
|
|
729
|
+
// a non-lilac model, so syncStatus clears instead of re-painting the stale
|
|
730
|
+
// captured in-flight lilac model's discount.
|
|
731
|
+
syncStatus(ctx);
|
|
691
732
|
});
|
|
692
733
|
|
|
693
|
-
pi.on("model_select", async (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
734
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
735
|
+
// ctx.model is the live session model (pi sets state.model before emitting
|
|
736
|
+
// this event), so syncStatus paints/clears consistently with every other
|
|
737
|
+
// handler — one source of truth for the footer.
|
|
738
|
+
syncStatus(ctx);
|
|
699
739
|
});
|
|
700
740
|
|
|
701
741
|
pi.on("session_tree", async (_event, ctx) => {
|
|
702
742
|
replayDiscountEvents(ctx);
|
|
703
|
-
|
|
704
|
-
if (model?.provider === "lilac") {
|
|
705
|
-
ctx.ui.setStatus("lilac", dimStatus(ctx, formatDiscountStatus(model.id)));
|
|
706
|
-
}
|
|
743
|
+
syncStatus(ctx);
|
|
707
744
|
});
|
|
708
745
|
|
|
709
746
|
// vLLM's streaming parser intermittently emits finish_reason: "tool_calls" without
|
package/models.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lilac-provider",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Lilac provider extension for pi - Access Kimi K2.6, GLM 5.1, and Gemma 4 models through Lilac's OpenAI-compatible API on idle GPUs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -286,7 +286,7 @@ for (const handler of handlers.get("model_select") || []) {
|
|
|
286
286
|
previousModel: undefined,
|
|
287
287
|
source: "set",
|
|
288
288
|
},
|
|
289
|
-
{ ui: mockUi }
|
|
289
|
+
{ ui: mockUi, model: { id: "moonshotai/kimi-k2.6", provider: "lilac" } }
|
|
290
290
|
);
|
|
291
291
|
}
|
|
292
292
|
assert(statuses.get("lilac") === "supply: healthy · sub-discount: 25%", "model_select keeps status for lilac model");
|
|
@@ -302,7 +302,7 @@ for (const handler of handlers.get("model_select") || []) {
|
|
|
302
302
|
previousModel: undefined,
|
|
303
303
|
source: "set",
|
|
304
304
|
},
|
|
305
|
-
{ ui: mockUi }
|
|
305
|
+
{ ui: mockUi, model: { id: "claude-sonnet-4", provider: "anthropic" } }
|
|
306
306
|
);
|
|
307
307
|
}
|
|
308
308
|
assert(statuses.get("lilac") === undefined, "model_select clears status for non-lilac model");
|
|
@@ -359,7 +359,7 @@ for (const handler of handlers.get("model_select") || []) {
|
|
|
359
359
|
previousModel: undefined,
|
|
360
360
|
source: "set",
|
|
361
361
|
},
|
|
362
|
-
{ ui: mockUi },
|
|
362
|
+
{ ui: mockUi, model: { id: "some/unknown-model", provider: "lilac" } },
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
365
|
assert(statuses.get("lilac") === "supply: —", "unknown model shows fallback dash");
|
|
@@ -373,7 +373,7 @@ for (const handler of handlers.get("model_select") || []) {
|
|
|
373
373
|
previousModel: undefined,
|
|
374
374
|
source: "set",
|
|
375
375
|
},
|
|
376
|
-
{ ui: mockUi },
|
|
376
|
+
{ ui: mockUi, model: { id: "moonshotai/kimi-k2.6", provider: "lilac" } },
|
|
377
377
|
);
|
|
378
378
|
}
|
|
379
379
|
assert(
|
|
@@ -541,6 +541,162 @@ assert(boundModel.cost.input === 0.525, "bound model cost mutated in place to 0.
|
|
|
541
541
|
assert(boundModel.cost.output === 2.625, "bound model output mutated to 3.50 * 0.75 = 2.625");
|
|
542
542
|
assert(boundModel.cost.cacheRead === 0.15, "bound model cacheRead mutated to 0.20 * 0.75 = 0.15");
|
|
543
543
|
|
|
544
|
+
// ─── Test 14: session_start clears status after switch during fetch ─────────
|
|
545
|
+
|
|
546
|
+
console.log("\n--- Test 14: session_start clears status after switch ---");
|
|
547
|
+
|
|
548
|
+
// Regression: the session_start background fetch resolves up to ~8s after hook
|
|
549
|
+
// start. If the user switches to a non-lilac model during that window, the
|
|
550
|
+
// deferred callback must NOT re-paint the stale captured lilac model's discount
|
|
551
|
+
// over the clear that model_select issued. syncStatus() reads the LIVE ctx.model
|
|
552
|
+
// (a lazy getter in production), so mutating startCtx.model mid-flight mimics
|
|
553
|
+
// the session model changing and the deferred paint clears instead.
|
|
554
|
+
globalThis.fetch = mockFetch({
|
|
555
|
+
"/models": { body: { data: [] } }, // no live models → liveModels null
|
|
556
|
+
"/status": {
|
|
557
|
+
body: {
|
|
558
|
+
models: [{
|
|
559
|
+
id: "moonshotai/kimi-k2.6",
|
|
560
|
+
current_subscription_supply_state: "healthy",
|
|
561
|
+
current_subscription_discount_percent: 25,
|
|
562
|
+
current_subscription_credit_multiplier: "0.75",
|
|
563
|
+
}],
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
}) as any;
|
|
567
|
+
|
|
568
|
+
statuses.clear();
|
|
569
|
+
const startCtx: any = {
|
|
570
|
+
modelRegistry: mockRegistry,
|
|
571
|
+
ui: mockUi,
|
|
572
|
+
model: { id: "moonshotai/kimi-k2.6", provider: "lilac" }, // start on lilac
|
|
573
|
+
sessionManager: { getBranch: () => [] },
|
|
574
|
+
};
|
|
575
|
+
for (const handler of handlers.get("session_start") || []) {
|
|
576
|
+
await handler({}, startCtx);
|
|
577
|
+
}
|
|
578
|
+
// Immediate paint reflects the lilac model (still selected at hook start).
|
|
579
|
+
assert(
|
|
580
|
+
statuses.get("lilac") === "supply: healthy · sub-discount: 25%",
|
|
581
|
+
"immediate session_start paint shows lilac discount",
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
// While the background fetch is in flight, the user switches to a non-lilac model.
|
|
585
|
+
startCtx.model = { id: "claude-sonnet-4", provider: "anthropic" };
|
|
586
|
+
|
|
587
|
+
// Flush the deferred .then() chain (resolveApiKey → fetch → syncStatus).
|
|
588
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
589
|
+
|
|
590
|
+
assert(
|
|
591
|
+
statuses.get("lilac") === undefined,
|
|
592
|
+
"deferred session_start clears status after switch to non-lilac (no stale re-paint)",
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// ─── Test 15: before_provider_request clears status after switch ─────────────
|
|
596
|
+
|
|
597
|
+
console.log("\n--- Test 15: before_provider_request clears status after switch ---");
|
|
598
|
+
|
|
599
|
+
// Regression: every 30s (when the discount TTL expires) before_provider_request
|
|
600
|
+
// awaits a fresh /status fetch. If the user switches to a non-lilac model during
|
|
601
|
+
// that await, the post-await paint must clear (live model) instead of re-painting
|
|
602
|
+
// the stale captured in-flight lilac model. Cost mutation still targets the
|
|
603
|
+
// captured in-flight model (Tests 12-13); only the DISPLAY follows the live model.
|
|
604
|
+
|
|
605
|
+
// Force the 30s TTL to look expired so the fetch path executes.
|
|
606
|
+
const realDateNow = Date.now;
|
|
607
|
+
Date.now = () => realDateNow.call(Date) + 60000;
|
|
608
|
+
try {
|
|
609
|
+
globalThis.fetch = mockFetch({
|
|
610
|
+
"/status": {
|
|
611
|
+
body: {
|
|
612
|
+
// CHANGED discount (low/10%) vs the cached healthy/25% → exercises the
|
|
613
|
+
// registerProvider + syncStatus path, not just the unchanged early-return.
|
|
614
|
+
models: [{
|
|
615
|
+
id: "moonshotai/kimi-k2.6",
|
|
616
|
+
current_subscription_supply_state: "low",
|
|
617
|
+
current_subscription_discount_percent: 10,
|
|
618
|
+
current_subscription_credit_multiplier: "0.90",
|
|
619
|
+
}],
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
}) as any;
|
|
623
|
+
|
|
624
|
+
statuses.clear();
|
|
625
|
+
// Live model holder; reassigning .model mid-await mimics ctx.model being a
|
|
626
|
+
// lazy getter to the session model.
|
|
627
|
+
const bprCtx: any = {
|
|
628
|
+
ui: mockUi,
|
|
629
|
+
model: { id: "moonshotai/kimi-k2.6", provider: "lilac" },
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Kick off before_provider_request; it captures inFlightModel=lilac, mutates
|
|
633
|
+
// cost, paints, then awaits the fetch (TTL expired).
|
|
634
|
+
const bprPromise = (async () => {
|
|
635
|
+
for (const handler of handlers.get("before_provider_request") || []) {
|
|
636
|
+
await handler({ type: "before_provider_request", payload: {} }, bprCtx);
|
|
637
|
+
}
|
|
638
|
+
})();
|
|
639
|
+
|
|
640
|
+
// Switch to non-lilac while the fetch is in flight (before the await resumes).
|
|
641
|
+
bprCtx.model = { id: "claude-sonnet-4", provider: "anthropic" };
|
|
642
|
+
|
|
643
|
+
await bprPromise;
|
|
644
|
+
|
|
645
|
+
assert(
|
|
646
|
+
statuses.get("lilac") === undefined,
|
|
647
|
+
"post-await syncStatus clears after switch to non-lilac (no stale re-paint)",
|
|
648
|
+
);
|
|
649
|
+
} finally {
|
|
650
|
+
Date.now = realDateNow;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ─── Test 16: session_start shows new lilac model after switch ───────────────
|
|
654
|
+
|
|
655
|
+
console.log("\n--- Test 16: session_start shows new lilac model after switch ---");
|
|
656
|
+
|
|
657
|
+
// Companion to Test 14: switching to a DIFFERENT lilac model during the fetch
|
|
658
|
+
// must paint the NEW model's discount, proving syncStatus reads the live model
|
|
659
|
+
// (not a stale capture that would show the old model's discount).
|
|
660
|
+
globalThis.fetch = mockFetch({
|
|
661
|
+
"/models": { body: { data: [] } },
|
|
662
|
+
"/status": {
|
|
663
|
+
body: {
|
|
664
|
+
models: [
|
|
665
|
+
{
|
|
666
|
+
id: "moonshotai/kimi-k2.6",
|
|
667
|
+
current_subscription_supply_state: "healthy",
|
|
668
|
+
current_subscription_discount_percent: 25,
|
|
669
|
+
current_subscription_credit_multiplier: "0.75",
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
id: "zai-org/glm-5.1",
|
|
673
|
+
current_subscription_supply_state: "high",
|
|
674
|
+
current_subscription_discount_percent: 50,
|
|
675
|
+
current_subscription_credit_multiplier: "0.50",
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
}) as any;
|
|
681
|
+
|
|
682
|
+
statuses.clear();
|
|
683
|
+
const startCtx2: any = {
|
|
684
|
+
modelRegistry: mockRegistry,
|
|
685
|
+
ui: mockUi,
|
|
686
|
+
model: { id: "moonshotai/kimi-k2.6", provider: "lilac" }, // start on kimi
|
|
687
|
+
sessionManager: { getBranch: () => [] },
|
|
688
|
+
};
|
|
689
|
+
for (const handler of handlers.get("session_start") || []) {
|
|
690
|
+
await handler({}, startCtx2);
|
|
691
|
+
}
|
|
692
|
+
// Switch to a different lilac model (glm) during the in-flight fetch.
|
|
693
|
+
startCtx2.model = { id: "zai-org/glm-5.1", provider: "lilac" };
|
|
694
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
695
|
+
assert(
|
|
696
|
+
statuses.get("lilac") === "supply: high · sub-discount: 50%",
|
|
697
|
+
"deferred session_start paints the NEW lilac model's discount (glm), not the stale kimi capture",
|
|
698
|
+
);
|
|
699
|
+
|
|
544
700
|
// ─── Cleanup ──────────────────────────────────────────────────────────────────
|
|
545
701
|
|
|
546
702
|
globalThis.fetch = originalFetch;
|