pi-free 2.0.13 → 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 +28 -0
- package/README.md +9 -5
- package/config.ts +15 -0
- package/constants.ts +3 -0
- package/index.ts +135 -0
- package/lib/built-in-toggle.ts +4 -4
- package/lib/probe-cache.ts +86 -0
- package/lib/provider-compat.ts +33 -0
- package/lib/registry.ts +25 -3
- package/lib/telemetry.ts +328 -0
- package/lib/util.ts +10 -1
- package/package.json +1 -1
- package/provider-failover/benchmark-lookup.ts +94 -8
- package/provider-failover/benchmarks-chunk-0.ts +599 -890
- package/provider-failover/benchmarks-chunk-1.ts +655 -924
- package/provider-failover/benchmarks-chunk-2.ts +675 -966
- package/provider-failover/benchmarks-chunk-3.ts +676 -967
- package/provider-failover/benchmarks-chunk-4.ts +704 -954
- package/provider-failover/benchmarks-chunk-5.ts +1301 -0
- package/provider-failover/hardcoded-benchmarks.ts +9 -3
- package/providers/cline/cline-models.ts +200 -68
- package/providers/cline/cline.ts +3 -3
- package/providers/dynamic-built-in/index.ts +1 -1
- package/providers/kilo/kilo.ts +2 -2
- package/providers/model-fetcher.ts +3 -1
- package/providers/nvidia/nvidia.ts +54 -16
- package/providers/ollama/ollama.ts +103 -46
- package/providers/opencode-session.ts +398 -371
- package/providers/qwen/qwen.ts +2 -2
- package/providers/routeway/routeway.ts +391 -0
package/providers/qwen/qwen.ts
CHANGED
|
@@ -108,7 +108,7 @@ export default async function qwenProvider(pi: ExtensionAPI) {
|
|
|
108
108
|
function registerProvider(m = models) {
|
|
109
109
|
pi.registerProvider(PROVIDER_QWEN, {
|
|
110
110
|
baseUrl: DEFAULT_BASE_URL,
|
|
111
|
-
apiKey: "QWEN_API_KEY",
|
|
111
|
+
apiKey: "$QWEN_API_KEY",
|
|
112
112
|
api: "openai-completions" as const,
|
|
113
113
|
headers: {
|
|
114
114
|
"User-Agent": "pi-free",
|
|
@@ -125,7 +125,7 @@ export default async function qwenProvider(pi: ExtensionAPI) {
|
|
|
125
125
|
const reRegister = createReRegister(pi, {
|
|
126
126
|
providerId: PROVIDER_QWEN,
|
|
127
127
|
baseUrl: DEFAULT_BASE_URL,
|
|
128
|
-
apiKey: "QWEN_API_KEY",
|
|
128
|
+
apiKey: "$QWEN_API_KEY",
|
|
129
129
|
oauth: oauthConfig as any,
|
|
130
130
|
});
|
|
131
131
|
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routeway AI Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* Routeway exposes an OpenAI-compatible chat completions API with a model
|
|
5
|
+
* catalog that includes free models marked by a `:free` suffix and zero token
|
|
6
|
+
* pricing.
|
|
7
|
+
*
|
|
8
|
+
* API: https://api.routeway.ai/v1
|
|
9
|
+
* Models: /v1/models
|
|
10
|
+
* Docs: https://docs.routeway.ai
|
|
11
|
+
*
|
|
12
|
+
* Setup:
|
|
13
|
+
* ROUTEWAY_API_KEY=sk-...
|
|
14
|
+
* # or add routeway_api_key to ~/.pi/free.json
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ExtensionAPI,
|
|
19
|
+
ProviderModelConfig,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import {
|
|
22
|
+
getRoutewayApiKey,
|
|
23
|
+
getRoutewayShowPaid,
|
|
24
|
+
loadConfigFile,
|
|
25
|
+
saveConfig,
|
|
26
|
+
} from "../../config.ts";
|
|
27
|
+
import {
|
|
28
|
+
BASE_URL_ROUTEWAY,
|
|
29
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
30
|
+
PROVIDER_ROUTEWAY,
|
|
31
|
+
} from "../../constants.ts";
|
|
32
|
+
import { applyHidden } from "../../config.ts";
|
|
33
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
34
|
+
import {
|
|
35
|
+
getProxyModelCompat,
|
|
36
|
+
isLikelyReasoningModel,
|
|
37
|
+
} from "../../lib/provider-compat.ts";
|
|
38
|
+
import {
|
|
39
|
+
getModelsDueForProbe,
|
|
40
|
+
recordModelProbeResults,
|
|
41
|
+
} from "../../lib/probe-cache.ts";
|
|
42
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
43
|
+
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
44
|
+
import { fetchWithTimeout } from "../../lib/util.ts";
|
|
45
|
+
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
46
|
+
|
|
47
|
+
const _logger = createLogger("routeway");
|
|
48
|
+
|
|
49
|
+
interface RoutewayPrice {
|
|
50
|
+
unit?: string;
|
|
51
|
+
price_per_million_t?: number;
|
|
52
|
+
price_per_token_usd?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface RoutewayModel {
|
|
56
|
+
id: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
short_name?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
context_length?: number;
|
|
61
|
+
available?: boolean;
|
|
62
|
+
type?: string;
|
|
63
|
+
endpoints?: string[];
|
|
64
|
+
pricing?: {
|
|
65
|
+
input?: RoutewayPrice;
|
|
66
|
+
output?: RoutewayPrice;
|
|
67
|
+
caching?: { read?: RoutewayPrice; write?: RoutewayPrice };
|
|
68
|
+
};
|
|
69
|
+
supported_parameters?: string[];
|
|
70
|
+
capabilities?: {
|
|
71
|
+
vision?: boolean;
|
|
72
|
+
function_call?: boolean;
|
|
73
|
+
reasoning?: boolean;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parsePricePerToken(price: RoutewayPrice | undefined): number {
|
|
78
|
+
if (!price) return 0;
|
|
79
|
+
if (typeof price.price_per_token_usd === "string") {
|
|
80
|
+
const parsed = Number.parseFloat(price.price_per_token_usd);
|
|
81
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
82
|
+
}
|
|
83
|
+
if (typeof price.price_per_million_t === "number") {
|
|
84
|
+
return price.price_per_million_t / 1_000_000;
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isChatModel(model: RoutewayModel): boolean {
|
|
90
|
+
return (
|
|
91
|
+
model.available !== false &&
|
|
92
|
+
(model.type === "chat.completions" ||
|
|
93
|
+
(model.endpoints ?? []).includes("/v1/chat/completions"))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mapRoutewayModel(
|
|
98
|
+
model: RoutewayModel,
|
|
99
|
+
): ProviderModelConfig & { _pricingKnown?: boolean } {
|
|
100
|
+
const rawName = model.short_name || model.name || model.id;
|
|
101
|
+
const name = cleanModelName(rawName);
|
|
102
|
+
const inputCost = parsePricePerToken(model.pricing?.input);
|
|
103
|
+
const outputCost = parsePricePerToken(model.pricing?.output);
|
|
104
|
+
const cacheRead = parsePricePerToken(model.pricing?.caching?.read);
|
|
105
|
+
const cacheWrite = parsePricePerToken(model.pricing?.caching?.write);
|
|
106
|
+
const hasPricing = !!(model.pricing?.input || model.pricing?.output);
|
|
107
|
+
const reasoning =
|
|
108
|
+
model.capabilities?.reasoning === true ||
|
|
109
|
+
(model.supported_parameters ?? []).includes("reasoning_effort") ||
|
|
110
|
+
isLikelyReasoningModel({ id: model.id, name });
|
|
111
|
+
const free = inputCost === 0 && outputCost === 0;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
id: model.id,
|
|
115
|
+
name: `${name} (Routeway)${free ? "" : " 💰"}`,
|
|
116
|
+
reasoning,
|
|
117
|
+
input: model.capabilities?.vision ? ["text", "image"] : ["text"],
|
|
118
|
+
cost: {
|
|
119
|
+
input: inputCost,
|
|
120
|
+
output: outputCost,
|
|
121
|
+
cacheRead,
|
|
122
|
+
cacheWrite,
|
|
123
|
+
},
|
|
124
|
+
contextWindow: model.context_length ?? 128_000,
|
|
125
|
+
maxTokens: 16_384,
|
|
126
|
+
compat: getProxyModelCompat({ id: model.id, name }),
|
|
127
|
+
_pricingKnown: hasPricing,
|
|
128
|
+
} as ProviderModelConfig & { _pricingKnown?: boolean };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function fetchRoutewayModels(
|
|
132
|
+
apiKey: string,
|
|
133
|
+
): Promise<ProviderModelConfig[]> {
|
|
134
|
+
_logger.info("[routeway] Fetching models from Routeway API...");
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetchWithRetry(
|
|
138
|
+
`${BASE_URL_ROUTEWAY}/models`,
|
|
139
|
+
{
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${apiKey}`,
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
3,
|
|
147
|
+
1000,
|
|
148
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`Routeway API error: ${response.status}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const json = (await response.json()) as { data?: RoutewayModel[] };
|
|
156
|
+
const models = (json.data ?? []).filter(isChatModel);
|
|
157
|
+
|
|
158
|
+
_logger.info(`[routeway] Fetched ${models.length} chat models`);
|
|
159
|
+
return applyHidden(models.map(mapRoutewayModel), PROVIDER_ROUTEWAY);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
_logger.error("[routeway] Failed to fetch models", {
|
|
162
|
+
error: error instanceof Error ? error.message : String(error),
|
|
163
|
+
});
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
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
|
+
|
|
287
|
+
export default async function routewayProvider(pi: ExtensionAPI) {
|
|
288
|
+
const apiKey = getRoutewayApiKey();
|
|
289
|
+
|
|
290
|
+
if (!apiKey) {
|
|
291
|
+
_logger.info(
|
|
292
|
+
"[routeway] Skipping — ROUTEWAY_API_KEY not set. Sign up at https://routeway.ai/",
|
|
293
|
+
);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const allModels = await fetchRoutewayModels(apiKey);
|
|
298
|
+
|
|
299
|
+
if (allModels.length === 0) {
|
|
300
|
+
_logger.warn("[routeway] No chat models available");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const freeModels = allModels.filter((m) =>
|
|
305
|
+
isFreeModel({ ...m, provider: PROVIDER_ROUTEWAY }, allModels),
|
|
306
|
+
);
|
|
307
|
+
const stored = { free: freeModels, all: allModels };
|
|
308
|
+
|
|
309
|
+
_logger.info(
|
|
310
|
+
`[routeway] Registered ${allModels.length} models (${freeModels.length} free)`,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const reRegister = createReRegister(pi, {
|
|
314
|
+
providerId: PROVIDER_ROUTEWAY,
|
|
315
|
+
baseUrl: BASE_URL_ROUTEWAY,
|
|
316
|
+
apiKey,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
registerWithGlobalToggle(PROVIDER_ROUTEWAY, stored, reRegister, true);
|
|
320
|
+
|
|
321
|
+
setupProvider(
|
|
322
|
+
pi,
|
|
323
|
+
{
|
|
324
|
+
providerId: PROVIDER_ROUTEWAY,
|
|
325
|
+
initialShowPaid: getRoutewayShowPaid(),
|
|
326
|
+
tosUrl: "https://routeway.ai/terms",
|
|
327
|
+
reRegister: (models, _stored) => {
|
|
328
|
+
if (_stored) {
|
|
329
|
+
stored.free = _stored.free;
|
|
330
|
+
stored.all = _stored.all;
|
|
331
|
+
}
|
|
332
|
+
reRegister(models);
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
stored,
|
|
336
|
+
);
|
|
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
|
+
|
|
387
|
+
const showPaid = getRoutewayShowPaid();
|
|
388
|
+
const initialModels =
|
|
389
|
+
showPaid && stored.all.length > 0 ? stored.all : freeModels;
|
|
390
|
+
reRegister(initialModels);
|
|
391
|
+
}
|