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/LICENSE +21 -0
- package/README.md +196 -0
- package/custom-models.json +1 -0
- package/index.ts +672 -0
- package/models.json +139 -0
- package/package.json +40 -0
- package/patch.json +64 -0
- package/scripts/test-discounts.ts +454 -0
- package/scripts/update-models.js +342 -0
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");
|