pi-makora-provider 1.7.0 → 1.8.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 CHANGED
@@ -21,13 +21,13 @@ _DeepSeek V4, Kimi K2.7 Code, GLM 5.2, Qwen 3.6 for [pi](https://github.com/eare
21
21
  | DeepSeek V4 Flash | `deepseek-ai/DeepSeek-V4-Flash` | Yes | returns `reasoning` field |
22
22
  | DeepSeek V4 Pro | `deepseek-ai/DeepSeek-V4-Pro` | Yes | returns `reasoning` field |
23
23
  | Gemma 4 26B A4B | `google/gemma-4-26B-A4B` | No | |
24
- | GLM 5.2 FP8 | `zai-org/GLM-5.2-FP8` | Yes | `enable_thinking` via `qwen-chat-template`; effort via `reasoning_effort` (only `high`/`max` distinct, per vLLM GLM-5.2 recipe); thinking levels aligned with neuralwatt GLM 5.2; returns `reasoning` field |
25
- | GLM 5.2 NVFP4 | `zai-org/GLM-5.2-NVFP4` | Yes | `enable_thinking` via `qwen-chat-template`; effort via `reasoning_effort` (only `high`/`max` distinct, per vLLM GLM-5.2 recipe); returns `reasoning` field |
26
- | Kimi K2.7 Code | `moonshotai/Kimi-K2.7-Code` | Yes | Reasoning on by default (thinking-only model); `preserve_thinking` via `chatTemplateKwargs` for multi-turn continuity; returns `reasoning` field |
24
+ | GLM 5.2 FP8 | `zai-org/GLM-5.2-FP8` | Yes | off via top-level `reasoning_effort: "none"` (only lever GLM responds to; `enable_thinking`/`thinking`/chat_template_kwargs toggles are ignored); effort `high`/`max` only (minimal/low/medium clamp to `high`); multi-turn continuity via `clear_thinking` (false when thinking on = preserve, true when off = clear; `preserve_thinking` is inert for GLM) injected via onPayload; returns `reasoning` field |
25
+ | GLM 5.2 NVFP4 | `zai-org/GLM-5.2-NVFP4` | Yes | off via top-level `reasoning_effort: "none"` (only lever GLM responds to; `enable_thinking`/`thinking`/chat_template_kwargs toggles are ignored); effort `high`/`max` only (minimal/low/medium clamp to `high`); multi-turn continuity via `clear_thinking` (false when thinking on = preserve, true when off = clear; `preserve_thinking` is inert for GLM) injected via onPayload; returns `reasoning` field |
26
+ | Kimi K2.7 Code | `moonshotai/Kimi-K2.7-Code` | Yes | on/off via namespaced `chat_template_kwargs.thinking` (toggled with thinking switch); `preserve_thinking` (toggled) for multi-turn continuity; onPayload aliases each assistant message's `reasoning` → `reasoning_content` (Kimi's template reads `reasoning_content` but pi-ai sends `reasoning`); returns `reasoning` field. `reasoning_effort` is not emitted (Kimi's template reads namespaced `thinking`, not `reasoning_effort`) |
27
27
  | Llama 3.3 70B FP8 | `amd/Llama-3.3-70B-Instruct-FP8-KV` | No | |
28
28
  | Llama 3.3 70B Instruct | `meta-llama/Llama-3.3-70B-Instruct` | No | |
29
- | Qwen 3.6 27B NVFP4 | `unsloth/Qwen3.6-27B-NVFP4` | Yes | `enable_thinking` via `qwen-chat-template`; `preserve_thinking` via `chatTemplateKwargs` for multi-turn continuity; returns `reasoning` field |
30
- | Qwen 3.6 35B A3B NVFP4 | `unsloth/Qwen3.6-35B-A3B-NVFP4` | Yes | `enable_thinking` via `qwen-chat-template`; `preserve_thinking` via `chatTemplateKwargs` for multi-turn continuity; returns `reasoning` field |
29
+ | Qwen 3.6 27B NVFP4 | `unsloth/Qwen3.6-27B-NVFP4` | Yes | off via top-level `reasoning_effort: "none"`; `preserve_thinking` (toggled with thinking switch) injected via onPayload for multi-turn continuity; returns `reasoning` field |
30
+ | Qwen 3.6 35B A3B NVFP4 | `unsloth/Qwen3.6-35B-A3B-NVFP4` | Yes | off via top-level `reasoning_effort: "none"`; `preserve_thinking` (toggled with thinking switch) injected via onPayload for multi-turn continuity; returns `reasoning` field |
31
31
  <!-- MODELS_TABLE_END -->
32
32
 
33
33
  ## Installation
package/index.ts CHANGED
@@ -47,6 +47,9 @@
47
47
  */
48
48
 
49
49
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
50
+ import type { SimpleStreamOptions, AssistantMessageEventStream } from "@earendil-works/pi-ai";
51
+ import { clampThinkingLevel } from "@earendil-works/pi-ai";
52
+ import { streamOpenAICompletions } from "@earendil-works/pi-ai/compat";
50
53
  import modelsData from "./models.json" with { type: "json" };
51
54
  import customModelsData from "./custom-models.json" with { type: "json" };
52
55
  import patchData from "./patch.json" with { type: "json" };
@@ -91,6 +94,13 @@ interface JsonModel {
91
94
  requiresToolResultName?: boolean;
92
95
  requiresAssistantAfterToolResult?: boolean;
93
96
  cacheControlFormat?: "anthropic";
97
+ /** When set, onPayload copies each assistant message's `reasoning` field into
98
+ * this field name before sending. Needed when a model's chat template reads
99
+ * prior reasoning from a different field than the one pi-ai sends it in.
100
+ * e.g. Kimi K2.7's template reads `reasoning_content`, but pi-ai sends
101
+ * `reasoning` (the field the model returns) — without the copy, preserve_thinking
102
+ * can't render the prior trace. */
103
+ assistantReasoningField?: string;
94
104
  /** Extra keys merged into vLLM `chat_template_kwargs` on every request.
95
105
  * Used for preserved-thinking flags like `preserve_thinking` / `clear_thinking`
96
106
  * that some chat templates require for multi-turn reasoning continuity. */
@@ -192,6 +202,177 @@ function buildModels(
192
202
  return Array.from(modelMap.values());
193
203
  }
194
204
 
205
+ // Thinking-off + preserved-thinking via streamSimple
206
+ //
207
+ // Makora's reasoning models need BOTH a thinking on/off switch AND multi-turn
208
+ // reasoning continuity (`preserve_thinking`). pi-ai's built-in `thinkingFormat`
209
+ // branches are mutually exclusive: the `chat-template` branch emits
210
+ // `chat_template_kwargs` (so it can carry `preserve_thinking`) but never a
211
+ // top-level `reasoning_effort`; the OpenAI fallback emits `reasoning_effort`
212
+ // (the lever GLM 5.2 and Qwen 3.6 actually respond to for off) but never
213
+ // `chat_template_kwargs`. No single format emits both.
214
+ //
215
+ // Behavioral E2E (see test-thinking-triggers.ts / test-preserve-deterministic.ts):
216
+ // GLM 5.2 — off ONLY via top-level `reasoning_effort: "none"`;
217
+ // `enable_thinking`/`thinking`/chat_template_kwargs toggles are ignored.
218
+ // Multi-turn continuity uses `clear_thinking: false` (NOT preserve_thinking
219
+ // — that flag is inert for GLM); toggled with the thinking switch
220
+ // (false when on = preserve, true when off = clear).
221
+ // Qwen 3.6 — off via top-level `reasoning_effort: "none"` (or `enable_thinking`).
222
+ // Multi-turn continuity uses `preserve_thinking: true`; reads the
223
+ // `reasoning` field pi-ai sends. Toggled with the thinking switch.
224
+ // Kimi K2.7 — off via namespaced `chat_template_kwargs.thinking: false`.
225
+ // Multi-turn continuity uses `preserve_thinking: true`, BUT the
226
+ // template reads `reasoning_content` while pi-ai sends `reasoning`
227
+ // (Makora doesn't gateway-alias the fields) — so onPayload also
228
+ // copies each assistant message's `reasoning` → `reasoning_content`.
229
+ // preserve_thinking IS functional (deterministic E2E: Qwen 3.6 27B recalls
230
+ // 1.00 with it on, 0.00 off) — it gates whether the prior reasoning trace
231
+ // is rendered into the next turn's prompt.
232
+ //
233
+ // So this provider registers a `streamSimple` wrapper that delegates to pi-ai's
234
+ // `streamOpenAICompletions` (keeping all its streaming/tool-calling/caching) and
235
+ // uses pi-ai's `onPayload` hook — which runs AFTER `buildParams` — to inject the
236
+ // `chat_template_kwargs` that the chosen `thinkingFormat` branch can't reach.
237
+ // `buildParams` still owns `reasoning_effort` (on → mapped effort; off → "none");
238
+ // `onPayload` owns `chat_template_kwargs` (preserve_thinking + Kimi's `thinking`).
239
+ // Per-model config lives in `compat.chatTemplateKwargs` (patch.json), using the
240
+ // same `{ "$var": "thinking.enabled" }` schema pi-ai's chat-template format uses.
241
+
242
+ /** Resolve one `chatTemplateKwargs` value (scalar or `{ $var }`) against the
243
+ * current thinking state. Mirrors pi-ai's `resolveChatTemplateKwargValue` so
244
+ * onPayload-injected values match what the built-in chat-template format would
245
+ * produce — but injected after buildParams so they coexist with reasoning_effort. */
246
+ export function resolveChatTemplateKwarg(
247
+ value: unknown,
248
+ model: JsonModel,
249
+ thinkingOn: boolean,
250
+ reasoningEffort: string | undefined,
251
+ ): unknown {
252
+ // Scalars pass through unchanged (e.g. static `preserve_thinking: true`).
253
+ if (value === null || typeof value !== "object") {
254
+ return value;
255
+ }
256
+ const spec = value as { $var?: string; omitWhenOff?: boolean; invert?: boolean };
257
+ if (spec.omitWhenOff && !thinkingOn) {
258
+ return undefined;
259
+ }
260
+ if (spec.$var === "thinking.enabled") {
261
+ return spec.invert ? !thinkingOn : thinkingOn;
262
+ }
263
+ if (spec.$var === "thinking.effort") {
264
+ const mapped = thinkingOn
265
+ ? model.thinkingLevelMap?.[reasoningEffort as string]
266
+ : model.thinkingLevelMap?.off;
267
+ if (mapped === undefined) return reasoningEffort;
268
+ return typeof mapped === "string" ? mapped : undefined;
269
+ }
270
+ return undefined;
271
+ }
272
+
273
+ /** Custom streamSimple: delegate to pi-ai's OpenAI completions streamer and
274
+ * inject per-model `chat_template_kwargs` (preserve_thinking / clear_thinking +
275
+ * Kimi's thinking toggle) via the onPayload hook, and alias each assistant
276
+ * message's reasoning field when a template reads a different field name.
277
+ * Models with neither chatTemplateKwargs nor assistantReasoningField (DeepSeek,
278
+ * Llama, Gemma) pass through unchanged — onPayload is not registered. */
279
+ export function streamMakora(
280
+ model: any,
281
+ context: any,
282
+ options?: SimpleStreamOptions,
283
+ ): AssistantMessageEventStream {
284
+ const apiKey = options?.apiKey || "";
285
+ if (!apiKey) {
286
+ throw new Error(
287
+ `No API key for Makora. Add it to ~/.pi/agent/auth.json under "makora", ` +
288
+ `set the MAKORA_OPTIMIZE_TOKEN env var, or use --api-key.`,
289
+ );
290
+ }
291
+
292
+ // pi-ai's streamer reads `model.api` to pick the OpenAI completions client. Our
293
+ // provider registers under `api: "makora"` (so pi routes to this streamSimple);
294
+ // override to `openai-completions` here so streamOpenAICompletions uses the
295
+ // standard client. Per-model baseUrl overrides (per-slug endpoints) are kept.
296
+ const makoraModel = { ...model, api: "openai-completions", baseUrl: model.baseUrl || BASE_URL };
297
+
298
+ // pi hands streamSimple providers the raw thinking selection as
299
+ // `options.reasoning` (a ThinkingLevel). The raw streamOpenAICompletions only
300
+ // reads `options.reasoningEffort`, so replicate the clamp+convert pi-ai's own
301
+ // streamSimple wrapper does — otherwise reasoning_effort never reaches the
302
+ // body and thinking levels silently do nothing. "off" → undefined (off).
303
+ const clampedReasoning = options?.reasoning
304
+ ? clampThinkingLevel(makoraModel, options.reasoning)
305
+ : undefined;
306
+ const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
307
+ const thinkingOn = reasoningEffort !== undefined;
308
+ const { reasoning: _reasoning, ...streamOptions } = options ?? {};
309
+
310
+ // Inject chat_template_kwargs after buildParams via onPayload. Any caller-
311
+ // supplied onPayload is chained first so it can inspect/replace the payload;
312
+ // our injection then merges into whatever chat_template_kwargs already exist.
313
+ // We also register onPayload when the model needs its prior-reasoning field
314
+ // aliased (assistantReasoningField) — e.g. Kimi's template reads
315
+ // `reasoning_content` but pi-ai sends `reasoning`; the copy makes the
316
+ // preserved trace visible to the template so preserve_thinking can render it.
317
+ const userOnPayload = (streamOptions as any).onPayload;
318
+ const extraKwargs = (makoraModel as JsonModel).compat?.chatTemplateKwargs;
319
+ const hasExtraKwargs =
320
+ !!extraKwargs && typeof extraKwargs === "object" && Object.keys(extraKwargs).length > 0;
321
+ const assistantReasoningField = (makoraModel as JsonModel).compat?.assistantReasoningField;
322
+ const needsFieldCopy = !!assistantReasoningField;
323
+ const onPayload =
324
+ hasExtraKwargs || needsFieldCopy || userOnPayload
325
+ ? async (params: any, mdl: any) => {
326
+ let p = params;
327
+ if (userOnPayload) {
328
+ const next = await userOnPayload(p, mdl);
329
+ if (next !== undefined) p = next;
330
+ }
331
+ if (needsFieldCopy && thinkingOn) {
332
+ // Only alias the prior reasoning field when thinking is ON — when the
333
+ // user turns thinking off we must NOT carry prior reasoning forward
334
+ // (preserve_thinking is inert for Kimi's field rendering; the field's
335
+ // presence is what gates continuity). Kimi's template ignores the
336
+ // `reasoning` field, so skipping the copy leaves nothing to render.
337
+ const field = assistantReasoningField!;
338
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
339
+ p = {
340
+ ...p,
341
+ messages: msgs.map((m: any) =>
342
+ m && m.role === "assistant" &&
343
+ typeof m.reasoning === "string" && m.reasoning.length > 0 &&
344
+ m[field] === undefined
345
+ ? { ...m, [field]: m.reasoning }
346
+ : m
347
+ ),
348
+ };
349
+ }
350
+ if (hasExtraKwargs) {
351
+ const resolved: Record<string, unknown> = {};
352
+ for (const [k, v] of Object.entries(extraKwargs!)) {
353
+ const r = resolveChatTemplateKwarg(v, makoraModel, thinkingOn, reasoningEffort as string | undefined);
354
+ if (r !== undefined) resolved[k] = r;
355
+ }
356
+ p = {
357
+ ...p,
358
+ chat_template_kwargs: {
359
+ ...(p?.chat_template_kwargs ?? {}),
360
+ ...resolved,
361
+ },
362
+ };
363
+ }
364
+ return p;
365
+ }
366
+ : undefined;
367
+
368
+ return streamOpenAICompletions(makoraModel, context, {
369
+ ...streamOptions,
370
+ reasoningEffort,
371
+ apiKey,
372
+ ...(onPayload ? { onPayload } : {}),
373
+ });
374
+ }
375
+
195
376
  // Extension Entry Point
196
377
 
197
378
  const PROVIDER_ID = "makora";
@@ -206,12 +387,16 @@ const allMakoraModels = buildModels(
206
387
  export default function (pi: ExtensionAPI) {
207
388
  const models = allMakoraModels;
208
389
 
209
- // apiKey resolution order: auth.json ("makora" key) → MAKORA_OPTIMIZE_TOKEN env var
390
+ // apiKey resolution order: auth.json ("makora" key) → MAKORA_OPTIMIZE_TOKEN env var.
391
+ // `api: "makora"` + `streamSimple` routes every Makora model through streamMakora
392
+ // (above), which delegates to pi-ai's OpenAI completions streamer and injects
393
+ // per-model chat_template_kwargs via onPayload.
210
394
  pi.registerProvider(PROVIDER_ID, {
211
395
  name: "Makora",
212
396
  baseUrl: BASE_URL,
213
397
  apiKey: "$MAKORA_OPTIMIZE_TOKEN",
214
- api: "openai-completions",
398
+ api: "makora",
399
+ streamSimple: streamMakora,
215
400
  models,
216
401
  });
217
402
 
package/models.json CHANGED
@@ -159,7 +159,7 @@
159
159
  "cacheRead": 0,
160
160
  "cacheWrite": 0
161
161
  },
162
- "contextWindow": 1048576,
162
+ "contextWindow": 980000,
163
163
  "maxTokens": 0,
164
164
  "compat": {
165
165
  "supportsDeveloperRole": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-makora-provider",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Makora provider extension for pi - Access DeepSeek V4, GLM 5.2, Kimi K2.7 Code, Llama 3.3, Qwen 3.6, and more through the Makora inference API",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/patch.json CHANGED
@@ -32,31 +32,31 @@
32
32
  },
33
33
  "unsloth/Qwen3.6-27B-NVFP4": {
34
34
  "reasoning": true,
35
- "notes": "`enable_thinking` via `qwen-chat-template`; `preserve_thinking` via `chatTemplateKwargs` for multi-turn continuity; returns `reasoning` field",
35
+ "notes": "off via top-level `reasoning_effort: \"none\"`; `preserve_thinking` (toggled with thinking switch) injected via onPayload for multi-turn continuity; returns `reasoning` field",
36
36
  "thinkingLevelMap": {
37
37
  "minimal": "low",
38
- "xhigh": "high"
38
+ "xhigh": "high",
39
+ "off": "none"
39
40
  },
40
41
  "compat": {
41
- "thinkingFormat": "qwen-chat-template",
42
42
  "supportsReasoningEffort": true,
43
43
  "chatTemplateKwargs": {
44
- "preserve_thinking": true
44
+ "preserve_thinking": { "$var": "thinking.enabled" }
45
45
  }
46
46
  }
47
47
  },
48
48
  "unsloth/Qwen3.6-35B-A3B-NVFP4": {
49
49
  "reasoning": true,
50
- "notes": "`enable_thinking` via `qwen-chat-template`; `preserve_thinking` via `chatTemplateKwargs` for multi-turn continuity; returns `reasoning` field",
50
+ "notes": "off via top-level `reasoning_effort: \"none\"`; `preserve_thinking` (toggled with thinking switch) injected via onPayload for multi-turn continuity; returns `reasoning` field",
51
51
  "thinkingLevelMap": {
52
52
  "minimal": "low",
53
- "xhigh": "high"
53
+ "xhigh": "high",
54
+ "off": "none"
54
55
  },
55
56
  "compat": {
56
- "thinkingFormat": "qwen-chat-template",
57
57
  "supportsReasoningEffort": true,
58
58
  "chatTemplateKwargs": {
59
- "preserve_thinking": true
59
+ "preserve_thinking": { "$var": "thinking.enabled" }
60
60
  }
61
61
  }
62
62
  },
@@ -66,47 +66,54 @@
66
66
  "text",
67
67
  "image"
68
68
  ],
69
- "notes": "Reasoning on by default (thinking-only model); `preserve_thinking` via `chatTemplateKwargs` for multi-turn continuity; returns `reasoning` field",
69
+ "notes": "on/off via namespaced `chat_template_kwargs.thinking` (toggled with thinking switch); `preserve_thinking` (toggled) for multi-turn continuity; onPayload aliases each assistant message's `reasoning` → `reasoning_content` (Kimi's template reads `reasoning_content` but pi-ai sends `reasoning`); returns `reasoning` field. `reasoning_effort` is not emitted (Kimi's template reads namespaced `thinking`, not `reasoning_effort`)",
70
70
  "thinkingLevelMap": {
71
71
  "minimal": "low",
72
72
  "xhigh": "high"
73
73
  },
74
74
  "compat": {
75
- "thinkingFormat": "qwen-chat-template",
76
- "supportsReasoningEffort": true,
75
+ "supportsReasoningEffort": false,
76
+ "assistantReasoningField": "reasoning_content",
77
77
  "chatTemplateKwargs": {
78
- "preserve_thinking": true
78
+ "thinking": { "$var": "thinking.enabled" },
79
+ "preserve_thinking": { "$var": "thinking.enabled" }
79
80
  }
80
81
  }
81
82
  },
82
83
  "zai-org/GLM-5.2-FP8": {
83
84
  "reasoning": true,
84
- "notes": "`enable_thinking` via `qwen-chat-template`; effort via `reasoning_effort` (only `high`/`max` distinct, per vLLM GLM-5.2 recipe); thinking levels aligned with neuralwatt GLM 5.2; returns `reasoning` field",
85
+ "notes": "off via top-level `reasoning_effort: \"none\"` (only lever GLM responds to; `enable_thinking`/`thinking`/chat_template_kwargs toggles are ignored); effort `high`/`max` only (minimal/low/medium clamp to `high`); multi-turn continuity via `clear_thinking` (false when thinking on = preserve, true when off = clear; `preserve_thinking` is inert for GLM) injected via onPayload; returns `reasoning` field",
85
86
  "thinkingLevelMap": {
86
87
  "minimal": null,
87
88
  "low": null,
88
89
  "medium": null,
89
90
  "high": "high",
90
- "xhigh": "max"
91
+ "xhigh": "max",
92
+ "off": "none"
91
93
  },
92
94
  "compat": {
93
- "thinkingFormat": "qwen-chat-template",
94
- "supportsReasoningEffort": true
95
+ "supportsReasoningEffort": true,
96
+ "chatTemplateKwargs": {
97
+ "clear_thinking": { "$var": "thinking.enabled", "invert": true }
98
+ }
95
99
  }
96
100
  },
97
101
  "zai-org/GLM-5.2-NVFP4": {
98
102
  "reasoning": true,
99
- "notes": "`enable_thinking` via `qwen-chat-template`; effort via `reasoning_effort` (only `high`/`max` distinct, per vLLM GLM-5.2 recipe); returns `reasoning` field",
103
+ "notes": "off via top-level `reasoning_effort: \"none\"` (only lever GLM responds to; `enable_thinking`/`thinking`/chat_template_kwargs toggles are ignored); effort `high`/`max` only (minimal/low/medium clamp to `high`); multi-turn continuity via `clear_thinking` (false when thinking on = preserve, true when off = clear; `preserve_thinking` is inert for GLM) injected via onPayload; returns `reasoning` field",
100
104
  "thinkingLevelMap": {
101
105
  "minimal": null,
102
106
  "low": null,
103
107
  "medium": null,
104
108
  "high": "high",
105
- "xhigh": "max"
109
+ "xhigh": "max",
110
+ "off": "none"
106
111
  },
107
112
  "compat": {
108
- "thinkingFormat": "qwen-chat-template",
109
- "supportsReasoningEffort": true
113
+ "supportsReasoningEffort": true,
114
+ "chatTemplateKwargs": {
115
+ "clear_thinking": { "$var": "thinking.enabled", "invert": true }
116
+ }
110
117
  }
111
118
  }
112
119
  }