pi-lilac-provider 1.1.0

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/models.json ADDED
@@ -0,0 +1,139 @@
1
+ [
2
+ {
3
+ "id": "google/gemma-4-31b-it",
4
+ "name": "Gemma 4",
5
+ "reasoning": true,
6
+ "input": [
7
+ "text",
8
+ "image"
9
+ ],
10
+ "cost": {
11
+ "input": 0.11,
12
+ "output": 0.35,
13
+ "cacheRead": 0,
14
+ "cacheWrite": 0
15
+ },
16
+ "contextWindow": 262144,
17
+ "maxTokens": 262144,
18
+ "compat": {
19
+ "supportsDeveloperRole": false,
20
+ "supportsStore": false,
21
+ "maxTokensField": "max_completion_tokens",
22
+ "thinkingFormat": "qwen-chat-template",
23
+ "zaiToolStream": true
24
+ }
25
+ },
26
+ {
27
+ "id": "zai-org/glm-5.1",
28
+ "name": "GLM 5.1",
29
+ "reasoning": true,
30
+ "input": [
31
+ "text"
32
+ ],
33
+ "cost": {
34
+ "input": 0.9,
35
+ "output": 3,
36
+ "cacheRead": 0.27,
37
+ "cacheWrite": 0
38
+ },
39
+ "contextWindow": 202752,
40
+ "maxTokens": 131072,
41
+ "compat": {
42
+ "supportsDeveloperRole": false,
43
+ "supportsStore": false,
44
+ "maxTokensField": "max_completion_tokens",
45
+ "thinkingFormat": "qwen-chat-template",
46
+ "zaiToolStream": true
47
+ }
48
+ },
49
+ {
50
+ "id": "zai-org/glm-5.2",
51
+ "name": "GLM 5.2",
52
+ "reasoning": true,
53
+ "input": [
54
+ "text"
55
+ ],
56
+ "cost": {
57
+ "input": 0.9,
58
+ "output": 3,
59
+ "cacheRead": 0.27,
60
+ "cacheWrite": 0
61
+ },
62
+ "contextWindow": 524288,
63
+ "maxTokens": 524288,
64
+ "compat": {
65
+ "supportsDeveloperRole": true,
66
+ "supportsStore": false,
67
+ "maxTokensField": "max_completion_tokens",
68
+ "thinkingFormat": "qwen-chat-template"
69
+ }
70
+ },
71
+ {
72
+ "id": "moonshotai/kimi-k2.6",
73
+ "name": "Kimi K2.6",
74
+ "reasoning": true,
75
+ "input": [
76
+ "text",
77
+ "image"
78
+ ],
79
+ "cost": {
80
+ "input": 0.7,
81
+ "output": 3.5,
82
+ "cacheRead": 0.2,
83
+ "cacheWrite": 0
84
+ },
85
+ "contextWindow": 262144,
86
+ "maxTokens": 262144,
87
+ "compat": {
88
+ "supportsDeveloperRole": false,
89
+ "supportsStore": false,
90
+ "maxTokensField": "max_completion_tokens",
91
+ "thinkingFormat": "qwen-chat-template",
92
+ "zaiToolStream": true
93
+ }
94
+ },
95
+ {
96
+ "id": "minimaxai/minimax-m2.7",
97
+ "name": "MiniMax M2.7",
98
+ "reasoning": true,
99
+ "input": [
100
+ "text"
101
+ ],
102
+ "cost": {
103
+ "input": 0.3,
104
+ "output": 1.2,
105
+ "cacheRead": 0.06,
106
+ "cacheWrite": 0
107
+ },
108
+ "contextWindow": 204800,
109
+ "maxTokens": 204800,
110
+ "compat": {
111
+ "supportsDeveloperRole": true,
112
+ "supportsStore": false,
113
+ "maxTokensField": "max_completion_tokens",
114
+ "thinkingFormat": "qwen-chat-template"
115
+ }
116
+ },
117
+ {
118
+ "id": "minimaxai/minimax-m3",
119
+ "name": "MiniMax M3",
120
+ "reasoning": true,
121
+ "input": [
122
+ "text"
123
+ ],
124
+ "cost": {
125
+ "input": 0.28,
126
+ "output": 1.1,
127
+ "cacheRead": 0.05,
128
+ "cacheWrite": 0
129
+ },
130
+ "contextWindow": 1048576,
131
+ "maxTokens": 1048576,
132
+ "compat": {
133
+ "supportsDeveloperRole": true,
134
+ "supportsStore": false,
135
+ "maxTokensField": "max_completion_tokens",
136
+ "thinkingFormat": "qwen-chat-template"
137
+ }
138
+ }
139
+ ]
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "pi-lilac-provider",
3
+ "version": "1.1.0",
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
+ "type": "module",
6
+ "main": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "models.json",
10
+ "patch.json",
11
+ "custom-models.json",
12
+ "scripts"
13
+ ],
14
+ "keywords": [
15
+ "pi",
16
+ "extension",
17
+ "provider",
18
+ "lilac",
19
+ "ai",
20
+ "llm",
21
+ "kimi",
22
+ "glm",
23
+ "gemma",
24
+ "idle-gpu"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "pi": {
29
+ "extensions": [
30
+ "./index.ts"
31
+ ]
32
+ },
33
+ "scripts": {
34
+ "clean": "echo 'nothing to clean'",
35
+ "build": "echo 'nothing to build'",
36
+ "check": "echo 'nothing to check'",
37
+ "test": "node scripts/test-discounts.ts",
38
+ "update-models": "node scripts/update-models.js"
39
+ }
40
+ }
package/patch.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "moonshotai/kimi-k2.6": {
3
+ "reasoning": true,
4
+ "cost": {
5
+ "input": 0.70,
6
+ "output": 3.50,
7
+ "cacheRead": 0.20
8
+ },
9
+ "compat": {
10
+ "thinkingFormat": "qwen-chat-template",
11
+ "maxTokensField": "max_completion_tokens",
12
+ "supportsDeveloperRole": false,
13
+ "supportsStore": false,
14
+ "supportsReasoningEffort": true
15
+ }
16
+ },
17
+ "zai-org/glm-5.1": {
18
+ "reasoning": true,
19
+ "maxTokens": 131072,
20
+ "cost": {
21
+ "input": 0.90,
22
+ "output": 3.00,
23
+ "cacheRead": 0.27
24
+ },
25
+ "compat": {
26
+ "thinkingFormat": "qwen-chat-template",
27
+ "maxTokensField": "max_completion_tokens",
28
+ "supportsDeveloperRole": false,
29
+ "supportsStore": false,
30
+ "zaiToolStream": true,
31
+ "supportsReasoningEffort": true
32
+ }
33
+ },
34
+ "zai-org/glm-5.2": {
35
+ "thinkingLevelMap": {
36
+ "off": "minimal",
37
+ "minimal": null,
38
+ "low": null,
39
+ "medium": null,
40
+ "high": "high",
41
+ "xhigh": "max"
42
+ }
43
+ },
44
+ "google/gemma-4-31b-it": {
45
+ "reasoning": true,
46
+ "cost": {
47
+ "input": 0.11,
48
+ "output": 0.35,
49
+ "cacheRead": 0
50
+ },
51
+ "compat": {
52
+ "thinkingFormat": "qwen-chat-template",
53
+ "maxTokensField": "max_completion_tokens",
54
+ "supportsDeveloperRole": true,
55
+ "supportsStore": false,
56
+ "supportsReasoningEffort": true
57
+ }
58
+ },
59
+ "minimaxai/minimax-m2.7": {
60
+ "compat": {
61
+ "supportsDeveloperRole": false
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * E2E test for Lilac discount metadata enumeration.
4
+ *
5
+ * Verifies:
6
+ * 1. fetchStatusDiscounts parses the /status endpoint (real response format).
7
+ * 2. applyDiscounts attaches metadata to matching models and mutates costs.
8
+ * 3. cacheDiscounts / loadCachedDiscounts round-trip correctly.
9
+ * 4. The provider extension registers models with discount metadata on init
10
+ * and refreshes them after session_start.
11
+ * 5. session_start replays persisted discount events and sets footer status.
12
+ * 6. model_select sets/clears footer status for lilac/non-lilac models.
13
+ * 7. turn_end appends discount entry to session JSONL.
14
+ * 8. before_provider_request refreshes discounts with a 30s cache.
15
+ * 9. formatDiscountStatus returns fallbacks when data is missing.
16
+ */
17
+
18
+ import type { ExtensionAPI, ModelRegistry } from "@earendil-works/pi-coding-agent";
19
+ import fs from "fs";
20
+
21
+ // Isolate cache to a temp directory so the test is deterministic.
22
+ const tmpHome = `/tmp/pi-lilac-test-${Date.now()}`;
23
+ fs.mkdirSync(tmpHome, { recursive: true });
24
+ process.env.HOME = tmpHome;
25
+
26
+ const originalFetch = globalThis.fetch;
27
+
28
+ function mockFetch(responses: Record<string, { status?: number; body?: unknown }>) {
29
+ return async (url: string, _init?: RequestInit) => {
30
+ for (const [pattern, response] of Object.entries(responses)) {
31
+ if (url.includes(pattern)) {
32
+ return new Response(JSON.stringify(response.body ?? {}), {
33
+ status: response.status ?? 200,
34
+ headers: { "content-type": "application/json" },
35
+ });
36
+ }
37
+ }
38
+ return new Response(JSON.stringify({ error: "not found" }), { status: 404 });
39
+ };
40
+ }
41
+
42
+ const {
43
+ default: registerLilac,
44
+ fetchStatusDiscounts,
45
+ applyDiscounts,
46
+ loadCachedDiscounts,
47
+ cacheDiscounts,
48
+ } = await import("../index.ts");
49
+
50
+ function assert(condition: boolean, message: string) {
51
+ if (condition) {
52
+ console.log(` ✓ ${message}`);
53
+ } else {
54
+ console.error(` ✗ ${message}`);
55
+ throw new Error(`Assertion failed: ${message}`);
56
+ }
57
+ }
58
+
59
+ // ─── Test 1: fetchStatusDiscounts (real API format) ────────────────────────────
60
+
61
+ console.log("\n--- Test 1: fetchStatusDiscounts ---");
62
+ globalThis.fetch = mockFetch({
63
+ "/status": {
64
+ body: {
65
+ updated_at: "2026-06-06T09:18:14Z",
66
+ current_subscription_supply_updated_at: "2026-06-06T09:11:49Z",
67
+ window: "24h",
68
+ window_secs: 86400,
69
+ stale: false,
70
+ models: [
71
+ {
72
+ id: "google/gemma-4-31b-it",
73
+ name: "Gemma 4",
74
+ tps: 62.67,
75
+ ttfb_seconds: 0.96,
76
+ uptime_pct: 100.0,
77
+ current_subscription_supply_state: "medium",
78
+ current_subscription_discount_percent: 25,
79
+ current_subscription_credit_multiplier: "0.75",
80
+ },
81
+ {
82
+ id: "zai-org/glm-5.1",
83
+ name: "GLM 5.1",
84
+ tps: 163.32,
85
+ ttfb_seconds: 0.92,
86
+ uptime_pct: 100.0,
87
+ current_subscription_supply_state: "high",
88
+ current_subscription_discount_percent: 50,
89
+ current_subscription_credit_multiplier: "0.50",
90
+ },
91
+ {
92
+ id: "minimaxai/minimax-m2.7",
93
+ name: "MiniMax M2.7",
94
+ tps: null,
95
+ ttfb_seconds: null,
96
+ uptime_pct: null,
97
+ current_subscription_supply_state: "low",
98
+ current_subscription_discount_percent: 0,
99
+ current_subscription_credit_multiplier: "1.00",
100
+ },
101
+ ],
102
+ },
103
+ },
104
+ }) as any;
105
+
106
+ const discounts = await fetchStatusDiscounts("test-key");
107
+ assert(discounts !== null, "returns a non-null result");
108
+ assert(discounts!.has("google/gemma-4-31b-it"), "includes gemma");
109
+ assert(discounts!.get("google/gemma-4-31b-it")!.discountPercent === 25, "gemma discount is 25%");
110
+ assert(discounts!.get("google/gemma-4-31b-it")!.supplyState === "medium", "gemma state is medium");
111
+ assert(discounts!.get("google/gemma-4-31b-it")!.creditMultiplier === 0.75, "gemma credit multiplier is 0.75");
112
+ assert(discounts!.get("zai-org/glm-5.1")!.creditMultiplier === 0.50, "glm credit multiplier is 0.50");
113
+ assert(discounts!.get("minimaxai/minimax-m2.7")!.creditMultiplier === 1.00, "minimax credit multiplier is 1.00 (no discount)");
114
+
115
+ // ─── Test 2: applyDiscounts ───────────────────────────────────────────────────
116
+
117
+ console.log("\n--- Test 2: applyDiscounts ---");
118
+ const models = [
119
+ { id: "google/gemma-4-31b-it", name: "Gemma 4", cost: { input: 0.11, output: 0.35, cacheRead: 0, cacheWrite: 0 } },
120
+ { id: "moonshotai/kimi-k2.6", name: "Kimi K2.6", cost: { input: 0.70, output: 3.50, cacheRead: 0.20, cacheWrite: 0 } },
121
+ ] as any[];
122
+ const applied = applyDiscounts(models, discounts);
123
+ assert(applied[0].discount != null, "gemma has discount field");
124
+ assert(applied[0].discount.discountPercent === 25, "gemma discount attached correctly");
125
+ // credit_multiplier 0.75 means pay 75% of list price
126
+ assert(applied[0].cost.input === 0.0825, "gemma input cost = 0.11 * 0.75 = 0.0825");
127
+ assert(applied[0].cost.output === 0.2625, "gemma output cost = 0.35 * 0.75 = 0.2625");
128
+ assert(applied[1].discount == null, "kimi has no discount (not in status)");
129
+ assert(applied[1].cost.input === 0.70, "kimi cost unchanged when no discount");
130
+
131
+ // ─── Test 3: cache round-trip ─────────────────────────────────────────────────
132
+
133
+ console.log("\n--- Test 3: cache round-trip ---");
134
+ const freshDiscounts = new Map([
135
+ [
136
+ "moonshotai/kimi-k2.6",
137
+ { supplyState: "medium", discountPercent: 25, creditMultiplier: 0.75 },
138
+ ],
139
+ ]);
140
+ cacheDiscounts(freshDiscounts);
141
+ const loaded = loadCachedDiscounts();
142
+ assert(loaded !== null, "loaded discounts are non-null");
143
+ assert(loaded!.has("moonshotai/kimi-k2.6"), "loaded discounts include kimi");
144
+ assert(loaded!.get("moonshotai/kimi-k2.6")!.discountPercent === 25, "cached discount percent preserved");
145
+ assert(loaded!.get("moonshotai/kimi-k2.6")!.creditMultiplier === 0.75, "cached credit multiplier preserved");
146
+
147
+ // ─── Test 4: provider e2e registration ─────────────────────────────────────────
148
+
149
+ console.log("\n--- Test 4: provider e2e registration ---");
150
+ globalThis.fetch = mockFetch({
151
+ "/models": {
152
+ body: {
153
+ data: [
154
+ {
155
+ id: "moonshotai/kimi-k2.6",
156
+ name: "Kimi K2.6",
157
+ supported_features: ["reasoning"],
158
+ architecture: { input_modalities: ["text", "image"] },
159
+ pricing: { prompt: "0.0000007", completion: "0.0000035", input_cache_read: "0.0000002" },
160
+ context_length: 262144,
161
+ top_provider: { max_completion_tokens: 262144 },
162
+ },
163
+ ],
164
+ },
165
+ },
166
+ "/status": {
167
+ body: {
168
+ updated_at: "2026-06-06T12:00:00Z",
169
+ current_subscription_supply_updated_at: "2026-06-06T12:00:00Z",
170
+ window: "24h",
171
+ window_secs: 86400,
172
+ stale: false,
173
+ models: [
174
+ {
175
+ id: "moonshotai/kimi-k2.6",
176
+ name: "Kimi K2.6",
177
+ tps: 75.2,
178
+ ttfb_seconds: 0.21,
179
+ uptime_pct: 99.99,
180
+ current_subscription_supply_state: "healthy",
181
+ current_subscription_discount_percent: 25,
182
+ current_subscription_credit_multiplier: "0.75",
183
+ },
184
+ ],
185
+ },
186
+ },
187
+ }) as any;
188
+
189
+ const providers: any[] = [];
190
+ const handlers = new Map<string, ((...args: any[]) => void | Promise<void>)[]>();
191
+ const statuses = new Map<string, string | undefined>();
192
+ const appendedEntries: { customType: string; data: any }[] = [];
193
+
194
+ const mockTheme = {
195
+ fg: (_color: string, text: string) => text, // strip theme for easy assertions
196
+ };
197
+
198
+ const mockUi = {
199
+ setStatus: (key: string, text: string | undefined) => {
200
+ statuses.set(key, text);
201
+ },
202
+ theme: mockTheme,
203
+ };
204
+
205
+ const mockApi: ExtensionAPI = {
206
+ registerProvider: (name: string, config: any) => {
207
+ providers.push({ name, config });
208
+ },
209
+ on: (event: string, handler: (...args: any[]) => void | Promise<void>) => {
210
+ if (!handlers.has(event)) handlers.set(event, []);
211
+ handlers.get(event)!.push(handler);
212
+ },
213
+ setStatus: mockUi.setStatus,
214
+ registerCommand: () => {},
215
+ setHiddenThinkingLabel: () => {},
216
+ setLabel: () => {},
217
+ appendEntry: (customType: string, data?: any) => {
218
+ appendedEntries.push({ customType, data });
219
+ },
220
+ exec: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
221
+ } as any;
222
+
223
+ registerLilac(mockApi);
224
+
225
+ const initialProvider = providers.find((p: any) => p.name === "lilac");
226
+ assert(initialProvider != null, "provider registered on init");
227
+ assert(initialProvider.config.models.length > 0, "models registered on init");
228
+
229
+ // Trigger session_start to fetch live data
230
+ const mockRegistry: ModelRegistry = {
231
+ getApiKeyForProvider: async () => "test-key",
232
+ } as any;
233
+
234
+ for (const handler of handlers.get("session_start") || []) {
235
+ await handler(
236
+ {},
237
+ {
238
+ modelRegistry: mockRegistry,
239
+ ui: mockUi,
240
+ model: { id: "moonshotai/kimi-k2.6", provider: "lilac" },
241
+ sessionManager: { getBranch: () => [] },
242
+ }
243
+ );
244
+ }
245
+
246
+ // Allow micro-tasks to flush
247
+ await new Promise((r) => setTimeout(r, 100));
248
+
249
+ const updatedProvider = providers[providers.length - 1];
250
+ assert(updatedProvider != null, "provider re-registered after session_start");
251
+
252
+ const kimi = updatedProvider.config.models.find((m: any) => m.id === "moonshotai/kimi-k2.6");
253
+ assert(kimi != null, "kimi model present after live fetch");
254
+ assert(kimi.discount != null, "kimi has discount metadata");
255
+ assert(kimi.discount.discountPercent === 25, "kimi discount is 25%");
256
+ assert(kimi.discount.creditMultiplier === 0.75, "kimi credit multiplier is 0.75");
257
+ // List costs after transformApiModel: input=0.70, output=3.50, cacheRead=0.20
258
+ // credit_multiplier 0.75 → effective cost = list * 0.75
259
+ assert(kimi.cost.input === 0.525, "kimi input cost = 0.70 * 0.75 = 0.525");
260
+ assert(kimi.cost.output === 2.625, "kimi output cost = 3.50 * 0.75 = 2.625");
261
+ assert(kimi.cost.cacheRead === 0.15, "kimi cacheRead cost = 0.20 * 0.75 = 0.15");
262
+
263
+ const gemma = updatedProvider.config.models.find((m: any) => m.id === "google/gemma-4-31b-it");
264
+ assert(gemma != null, "gemma model still present (from embedded fallback)");
265
+ assert(gemma.discount == null, "gemma has no discount (not in status response)");
266
+ assert(gemma.cost.input === 0.11, "gemma cost unchanged (no discount in status)");
267
+
268
+ // ─── Test 5: session_start sets footer status ─────────────────────────────────
269
+
270
+ console.log("\n--- Test 5: session_start sets footer status ---");
271
+ assert(statuses.get("lilac") === "supply: healthy · sub-discount: 25%", "status set after session_start in correct format");
272
+
273
+ // ─── Test 6: model_select for lilac model updates status ──────────────────────
274
+
275
+ console.log("\n--- Test 6: model_select for lilac model ---");
276
+ for (const handler of handlers.get("model_select") || []) {
277
+ await handler(
278
+ {
279
+ type: "model_select",
280
+ model: { id: "moonshotai/kimi-k2.6", provider: "lilac" },
281
+ previousModel: undefined,
282
+ source: "set",
283
+ },
284
+ { ui: mockUi }
285
+ );
286
+ }
287
+ assert(statuses.get("lilac") === "supply: healthy · sub-discount: 25%", "model_select keeps status for lilac model");
288
+
289
+ // ─── Test 7: model_select for non-lilac model clears status ───────────────────
290
+
291
+ console.log("\n--- Test 7: model_select for non-lilac model ---");
292
+ for (const handler of handlers.get("model_select") || []) {
293
+ await handler(
294
+ {
295
+ type: "model_select",
296
+ model: { id: "claude-sonnet-4", provider: "anthropic" },
297
+ previousModel: undefined,
298
+ source: "set",
299
+ },
300
+ { ui: mockUi }
301
+ );
302
+ }
303
+ assert(statuses.get("lilac") === undefined, "model_select clears status for non-lilac model");
304
+
305
+ // ─── Test 8: before_provider_request with fresh cache sets status ─────────────
306
+
307
+ console.log("\n--- Test 8: before_provider_request with fresh cache ---");
308
+ for (const handler of handlers.get("before_provider_request") || []) {
309
+ await handler(
310
+ { type: "before_provider_request", payload: {} },
311
+ {
312
+ ui: mockUi,
313
+ model: { id: "moonshotai/kimi-k2.6", provider: "lilac" },
314
+ }
315
+ );
316
+ }
317
+ assert(statuses.get("lilac") === "supply: healthy · sub-discount: 25%", "before_provider_request sets status when cache is fresh");
318
+
319
+ // ─── Test 9: turn_end appends discount entry ──────────────────────────────────
320
+
321
+ console.log("\n--- Test 9: turn_end appends discount entry ---");
322
+ for (const handler of handlers.get("turn_end") || []) {
323
+ await handler(
324
+ { type: "turn_end" },
325
+ {
326
+ ui: mockUi,
327
+ model: { id: "moonshotai/kimi-k2.6", provider: "lilac" },
328
+ }
329
+ );
330
+ }
331
+ assert(appendedEntries.length > 0, "at least one entry was appended");
332
+ const discountEntry = appendedEntries.find(e => e.customType === "lilac-discount");
333
+ assert(discountEntry != null, "lilac-discount entry was appended");
334
+ assert(discountEntry.data.modelId === "moonshotai/kimi-k2.6", "entry has correct modelId");
335
+ assert(discountEntry.data.discountPercent === 25, "entry has correct discountPercent");
336
+ assert(discountEntry.data.creditMultiplier === 0.75, "entry has correct creditMultiplier");
337
+ assert(discountEntry.data.supplyState === "healthy", "entry has correct supplyState");
338
+
339
+ // ─── Test 10: formatDiscountStatus fallbacks ───────────────────────────────────
340
+
341
+ console.log("\n--- Test 10: formatDiscountStatus fallbacks ---");
342
+ assert(
343
+ statuses.get("lilac") === "supply: healthy · sub-discount: 25%",
344
+ "known model shows full discount status",
345
+ );
346
+
347
+ // Unknown model (not in latestDiscounts) shows fallback dash
348
+ statuses.delete("lilac");
349
+ for (const handler of handlers.get("model_select") || []) {
350
+ await handler(
351
+ {
352
+ type: "model_select",
353
+ model: { id: "some/unknown-model", provider: "lilac" },
354
+ previousModel: undefined,
355
+ source: "set",
356
+ },
357
+ { ui: mockUi },
358
+ );
359
+ }
360
+ assert(statuses.get("lilac") === "supply: —", "unknown model shows fallback dash");
361
+
362
+ // Known model restores full status
363
+ for (const handler of handlers.get("model_select") || []) {
364
+ await handler(
365
+ {
366
+ type: "model_select",
367
+ model: { id: "moonshotai/kimi-k2.6", provider: "lilac" },
368
+ previousModel: undefined,
369
+ source: "set",
370
+ },
371
+ { ui: mockUi },
372
+ );
373
+ }
374
+ assert(
375
+ statuses.get("lilac") === "supply: healthy · sub-discount: 25%",
376
+ "known model restores full discount status",
377
+ );
378
+
379
+ // ─── Test 11: re-register stability ───────────────────────────────────────────
380
+
381
+ console.log("\n--- Test 11: re-register stability ---");
382
+
383
+ // Simulate multiple rapid re-registers (e.g. before_provider_request fires
384
+ // right after session_start's background fetch completes). Each re-register
385
+ // should produce a clean model list — no duplicates, no missing models.
386
+ //
387
+ // In pi's real runtime:
388
+ // 1. pi captures state.model (old object) for the in-flight stream
389
+ // 2. before_provider_request re-registers (creates new model objects in registry)
390
+ // 3. _refreshCurrentModelFromRegistry updates state.model to the new object
391
+ // 4. the in-flight stream still uses the old model reference for cost calc
392
+ // 5. the next turn picks up the new model with updated costs
393
+ //
394
+ // So re-registering never corrupts an in-flight request — the stream holds
395
+ // its own model reference. We verify the provider produces a stable model list
396
+ // across successive re-registers.
397
+
398
+ const modelIdsAfterSignup = updatedProvider.config.models.map((m: any) => m.id).sort();
399
+
400
+ // Build fresh model list with known LIST prices for cost verification
401
+ const freshModels = updatedProvider.config.models.map((m: any) => ({
402
+ ...m,
403
+ cost: { input: 0.70, output: 3.50, cacheRead: 0.20, cacheWrite: 0 },
404
+ discount: undefined,
405
+ }));
406
+
407
+ // Re-register with changed discounts
408
+ const changedDiscounts = new Map([
409
+ ["moonshotai/kimi-k2.6", { supplyState: "low", discountPercent: 10, creditMultiplier: 0.90 }],
410
+ ]);
411
+
412
+ const reRegisterModels = applyDiscounts(freshModels, changedDiscounts);
413
+ mockApi.registerProvider("lilac", {
414
+ baseUrl: "https://api.getlilac.com/v1",
415
+ apiKey: "$LILAC_API_KEY",
416
+ api: "openai-completions",
417
+ models: reRegisterModels,
418
+ });
419
+ const afterFirst = providers[providers.length - 1];
420
+ const modelIdsAfterFirst = afterFirst.config.models.map((m: any) => m.id).sort();
421
+ assert(modelIdsAfterFirst.length === modelIdsAfterSignup.length, "model count preserved after re-register");
422
+ assert(JSON.stringify(modelIdsAfterFirst) === JSON.stringify(modelIdsAfterSignup), "model IDs stable after re-register");
423
+
424
+ // Re-register again with original discounts (simulates rapid successive updates)
425
+ // Use the same discount data that session_start fetched (which includes kimi at 25%)
426
+ const sessionDiscounts = new Map([
427
+ ["moonshotai/kimi-k2.6", { supplyState: "healthy", discountPercent: 25, creditMultiplier: 0.75 }],
428
+ ]);
429
+ const reRegisterModels2 = applyDiscounts(
430
+ freshModels.map((m: any) => ({ ...m, discount: undefined })),
431
+ sessionDiscounts,
432
+ );
433
+ mockApi.registerProvider("lilac", {
434
+ baseUrl: "https://api.getlilac.com/v1",
435
+ apiKey: "$LILAC_API_KEY",
436
+ api: "openai-completions",
437
+ models: reRegisterModels2,
438
+ });
439
+ const afterSecond = providers[providers.length - 1];
440
+ const modelIdsAfterSecond = afterSecond.config.models.map((m: any) => m.id).sort();
441
+ assert(modelIdsAfterSecond.length === modelIdsAfterSignup.length, "model count preserved after second re-register");
442
+ assert(JSON.stringify(modelIdsAfterSecond) === JSON.stringify(modelIdsAfterSignup), "model IDs stable after second re-register");
443
+
444
+ // Verify the costs reflect the LATEST registration (not stale from an earlier one)
445
+ const kimiFinal = afterSecond.config.models.find((m: any) => m.id === "moonshotai/kimi-k2.6");
446
+ assert(kimiFinal.discount.discountPercent === 25, "final registration uses correct discount (not stale from first re-register)");
447
+ assert(kimiFinal.cost.input === 0.525, "final registration uses correct cost (0.70 * 0.75)");
448
+
449
+ // ─── Cleanup ──────────────────────────────────────────────────────────────────
450
+
451
+ globalThis.fetch = originalFetch;
452
+ fs.rmSync(tmpHome, { recursive: true, force: true });
453
+
454
+ console.log("\n--- All tests passed ---\n");