pi-neuralwatt-provider 1.2.4 → 1.3.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/README.md +35 -5
- package/index.ts +117 -3
- package/models.json +0 -46
- package/npm-shrinkwrap.json +1 -1
- package/package.json +16 -15
- package/patch.json +49 -15
- package/scripts/probe-metadata.mjs +0 -0
- package/scripts/test-image-limit-stream.mjs +0 -0
- package/scripts/test-image-limit.mjs +0 -0
- package/tests/__mocks__/pi-ai.ts +1 -0
- package/tests/thinking-levels.test.ts +202 -0
- package/vitest.config.ts +4 -0
package/README.md
CHANGED
|
@@ -38,8 +38,6 @@ _Kimi, GLM, Qwen, DeepSeek — with real-time ⚡ energy/cost per session for [p
|
|
|
38
38
|
| GLM-5.2 (short, fast) | 200K | ❌ | ❌ | $1.45 | $4.50 |
|
|
39
39
|
| GLM-5.2 (short, flex) | 200K | ❌ | ✅ | $1.45 | $4.50 |
|
|
40
40
|
| GLM-5.2 (short) | 200K | ❌ | ✅ | $1.45 | $4.50 |
|
|
41
|
-
| Kimi K2.5 | 262K | ✅ | ✅ | $0.52 | $2.59 |
|
|
42
|
-
| Kimi K2.5 Fast | 262K | ✅ | ❌ | $0.52 | $2.59 |
|
|
43
41
|
| Kimi K2.6 | 262K | ✅ | ✅ | $0.69 | $3.22 |
|
|
44
42
|
| Kimi K2.6 (flex) | 262K | ✅ | ✅ | $0.69 | $3.22 |
|
|
45
43
|
| Kimi K2.6 Fast | 262K | ✅ | ❌ | $0.69 | $3.22 |
|
|
@@ -137,12 +135,19 @@ pi
|
|
|
137
135
|
|
|
138
136
|
### Compat Settings
|
|
139
137
|
|
|
140
|
-
Neuralwatt's API
|
|
138
|
+
Neuralwatt's API provides compatibility and capability metadata (pricing, reasoning, vision, `developer_role`, `reasoning_effort`, `max_images`) directly in the `/v1/models` response. The `update-models.js` script reads these and writes them into `models.json`. Only genuinely incorrect API data needs a manual override in `patch.json`.
|
|
141
139
|
|
|
142
|
-
Currently configured compat settings
|
|
140
|
+
Currently configured compat settings:
|
|
143
141
|
|
|
144
142
|
- **`supportsDeveloperRole: false`** — All models. vLLM doesn't support the `developer` role; pi sends system prompts as `system` messages instead.
|
|
145
|
-
- **`supportsReasoningEffort: true`** —
|
|
143
|
+
- **`supportsReasoningEffort: true`** — GLM-5.2. Sends the `reasoning_effort` parameter (maps pi's `/reasoning` levels onto GLM-5.2's native `high`/`max`/`minimal` via `thinkingLevelMap`).
|
|
144
|
+
- **`requiresReasoningContentOnAssistantMessages: true`** — Kimi K2.6/K2.7 reasoning variants. Pi-ai replays the model's prior-turn `reasoning` field on every assistant message so the model can continue its chain-of-thought across turns. All Neuralwatt reasoning models get this Layer-A replay automatically (the gateway aliases `reasoning` ↔ `reasoning_content`); this flag adds an empty `reasoning_content` scaffold for turns with no thinking block.
|
|
145
|
+
- **`chatTemplateKwargs`** — Raw `chat_template_kwargs` merged into every request via pi-ai's `onPayload` hook, mirroring vLLM's request field of the same name. Used to opt reasoning models into **full-history reasoning preservation** (vLLM's Jinja templates otherwise trim older assistant reasoning in alternating chat). The flags are template-level and family-specific — NOT a generic boolean:
|
|
146
|
+
- **Kimi K2.6 / K2.7** → `{ "preserve_thinking": true }` — keeps the full reasoning history across turns (doc-backed; behavioral E2E: 0/6 → 6/6 recall).
|
|
147
|
+
- **GLM-5.2 family** → `{ "clear_thinking": false }` — stops the template clearing older reasoning (functional: 1/4 → 4/4 recall, confirmed family-wide).
|
|
148
|
+
- **GLM-5.1 / Qwen3.x / non-reasoning `-fast`** — no kwarg (their templates expose no flag; they rely on Layer-A replay only).
|
|
149
|
+
|
|
150
|
+
These are injected alongside `reasoning_effort` (NOT via `thinkingFormat: "chat-template"`, which would displace the OpenAI `reasoning_effort` path) so thinking-level control and full-history preservation coexist.
|
|
146
151
|
|
|
147
152
|
### Custom Stream Handler
|
|
148
153
|
|
|
@@ -180,6 +185,8 @@ For reasoning models, control thinking depth:
|
|
|
180
185
|
|
|
181
186
|
Values: `none`, `low`, `medium`, `high`
|
|
182
187
|
|
|
188
|
+
Full-history reasoning preservation is **on by default** for Kimi K2.6/K2.7 and the GLM-5.2 family (see [Compat Settings](#compat-settings)). Override it per-model via [Model Overrides](#model-overrides).
|
|
189
|
+
|
|
183
190
|
## Display Configuration
|
|
184
191
|
|
|
185
192
|
Energy and quota are independently configurable. Create `~/.pi/agent/extensions/neuralwatt.json`:
|
|
@@ -213,6 +220,29 @@ The file is auto-populated with defaults on first run.
|
|
|
213
220
|
}
|
|
214
221
|
```
|
|
215
222
|
|
|
223
|
+
### Model Overrides
|
|
224
|
+
|
|
225
|
+
`modelOverrides` lets you override compat flags and other model properties per model id, **on top of** `patch.json` + `custom-models.json`, without editing the extension. Keyed by model id; `compat` and `thinkingLevelMap` are deep-merged (toggle one flag without redeclaring the rest), scalars are replaced. Applied at session start, so edits take effect on the next `pi` session.
|
|
226
|
+
|
|
227
|
+
```jsonc
|
|
228
|
+
{
|
|
229
|
+
"energy": "widget",
|
|
230
|
+
"quota": "widget",
|
|
231
|
+
"mcr": "widget",
|
|
232
|
+
"hideOnOtherProvider": false,
|
|
233
|
+
"modelOverrides": {
|
|
234
|
+
// Disable full-history reasoning for kimi-k2.6 (e.g. to save tokens):
|
|
235
|
+
"kimi-k2.6": { "compat": { "chatTemplateKwargs": { "preserve_thinking": false } } },
|
|
236
|
+
// Override a single thinking level without redeclaring the map:
|
|
237
|
+
"glm-5.2": { "thinkingLevelMap": { "high": "max" }, "compat": { "chatTemplateKwargs": { "clear_thinking": true } } },
|
|
238
|
+
// Force a smaller image cap:
|
|
239
|
+
"kimi-k2.7-code": { "vision": { "maxImagesPerRequest": 4 } }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The full set of overridable fields matches the model schema (`compat`, `thinkingLevelMap`, `vision`, `cost`, `contextWindow`, `maxTokens`, `reasoning`, `input`). See [Compat Settings](#compat-settings) for the catalog of compat flags and what `chatTemplateKwargs` values mean per family.
|
|
245
|
+
|
|
216
246
|
## Energy Reporting
|
|
217
247
|
|
|
218
248
|
Neuralwatt provides real-time energy consumption data with every API response. This extension captures it and displays a running total in a dedicated status widget between the editor and the pi footer:
|
package/index.ts
CHANGED
|
@@ -81,6 +81,17 @@ interface NeuralwattConfig {
|
|
|
81
81
|
// When true, hide energy/quota/MCR display if the active model's provider
|
|
82
82
|
// is not "neuralwatt". Prevents stale display after switching providers.
|
|
83
83
|
hideOnOtherProvider: boolean;
|
|
84
|
+
// Per-model overrides applied ON TOP of patch.json + custom-models.json, keyed
|
|
85
|
+
// by model id. Lets a user override compat flags (e.g. toggle
|
|
86
|
+
// chat_template_kwargs) without editing the extension. Deep-merges `compat`
|
|
87
|
+
// and `thinkingLevelMap`; replaces scalars. See README "Model Overrides".
|
|
88
|
+
modelOverrides?: Record<string, ModelOverride>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ModelOverride {
|
|
92
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
93
|
+
compat?: Record<string, any>;
|
|
94
|
+
vision?: { maxImagesPerRequest?: number };
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
const CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "extensions", "neuralwatt.json");
|
|
@@ -102,6 +113,7 @@ function loadConfig(): NeuralwattConfig {
|
|
|
102
113
|
quota: parseDisplayMode(raw.quota, "widget"),
|
|
103
114
|
mcr: parseDisplayMode(raw.mcr, "widget"),
|
|
104
115
|
hideOnOtherProvider: typeof raw.hideOnOtherProvider === "boolean" ? raw.hideOnOtherProvider : false,
|
|
116
|
+
modelOverrides: parseModelOverrides(raw.modelOverrides),
|
|
105
117
|
};
|
|
106
118
|
} catch {
|
|
107
119
|
// Config file missing or invalid — populate with defaults so the user can discover it
|
|
@@ -115,6 +127,29 @@ function loadConfig(): NeuralwattConfig {
|
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
|
|
130
|
+
// Validate user-supplied modelOverrides from the config file. Non-object / non-string
|
|
131
|
+
// ids are dropped silently so a malformed file doesn't crash model registration.
|
|
132
|
+
function parseModelOverrides(raw: unknown): Record<string, ModelOverride> | undefined {
|
|
133
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
134
|
+
const result: Record<string, ModelOverride> = {};
|
|
135
|
+
for (const [id, override] of Object.entries(raw as Record<string, unknown>)) {
|
|
136
|
+
if (typeof id !== "string" || !override || typeof override !== "object" || Array.isArray(override)) continue;
|
|
137
|
+
const o = override as Record<string, unknown>;
|
|
138
|
+
const parsed: ModelOverride = {};
|
|
139
|
+
if (o.thinkingLevelMap && typeof o.thinkingLevelMap === "object") {
|
|
140
|
+
const m: Record<string, string | null> = {};
|
|
141
|
+
for (const [k, v] of Object.entries(o.thinkingLevelMap as Record<string, unknown>)) {
|
|
142
|
+
if (v === null || typeof v === "string") m[k] = v;
|
|
143
|
+
}
|
|
144
|
+
if (Object.keys(m).length > 0) parsed.thinkingLevelMap = m;
|
|
145
|
+
}
|
|
146
|
+
if (o.compat && typeof o.compat === "object") parsed.compat = o.compat as Record<string, any>;
|
|
147
|
+
if (o.vision && typeof o.vision === "object") parsed.vision = o.vision as { maxImagesPerRequest?: number };
|
|
148
|
+
if (Object.keys(parsed).length > 0) result[id] = parsed;
|
|
149
|
+
}
|
|
150
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
118
153
|
let config = loadConfig();
|
|
119
154
|
|
|
120
155
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -147,6 +182,17 @@ interface NeuralwattModel {
|
|
|
147
182
|
supportsReasoningEffort?: boolean;
|
|
148
183
|
requiresAssistantAfterToolResult?: boolean;
|
|
149
184
|
requiresReasoningContentOnAssistantMessages?: boolean;
|
|
185
|
+
// Raw chat_template_kwargs merged into every chat-completions request via
|
|
186
|
+
// pi-ai's onPayload hook. Mirrors vLLM's request field of the same name; the
|
|
187
|
+
// values are template-level flags the model's Jinja chat template reads, so
|
|
188
|
+
// they're model-family-specific (NOT a generic boolean). Injected alongside
|
|
189
|
+
// reasoning_effort (not via thinkingFormat: "chat-template" — that branch is
|
|
190
|
+
// mutually exclusive with the openai reasoning_effort path). Behavioral E2E:
|
|
191
|
+
// Kimi K2.6/K2.7 → { "preserve_thinking": true } (template keeps full reasoning history across turns)
|
|
192
|
+
// GLM-5.x family → { "clear_thinking": false } (template stops clearing older assistant reasoning)
|
|
193
|
+
// GLM-5.1 / Qwen3.x expose no family-wide flag; Layer-A replay (the `reasoning`
|
|
194
|
+
// field, aliased reasoning <-> reasoning_content by the gateway) still applies to all.
|
|
195
|
+
chatTemplateKwargs?: Record<string, string | number | boolean | null>;
|
|
150
196
|
};
|
|
151
197
|
vision?: {
|
|
152
198
|
maxImagesPerRequest?: number;
|
|
@@ -177,11 +223,30 @@ function applyPatch(model: NeuralwattModel, patch: Record<string, any>): Neuralw
|
|
|
177
223
|
return result;
|
|
178
224
|
}
|
|
179
225
|
|
|
180
|
-
|
|
226
|
+
// Apply a user-supplied modelOverride (from neuralwatt.json) on top of a built
|
|
227
|
+
// model. Same deep-merge semantics as applyPatch for compat/vision/cost, plus
|
|
228
|
+
// thinkingLevelMap (so a user can override a single thinking level without
|
|
229
|
+
// redeclaring the whole map). Scalars are replaced. No reasoning-cleanup
|
|
230
|
+
// (unlike applyPatch) — the user's override is authoritative.
|
|
231
|
+
function applyModelOverride(model: NeuralwattModel, override: ModelOverride): NeuralwattModel {
|
|
232
|
+
const result = { ...model };
|
|
233
|
+
const NESTED_KEYS = new Set(["compat", "vision", "cost", "thinkingLevelMap"]);
|
|
234
|
+
for (const [key, value] of Object.entries(override)) {
|
|
235
|
+
if (NESTED_KEYS.has(key) && typeof value === "object" && value !== null && typeof (result as any)[key] === "object") {
|
|
236
|
+
(result as any)[key] = { ...(result as any)[key], ...value };
|
|
237
|
+
} else {
|
|
238
|
+
(result as any)[key] = value;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Full pipeline: base → patch → custom → user modelOverrides → result */
|
|
181
245
|
function buildModels(
|
|
182
246
|
base: NeuralwattModel[],
|
|
183
247
|
custom: NeuralwattModel[],
|
|
184
248
|
patchList: Record<string, any>,
|
|
249
|
+
overrides: Record<string, ModelOverride> = {},
|
|
185
250
|
): NeuralwattModel[] {
|
|
186
251
|
const modelMap = new Map<string, NeuralwattModel>();
|
|
187
252
|
|
|
@@ -210,6 +275,17 @@ function buildModels(
|
|
|
210
275
|
}
|
|
211
276
|
}
|
|
212
277
|
|
|
278
|
+
// User-supplied modelOverrides (from ~/.pi/agent/extensions/neuralwatt.json)
|
|
279
|
+
// applied LAST so they win over patch.json + custom-models.json. Deep-merges
|
|
280
|
+
// compat / thinkingLevelMap / vision so a user can toggle a single flag
|
|
281
|
+
// (e.g. chatTemplateKwargs.preserve_thinking) without redeclaring the rest.
|
|
282
|
+
for (const [id, override] of Object.entries(overrides)) {
|
|
283
|
+
const existing = modelMap.get(id);
|
|
284
|
+
if (existing) {
|
|
285
|
+
modelMap.set(id, applyModelOverride(existing, override));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
213
289
|
return Array.from(modelMap.values()).map((model) => {
|
|
214
290
|
const result: any = {
|
|
215
291
|
id: model.id,
|
|
@@ -1190,6 +1266,39 @@ export function streamNeuralwatt(
|
|
|
1190
1266
|
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
|
1191
1267
|
const { reasoning: _reasoning, ...streamOptions } = options ?? {};
|
|
1192
1268
|
|
|
1269
|
+
// Preserved thinking (full-history reasoning): when a model opts in via
|
|
1270
|
+
// `compat.chatTemplateKwargs`, inject those chat_template_kwargs into the
|
|
1271
|
+
// request body through pi-ai's onPayload hook. We do this here rather than
|
|
1272
|
+
// via thinkingFormat: "chat-template" because the chat-template branch is
|
|
1273
|
+
// mutually exclusive with the openai `reasoning_effort` path — using
|
|
1274
|
+
// onPayload keeps reasoning_effort (thinking-level control) AND adds the
|
|
1275
|
+
// preserve kwargs. Any caller-supplied onPayload is chained first so it can
|
|
1276
|
+
// inspect/replace the payload; our injection then merges into whatever
|
|
1277
|
+
// chat_template_kwargs the caller (or pi-ai) already set.
|
|
1278
|
+
const userOnPayload = streamOptions.onPayload;
|
|
1279
|
+
const extraKwargs = neuralwattModel.compat?.chatTemplateKwargs;
|
|
1280
|
+
const hasExtraKwargs =
|
|
1281
|
+
!!extraKwargs && typeof extraKwargs === "object" && Object.keys(extraKwargs).length > 0;
|
|
1282
|
+
const onPayload = hasExtraKwargs || userOnPayload
|
|
1283
|
+
? async (params: any, mdl: any) => {
|
|
1284
|
+
let p = params;
|
|
1285
|
+
if (userOnPayload) {
|
|
1286
|
+
const next = await userOnPayload(p, mdl);
|
|
1287
|
+
if (next !== undefined) p = next;
|
|
1288
|
+
}
|
|
1289
|
+
if (hasExtraKwargs) {
|
|
1290
|
+
p = {
|
|
1291
|
+
...p,
|
|
1292
|
+
chat_template_kwargs: {
|
|
1293
|
+
...(p?.chat_template_kwargs ?? {}),
|
|
1294
|
+
...extraKwargs,
|
|
1295
|
+
},
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
return p;
|
|
1299
|
+
}
|
|
1300
|
+
: undefined;
|
|
1301
|
+
|
|
1193
1302
|
const originalFetch = globalThis.fetch;
|
|
1194
1303
|
|
|
1195
1304
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
@@ -1208,6 +1317,7 @@ export function streamNeuralwatt(
|
|
|
1208
1317
|
...streamOptions,
|
|
1209
1318
|
reasoningEffort,
|
|
1210
1319
|
apiKey,
|
|
1320
|
+
...(onPayload ? { onPayload } : {}),
|
|
1211
1321
|
});
|
|
1212
1322
|
|
|
1213
1323
|
const originalEnd = stream.end.bind(stream);
|
|
@@ -1237,7 +1347,7 @@ export function getStaleModels(): NeuralwattModel[] {
|
|
|
1237
1347
|
const custom = customModelsData as NeuralwattModel[];
|
|
1238
1348
|
const patches = patchesData as Record<string, any>;
|
|
1239
1349
|
const staleBase = loadStaleModels(embedded);
|
|
1240
|
-
_staleModelsCache = buildModels(staleBase, custom, patches);
|
|
1350
|
+
_staleModelsCache = buildModels(staleBase, custom, patches, config.modelOverrides);
|
|
1241
1351
|
}
|
|
1242
1352
|
return _staleModelsCache;
|
|
1243
1353
|
}
|
|
@@ -1273,6 +1383,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1273
1383
|
config = loadConfig();
|
|
1274
1384
|
resetSessionState();
|
|
1275
1385
|
cachedQuota = null;
|
|
1386
|
+
// Bust the stale-models cache so a user-edited neuralwatt.json (e.g. toggled
|
|
1387
|
+
// modelOverrides) takes effect this session instead of serving the
|
|
1388
|
+
// module-load snapshot until the background revalidate swaps it in.
|
|
1389
|
+
_staleModelsCache = null;
|
|
1276
1390
|
replayEnergyEvents(ctx);
|
|
1277
1391
|
// Re-register on session_start to guarantee our provider identity
|
|
1278
1392
|
// (api, streamSimple, headers) wins over any load-time registration
|
|
@@ -1294,7 +1408,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1294
1408
|
}
|
|
1295
1409
|
revalidateModels(cachedApiKey, embeddedModels, signal).then((freshBase) => {
|
|
1296
1410
|
if (freshBase && !signal.aborted) {
|
|
1297
|
-
pi.registerProvider("neuralwatt", makeProviderConfig(buildModels(freshBase, customModels, patches)));
|
|
1411
|
+
pi.registerProvider("neuralwatt", makeProviderConfig(buildModels(freshBase, customModels, patches, config.modelOverrides)));
|
|
1298
1412
|
}
|
|
1299
1413
|
});
|
|
1300
1414
|
});
|
package/models.json
CHANGED
|
@@ -139,52 +139,6 @@
|
|
|
139
139
|
"supportsReasoningEffort": true
|
|
140
140
|
}
|
|
141
141
|
},
|
|
142
|
-
{
|
|
143
|
-
"id": "moonshotai/Kimi-K2.5",
|
|
144
|
-
"name": "Kimi K2.5",
|
|
145
|
-
"reasoning": true,
|
|
146
|
-
"input": [
|
|
147
|
-
"text",
|
|
148
|
-
"image"
|
|
149
|
-
],
|
|
150
|
-
"cost": {
|
|
151
|
-
"input": 0.52,
|
|
152
|
-
"output": 2.59,
|
|
153
|
-
"cacheRead": 0.13,
|
|
154
|
-
"cacheWrite": 0
|
|
155
|
-
},
|
|
156
|
-
"contextWindow": 262128,
|
|
157
|
-
"maxTokens": 262128,
|
|
158
|
-
"compat": {
|
|
159
|
-
"supportsDeveloperRole": false
|
|
160
|
-
},
|
|
161
|
-
"vision": {
|
|
162
|
-
"maxImagesPerRequest": 20
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
"id": "kimi-k2.5-fast",
|
|
167
|
-
"name": "Kimi K2.5 Fast",
|
|
168
|
-
"reasoning": false,
|
|
169
|
-
"input": [
|
|
170
|
-
"text",
|
|
171
|
-
"image"
|
|
172
|
-
],
|
|
173
|
-
"cost": {
|
|
174
|
-
"input": 0.52,
|
|
175
|
-
"output": 2.59,
|
|
176
|
-
"cacheRead": 0.13,
|
|
177
|
-
"cacheWrite": 0
|
|
178
|
-
},
|
|
179
|
-
"contextWindow": 262128,
|
|
180
|
-
"maxTokens": 262128,
|
|
181
|
-
"compat": {
|
|
182
|
-
"supportsDeveloperRole": false
|
|
183
|
-
},
|
|
184
|
-
"vision": {
|
|
185
|
-
"maxImagesPerRequest": 20
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
142
|
{
|
|
189
143
|
"id": "kimi-k2.6",
|
|
190
144
|
"name": "Kimi K2.6",
|
package/npm-shrinkwrap.json
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-neuralwatt-provider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Neuralwatt provider extension for pi - Access Kimi, GLM, Qwen, GPT-OSS, Devstral, and MiniMax models through the Neuralwatt API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "echo 'nothing to clean'",
|
|
9
|
+
"build": "echo 'nothing to build'",
|
|
10
|
+
"check": "echo 'nothing to check'",
|
|
11
|
+
"lint:dead": "knip --no-gitignore",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"probe:image-limit": "node scripts/test-image-limit.mjs",
|
|
15
|
+
"probe:image-limit-stream": "node scripts/test-image-limit-stream.mjs",
|
|
16
|
+
"probe:vision-models": "node scripts/probe-vision-models.mjs",
|
|
17
|
+
"update-models": "node scripts/update-models.js",
|
|
18
|
+
"sync-mcr": "curl -sL https://raw.githubusercontent.com/neuralwatt/neuralwatt-tools/refs/heads/main/plugins/pi/extensions/neuralwatt-mcr.ts -o chad-mcr-upstream.ts"
|
|
19
|
+
},
|
|
7
20
|
"keywords": [
|
|
8
21
|
"pi",
|
|
9
22
|
"extension",
|
|
@@ -33,17 +46,5 @@
|
|
|
33
46
|
"./neuralwatt-mcr.ts"
|
|
34
47
|
]
|
|
35
48
|
},
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"build": "echo 'nothing to build'",
|
|
39
|
-
"check": "echo 'nothing to check'",
|
|
40
|
-
"lint:dead": "knip --no-gitignore",
|
|
41
|
-
"test": "vitest run",
|
|
42
|
-
"test:watch": "vitest",
|
|
43
|
-
"probe:image-limit": "node scripts/test-image-limit.mjs",
|
|
44
|
-
"probe:image-limit-stream": "node scripts/test-image-limit-stream.mjs",
|
|
45
|
-
"probe:vision-models": "node scripts/probe-vision-models.mjs",
|
|
46
|
-
"update-models": "node scripts/update-models.js",
|
|
47
|
-
"sync-mcr": "curl -sL https://raw.githubusercontent.com/neuralwatt/neuralwatt-tools/refs/heads/main/plugins/pi/extensions/neuralwatt-mcr.ts -o chad-mcr-upstream.ts"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
49
|
+
"packageManager": "pnpm@11.6.0"
|
|
50
|
+
}
|
package/patch.json
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
"medium": null,
|
|
8
8
|
"high": "high",
|
|
9
9
|
"xhigh": "max"
|
|
10
|
+
},
|
|
11
|
+
"compat": {
|
|
12
|
+
"chatTemplateKwargs": {
|
|
13
|
+
"clear_thinking": false
|
|
14
|
+
}
|
|
10
15
|
}
|
|
11
16
|
},
|
|
12
17
|
"glm-5.2-fast": {
|
|
@@ -22,6 +27,11 @@
|
|
|
22
27
|
"medium": null,
|
|
23
28
|
"high": "high",
|
|
24
29
|
"xhigh": "max"
|
|
30
|
+
},
|
|
31
|
+
"compat": {
|
|
32
|
+
"chatTemplateKwargs": {
|
|
33
|
+
"clear_thinking": false
|
|
34
|
+
}
|
|
25
35
|
}
|
|
26
36
|
},
|
|
27
37
|
"glm-5.2-short": {
|
|
@@ -32,24 +42,18 @@
|
|
|
32
42
|
"medium": null,
|
|
33
43
|
"high": "high",
|
|
34
44
|
"xhigh": "max"
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
"moonshotai/Kimi-K2.5": {
|
|
38
|
-
"thinkingLevelMap": {
|
|
39
|
-
"minimal": null,
|
|
40
|
-
"low": "low",
|
|
41
|
-
"medium": "medium",
|
|
42
|
-
"high": "high",
|
|
43
|
-
"xhigh": null
|
|
44
45
|
},
|
|
45
46
|
"compat": {
|
|
46
|
-
"
|
|
47
|
-
|
|
47
|
+
"chatTemplateKwargs": {
|
|
48
|
+
"clear_thinking": false
|
|
49
|
+
}
|
|
48
50
|
}
|
|
49
51
|
},
|
|
50
|
-
"
|
|
52
|
+
"glm-5.2-short-flex": {
|
|
51
53
|
"compat": {
|
|
52
|
-
"
|
|
54
|
+
"chatTemplateKwargs": {
|
|
55
|
+
"clear_thinking": false
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
},
|
|
55
59
|
"kimi-k2.6": {
|
|
@@ -62,7 +66,10 @@
|
|
|
62
66
|
},
|
|
63
67
|
"compat": {
|
|
64
68
|
"maxTokensField": "max_tokens",
|
|
65
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
69
|
+
"requiresReasoningContentOnAssistantMessages": true,
|
|
70
|
+
"chatTemplateKwargs": {
|
|
71
|
+
"preserve_thinking": true
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
},
|
|
68
75
|
"kimi-k2.6-fast": {
|
|
@@ -70,6 +77,14 @@
|
|
|
70
77
|
"maxTokensField": "max_tokens"
|
|
71
78
|
}
|
|
72
79
|
},
|
|
80
|
+
"kimi-k2.6-flex": {
|
|
81
|
+
"compat": {
|
|
82
|
+
"requiresReasoningContentOnAssistantMessages": true,
|
|
83
|
+
"chatTemplateKwargs": {
|
|
84
|
+
"preserve_thinking": true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
73
88
|
"kimi-k2.7-code": {
|
|
74
89
|
"thinkingLevelMap": {
|
|
75
90
|
"minimal": null,
|
|
@@ -80,7 +95,26 @@
|
|
|
80
95
|
},
|
|
81
96
|
"compat": {
|
|
82
97
|
"maxTokensField": "max_tokens",
|
|
83
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
98
|
+
"requiresReasoningContentOnAssistantMessages": true,
|
|
99
|
+
"chatTemplateKwargs": {
|
|
100
|
+
"preserve_thinking": true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"kimi-k2.7-code-flex": {
|
|
105
|
+
"compat": {
|
|
106
|
+
"requiresReasoningContentOnAssistantMessages": true,
|
|
107
|
+
"chatTemplateKwargs": {
|
|
108
|
+
"preserve_thinking": true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"neuralwatt/kimi-k2.6-long": {
|
|
113
|
+
"compat": {
|
|
114
|
+
"requiresReasoningContentOnAssistantMessages": true,
|
|
115
|
+
"chatTemplateKwargs": {
|
|
116
|
+
"preserve_thinking": true
|
|
117
|
+
}
|
|
84
118
|
}
|
|
85
119
|
},
|
|
86
120
|
"qwen3.5-397b-fast": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/tests/__mocks__/pi-ai.ts
CHANGED
|
@@ -129,3 +129,205 @@ describe("GLM-5.2 family patch.json thinkingLevelMap", () => {
|
|
|
129
129
|
expect(patches["glm-5.2"]?.thinkingLevelMap?.off).toBe("minimal");
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
describe("chatTemplateKwargs onPayload injection", () => {
|
|
134
|
+
let originalFetch: typeof globalThis.fetch;
|
|
135
|
+
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
originalFetch = globalThis.fetch;
|
|
138
|
+
__resetStreamCalls();
|
|
139
|
+
__setClamp((_m, level) => level);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
globalThis.fetch = originalFetch;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Kimi K2.6 model shaped as buildModels() produces it after patch.json:
|
|
147
|
+
// chatTemplateKwargs: { preserve_thinking: true } is set on the compat block.
|
|
148
|
+
const kimi26 = {
|
|
149
|
+
id: "kimi-k2.6",
|
|
150
|
+
provider: "neuralwatt",
|
|
151
|
+
reasoning: true,
|
|
152
|
+
input: ["text", "image"],
|
|
153
|
+
thinkingLevelMap: { minimal: null, low: "low", medium: "medium", high: "high", xhigh: null },
|
|
154
|
+
compat: {
|
|
155
|
+
supportsDeveloperRole: false,
|
|
156
|
+
maxTokensField: "max_tokens",
|
|
157
|
+
requiresReasoningContentOnAssistantMessages: true,
|
|
158
|
+
chatTemplateKwargs: { preserve_thinking: true },
|
|
159
|
+
},
|
|
160
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
161
|
+
contextWindow: 262144,
|
|
162
|
+
maxTokens: 262144,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const ctx = { messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }] };
|
|
166
|
+
|
|
167
|
+
it("registers an onPayload hook when model.compat.chatTemplateKwargs is set", () => {
|
|
168
|
+
const stream = streamNeuralwatt(kimi26, ctx, { apiKey: "sk-test", reasoning: "high" } as any);
|
|
169
|
+
stream.end();
|
|
170
|
+
|
|
171
|
+
expect(__streamCalls).toHaveLength(1);
|
|
172
|
+
expect(typeof __streamCalls[0].options.onPayload).toBe("function");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("injects chat_template_kwargs.preserve_thinking = true into the payload", async () => {
|
|
176
|
+
const stream = streamNeuralwatt(kimi26, ctx, { apiKey: "sk-test", reasoning: "high" } as any);
|
|
177
|
+
stream.end();
|
|
178
|
+
|
|
179
|
+
const onPayload = __streamCalls[0].options.onPayload;
|
|
180
|
+
const original = { model: "kimi-k2.6", messages: [], reasoning_effort: "high" };
|
|
181
|
+
const result = await onPayload(original, kimi26);
|
|
182
|
+
|
|
183
|
+
expect(result).toEqual({
|
|
184
|
+
...original,
|
|
185
|
+
chat_template_kwargs: { preserve_thinking: true },
|
|
186
|
+
});
|
|
187
|
+
// reasoning_effort is preserved (onPayload doesn't displace it)
|
|
188
|
+
expect(result.reasoning_effort).toBe("high");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("injects chat_template_kwargs.clear_thinking = false for GLM-5.2 family", async () => {
|
|
192
|
+
// GLM-5.x reasoning variants opt into full-history via clear_thinking: false
|
|
193
|
+
// (behavioral E2E: 1/4 → 4/4 recall). The kwarg is template-level and family-
|
|
194
|
+
// specific; the onPayload path injects whatever chatTemplateKwargs lists.
|
|
195
|
+
const glm52Model = {
|
|
196
|
+
...glm52,
|
|
197
|
+
id: "glm-5.2",
|
|
198
|
+
compat: { supportsDeveloperRole: false, chatTemplateKwargs: { clear_thinking: false } },
|
|
199
|
+
};
|
|
200
|
+
const stream = streamNeuralwatt(glm52Model, ctx, { apiKey: "sk-test", reasoning: "high" } as any);
|
|
201
|
+
stream.end();
|
|
202
|
+
|
|
203
|
+
const onPayload = __streamCalls[0].options.onPayload;
|
|
204
|
+
const result = await onPayload({ model: "glm-5.2", reasoning_effort: "high" }, glm52Model);
|
|
205
|
+
expect(result.chat_template_kwargs).toEqual({ clear_thinking: false });
|
|
206
|
+
expect(result.reasoning_effort).toBe("high");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("merges into pre-existing chat_template_kwargs instead of clobbering", async () => {
|
|
210
|
+
const stream = streamNeuralwatt(kimi26, ctx, { apiKey: "sk-test", reasoning: "high" } as any);
|
|
211
|
+
stream.end();
|
|
212
|
+
|
|
213
|
+
const onPayload = __streamCalls[0].options.onPayload;
|
|
214
|
+
const original = { model: "kimi-k2.6", chat_template_kwargs: { enable_thinking: true } };
|
|
215
|
+
const result = await onPayload(original, kimi26);
|
|
216
|
+
|
|
217
|
+
expect(result.chat_template_kwargs).toEqual({ enable_thinking: true, preserve_thinking: true });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("does NOT register onPayload when chatTemplateKwargs is absent (e.g. Qwen / non-reasoning)", () => {
|
|
221
|
+
// Qwen3.x and Kimi -fast (non-reasoning) have no full-history kwarg (not
|
|
222
|
+
// exposed by their chat template / nothing to preserve). They rely on the
|
|
223
|
+
// intrinsic Layer-A replay (pi-ai replays the `reasoning` field; the gateway
|
|
224
|
+
// aliases reasoning <-> reasoning_content). glm52 here has no chatTemplateKwargs,
|
|
225
|
+
// standing in for such a model.
|
|
226
|
+
const stream = streamNeuralwatt(glm52, ctx, { apiKey: "sk-test", reasoning: "high" } as any);
|
|
227
|
+
stream.end();
|
|
228
|
+
|
|
229
|
+
expect(__streamCalls[0].options.onPayload).toBeUndefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("chains a caller-supplied onPayload first, then injects preserve_thinking", async () => {
|
|
233
|
+
const userPayload = vi.fn((p: any) => ({ ...p, reason: "user-saw-it", model: p.model }));
|
|
234
|
+
const stream = streamNeuralwatt(kimi26, ctx, {
|
|
235
|
+
apiKey: "sk-test",
|
|
236
|
+
reasoning: "high",
|
|
237
|
+
onPayload: userPayload,
|
|
238
|
+
} as any);
|
|
239
|
+
stream.end();
|
|
240
|
+
|
|
241
|
+
const onPayload = __streamCalls[0].options.onPayload;
|
|
242
|
+
const result = await onPayload({ model: "kimi-k2.6" }, kimi26);
|
|
243
|
+
|
|
244
|
+
expect(userPayload).toHaveBeenCalledTimes(1);
|
|
245
|
+
expect(result.reason).toBe("user-saw-it");
|
|
246
|
+
expect(result.chat_template_kwargs).toEqual({ preserve_thinking: true });
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("patch.json chatTemplateKwargs enablement (behavioral E2E-verified)", () => {
|
|
251
|
+
const patches = patchesData as Record<string, any>;
|
|
252
|
+
|
|
253
|
+
// Kimi K2.6/K2.7 reasoning variants: preserve_thinking: true
|
|
254
|
+
// (doc-backed; behavioral E2E: 0/6 → 6/6 recall)
|
|
255
|
+
const kimi = ["kimi-k2.6", "kimi-k2.6-flex", "neuralwatt/kimi-k2.6-long", "kimi-k2.7-code", "kimi-k2.7-code-flex"];
|
|
256
|
+
for (const id of kimi) {
|
|
257
|
+
it(`${id} opts into full-history via preserve_thinking: true`, () => {
|
|
258
|
+
expect(patches[id]?.compat?.chatTemplateKwargs).toEqual({ preserve_thinking: true });
|
|
259
|
+
// Layer-A empty-scaffold stays on alongside it
|
|
260
|
+
expect(patches[id]?.compat?.requiresReasoningContentOnAssistantMessages).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// GLM-5.2 reasoning variants: clear_thinking: false
|
|
265
|
+
// (NOT in the docs' full-history table, which lists clear_thinking only for
|
|
266
|
+
// GLM-5.1; but behavioral E2E proved it functional on GLM-5.2: 1/4 → 4/4 recall,
|
|
267
|
+
// confirmed family-wide on base/short/flex). Non-reasoning -fast variants excluded.
|
|
268
|
+
const glm = ["glm-5.2", "glm-5.2-flex", "glm-5.2-short", "glm-5.2-short-flex"];
|
|
269
|
+
for (const id of glm) {
|
|
270
|
+
it(`${id} opts into full-history via clear_thinking: false`, () => {
|
|
271
|
+
expect(patches[id]?.compat?.chatTemplateKwargs).toEqual({ clear_thinking: false });
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Models with no full-history kwarg: non-reasoning -fast variants (nothing to
|
|
276
|
+
// preserve) and Qwen (chat template exposes no flag).
|
|
277
|
+
const none = ["kimi-k2.6-fast", "glm-5.2-fast", "qwen3.5-397b-fast", "qwen3.6-35b-fast"];
|
|
278
|
+
for (const id of none) {
|
|
279
|
+
it(`${id} sets NO chatTemplateKwargs (non-reasoning / not exposed)`, () => {
|
|
280
|
+
expect(patches[id]?.compat?.chatTemplateKwargs).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("modelOverrides (user config) applied on top of patch", () => {
|
|
286
|
+
// Mirrors buildModels(base, custom, patch, overrides) + applyModelOverride.
|
|
287
|
+
// Defined inline so the test stays self-contained without importing the
|
|
288
|
+
// (private) buildModels; it validates the override semantics the feature
|
|
289
|
+
// promises: deep-merge compat, replace scalars, win over patch.json.
|
|
290
|
+
function applyModelOverride(model: any, override: any): any {
|
|
291
|
+
const result = { ...model };
|
|
292
|
+
const NESTED = new Set(["compat", "vision", "cost", "thinkingLevelMap"]);
|
|
293
|
+
for (const [k, v] of Object.entries(override)) {
|
|
294
|
+
if (NESTED.has(k) && typeof v === "object" && v !== null && typeof result[k] === "object") {
|
|
295
|
+
result[k] = { ...result[k], ...v };
|
|
296
|
+
} else {
|
|
297
|
+
result[k] = v;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const base = {
|
|
304
|
+
id: "kimi-k2.6",
|
|
305
|
+
provider: "neuralwatt",
|
|
306
|
+
reasoning: true,
|
|
307
|
+
compat: { supportsDeveloperRole: false, chatTemplateKwargs: { preserve_thinking: true } },
|
|
308
|
+
thinkingLevelMap: { low: "low", high: "high" },
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
it("override wins over patch.json for a compat flag it sets", () => {
|
|
312
|
+
// User disables preserve_thinking via override → must win over patch.json's true.
|
|
313
|
+
const out = applyModelOverride(base, { compat: { chatTemplateKwargs: { preserve_thinking: false } } });
|
|
314
|
+
expect(out.compat.chatTemplateKwargs.preserve_thinking).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("deep-merges compat so non-overridden flags survive", () => {
|
|
318
|
+
// User toggles only chatTemplateKwargs; supportsDeveloperRole must survive.
|
|
319
|
+
const out = applyModelOverride(base, { compat: { chatTemplateKwargs: {} } });
|
|
320
|
+
expect(out.compat.supportsDeveloperRole).toBe(false);
|
|
321
|
+
expect(out.compat.chatTemplateKwargs).toEqual({});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("deep-merges thinkingLevelMap so a single level can be overridden", () => {
|
|
325
|
+
const out = applyModelOverride(base, { thinkingLevelMap: { high: null } });
|
|
326
|
+
expect(out.thinkingLevelMap).toEqual({ low: "low", high: null });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("replace-semantics for scalar fields (e.g. reasoning)", () => {
|
|
330
|
+
const out = applyModelOverride(base, { reasoning: false });
|
|
331
|
+
expect(out.reasoning).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
});
|
package/vitest.config.ts
CHANGED
|
@@ -10,6 +10,10 @@ export default defineConfig({
|
|
|
10
10
|
},
|
|
11
11
|
resolve: {
|
|
12
12
|
alias: {
|
|
13
|
+
// index.ts imports streamOpenAICompletions from the `/compat` subpath;
|
|
14
|
+
// the exact-match `@earendil-works/pi-ai` alias below doesn't cover it,
|
|
15
|
+
// so without this entry every test that imports ../index fails to load.
|
|
16
|
+
"@earendil-works/pi-ai/compat": path.resolve(__dirname, "tests/__mocks__/pi-ai.ts"),
|
|
13
17
|
"@earendil-works/pi-ai": path.resolve(__dirname, "tests/__mocks__/pi-ai.ts"),
|
|
14
18
|
"@earendil-works/pi-coding-agent": path.resolve(__dirname, "tests/__mocks__/pi-coding-agent.ts"),
|
|
15
19
|
},
|