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 +16 -0
- package/README.md +5 -4
- package/lib/provider-compat.ts +33 -0
- package/package.json +1 -1
- package/providers/cline/cline-models.ts +5 -1
- package/providers/cline/cline.ts +3 -3
- package/providers/nvidia/nvidia.ts +7 -1
- package/providers/routeway/routeway.ts +179 -1
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
|
|
471
|
-
|
|
|
472
|
-
| `/probe-nvidia`
|
|
473
|
-
| `/probe-ollama`
|
|
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
|
|
package/lib/provider-compat.ts
CHANGED
|
@@ -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
|
@@ -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<
|
package/providers/cline/cline.ts
CHANGED
|
@@ -73,9 +73,9 @@ function toApiKey(credentials: OAuthCredentials): string {
|
|
|
73
73
|
// =============================================================================
|
|
74
74
|
|
|
75
75
|
const TASK_PROGRESS_BLOCK = `
|
|
76
|
-
# task_progress List (Optional
|
|
76
|
+
# task_progress List (Optional)
|
|
77
77
|
|
|
78
|
-
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
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;
|