pi-wafer-provider 1.1.7 → 1.1.8
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/.github/FUNDING.yml +4 -0
- package/.pi/autoresearch/session-id +1 -0
- package/.pi/messenger/channels/memory.jsonl +1 -0
- package/.pi/messenger/session-id +1 -0
- package/AGENTS.md +57 -0
- package/custom-models.json +1 -0
- package/deprecated-models.json +75 -0
- package/models.json +142 -0
- package/package.json +1 -5
- package/patch.json +48 -0
- package/scripts/e2e-kimi-k2.6.mjs +218 -0
- package/scripts/update-models.js +483 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
019da9d5-6cb9-764d-a39e-ce1dbd18a3b2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"_meta":true,"v":1,"id":"memory","type":"named","createdAt":"2026-05-22T02:22:08.562Z","description":"Cross-session knowledge and insights"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
019e9c1b-cad3-7bd6-a910-c015d0788505
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## DO NOT EDIT — Auto-generated Files
|
|
4
|
+
|
|
5
|
+
The following files are **idempotent** and regenerated by `scripts/update-models.js`. Never edit them directly — your changes will be overwritten on the next model sync.
|
|
6
|
+
|
|
7
|
+
| File | Why it's auto-generated |
|
|
8
|
+
|------|------------------------|
|
|
9
|
+
| `models.json` | Built from the provider API. `update-models.js` fetches models, preserves curated data for known IDs, and writes this file. |
|
|
10
|
+
| `deprecated-models.json` | Graveyard for models the API delisted. update-models.js stamps them with deprecatedAt and pi keeps serving them for a 2-week grace period, then evicts them. Never edit by hand. |
|
|
11
|
+
| `README.md` (model table) | The table under `## Available Models` is replaced in-place by `update-models.js` after merging base models → patch → custom models. |
|
|
12
|
+
|
|
13
|
+
## Correct Files to Edit
|
|
14
|
+
|
|
15
|
+
When a model needs overrides, new properties, or corrections, edit the appropriate source file below. These are the **source of truth** that the update script reads but never writes.
|
|
16
|
+
|
|
17
|
+
| File | Purpose |
|
|
18
|
+
|------|---------|
|
|
19
|
+
| `patch.json` | Per-model overrides keyed by model ID. Add reasoning flags, compat settings, pricing corrections, thinking level maps, etc. Applied on top of `models.json` at runtime and for README generation. |
|
|
20
|
+
| `custom-models.json` | Models that don't exist in the provider API (hidden models, router endpoints, cross-provider aliases). Merged after patch. Format: array of full model objects (same schema as `models.json` entries). |
|
|
21
|
+
| `index.ts` | Provider extension code. |
|
|
22
|
+
| `scripts/update-models.js` | The sync script itself (edit only if changing how models are fetched/transformed). |
|
|
23
|
+
|
|
24
|
+
## Data Flow
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Provider API ──fetch──► models.json ──apply──► patch.json ──merge──► custom-models.json
|
|
28
|
+
│ │ │
|
|
29
|
+
└────────────────────────────┴──────────────────────┘
|
|
30
|
+
│
|
|
31
|
+
README model table
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
1. `models.json` — base data from the provider API (auto-generated, DO NOT EDIT)
|
|
35
|
+
2. `patch.json` — overrides applied on top (EDIT THIS for corrections/enrichments)
|
|
36
|
+
3. `custom-models.json` — additional models not in the API (EDIT THIS for new models)
|
|
37
|
+
4. README table — rendered from the merged result of all three (auto-generated, DO NOT EDIT)
|
|
38
|
+
|
|
39
|
+
## Common Tasks
|
|
40
|
+
|
|
41
|
+
### Add a compat setting or override pricing for an existing model
|
|
42
|
+
→ Edit `patch.json`. Add an entry keyed by the model's `id`.
|
|
43
|
+
|
|
44
|
+
### Add a model not available in the provider API
|
|
45
|
+
→ Edit `custom-models.json`. Add a full model object to the array.
|
|
46
|
+
|
|
47
|
+
### Update models from the provider API
|
|
48
|
+
→ Run `node scripts/update-models.js` (may require an API key env var).
|
|
49
|
+
|
|
50
|
+
### Regenerate the README model table
|
|
51
|
+
→ Run `node scripts/update-models.js` — it updates both `models.json` and the README table.
|
|
52
|
+
|
|
53
|
+
## TL;DR
|
|
54
|
+
|
|
55
|
+
- **Never edit `models.json`** — edit `patch.json` instead.
|
|
56
|
+
- **Never edit the README model table** — run the update script instead.
|
|
57
|
+
- `patch.json` and `custom-models.json` are the source files you should modify.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"MiniMax-M3": {
|
|
3
|
+
"id": "MiniMax-M3",
|
|
4
|
+
"name": "MiniMax-M3",
|
|
5
|
+
"reasoning": true,
|
|
6
|
+
"input": [
|
|
7
|
+
"text",
|
|
8
|
+
"image"
|
|
9
|
+
],
|
|
10
|
+
"cost": {
|
|
11
|
+
"input": 0.33,
|
|
12
|
+
"output": 1.32,
|
|
13
|
+
"cacheRead": 0.07,
|
|
14
|
+
"cacheWrite": 0
|
|
15
|
+
},
|
|
16
|
+
"contextWindow": 1048576,
|
|
17
|
+
"maxTokens": 16384,
|
|
18
|
+
"compat": {
|
|
19
|
+
"maxTokensField": "max_completion_tokens",
|
|
20
|
+
"supportsDeveloperRole": false,
|
|
21
|
+
"supportsStore": false,
|
|
22
|
+
"supportsZdr": false,
|
|
23
|
+
"supportsReasoningEffort": true
|
|
24
|
+
},
|
|
25
|
+
"deprecatedAt": "2026-08-10T02:00:21.380Z"
|
|
26
|
+
},
|
|
27
|
+
"GLM-5.1": {
|
|
28
|
+
"id": "GLM-5.1",
|
|
29
|
+
"name": "GLM-5.1",
|
|
30
|
+
"reasoning": true,
|
|
31
|
+
"input": [
|
|
32
|
+
"text"
|
|
33
|
+
],
|
|
34
|
+
"cost": {
|
|
35
|
+
"input": 1,
|
|
36
|
+
"output": 3.2,
|
|
37
|
+
"cacheRead": 0.1,
|
|
38
|
+
"cacheWrite": 0
|
|
39
|
+
},
|
|
40
|
+
"contextWindow": 202752,
|
|
41
|
+
"maxTokens": 32768,
|
|
42
|
+
"compat": {
|
|
43
|
+
"maxTokensField": "max_completion_tokens",
|
|
44
|
+
"supportsDeveloperRole": false,
|
|
45
|
+
"supportsZdr": true,
|
|
46
|
+
"supportsStore": false,
|
|
47
|
+
"supportsReasoningEffort": true
|
|
48
|
+
},
|
|
49
|
+
"deprecatedAt": "2026-08-18T02:00:24.936Z"
|
|
50
|
+
},
|
|
51
|
+
"glm5.2-fast": {
|
|
52
|
+
"id": "glm5.2-fast",
|
|
53
|
+
"name": "GLM5.2-Fast",
|
|
54
|
+
"reasoning": true,
|
|
55
|
+
"input": [
|
|
56
|
+
"text"
|
|
57
|
+
],
|
|
58
|
+
"cost": {
|
|
59
|
+
"input": 2.1,
|
|
60
|
+
"output": 6.6,
|
|
61
|
+
"cacheRead": 0.21,
|
|
62
|
+
"cacheWrite": 0
|
|
63
|
+
},
|
|
64
|
+
"contextWindow": 1048576,
|
|
65
|
+
"maxTokens": 16384,
|
|
66
|
+
"compat": {
|
|
67
|
+
"maxTokensField": "max_completion_tokens",
|
|
68
|
+
"supportsDeveloperRole": false,
|
|
69
|
+
"supportsStore": false,
|
|
70
|
+
"supportsZdr": true,
|
|
71
|
+
"supportsReasoningEffort": true
|
|
72
|
+
},
|
|
73
|
+
"deprecatedAt": "2026-08-18T02:00:24.936Z"
|
|
74
|
+
}
|
|
75
|
+
}
|
package/models.json
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "DeepSeek-V4-Flash-0731-Fast",
|
|
4
|
+
"name": "DeepSeek-V4-Flash-0731-Fast",
|
|
5
|
+
"reasoning": true,
|
|
6
|
+
"input": [
|
|
7
|
+
"text"
|
|
8
|
+
],
|
|
9
|
+
"cost": {
|
|
10
|
+
"input": 0.28,
|
|
11
|
+
"output": 0.56,
|
|
12
|
+
"cacheRead": 0.07,
|
|
13
|
+
"cacheWrite": 0
|
|
14
|
+
},
|
|
15
|
+
"contextWindow": 1048576,
|
|
16
|
+
"maxTokens": 1048576,
|
|
17
|
+
"compat": {
|
|
18
|
+
"maxTokensField": "max_completion_tokens",
|
|
19
|
+
"supportsDeveloperRole": false,
|
|
20
|
+
"supportsStore": false,
|
|
21
|
+
"supportsZdr": true,
|
|
22
|
+
"supportsReasoningEffort": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "GLM-5.2",
|
|
27
|
+
"name": "GLM-5.2",
|
|
28
|
+
"reasoning": true,
|
|
29
|
+
"input": [
|
|
30
|
+
"text"
|
|
31
|
+
],
|
|
32
|
+
"cost": {
|
|
33
|
+
"input": 1.26,
|
|
34
|
+
"output": 3.96,
|
|
35
|
+
"cacheRead": 0.23,
|
|
36
|
+
"cacheWrite": 0
|
|
37
|
+
},
|
|
38
|
+
"contextWindow": 1048576,
|
|
39
|
+
"maxTokens": 16384,
|
|
40
|
+
"compat": {
|
|
41
|
+
"maxTokensField": "max_completion_tokens",
|
|
42
|
+
"supportsDeveloperRole": false,
|
|
43
|
+
"supportsStore": false,
|
|
44
|
+
"supportsZdr": true,
|
|
45
|
+
"supportsReasoningEffort": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "Kimi-K2.6",
|
|
50
|
+
"name": "Kimi-K2.6",
|
|
51
|
+
"reasoning": true,
|
|
52
|
+
"input": [
|
|
53
|
+
"text",
|
|
54
|
+
"image"
|
|
55
|
+
],
|
|
56
|
+
"cost": {
|
|
57
|
+
"input": 1.14,
|
|
58
|
+
"output": 4.8,
|
|
59
|
+
"cacheRead": 0.19,
|
|
60
|
+
"cacheWrite": 0
|
|
61
|
+
},
|
|
62
|
+
"contextWindow": 262144,
|
|
63
|
+
"maxTokens": 32768,
|
|
64
|
+
"compat": {
|
|
65
|
+
"maxTokensField": "max_completion_tokens",
|
|
66
|
+
"supportsDeveloperRole": false,
|
|
67
|
+
"supportsZdr": false,
|
|
68
|
+
"supportsStore": false,
|
|
69
|
+
"supportsReasoningEffort": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "Kimi-K3",
|
|
74
|
+
"name": "Kimi-K3",
|
|
75
|
+
"reasoning": true,
|
|
76
|
+
"input": [
|
|
77
|
+
"text",
|
|
78
|
+
"image"
|
|
79
|
+
],
|
|
80
|
+
"cost": {
|
|
81
|
+
"input": 3,
|
|
82
|
+
"output": 15,
|
|
83
|
+
"cacheRead": 0.3,
|
|
84
|
+
"cacheWrite": 0
|
|
85
|
+
},
|
|
86
|
+
"contextWindow": 1048576,
|
|
87
|
+
"maxTokens": 16384,
|
|
88
|
+
"compat": {
|
|
89
|
+
"maxTokensField": "max_completion_tokens",
|
|
90
|
+
"supportsDeveloperRole": false,
|
|
91
|
+
"supportsStore": false,
|
|
92
|
+
"supportsZdr": true,
|
|
93
|
+
"supportsReasoningEffort": true
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "kimi-k3-fast",
|
|
98
|
+
"name": "Kimi-K3-Fast",
|
|
99
|
+
"reasoning": true,
|
|
100
|
+
"input": [
|
|
101
|
+
"text",
|
|
102
|
+
"image"
|
|
103
|
+
],
|
|
104
|
+
"cost": {
|
|
105
|
+
"input": 4.5,
|
|
106
|
+
"output": 22.5,
|
|
107
|
+
"cacheRead": 0.45,
|
|
108
|
+
"cacheWrite": 0
|
|
109
|
+
},
|
|
110
|
+
"contextWindow": 1048576,
|
|
111
|
+
"maxTokens": 16384,
|
|
112
|
+
"compat": {
|
|
113
|
+
"maxTokensField": "max_completion_tokens",
|
|
114
|
+
"supportsDeveloperRole": false,
|
|
115
|
+
"supportsStore": false,
|
|
116
|
+
"supportsZdr": true,
|
|
117
|
+
"supportsReasoningEffort": true
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "Qwen3.5-397B-A17B",
|
|
122
|
+
"name": "Qwen 3.5 397B (A17B)",
|
|
123
|
+
"reasoning": false,
|
|
124
|
+
"input": [
|
|
125
|
+
"text"
|
|
126
|
+
],
|
|
127
|
+
"cost": {
|
|
128
|
+
"input": 0,
|
|
129
|
+
"output": 0,
|
|
130
|
+
"cacheRead": 0,
|
|
131
|
+
"cacheWrite": 0
|
|
132
|
+
},
|
|
133
|
+
"contextWindow": 262144,
|
|
134
|
+
"maxTokens": 32768,
|
|
135
|
+
"compat": {
|
|
136
|
+
"maxTokensField": "max_completion_tokens",
|
|
137
|
+
"supportsDeveloperRole": false,
|
|
138
|
+
"supportsZdr": false,
|
|
139
|
+
"supportsStore": false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-wafer-provider",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "Wafer Serverless provider extension for pi - Access Qwen3.5-397B-A17B, GLM-5.1, Kimi K2.6, and DeepSeek-V4 through the Wafer Serverless API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -22,10 +22,6 @@
|
|
|
22
22
|
"./index.ts"
|
|
23
23
|
]
|
|
24
24
|
},
|
|
25
|
-
"files": [
|
|
26
|
-
"README.md",
|
|
27
|
-
"LICENSE"
|
|
28
|
-
],
|
|
29
25
|
"scripts": {
|
|
30
26
|
"clean": "echo 'nothing to clean'",
|
|
31
27
|
"build": "echo 'nothing to build'",
|
package/patch.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kimi-k3-fast": {
|
|
3
|
+
"thinkingLevelMap": {
|
|
4
|
+
"off": null,
|
|
5
|
+
"minimal": null,
|
|
6
|
+
"low": "low",
|
|
7
|
+
"medium": null,
|
|
8
|
+
"high": "high",
|
|
9
|
+
"xhigh": null,
|
|
10
|
+
"max": "max"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"Qwen3.5-397B-A17B": {
|
|
14
|
+
"providers": [
|
|
15
|
+
"wafer-serverless"
|
|
16
|
+
],
|
|
17
|
+
"reasoning": true,
|
|
18
|
+
"compat": {
|
|
19
|
+
"thinkingFormat": "qwen",
|
|
20
|
+
"maxTokensField": "max_completion_tokens",
|
|
21
|
+
"supportsDeveloperRole": false,
|
|
22
|
+
"supportsReasoningEffort": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"GLM-5.1": {
|
|
26
|
+
"providers": [
|
|
27
|
+
"wafer-serverless"
|
|
28
|
+
],
|
|
29
|
+
"reasoning": true,
|
|
30
|
+
"compat": {
|
|
31
|
+
"thinkingFormat": "zai",
|
|
32
|
+
"maxTokensField": "max_completion_tokens",
|
|
33
|
+
"supportsDeveloperRole": false,
|
|
34
|
+
"supportsReasoningEffort": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"Kimi-K2.6": {
|
|
38
|
+
"providers": [
|
|
39
|
+
"wafer-serverless"
|
|
40
|
+
],
|
|
41
|
+
"reasoning": true,
|
|
42
|
+
"compat": {
|
|
43
|
+
"maxTokensField": "max_completion_tokens",
|
|
44
|
+
"supportsDeveloperRole": false,
|
|
45
|
+
"supportsReasoningEffort": true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* E2E test for Kimi K2.6 on Wafer (pass.wafer.ai).
|
|
4
|
+
*
|
|
5
|
+
* Tests:
|
|
6
|
+
* 1. Basic streaming with reasoning prompt (delta.reasoning / reasoning_content)
|
|
7
|
+
* 2. Non-streaming (reasoning_content structure)
|
|
8
|
+
* 3. Preserved Thinking via thinking.keep="all"
|
|
9
|
+
* 4. Preserved Thinking via chat_template_kwargs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const API_KEY = process.env.WAFER_SERVERLESS_API_KEY || process.env.WAFER_API_KEY;
|
|
13
|
+
const MODEL = process.env.MODEL || "Kimi-K2.6";
|
|
14
|
+
const BASE_URL = "https://pass.wafer.ai/v1/chat/completions";
|
|
15
|
+
|
|
16
|
+
if (!API_KEY) {
|
|
17
|
+
console.error("❌ WAFER_SERVERLESS_API_KEY (or WAFER_API_KEY) is not set.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function streamRequest(label, body) {
|
|
22
|
+
console.log(`\n═══════════════════════════════════════════════════`);
|
|
23
|
+
console.log(` TEST: ${label}`);
|
|
24
|
+
console.log(` Model: ${MODEL}`);
|
|
25
|
+
console.log(` Base URL: ${BASE_URL}`);
|
|
26
|
+
console.log(`═══════════════════════════════════════════════════`);
|
|
27
|
+
|
|
28
|
+
const res = await fetch(BASE_URL, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
33
|
+
"Wafer-ZDR": "required",
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
console.error(`❌ HTTP ${res.status}: ${text}`);
|
|
41
|
+
return { ok: false, error: text };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const reader = res.body.getReader();
|
|
45
|
+
const decoder = new TextDecoder();
|
|
46
|
+
let buffer = "";
|
|
47
|
+
|
|
48
|
+
let reasoningChunks = [];
|
|
49
|
+
let contentChunks = [];
|
|
50
|
+
let hasDone = false;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
while (true) {
|
|
54
|
+
const { done, value } = await reader.read();
|
|
55
|
+
if (done) break;
|
|
56
|
+
|
|
57
|
+
buffer += decoder.decode(value, { stream: true });
|
|
58
|
+
const lines = buffer.split("\n");
|
|
59
|
+
buffer = lines.pop() || "";
|
|
60
|
+
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
if (trimmed.startsWith("data: ")) {
|
|
64
|
+
const payload = trimmed.slice(6);
|
|
65
|
+
if (payload === "[DONE]") {
|
|
66
|
+
hasDone = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const obj = JSON.parse(payload);
|
|
71
|
+
const delta = obj.choices?.[0]?.delta || {};
|
|
72
|
+
if (delta.reasoning) reasoningChunks.push(delta.reasoning);
|
|
73
|
+
if (delta.content) contentChunks.push(delta.content);
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error(`💥 Stream error: ${e.message}`);
|
|
80
|
+
} finally {
|
|
81
|
+
reader.releaseLock();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(` Reasoning chunks: ${reasoningChunks.length}`);
|
|
85
|
+
console.log(` Content chunks: ${contentChunks.length}`);
|
|
86
|
+
console.log(` [DONE] seen: ${hasDone}`);
|
|
87
|
+
|
|
88
|
+
if (reasoningChunks.length > 0) {
|
|
89
|
+
console.log(`\n --- First reasoning chunk ---`);
|
|
90
|
+
console.log(` ${reasoningChunks[0].substring(0, 200)}`);
|
|
91
|
+
}
|
|
92
|
+
if (contentChunks.length > 0) {
|
|
93
|
+
const fullContent = contentChunks.join("");
|
|
94
|
+
console.log(`\n --- Final content (first 300 chars) ---`);
|
|
95
|
+
console.log(` ${fullContent.substring(0, 300)}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { ok: true, reasoning: reasoningChunks, content: contentChunks };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function nonStreamRequest(label, body) {
|
|
102
|
+
console.log(`\n═══════════════════════════════════════════════════`);
|
|
103
|
+
console.log(` TEST: ${label}`);
|
|
104
|
+
console.log(` Model: ${MODEL}`);
|
|
105
|
+
console.log(` Base URL: ${BASE_URL}`);
|
|
106
|
+
console.log(`═══════════════════════════════════════════════════`);
|
|
107
|
+
|
|
108
|
+
const res = await fetch(BASE_URL, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
113
|
+
"Wafer-ZDR": "required",
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify(body),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const data = await res.json();
|
|
119
|
+
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
console.error(`❌ HTTP ${res.status}: ${JSON.stringify(data)}`);
|
|
122
|
+
return { ok: false, error: data };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const choice = data.choices?.[0];
|
|
126
|
+
const msg = choice?.message || {};
|
|
127
|
+
|
|
128
|
+
console.log(` keys: ${JSON.stringify(Object.keys(msg))}`);
|
|
129
|
+
console.log(` has reasoning_content: ${"reasoning_content" in msg}`);
|
|
130
|
+
console.log(` has reasoning: ${"reasoning" in msg}`);
|
|
131
|
+
if (msg.reasoning_content) {
|
|
132
|
+
console.log(` reasoning_content len: ${msg.reasoning_content.length}`);
|
|
133
|
+
console.log(` --- reasoning_content (first 300) ---`);
|
|
134
|
+
console.log(` ${msg.reasoning_content.substring(0, 300)}`);
|
|
135
|
+
}
|
|
136
|
+
if (msg.reasoning) {
|
|
137
|
+
console.log(` reasoning: ${msg.reasoning.substring(0, 300)}`);
|
|
138
|
+
}
|
|
139
|
+
console.log(` content: ${(msg.content || "").substring(0, 300)}`);
|
|
140
|
+
console.log(` usage: ${JSON.stringify(data.usage)}`);
|
|
141
|
+
|
|
142
|
+
return { ok: true, message: msg, usage: data.usage };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function main() {
|
|
146
|
+
console.log(`E2E Testing Kimi K2.6 on Wafer`);
|
|
147
|
+
console.log(`Base URL: ${BASE_URL}`);
|
|
148
|
+
|
|
149
|
+
// Test 1: Basic streaming
|
|
150
|
+
await streamRequest("Basic stream (reasoning prompt)", {
|
|
151
|
+
model: MODEL,
|
|
152
|
+
messages: [{ role: "user", content: "What is 13 × 17? Show your reasoning." }],
|
|
153
|
+
max_tokens: 500,
|
|
154
|
+
stream: true,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Test 2: Non-streaming
|
|
158
|
+
await nonStreamRequest("Non-stream (inspect structure)", {
|
|
159
|
+
model: MODEL,
|
|
160
|
+
messages: [{ role: "user", content: "What is 13 × 17? Show your reasoning." }],
|
|
161
|
+
max_tokens: 500,
|
|
162
|
+
stream: false,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Test 3: Preserved Thinking via extra_body thinking.keep
|
|
166
|
+
const r3 = await streamRequest("Stream with thinking.keep='all' (extra_body)", {
|
|
167
|
+
model: MODEL,
|
|
168
|
+
messages: [
|
|
169
|
+
{ role: "user", content: "Tell me three random numbers." },
|
|
170
|
+
{
|
|
171
|
+
role: "assistant",
|
|
172
|
+
reasoning_content: "I generated five numbers and told the first three: 473, 921, 235, and the other two were 215, 222.",
|
|
173
|
+
content: "473, 921, 235",
|
|
174
|
+
},
|
|
175
|
+
{ role: "user", content: "What are the other two numbers you have in mind?" },
|
|
176
|
+
],
|
|
177
|
+
max_tokens: 500,
|
|
178
|
+
stream: true,
|
|
179
|
+
extra_body: { thinking: { type: "enabled", keep: "all" } },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (r3.ok && r3.content.length > 0) {
|
|
183
|
+
const fullContent = r3.content.join("");
|
|
184
|
+
const got215 = fullContent.includes("215");
|
|
185
|
+
const got222 = fullContent.includes("222");
|
|
186
|
+
console.log(`\n 🧪 Preserved Thinking (extra_body) check: mentions 215=${got215}, mentions 222=${got222}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Test 4: Preserved Thinking via chat_template_kwargs
|
|
190
|
+
const r4 = await streamRequest("Stream with chat_template_kwargs preserve_thinking", {
|
|
191
|
+
model: MODEL,
|
|
192
|
+
messages: [
|
|
193
|
+
{ role: "user", content: "Tell me three random numbers." },
|
|
194
|
+
{
|
|
195
|
+
role: "assistant",
|
|
196
|
+
reasoning_content: "I generated five numbers and told the first three: 473, 921, 235, and kept 215 and 222 hidden.",
|
|
197
|
+
content: "473, 921, 235",
|
|
198
|
+
},
|
|
199
|
+
{ role: "user", content: "What are the other two numbers you have in mind?" },
|
|
200
|
+
],
|
|
201
|
+
max_tokens: 500,
|
|
202
|
+
stream: true,
|
|
203
|
+
extra_body: { chat_template_kwargs: { thinking: true, preserve_thinking: true } },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (r4.ok && r4.content.length > 0) {
|
|
207
|
+
const fullContent = r4.content.join("");
|
|
208
|
+
const got215 = fullContent.includes("215");
|
|
209
|
+
const got222 = fullContent.includes("222");
|
|
210
|
+
console.log(`\n 🧪 Preserved Thinking (chat_template_kwargs) check: mentions 215=${got215}, mentions 222=${got222}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(`\n═══════════════════════════════════════════════════`);
|
|
214
|
+
console.log(` E2E Test Complete`);
|
|
215
|
+
console.log(`═══════════════════════════════════════════════════`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Update Wafer models from API
|
|
4
|
+
*
|
|
5
|
+
* Fetches models from https://pass.wafer.ai/v1/models and updates:
|
|
6
|
+
* - models.json: Provider model definitions (enriched with pricing & compat)
|
|
7
|
+
* - README.md: Model table in the Available Models section
|
|
8
|
+
*
|
|
9
|
+
* The Wafer /v1/models API returns model limits plus nested capability, pricing,
|
|
10
|
+
* modality, and ZDR metadata. models.json mirrors those API-owned fields while
|
|
11
|
+
* patch.json remains the source of truth for thinking controls and corrections.
|
|
12
|
+
*
|
|
13
|
+
* patch.json is applied at runtime by the provider — not baked into models.json.
|
|
14
|
+
*
|
|
15
|
+
* API key: the stored `wafer-serverless` credential in ~/.pi/agent/auth.json wins, then
|
|
16
|
+
* the WAFER_SERVERLESS_API_KEY (or WAFER_API_KEY) environment variable. The script refuses to run without one.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import { execSync } from 'child_process';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
// pi's agent directory: PI_CODING_AGENT_DIR (with ~ expansion) or ~/.pi/agent.
|
|
28
|
+
function piAgentDir() {
|
|
29
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
30
|
+
if (envDir) {
|
|
31
|
+
return envDir.startsWith('~/') || envDir === '~'
|
|
32
|
+
? path.join(os.homedir(), envDir.slice(1))
|
|
33
|
+
: envDir;
|
|
34
|
+
}
|
|
35
|
+
return path.join(os.homedir(), '.pi', 'agent');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const AUTH_JSON_PATH = path.join(piAgentDir(), 'auth.json');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a configured value using pi's semantics (resolve-config-value.ts in
|
|
42
|
+
* pi-mono): "!command" runs via the shell (10s timeout) and uses trimmed
|
|
43
|
+
* stdout; "$VAR" / "${VAR}" interpolate environment variables ("$$" escapes a
|
|
44
|
+
* literal "$", "$!" a literal "!"); anything else is a literal. Returns
|
|
45
|
+
* undefined when a referenced env var is unset or a command fails.
|
|
46
|
+
*/
|
|
47
|
+
function resolveConfigValue(config, env) {
|
|
48
|
+
if (typeof config !== 'string' || config.length === 0) return undefined;
|
|
49
|
+
if (config.startsWith('!')) {
|
|
50
|
+
try {
|
|
51
|
+
const out = execSync(config.slice(1), {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
timeout: 10000,
|
|
54
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
55
|
+
});
|
|
56
|
+
return out.trim() || undefined;
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
62
|
+
let resolved = '';
|
|
63
|
+
let index = 0;
|
|
64
|
+
while (index < config.length) {
|
|
65
|
+
const dollar = config.indexOf('$', index);
|
|
66
|
+
if (dollar < 0) {
|
|
67
|
+
resolved += config.slice(index);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
resolved += config.slice(index, dollar);
|
|
71
|
+
const next = config[dollar + 1];
|
|
72
|
+
let name;
|
|
73
|
+
if (next === '$' || next === '!') {
|
|
74
|
+
resolved += next;
|
|
75
|
+
index = dollar + 2;
|
|
76
|
+
continue;
|
|
77
|
+
} else if (next === '{') {
|
|
78
|
+
const end = config.indexOf('}', dollar + 2);
|
|
79
|
+
if (end < 0) {
|
|
80
|
+
resolved += '$';
|
|
81
|
+
index = dollar + 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const inner = config.slice(dollar + 2, end);
|
|
85
|
+
if (!ENV_NAME_RE.test(inner)) {
|
|
86
|
+
resolved += config.slice(dollar, end + 1);
|
|
87
|
+
index = end + 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
name = inner;
|
|
91
|
+
index = end + 1;
|
|
92
|
+
} else {
|
|
93
|
+
const match = config.slice(dollar + 1).match(/^[A-Za-z_][A-Za-z0-9_]*/);
|
|
94
|
+
if (!match) {
|
|
95
|
+
resolved += '$';
|
|
96
|
+
index = dollar + 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
name = match[0];
|
|
100
|
+
index = dollar + 1 + name.length;
|
|
101
|
+
}
|
|
102
|
+
const value = (env && env[name]) || process.env[name] || undefined;
|
|
103
|
+
if (value === undefined) return undefined;
|
|
104
|
+
resolved += value;
|
|
105
|
+
}
|
|
106
|
+
return resolved;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The API key, resolved the way pi itself resolves it for this provider: the
|
|
111
|
+
* stored `wafer-serverless` credential in ~/.pi/agent/auth.json wins, then
|
|
112
|
+
* the WAFER_SERVERLESS_API_KEY (or WAFER_API_KEY) environment variable.
|
|
113
|
+
*/
|
|
114
|
+
function resolveApiKey() {
|
|
115
|
+
try {
|
|
116
|
+
const auth = JSON.parse(fs.readFileSync(AUTH_JSON_PATH, 'utf8'));
|
|
117
|
+
const credential = auth?.['wafer-serverless'];
|
|
118
|
+
if (credential && credential.type === 'api_key' && typeof credential.key === 'string') {
|
|
119
|
+
const key = resolveConfigValue(credential.key, credential.env);
|
|
120
|
+
if (key) return key;
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Missing or unparseable auth.json: fall through to the env var.
|
|
124
|
+
}
|
|
125
|
+
return process.env.WAFER_SERVERLESS_API_KEY || process.env.WAFER_API_KEY || undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const MODELS_API_URL = 'https://pass.wafer.ai/v1/models';
|
|
129
|
+
const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
|
|
130
|
+
const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
|
|
131
|
+
const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, '..', 'custom-models.json');
|
|
132
|
+
const README_PATH = path.join(__dirname, '..', 'README.md');
|
|
133
|
+
|
|
134
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function loadJson(filePath) {
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
139
|
+
} catch {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function saveJson(filePath, data) {
|
|
145
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
146
|
+
console.log(`✓ Saved ${path.basename(filePath)}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── API fetch ───────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
async function fetchModels() {
|
|
152
|
+
const apiKey = resolveApiKey();
|
|
153
|
+
if (!apiKey) {
|
|
154
|
+
throw new Error('No API key found: no `wafer-serverless` credential resolved from ' + AUTH_JSON_PATH + ' and WAFER_SERVERLESS_API_KEY (or WAFER_API_KEY) is not set');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(`Fetching models from ${MODELS_API_URL}...`);
|
|
158
|
+
const response = await fetch(MODELS_API_URL, {
|
|
159
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = await response.json();
|
|
167
|
+
const models = Array.isArray(data) ? data : (data.data || []);
|
|
168
|
+
console.log(`✓ Fetched ${models.length} models from API`);
|
|
169
|
+
return models;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Transform API model → models.json entry ────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function transformApiModel(apiModel, existingModelsMap) {
|
|
175
|
+
const id = apiModel.id;
|
|
176
|
+
|
|
177
|
+
const details = apiModel.wafer || {};
|
|
178
|
+
const capabilities = details.capabilities || {};
|
|
179
|
+
const pricing = details.pricing || {};
|
|
180
|
+
const reasoning = capabilities.reasoning === true;
|
|
181
|
+
const input = capabilities.vision === true ? ['text', 'image'] : ['text'];
|
|
182
|
+
const cost = {
|
|
183
|
+
input: (pricing.input_cents_per_million || 0) / 100,
|
|
184
|
+
output: (pricing.output_cents_per_million || 0) / 100,
|
|
185
|
+
cacheRead: (pricing.cache_read_cents_per_million || 0) / 100,
|
|
186
|
+
cacheWrite: 0,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (existingModelsMap[id]) {
|
|
190
|
+
const existing = { ...existingModelsMap[id] };
|
|
191
|
+
existing.name = details.display_name || existing.name;
|
|
192
|
+
existing.reasoning = reasoning;
|
|
193
|
+
existing.input = input;
|
|
194
|
+
existing.cost = cost;
|
|
195
|
+
existing.contextWindow = details.context_length || apiModel.max_model_len || existing.contextWindow;
|
|
196
|
+
existing.compat = {
|
|
197
|
+
...(existing.compat || {}),
|
|
198
|
+
supportsStore: false,
|
|
199
|
+
supportsDeveloperRole: false,
|
|
200
|
+
maxTokensField: 'max_completion_tokens',
|
|
201
|
+
supportsZdr: apiModel.zdr_supported,
|
|
202
|
+
...(reasoning ? { supportsReasoningEffort: true } : {}),
|
|
203
|
+
};
|
|
204
|
+
return existing;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const contextWindow = details.context_length || apiModel.max_model_len || 131072;
|
|
208
|
+
const model = {
|
|
209
|
+
id,
|
|
210
|
+
name: details.display_name || generateDisplayName(id),
|
|
211
|
+
reasoning,
|
|
212
|
+
input,
|
|
213
|
+
cost,
|
|
214
|
+
contextWindow,
|
|
215
|
+
maxTokens: contextWindow,
|
|
216
|
+
compat: {
|
|
217
|
+
maxTokensField: 'max_completion_tokens',
|
|
218
|
+
supportsDeveloperRole: false,
|
|
219
|
+
supportsStore: false,
|
|
220
|
+
supportsZdr: apiModel.zdr_supported,
|
|
221
|
+
...(reasoning ? { supportsReasoningEffort: true } : {}),
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return model;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function generateDisplayName(id) {
|
|
229
|
+
// Fallback: prettify the ID
|
|
230
|
+
return id
|
|
231
|
+
.replace(/[-_]/g, ' ')
|
|
232
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Patch & Custom Models ──────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function applyPatch(model, patch) {
|
|
238
|
+
const result = { ...model };
|
|
239
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
240
|
+
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
|
241
|
+
if (patch.input !== undefined) result.input = patch.input;
|
|
242
|
+
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
|
243
|
+
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
|
244
|
+
if (patch.cost) {
|
|
245
|
+
result.cost = {
|
|
246
|
+
input: patch.cost.input ?? result.cost.input,
|
|
247
|
+
output: patch.cost.output ?? result.cost.output,
|
|
248
|
+
cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
|
|
249
|
+
cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (patch.compat) {
|
|
253
|
+
result.compat = { ...(result.compat || {}), ...patch.compat };
|
|
254
|
+
}
|
|
255
|
+
if (patch.thinkingLevelMap !== undefined) result.thinkingLevelMap = patch.thinkingLevelMap;
|
|
256
|
+
if (!result.reasoning && result.compat?.thinkingFormat) {
|
|
257
|
+
delete result.compat.thinkingFormat;
|
|
258
|
+
}
|
|
259
|
+
if (result.compat && Object.keys(result.compat).length === 0) {
|
|
260
|
+
delete result.compat;
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildModels(baseModels, customModels, patchData) {
|
|
266
|
+
const modelMap = new Map();
|
|
267
|
+
for (const model of baseModels) {
|
|
268
|
+
modelMap.set(model.id, model);
|
|
269
|
+
}
|
|
270
|
+
for (const [id, patchEntry] of Object.entries(patchData)) {
|
|
271
|
+
const existing = modelMap.get(id);
|
|
272
|
+
if (existing) {
|
|
273
|
+
modelMap.set(id, applyPatch(existing, patchEntry));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const model of customModels) {
|
|
277
|
+
const existing = modelMap.get(model.id);
|
|
278
|
+
const patchEntry = patchData[model.id];
|
|
279
|
+
if (existing && patchEntry) {
|
|
280
|
+
modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
281
|
+
} else if (existing) {
|
|
282
|
+
modelMap.set(model.id, model);
|
|
283
|
+
} else if (patchEntry) {
|
|
284
|
+
modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
285
|
+
} else {
|
|
286
|
+
modelMap.set(model.id, model);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return Array.from(modelMap.values());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── README generation ──────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
function formatContext(n) {
|
|
295
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(0)}M`;
|
|
296
|
+
if (n >= 1000) return `${Math.round(n / 1000)}K`;
|
|
297
|
+
return n.toString();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function formatCost(cost) {
|
|
301
|
+
if (cost === 0) return '—';
|
|
302
|
+
if (cost === null || cost === undefined) return '—';
|
|
303
|
+
return '$' + cost.toFixed(2);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function generateReadmeTable(models) {
|
|
307
|
+
const lines = [
|
|
308
|
+
'| Model | Type | Context | Max Output | Input Cost | Output Cost | Cached Input |',
|
|
309
|
+
'|-------|------|---------|------------|------------|-------------|--------------|',
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
for (const model of models) {
|
|
313
|
+
const type = model.input.includes('image') ? 'Text + Image' : 'Text';
|
|
314
|
+
const context = formatContext(model.contextWindow);
|
|
315
|
+
const maxOutput = formatContext(model.maxTokens);
|
|
316
|
+
const inputCost = formatCost(model.cost.input);
|
|
317
|
+
const outputCost = formatCost(model.cost.output);
|
|
318
|
+
const cacheCost = formatCost(model.cost.cacheRead);
|
|
319
|
+
|
|
320
|
+
lines.push(`| ${model.name} | ${type} | ${context} | ${maxOutput} | ${inputCost} | ${outputCost} | ${cacheCost} |`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return lines.join('\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function updateReadme(models) {
|
|
327
|
+
let readme = fs.readFileSync(README_PATH, 'utf8');
|
|
328
|
+
const newTable = generateReadmeTable(models);
|
|
329
|
+
|
|
330
|
+
const tableRegex = /(## Available Models\n\n)\| Model \| Type \| Context[^\n]+\|\n\|[-| ]+\|(\n\|[^\n]+\|)*\n*/;
|
|
331
|
+
|
|
332
|
+
if (tableRegex.test(readme)) {
|
|
333
|
+
readme = readme.replace(tableRegex, (match, header) => `${header}${newTable}\n\n`);
|
|
334
|
+
fs.writeFileSync(README_PATH, readme);
|
|
335
|
+
console.log('✓ Updated README.md');
|
|
336
|
+
} else {
|
|
337
|
+
console.warn('⚠ Could not find model table in "## Available Models" section');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
// Grace period for delisted models: update-models.js moves models the API no
|
|
344
|
+
// longer lists into deprecated-models.json (stamped with deprecatedAt) instead
|
|
345
|
+
// of dropping them; the runtime appends them back so sessions and saved model
|
|
346
|
+
// settings keep working, and after 14 days they are evicted permanently.
|
|
347
|
+
const DEPRECATED_MODEL_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Reconcile deprecated-models.json against the freshly fetched model list.
|
|
351
|
+
* - in old models.json but not the API: moved into the deprecated file
|
|
352
|
+
* (deprecatedAt = now; preserved on repeat runs so the grace clock is not reset)
|
|
353
|
+
* - back in the API: resurrected (dropped from the deprecated file)
|
|
354
|
+
* - deprecatedAt older than 14 days: evicted permanently
|
|
355
|
+
* Must run BEFORE the new models.json is written; it reads the old file itself.
|
|
356
|
+
*/
|
|
357
|
+
function updateDeprecatedModels(modelsJsonPath, newModels) {
|
|
358
|
+
const deprecatedPath = path.join(path.dirname(modelsJsonPath), 'deprecated-models.json');
|
|
359
|
+
|
|
360
|
+
let oldModels = [];
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(fs.readFileSync(modelsJsonPath, 'utf8'));
|
|
363
|
+
if (Array.isArray(parsed)) oldModels = parsed;
|
|
364
|
+
} catch { /* first run: no previous models.json */ }
|
|
365
|
+
|
|
366
|
+
let deprecated = {};
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(fs.readFileSync(deprecatedPath, 'utf8'));
|
|
369
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) deprecated = parsed;
|
|
370
|
+
} catch { /* no graveyard yet */ }
|
|
371
|
+
|
|
372
|
+
const currentIds = new Set(newModels.map(m => m.id));
|
|
373
|
+
const now = new Date().toISOString();
|
|
374
|
+
const added = [];
|
|
375
|
+
const resurrected = [];
|
|
376
|
+
const evicted = [];
|
|
377
|
+
|
|
378
|
+
for (const old of oldModels) {
|
|
379
|
+
if (old && old.id && !currentIds.has(old.id) && !deprecated[old.id]) {
|
|
380
|
+
deprecated[old.id] = { ...old, deprecatedAt: now };
|
|
381
|
+
added.push(old.id);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const [id, entry] of Object.entries(deprecated)) {
|
|
386
|
+
if (currentIds.has(id)) {
|
|
387
|
+
delete deprecated[id];
|
|
388
|
+
resurrected.push(id);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const removedAt = Date.parse(entry && entry.deprecatedAt ? entry.deprecatedAt : '');
|
|
392
|
+
if (Number.isNaN(removedAt) || Date.now() - removedAt > DEPRECATED_MODEL_TTL_MS) {
|
|
393
|
+
delete deprecated[id];
|
|
394
|
+
evicted.push(id);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (added.length > 0 || resurrected.length > 0 || evicted.length > 0) {
|
|
399
|
+
fs.writeFileSync(deprecatedPath, JSON.stringify(deprecated, null, 2) + '\n');
|
|
400
|
+
console.log('Updated deprecated-models.json ' + JSON.stringify({ added, resurrected, evicted }));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Grace-period deprecated models (deprecatedAt within TTL) with metadata stripped.
|
|
406
|
+
* Keeps the README table serving models that are delisted but still within their
|
|
407
|
+
* 14-day grace window.
|
|
408
|
+
*/
|
|
409
|
+
function withDeprecatedForReadme(models) {
|
|
410
|
+
const deprecatedPath = path.join(path.dirname(MODELS_JSON_PATH), 'deprecated-models.json');
|
|
411
|
+
let deprecated = {};
|
|
412
|
+
try {
|
|
413
|
+
const parsed = JSON.parse(fs.readFileSync(deprecatedPath, 'utf8'));
|
|
414
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) deprecated = parsed;
|
|
415
|
+
} catch { /* no graveyard yet */ }
|
|
416
|
+
const now = Date.now();
|
|
417
|
+
const seen = new Set(models.map(m => m.id));
|
|
418
|
+
const extras = [];
|
|
419
|
+
for (const entry of Object.values(deprecated)) {
|
|
420
|
+
if (!entry || !entry.id || seen.has(entry.id)) continue;
|
|
421
|
+
const removedAt = Date.parse(entry.deprecatedAt || '');
|
|
422
|
+
if (Number.isNaN(removedAt) || now - removedAt > DEPRECATED_MODEL_TTL_MS) continue;
|
|
423
|
+
const m = { ...entry };
|
|
424
|
+
delete m.deprecatedAt;
|
|
425
|
+
extras.push(m);
|
|
426
|
+
}
|
|
427
|
+
return extras.length > 0 ? [...models, ...extras] : models;
|
|
428
|
+
}
|
|
429
|
+
async function main() {
|
|
430
|
+
try {
|
|
431
|
+
const apiModels = await fetchModels();
|
|
432
|
+
|
|
433
|
+
// Load existing models.json — source of truth for curated specs
|
|
434
|
+
const existingModels = loadJson(MODELS_JSON_PATH);
|
|
435
|
+
const existingModelsMap = {};
|
|
436
|
+
for (const m of (Array.isArray(existingModels) ? existingModels : [])) {
|
|
437
|
+
existingModelsMap[m.id] = m;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Transform API models, preserving existing data where available
|
|
441
|
+
let models = apiModels.map(m =>
|
|
442
|
+
transformApiModel(m, existingModelsMap)
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// Live API is authoritative — models absent from API are removed
|
|
446
|
+
// (embedded data is already used for enrichment in transformApiModel)
|
|
447
|
+
|
|
448
|
+
// Sort by model name
|
|
449
|
+
models.sort((a, b) => a.name.localeCompare(b.name));
|
|
450
|
+
|
|
451
|
+
// Save models.json (pure API output, no patch/custom baked in)
|
|
452
|
+
// Move delisted models to deprecated-models.json BEFORE models.json is overwritten
|
|
453
|
+
updateDeprecatedModels(MODELS_JSON_PATH, models);
|
|
454
|
+
saveJson(MODELS_JSON_PATH, models);
|
|
455
|
+
|
|
456
|
+
// Build full model list for README: base → patch → custom
|
|
457
|
+
const patchData = loadJson(PATCH_JSON_PATH);
|
|
458
|
+
const customModels = loadJson(CUSTOM_MODELS_JSON_PATH);
|
|
459
|
+
const readmeModels = buildModels(models, Array.isArray(customModels) ? customModels : [], patchData);
|
|
460
|
+
readmeModels.sort((a, b) => a.name.localeCompare(b.name));
|
|
461
|
+
|
|
462
|
+
// Update README
|
|
463
|
+
updateReadme(readmeModels);
|
|
464
|
+
|
|
465
|
+
// Summary
|
|
466
|
+
const newIds = new Set(models.map(m => m.id));
|
|
467
|
+
const oldIds = new Set(Object.keys(existingModelsMap));
|
|
468
|
+
const added = [...newIds].filter(id => !oldIds.has(id));
|
|
469
|
+
const removed = [...oldIds].filter(id => !newIds.has(id));
|
|
470
|
+
|
|
471
|
+
console.log('\n--- Summary ---');
|
|
472
|
+
console.log(`Total models: ${models.length}`);
|
|
473
|
+
console.log(`Vision models: ${models.filter(m => m.input.includes('image')).length}`);
|
|
474
|
+
if (added.length > 0) console.log(`New models: ${added.join(', ')} — curate models.json manually`);
|
|
475
|
+
if (removed.length > 0) console.log(`Removed models: ${removed.join(', ')}`);
|
|
476
|
+
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error('Error:', error.message);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
main();
|