pi-free 2.0.14 → 2.0.15

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/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.15] - 2026-06-02
11
+
12
+ ### Fixed
13
+
14
+ - **Qwen 3.7 reasoning compat** — `qwen/qwen3.7-max` on Cline/OpenRouter uses DeepSeek-style `reasoning_content` format. Added `DEEPSEEK_PROXY_COMPAT` so Pi preserves and replays reasoning tokens correctly, preventing plan-mode hangs ([#213](https://github.com/apmantza/pi-free/pull/213)).
15
+
16
+ - **Kimi K2.6 reasoning compat** — Kimi models on NVIDIA/OpenRouter need `requiresReasoningContentOnAssistantMessages: true` to correctly replay reasoning tokens in assistant messages. Without it, the model gets stuck when trying to call tools or produce output after thinking. Refs [earendil-works/pi#5309](https://github.com/earendil-works/pi/issues/5309) ([#213](https://github.com/apmantza/pi-free/pull/213)).
17
+
18
+ - **MiniMax reasoning compat** — MiniMax M3 and other MiniMax models now have full DeepSeek-style compat (`thinkingFormat: "deepseek"`, `requiresReasoningContentOnAssistantMessages: true`). Previously, models marked `reasoning: true` without `thinkingFormat` caused Pi to enter plan mode but couldn't parse the reasoning tokens, resulting in hangs ([#212](https://github.com/apmantza/pi-free/pull/212), [#213](https://github.com/apmantza/pi-free/pull/213)).
19
+
20
+ ### Added
21
+
22
+ - **`/probe-routeway` command** — Tests each Routeway model with a minimal chat request and auto-hides models that return 5xx or 404 errors. Runs lazily on first `session_start` with 24h probe cache TTL. Follows the same pattern as `/probe-nvidia` ([#213](https://github.com/apmantza/pi-free/pull/213)).
23
+
10
24
  ## [2.0.14] - 2026-06-02
11
25
 
12
26
  ### Added
@@ -19,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
33
 
20
34
  - **`_pricingKnown` / `_freeKnown` authoritatve flag** — Providers can now signal whether pricing data is authoritative via `_pricingKnown`. When `false`, `isFreeModel` falls back to name-based detection. Kilo's `isFree` API flag now flows through as `_freeKnown` ([#209](https://github.com/apmantza/pi-free/pull/209)).
21
35
 
36
+ - **MiniMax reasoning compat** — MiniMax M3 and other MiniMax models now have `supportsReasoningEffort: true` compat settings. Previously, models marked `reasoning: true` without compat caused Pi to enter plan mode without knowing the thinking format, resulting in hangs.
37
+
22
38
  ## [2.0.13] - 2026-05-21
23
39
 
24
40
  ### Added
package/README.md CHANGED
@@ -467,10 +467,11 @@ Each provider has toggle commands to switch between free and all models:
467
467
 
468
468
  Test models for 404/403 errors and auto-hide broken ones:
469
469
 
470
- | Command | What it does |
471
- | --------------- | ----------------------------------------------------------- |
472
- | `/probe-nvidia` | Test all NVIDIA models, auto-hide 404s in `~/.pi/free.json` |
473
- | `/probe-ollama` | Test all Ollama models, auto-hide 403s in `~/.pi/free.json` |
470
+ | Command | What it does |
471
+ | ----------------- | ----------------------------------------------------------- |
472
+ | `/probe-nvidia` | Test all NVIDIA models, auto-hide 404s in `~/.pi/free.json` |
473
+ | `/probe-ollama` | Test all Ollama models, auto-hide 403s in `~/.pi/free.json` |
474
+ | `/probe-routeway` | Test all Routeway models, auto-hide 5xx/404s |
474
475
 
475
476
  **How it works:**
476
477
 
@@ -23,6 +23,10 @@ export function isLikelyReasoningModel(model: ProviderModelIdentity): boolean {
23
23
  const haystack = `${model.id} ${model.name ?? ""}`.toLowerCase();
24
24
  return (
25
25
  isDeepSeekModel(model) ||
26
+ haystack.includes("minimax") ||
27
+ haystack.includes("kimi") ||
28
+ haystack.includes("qwen3.7") ||
29
+ haystack.includes("qwen3-7") ||
26
30
  haystack.includes("thinking") ||
27
31
  haystack.includes("reasoning") ||
28
32
  haystack.includes("reasoner") ||
@@ -42,5 +46,34 @@ export function getProxyModelCompat(
42
46
  return DEEPSEEK_PROXY_COMPAT;
43
47
  }
44
48
 
49
+ // MiniMax on OpenRouter/Cline uses reasoning_content (DeepSeek format)
50
+ if (model.id.toLowerCase().includes("minimax")) {
51
+ return {
52
+ supportsStore: false,
53
+ supportsDeveloperRole: false,
54
+ supportsReasoningEffort: true,
55
+ requiresReasoningContentOnAssistantMessages: true,
56
+ thinkingFormat: "deepseek",
57
+ };
58
+ }
59
+
60
+ // Qwen 3.7+ on OpenRouter/Cline uses reasoning_content (DeepSeek format)
61
+ if (
62
+ model.id.toLowerCase().includes("qwen3.7") ||
63
+ model.id.toLowerCase().includes("qwen3-7")
64
+ ) {
65
+ return DEEPSEEK_PROXY_COMPAT;
66
+ }
67
+
68
+ // Kimi K2.6 needs reasoning_content on assistant messages (OpenRouter issue #5309)
69
+ if (model.id.toLowerCase().includes("kimi")) {
70
+ return {
71
+ supportsStore: false,
72
+ supportsDeveloperRole: false,
73
+ supportsReasoningEffort: true,
74
+ requiresReasoningContentOnAssistantMessages: true,
75
+ };
76
+ }
77
+
45
78
  return undefined;
46
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-free",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "type": "module",
5
5
  "description": "AI model providers for Pi with free model filtering and dynamic model fetching",
6
6
  "keywords": [
@@ -14,6 +14,7 @@ import {
14
14
  PROVIDER_CLINE,
15
15
  } from "../../constants.ts";
16
16
  import type { ProviderModelConfig } from "../../lib/types.ts";
17
+ import { getProxyModelCompat } from "../../lib/provider-compat.ts";
17
18
  import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
18
19
 
19
20
  interface ClineRaw {
@@ -164,8 +165,11 @@ function modelFromCatalog(
164
165
  contextWindow:
165
166
  info.context_length ?? info.top_provider?.context_length ?? 128_000,
166
167
  maxTokens: info.top_provider?.max_completion_tokens ?? 8_192,
168
+ ...(getProxyModelCompat({ id: info.id, name: info.name })
169
+ ? { compat: getProxyModelCompat({ id: info.id, name: info.name }) }
170
+ : {}),
167
171
  _pricingKnown: info.pricing !== null && info.pricing !== undefined,
168
- };
172
+ } as ProviderModelConfig & { _pricingKnown?: boolean; compat?: any };
169
173
  }
170
174
 
171
175
  async function fetchClineRecommendedFreeModels(): Promise<
@@ -73,9 +73,9 @@ function toApiKey(credentials: OAuthCredentials): string {
73
73
  // =============================================================================
74
74
 
75
75
  const TASK_PROGRESS_BLOCK = `
76
- # task_progress List (Optional - Plan Mode)
76
+ # task_progress List (Optional)
77
77
 
78
- While in PLAN MODE, if you've outlined concrete steps or requirements for the user, you may include a preliminary todo list using the task_progress parameter.
78
+ You may include a todo list using the task_progress parameter to track progress on multi-step tasks.
79
79
 
80
80
  1. To create or update a todo list, include the task_progress parameter in the next tool call
81
81
  2. Review each item and update its status:
@@ -100,7 +100,7 @@ function buildEnvironmentDetails(): string {
100
100
  0 / 204.8K tokens used (0%)
101
101
 
102
102
  # Current Mode
103
- PLAN MODE
103
+ ACT MODE
104
104
  </environmentDetails>`;
105
105
  }
106
106
 
@@ -35,6 +35,10 @@ import {
35
35
  getModelsDueForProbe,
36
36
  recordModelProbeResults,
37
37
  } from "../../lib/probe-cache.ts";
38
+ import {
39
+ getProxyModelCompat,
40
+ isLikelyReasoningModel,
41
+ } from "../../lib/provider-compat.ts";
38
42
  import { registerWithGlobalToggle } from "../../lib/registry.ts";
39
43
  import type { ModelsDevModel, ModelsDevProvider } from "../../lib/types.ts";
40
44
  import {
@@ -155,7 +159,8 @@ function inferModelFromId(id: string): ModelsDevModel | null {
155
159
  .replaceAll(/\b(\d+(?:\.\d+)?)b\b/gi, "$1B");
156
160
 
157
161
  const hasVision = /vision|multimodal|vl/i.test(id);
158
- const hasReasoning = /reason|r1|thinking/i.test(id);
162
+ const hasReasoning =
163
+ /reason|r1|thinking/i.test(id) || isLikelyReasoningModel({ id, name });
159
164
 
160
165
  return {
161
166
  id,
@@ -277,6 +282,7 @@ async function fetchNvidiaModels(
277
282
  },
278
283
  contextWindow: m.limit.context,
279
284
  maxTokens: m.limit.output,
285
+ compat: getProxyModelCompat({ id: m.id, name: m.name }),
280
286
  }),
281
287
  ),
282
288
  PROVIDER_NVIDIA,
@@ -18,7 +18,12 @@ import type {
18
18
  ExtensionAPI,
19
19
  ProviderModelConfig,
20
20
  } from "@earendil-works/pi-coding-agent";
21
- import { getRoutewayApiKey, getRoutewayShowPaid } from "../../config.ts";
21
+ import {
22
+ getRoutewayApiKey,
23
+ getRoutewayShowPaid,
24
+ loadConfigFile,
25
+ saveConfig,
26
+ } from "../../config.ts";
22
27
  import {
23
28
  BASE_URL_ROUTEWAY,
24
29
  DEFAULT_FETCH_TIMEOUT_MS,
@@ -30,8 +35,13 @@ import {
30
35
  getProxyModelCompat,
31
36
  isLikelyReasoningModel,
32
37
  } from "../../lib/provider-compat.ts";
38
+ import {
39
+ getModelsDueForProbe,
40
+ recordModelProbeResults,
41
+ } from "../../lib/probe-cache.ts";
33
42
  import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
34
43
  import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
44
+ import { fetchWithTimeout } from "../../lib/util.ts";
35
45
  import { createReRegister, setupProvider } from "../../provider-helper.ts";
36
46
 
37
47
  const _logger = createLogger("routeway");
@@ -155,6 +165,125 @@ async function fetchRoutewayModels(
155
165
  }
156
166
  }
157
167
 
168
+ // =============================================================================
169
+ // Probe
170
+ // =============================================================================
171
+
172
+ async function probeRoutewayModel(
173
+ apiKey: string,
174
+ modelId: string,
175
+ ): Promise<"ok" | "broken" | "unknown"> {
176
+ try {
177
+ const response = await fetchWithTimeout(
178
+ `${BASE_URL_ROUTEWAY}/chat/completions`,
179
+ {
180
+ method: "POST",
181
+ headers: {
182
+ Authorization: `Bearer ${apiKey}`,
183
+ "Content-Type": "application/json",
184
+ "User-Agent": "pi-free-providers",
185
+ },
186
+ body: JSON.stringify({
187
+ model: modelId,
188
+ messages: [{ role: "user", content: "hi" }],
189
+ max_tokens: 1,
190
+ }),
191
+ },
192
+ 10000, // 10 second timeout
193
+ );
194
+
195
+ // 5xx = upstream server error (model unavailable)
196
+ if (response.status >= 500) return "broken";
197
+ // 404 = model not found / not provisioned
198
+ if (response.status === 404) return "broken";
199
+ // 429 = rate limited (model works)
200
+ if (response.status === 429) return "ok";
201
+ // 401 = auth issue (model exists, key issue)
202
+ if (response.status === 401) return "ok";
203
+ // 400 = bad request (model exists, param issue)
204
+ if (response.status === 400) return "ok";
205
+ // 200 = success
206
+ if (response.ok) return "ok";
207
+ return "ok";
208
+ } catch {
209
+ return "unknown";
210
+ }
211
+ }
212
+
213
+ async function runRoutewayProbe(
214
+ apiKey: string,
215
+ modelsToTest: ProviderModelConfig[],
216
+ stored: { free: ProviderModelConfig[]; all: ProviderModelConfig[] },
217
+ reRegister: (models: ProviderModelConfig[]) => void,
218
+ options: { useCache?: boolean } = {},
219
+ ): Promise<string[]> {
220
+ const modelIdsToProbe = options.useCache
221
+ ? new Set(
222
+ getModelsDueForProbe(
223
+ PROVIDER_ROUTEWAY,
224
+ modelsToTest.map((m) => m.id),
225
+ ),
226
+ )
227
+ : undefined;
228
+ const probeCandidates = modelIdsToProbe
229
+ ? modelsToTest.filter((m) => modelIdsToProbe.has(m.id))
230
+ : modelsToTest;
231
+
232
+ if (probeCandidates.length === 0) {
233
+ _logger.info("Auto-probe: Routeway probe cache is fresh");
234
+ return [];
235
+ }
236
+
237
+ const broken: string[] = [];
238
+ const cacheableResults: Array<{ modelId: string; status: "ok" | "broken" }> =
239
+ [];
240
+ const batchSize = 5;
241
+
242
+ for (let i = 0; i < probeCandidates.length; i += batchSize) {
243
+ const batch = probeCandidates.slice(i, i + batchSize);
244
+ const results = await Promise.all(
245
+ batch.map(async (m) => {
246
+ const status = await probeRoutewayModel(apiKey, m.id);
247
+ return { id: m.id, status };
248
+ }),
249
+ );
250
+ for (const r of results) {
251
+ if (r.status === "broken") broken.push(r.id);
252
+ if (r.status !== "unknown") {
253
+ cacheableResults.push({ modelId: r.id, status: r.status });
254
+ }
255
+ }
256
+ }
257
+
258
+ recordModelProbeResults(PROVIDER_ROUTEWAY, cacheableResults);
259
+
260
+ if (broken.length === 0) {
261
+ _logger.info("Auto-probe: all checked Routeway models are routable");
262
+ return [];
263
+ }
264
+
265
+ // Auto-hide broken models in config (provider-scoped)
266
+ const cfg = loadConfigFile();
267
+ const existingHidden = new Set(cfg.hidden_models ?? []);
268
+ for (const id of broken) existingHidden.add(`${PROVIDER_ROUTEWAY}/${id}`);
269
+ saveConfig({ hidden_models: Array.from(existingHidden) });
270
+
271
+ // Re-register so hidden models disappear immediately
272
+ const filtered = await fetchRoutewayModels(apiKey);
273
+ stored.free = filtered;
274
+ stored.all = filtered;
275
+ reRegister(filtered);
276
+
277
+ _logger.info(
278
+ `Auto-probe: found ${broken.length} broken models (auto-hidden)`,
279
+ );
280
+ return broken;
281
+ }
282
+
283
+ // =============================================================================
284
+ // Extension Entry Point
285
+ // =============================================================================
286
+
158
287
  export default async function routewayProvider(pi: ExtensionAPI) {
159
288
  const apiKey = getRoutewayApiKey();
160
289
 
@@ -206,6 +335,55 @@ export default async function routewayProvider(pi: ExtensionAPI) {
206
335
  stored,
207
336
  );
208
337
 
338
+ // ── Lazy auto-probe on first session_start ──────────────────────
339
+ let _autoProbeDone = false;
340
+ pi.on("session_start", async () => {
341
+ if (_autoProbeDone || !apiKey) return;
342
+ _autoProbeDone = true;
343
+ _logger.info("Starting lazy auto-probe of Routeway models...");
344
+ runRoutewayProbe(apiKey, allModels, stored, reRegister, {
345
+ useCache: true,
346
+ }).catch((err) => {
347
+ _logger.warn("Auto-probe failed", {
348
+ error: err instanceof Error ? err.message : String(err),
349
+ });
350
+ });
351
+ });
352
+
353
+ // ── Probe command: test all registered models for 5xx ─────────────
354
+ pi.registerCommand("probe-routeway", {
355
+ description:
356
+ "Test all Routeway models for server errors and auto-hide broken ones",
357
+ handler: async (_args, ctx) => {
358
+ if (!apiKey) {
359
+ ctx.ui.notify("ROUTEWAY_API_KEY not set", "error");
360
+ return;
361
+ }
362
+
363
+ const modelsToTest = allModels;
364
+ ctx.ui.notify(
365
+ `Probing ${modelsToTest.length} Routeway models…`,
366
+ "info",
367
+ );
368
+
369
+ await runRoutewayProbe(apiKey, modelsToTest, stored, reRegister);
370
+
371
+ // Check if any were hidden (re-read config)
372
+ const cfgAfter = loadConfigFile();
373
+ const newHidden = (cfgAfter.hidden_models ?? []).filter((h) =>
374
+ h.startsWith(`${PROVIDER_ROUTEWAY}/`),
375
+ );
376
+ if (newHidden.length > 0) {
377
+ ctx.ui.notify(
378
+ `Found ${newHidden.length} broken models (auto-hidden):\n${newHidden.join("\n")}`,
379
+ "warning",
380
+ );
381
+ } else {
382
+ ctx.ui.notify("All Routeway models are routable ✅", "info");
383
+ }
384
+ },
385
+ });
386
+
209
387
  const showPaid = getRoutewayShowPaid();
210
388
  const initialModels =
211
389
  showPaid && stored.all.length > 0 ? stored.all : freeModels;