pi-free 2.0.14 → 2.1.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/CHANGELOG.md +90 -0
- package/README.md +64 -78
- package/banner.svg +21 -36
- package/config.ts +123 -9
- package/constants.ts +3 -9
- package/index.ts +14 -15
- package/lib/built-in-toggle.ts +29 -16
- package/lib/json-persistence.ts +90 -22
- package/lib/logger.ts +21 -12
- package/lib/model-detection.ts +2 -12
- package/lib/model-enhancer.ts +11 -2
- package/lib/model-metadata.ts +387 -0
- package/lib/open-browser.ts +74 -24
- package/lib/paths.ts +90 -0
- package/lib/probe-cache.ts +19 -19
- package/lib/provider-cache.ts +74 -28
- package/lib/provider-compat.ts +58 -9
- package/lib/provider-probe.ts +188 -0
- package/lib/registry.ts +1 -5
- package/lib/session-start-metrics.ts +46 -0
- package/lib/telemetry.ts +115 -86
- package/lib/types.ts +22 -2
- package/lib/util.ts +80 -21
- package/package.json +7 -2
- package/provider-failover/benchmark-lookup.ts +17 -5
- package/provider-helper.ts +11 -2
- package/providers/cline/cline-models.ts +12 -2
- package/providers/cline/cline-xml-bridge.ts +974 -0
- package/providers/cline/cline.ts +67 -176
- package/providers/crofai/crofai.ts +6 -1
- package/providers/deepinfra/deepinfra.ts +69 -2
- package/providers/dynamic-built-in/index.ts +237 -2
- package/providers/kilo/kilo-models.ts +3 -1
- package/providers/kilo/kilo.ts +268 -41
- package/providers/model-fetcher.ts +18 -55
- package/providers/novita/novita.ts +69 -2
- package/providers/ollama/ollama.ts +48 -24
- package/providers/opencode-session.ts +67 -2
- package/providers/routeway/routeway.ts +188 -2
- package/providers/sambanova/sambanova.ts +67 -1
- package/providers/together/together.ts +69 -2
- package/providers/tokenrouter/tokenrouter.ts +378 -0
- package/providers/zenmux/zenmux.ts +6 -1
- package/scripts/check-extensions.mjs +32 -16
- package/providers/nvidia/nvidia.ts +0 -504
|
@@ -36,9 +36,12 @@ import {
|
|
|
36
36
|
} from "../../constants.ts";
|
|
37
37
|
import { createLogger } from "../../lib/logger.ts";
|
|
38
38
|
import {
|
|
39
|
+
DEFAULT_PROVIDER_CACHE_TTL_MS,
|
|
40
|
+
isProviderCacheFresh,
|
|
39
41
|
loadProviderCache,
|
|
40
42
|
saveProviderCache,
|
|
41
43
|
} from "../../lib/provider-cache.ts";
|
|
44
|
+
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
42
45
|
import {
|
|
43
46
|
getModelsDueForProbe,
|
|
44
47
|
recordModelProbeResults,
|
|
@@ -427,7 +430,7 @@ async function runOllamaProbe(
|
|
|
427
430
|
}
|
|
428
431
|
}
|
|
429
432
|
|
|
430
|
-
recordModelProbeResults(PROVIDER_OLLAMA, cacheableResults);
|
|
433
|
+
await recordModelProbeResults(PROVIDER_OLLAMA, cacheableResults);
|
|
431
434
|
|
|
432
435
|
if (notFound.length === 0) {
|
|
433
436
|
_logger.info("Auto-probe: all checked Ollama models are accessible");
|
|
@@ -445,7 +448,7 @@ async function runOllamaProbe(
|
|
|
445
448
|
// Re-fetch and re-register so hidden models disappear immediately
|
|
446
449
|
try {
|
|
447
450
|
const fresh = await fetchAllModels(apiKey);
|
|
448
|
-
saveProviderCache(PROVIDER_OLLAMA, fresh);
|
|
451
|
+
await saveProviderCache(PROVIDER_OLLAMA, fresh);
|
|
449
452
|
applyModels(fresh);
|
|
450
453
|
} catch {
|
|
451
454
|
// If refresh fails, keep current models. The next refresh/probe will retry.
|
|
@@ -517,14 +520,14 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
|
|
|
517
520
|
_logger.info(
|
|
518
521
|
`[ollama-cloud] Registered ${initialModels.length} models` +
|
|
519
522
|
(fromCache ? " (from cache)" : " (fallback)") +
|
|
520
|
-
",
|
|
523
|
+
", refresh scheduled on session start...",
|
|
521
524
|
);
|
|
522
525
|
|
|
523
526
|
// ── Background refresh ─────────────────────────────────────────
|
|
524
527
|
async function refreshModels(): Promise<ProviderModelConfig[]> {
|
|
525
528
|
try {
|
|
526
529
|
const freshModels = await fetchAllModels(apiKey!);
|
|
527
|
-
saveProviderCache(PROVIDER_OLLAMA, freshModels);
|
|
530
|
+
await saveProviderCache(PROVIDER_OLLAMA, freshModels);
|
|
528
531
|
return freshModels;
|
|
529
532
|
} catch (error) {
|
|
530
533
|
_logger.error("[ollama-cloud] Background refresh failed", {
|
|
@@ -543,7 +546,7 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
|
|
|
543
546
|
ctx.ui.notify("Refreshing Ollama Cloud models…", "info");
|
|
544
547
|
try {
|
|
545
548
|
const fresh = await fetchAllModels(apiKey!);
|
|
546
|
-
saveProviderCache(PROVIDER_OLLAMA, fresh);
|
|
549
|
+
await saveProviderCache(PROVIDER_OLLAMA, fresh);
|
|
547
550
|
applyModelList(fresh);
|
|
548
551
|
ctx.ui.notify(
|
|
549
552
|
`Registered ${fresh.length} Ollama Cloud models (refresh complete)`,
|
|
@@ -601,30 +604,51 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
|
|
|
601
604
|
ctx.ui.setStatus(`${PROVIDER_OLLAMA}-status`, `ollama: ${count} models`);
|
|
602
605
|
});
|
|
603
606
|
|
|
607
|
+
const runProbeInBackground = (models: ProviderModelConfig[]) => {
|
|
608
|
+
runOllamaProbe(apiKey, models, applyModelList, { useCache: true }).catch(
|
|
609
|
+
(error) => {
|
|
610
|
+
_logger.warn("Auto-probe failed", {
|
|
611
|
+
error: error instanceof Error ? error.message : String(error),
|
|
612
|
+
});
|
|
613
|
+
},
|
|
614
|
+
);
|
|
615
|
+
};
|
|
616
|
+
|
|
604
617
|
// ── Background refresh on session_start ─────────────────────────
|
|
605
|
-
let
|
|
618
|
+
let refreshInFlight: Promise<void> | undefined;
|
|
606
619
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
607
|
-
pi.on(
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
620
|
+
pi.on(
|
|
621
|
+
"session_start" as any,
|
|
622
|
+
wrapSessionStartHandler("ollama-cloud", (_event: any, ctx: any) => {
|
|
623
|
+
if (refreshInFlight) return Promise.resolve();
|
|
624
|
+
|
|
625
|
+
if (
|
|
626
|
+
isProviderCacheFresh(PROVIDER_OLLAMA, DEFAULT_PROVIDER_CACHE_TTL_MS)
|
|
627
|
+
) {
|
|
628
|
+
_logger.info(
|
|
629
|
+
"session_start: Ollama Cloud cache is fresh; skipping refresh",
|
|
630
|
+
);
|
|
631
|
+
runProbeInBackground(allModels);
|
|
632
|
+
return Promise.resolve();
|
|
633
|
+
}
|
|
612
634
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
635
|
+
refreshInFlight = refreshModels()
|
|
636
|
+
.then((fresh) => {
|
|
637
|
+
applyModelList(fresh);
|
|
638
|
+
ctx.ui.notify(`Ollama Cloud: ${fresh.length} models ready`, "info");
|
|
639
|
+
runProbeInBackground(fresh);
|
|
640
|
+
})
|
|
641
|
+
.catch((error) => {
|
|
642
|
+
_logger.warn("Background refresh failed", {
|
|
620
643
|
error: error instanceof Error ? error.message : String(error),
|
|
621
644
|
});
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
645
|
+
})
|
|
646
|
+
.finally(() => {
|
|
647
|
+
refreshInFlight = undefined;
|
|
648
|
+
});
|
|
649
|
+
return Promise.resolve();
|
|
650
|
+
}),
|
|
651
|
+
);
|
|
628
652
|
}
|
|
629
653
|
|
|
630
654
|
// =============================================================================
|
|
@@ -352,6 +352,18 @@ export function createOpenCodeStreamSimple(
|
|
|
352
352
|
const headers = createOpenCodeHeaders(tracker, options?.headers);
|
|
353
353
|
const stream = new DeferredAssistantMessageEventStream();
|
|
354
354
|
|
|
355
|
+
// Sanitize context messages for Anthropic/OpenAI compatibility.
|
|
356
|
+
// OpenCode proxies to Anthropic which strictly enforces alternating
|
|
357
|
+
// user/assistant turns. This fixes consecutive assistant messages,
|
|
358
|
+
// leading assistant messages, and trailing assistant messages.
|
|
359
|
+
const sanitizedMessages = sanitizeMessagesForOpenCode(
|
|
360
|
+
context.messages as unknown[],
|
|
361
|
+
);
|
|
362
|
+
const sanitizedContext: Context = {
|
|
363
|
+
...context,
|
|
364
|
+
messages: sanitizedMessages as Context["messages"],
|
|
365
|
+
};
|
|
366
|
+
|
|
355
367
|
void (async () => {
|
|
356
368
|
try {
|
|
357
369
|
if (isAnthropicOpenCodeEndpoint(model)) {
|
|
@@ -364,7 +376,7 @@ export function createOpenCodeStreamSimple(
|
|
|
364
376
|
...model,
|
|
365
377
|
api: "anthropic-messages",
|
|
366
378
|
} as Model<"anthropic-messages">,
|
|
367
|
-
|
|
379
|
+
sanitizedContext,
|
|
368
380
|
{ ...options, headers },
|
|
369
381
|
),
|
|
370
382
|
);
|
|
@@ -382,7 +394,7 @@ export function createOpenCodeStreamSimple(
|
|
|
382
394
|
...model,
|
|
383
395
|
api: "openai-completions",
|
|
384
396
|
} as Model<"openai-completions">,
|
|
385
|
-
|
|
397
|
+
sanitizedContext,
|
|
386
398
|
{ ...options, headers },
|
|
387
399
|
),
|
|
388
400
|
);
|
|
@@ -396,3 +408,56 @@ export function createOpenCodeStreamSimple(
|
|
|
396
408
|
return stream as unknown as AssistantMessageEventStream;
|
|
397
409
|
};
|
|
398
410
|
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Sanitize message history for OpenCode's backends.
|
|
414
|
+
*
|
|
415
|
+
* OpenCode proxies to Anthropic and OpenAI. Anthropic strictly enforces
|
|
416
|
+
* alternating user/assistant turns and rejects:
|
|
417
|
+
* - consecutive assistant messages
|
|
418
|
+
* - conversations that start with assistant
|
|
419
|
+
* - conversations that end with assistant
|
|
420
|
+
*
|
|
421
|
+
* This sanitizer fixes all three issues with minimal placeholder messages.
|
|
422
|
+
*/
|
|
423
|
+
export function sanitizeMessagesForOpenCode(messages: unknown[]): unknown[] {
|
|
424
|
+
if (!Array.isArray(messages)) return messages;
|
|
425
|
+
|
|
426
|
+
const sanitized: unknown[] = [];
|
|
427
|
+
let hasNonSystem = false;
|
|
428
|
+
|
|
429
|
+
for (const raw of messages) {
|
|
430
|
+
if (!raw || typeof raw !== "object") continue;
|
|
431
|
+
const msg = raw as { role?: string; content?: unknown };
|
|
432
|
+
const role = msg.role;
|
|
433
|
+
if (!role) continue;
|
|
434
|
+
|
|
435
|
+
if (role === "system") {
|
|
436
|
+
sanitized.push(raw);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Skip leading assistant messages before any user/tool message
|
|
441
|
+
if (role === "assistant" && !hasNonSystem) continue;
|
|
442
|
+
|
|
443
|
+
hasNonSystem = true;
|
|
444
|
+
|
|
445
|
+
// Insert placeholder user message between consecutive assistant messages
|
|
446
|
+
const last = sanitized[sanitized.length - 1] as
|
|
447
|
+
| { role?: string }
|
|
448
|
+
| undefined;
|
|
449
|
+
if (role === "assistant" && last?.role === "assistant") {
|
|
450
|
+
sanitized.push({ role: "user", content: " " });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
sanitized.push(raw);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Ensure conversation ends with a user message
|
|
457
|
+
const last = sanitized[sanitized.length - 1] as { role?: string } | undefined;
|
|
458
|
+
if (last?.role === "assistant") {
|
|
459
|
+
sanitized.push({ role: "user", content: " " });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return sanitized;
|
|
463
|
+
}
|
|
@@ -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,
|
|
@@ -26,12 +31,19 @@ import {
|
|
|
26
31
|
} from "../../constants.ts";
|
|
27
32
|
import { applyHidden } from "../../config.ts";
|
|
28
33
|
import { createLogger } from "../../lib/logger.ts";
|
|
34
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
29
35
|
import {
|
|
30
36
|
getProxyModelCompat,
|
|
31
37
|
isLikelyReasoningModel,
|
|
32
38
|
} from "../../lib/provider-compat.ts";
|
|
39
|
+
import {
|
|
40
|
+
getModelsDueForProbe,
|
|
41
|
+
recordModelProbeResults,
|
|
42
|
+
} from "../../lib/probe-cache.ts";
|
|
33
43
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
44
|
+
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
34
45
|
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
46
|
+
import { fetchWithTimeout } from "../../lib/util.ts";
|
|
35
47
|
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
36
48
|
|
|
37
49
|
const _logger = createLogger("routeway");
|
|
@@ -146,7 +158,13 @@ async function fetchRoutewayModels(
|
|
|
146
158
|
const models = (json.data ?? []).filter(isChatModel);
|
|
147
159
|
|
|
148
160
|
_logger.info(`[routeway] Fetched ${models.length} chat models`);
|
|
149
|
-
|
|
161
|
+
const enriched = await safeEnrichModelsWithModelsDev(
|
|
162
|
+
models.map(mapRoutewayModel),
|
|
163
|
+
{
|
|
164
|
+
providerId: PROVIDER_ROUTEWAY,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
return applyHidden(enriched, PROVIDER_ROUTEWAY);
|
|
150
168
|
} catch (error) {
|
|
151
169
|
_logger.error("[routeway] Failed to fetch models", {
|
|
152
170
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -155,6 +173,125 @@ async function fetchRoutewayModels(
|
|
|
155
173
|
}
|
|
156
174
|
}
|
|
157
175
|
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Probe
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
async function probeRoutewayModel(
|
|
181
|
+
apiKey: string,
|
|
182
|
+
modelId: string,
|
|
183
|
+
): Promise<"ok" | "broken" | "unknown"> {
|
|
184
|
+
try {
|
|
185
|
+
const response = await fetchWithTimeout(
|
|
186
|
+
`${BASE_URL_ROUTEWAY}/chat/completions`,
|
|
187
|
+
{
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: {
|
|
190
|
+
Authorization: `Bearer ${apiKey}`,
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
"User-Agent": "pi-free-providers",
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
model: modelId,
|
|
196
|
+
messages: [{ role: "user", content: "hi" }],
|
|
197
|
+
max_tokens: 1,
|
|
198
|
+
}),
|
|
199
|
+
},
|
|
200
|
+
10000, // 10 second timeout
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// 5xx = upstream server error (model unavailable)
|
|
204
|
+
if (response.status >= 500) return "broken";
|
|
205
|
+
// 404 = model not found / not provisioned
|
|
206
|
+
if (response.status === 404) return "broken";
|
|
207
|
+
// 429 = rate limited (model works)
|
|
208
|
+
if (response.status === 429) return "ok";
|
|
209
|
+
// 401 = auth issue (model exists, key issue)
|
|
210
|
+
if (response.status === 401) return "ok";
|
|
211
|
+
// 400 = bad request (model exists, param issue)
|
|
212
|
+
if (response.status === 400) return "ok";
|
|
213
|
+
// 200 = success
|
|
214
|
+
if (response.ok) return "ok";
|
|
215
|
+
return "ok";
|
|
216
|
+
} catch {
|
|
217
|
+
return "unknown";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function runRoutewayProbe(
|
|
222
|
+
apiKey: string,
|
|
223
|
+
modelsToTest: ProviderModelConfig[],
|
|
224
|
+
stored: { free: ProviderModelConfig[]; all: ProviderModelConfig[] },
|
|
225
|
+
reRegister: (models: ProviderModelConfig[]) => void,
|
|
226
|
+
options: { useCache?: boolean } = {},
|
|
227
|
+
): Promise<string[]> {
|
|
228
|
+
const modelIdsToProbe = options.useCache
|
|
229
|
+
? new Set(
|
|
230
|
+
getModelsDueForProbe(
|
|
231
|
+
PROVIDER_ROUTEWAY,
|
|
232
|
+
modelsToTest.map((m) => m.id),
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
: undefined;
|
|
236
|
+
const probeCandidates = modelIdsToProbe
|
|
237
|
+
? modelsToTest.filter((m) => modelIdsToProbe.has(m.id))
|
|
238
|
+
: modelsToTest;
|
|
239
|
+
|
|
240
|
+
if (probeCandidates.length === 0) {
|
|
241
|
+
_logger.info("Auto-probe: Routeway probe cache is fresh");
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const broken: string[] = [];
|
|
246
|
+
const cacheableResults: Array<{ modelId: string; status: "ok" | "broken" }> =
|
|
247
|
+
[];
|
|
248
|
+
const batchSize = 5;
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < probeCandidates.length; i += batchSize) {
|
|
251
|
+
const batch = probeCandidates.slice(i, i + batchSize);
|
|
252
|
+
const results = await Promise.all(
|
|
253
|
+
batch.map(async (m) => {
|
|
254
|
+
const status = await probeRoutewayModel(apiKey, m.id);
|
|
255
|
+
return { id: m.id, status };
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
for (const r of results) {
|
|
259
|
+
if (r.status === "broken") broken.push(r.id);
|
|
260
|
+
if (r.status !== "unknown") {
|
|
261
|
+
cacheableResults.push({ modelId: r.id, status: r.status });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await recordModelProbeResults(PROVIDER_ROUTEWAY, cacheableResults);
|
|
267
|
+
|
|
268
|
+
if (broken.length === 0) {
|
|
269
|
+
_logger.info("Auto-probe: all checked Routeway models are routable");
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Auto-hide broken models in config (provider-scoped)
|
|
274
|
+
const cfg = loadConfigFile();
|
|
275
|
+
const existingHidden = new Set(cfg.hidden_models ?? []);
|
|
276
|
+
for (const id of broken) existingHidden.add(`${PROVIDER_ROUTEWAY}/${id}`);
|
|
277
|
+
saveConfig({ hidden_models: Array.from(existingHidden) });
|
|
278
|
+
|
|
279
|
+
// Re-register so hidden models disappear immediately
|
|
280
|
+
const filtered = await fetchRoutewayModels(apiKey);
|
|
281
|
+
stored.free = filtered;
|
|
282
|
+
stored.all = filtered;
|
|
283
|
+
reRegister(filtered);
|
|
284
|
+
|
|
285
|
+
_logger.info(
|
|
286
|
+
`Auto-probe: found ${broken.length} broken models (auto-hidden)`,
|
|
287
|
+
);
|
|
288
|
+
return broken;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// Extension Entry Point
|
|
293
|
+
// =============================================================================
|
|
294
|
+
|
|
158
295
|
export default async function routewayProvider(pi: ExtensionAPI) {
|
|
159
296
|
const apiKey = getRoutewayApiKey();
|
|
160
297
|
|
|
@@ -206,6 +343,55 @@ export default async function routewayProvider(pi: ExtensionAPI) {
|
|
|
206
343
|
stored,
|
|
207
344
|
);
|
|
208
345
|
|
|
346
|
+
// ── Lazy auto-probe on first session_start ──────────────────────
|
|
347
|
+
let _autoProbeDone = false;
|
|
348
|
+
pi.on(
|
|
349
|
+
"session_start",
|
|
350
|
+
wrapSessionStartHandler("routeway", async () => {
|
|
351
|
+
if (_autoProbeDone || !apiKey) return;
|
|
352
|
+
_autoProbeDone = true;
|
|
353
|
+
_logger.info("Starting lazy auto-probe of Routeway models...");
|
|
354
|
+
runRoutewayProbe(apiKey, allModels, stored, reRegister, {
|
|
355
|
+
useCache: true,
|
|
356
|
+
}).catch((err) => {
|
|
357
|
+
_logger.warn("Auto-probe failed", {
|
|
358
|
+
error: err instanceof Error ? err.message : String(err),
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// ── Probe command: test all registered models for 5xx ─────────────
|
|
365
|
+
pi.registerCommand("probe-routeway", {
|
|
366
|
+
description:
|
|
367
|
+
"Test all Routeway models for server errors and auto-hide broken ones",
|
|
368
|
+
handler: async (_args, ctx) => {
|
|
369
|
+
if (!apiKey) {
|
|
370
|
+
ctx.ui.notify("ROUTEWAY_API_KEY not set", "error");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const modelsToTest = allModels;
|
|
375
|
+
ctx.ui.notify(`Probing ${modelsToTest.length} Routeway models…`, "info");
|
|
376
|
+
|
|
377
|
+
await runRoutewayProbe(apiKey, modelsToTest, stored, reRegister);
|
|
378
|
+
|
|
379
|
+
// Check if any were hidden (re-read config)
|
|
380
|
+
const cfgAfter = loadConfigFile();
|
|
381
|
+
const newHidden = (cfgAfter.hidden_models ?? []).filter((h) =>
|
|
382
|
+
h.startsWith(`${PROVIDER_ROUTEWAY}/`),
|
|
383
|
+
);
|
|
384
|
+
if (newHidden.length > 0) {
|
|
385
|
+
ctx.ui.notify(
|
|
386
|
+
`Found ${newHidden.length} broken models (auto-hidden):\n${newHidden.join("\n")}`,
|
|
387
|
+
"warning",
|
|
388
|
+
);
|
|
389
|
+
} else {
|
|
390
|
+
ctx.ui.notify("All Routeway models are routable ✅", "info");
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
209
395
|
const showPaid = getRoutewayShowPaid();
|
|
210
396
|
const initialModels =
|
|
211
397
|
showPaid && stored.all.length > 0 ? stored.all : freeModels;
|
|
@@ -31,8 +31,13 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
31
31
|
import { getSambanovaApiKey, getSambanovaShowPaid } from "../../config.ts";
|
|
32
32
|
import { BASE_URL_SAMBANOVA, PROVIDER_SAMBANOVA } from "../../constants.ts";
|
|
33
33
|
import { createLogger } from "../../lib/logger.ts";
|
|
34
|
+
import { createProviderProbe } from "../../lib/provider-probe.ts";
|
|
34
35
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
35
|
-
import {
|
|
36
|
+
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
37
|
+
import {
|
|
38
|
+
fetchOpenAICompatibleModels,
|
|
39
|
+
fetchWithTimeout,
|
|
40
|
+
} from "../../lib/util.ts";
|
|
36
41
|
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
37
42
|
|
|
38
43
|
const _logger = createLogger("sambanova");
|
|
@@ -112,4 +117,65 @@ export default async function sambanovaProvider(pi: ExtensionAPI) {
|
|
|
112
117
|
const initialModels =
|
|
113
118
|
showPaid && stored.all.length > 0 ? stored.all : freeModels;
|
|
114
119
|
reRegister(initialModels);
|
|
120
|
+
|
|
121
|
+
// ── Probe support ──────────────────────────────────────────────
|
|
122
|
+
const probe = createProviderProbe({
|
|
123
|
+
providerId: PROVIDER_SAMBANOVA,
|
|
124
|
+
probeModel: async (_apiKey: string, modelId: string) => {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetchWithTimeout(
|
|
127
|
+
`${BASE_URL_SAMBANOVA}/chat/completions`,
|
|
128
|
+
{
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer ${apiKey}`,
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
"User-Agent": "pi-free-providers",
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
model: modelId,
|
|
137
|
+
messages: [{ role: "user", content: "hi" }],
|
|
138
|
+
max_tokens: 1,
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
10_000,
|
|
142
|
+
);
|
|
143
|
+
// SambaNova may return 404 for preview/unavailable models
|
|
144
|
+
if (response.status === 404 || response.status >= 500) return "broken";
|
|
145
|
+
if (response.status === 429) return "ok";
|
|
146
|
+
if (response.ok) return "ok";
|
|
147
|
+
return "ok";
|
|
148
|
+
} catch {
|
|
149
|
+
return "unknown";
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Probe command
|
|
155
|
+
pi.registerCommand(`probe-${PROVIDER_SAMBANOVA}`, {
|
|
156
|
+
description: "Test all SambaNova models for availability",
|
|
157
|
+
handler: async (_args, ctx) => {
|
|
158
|
+
ctx.ui.notify(`Probing ${allModels.length} SambaNova models…`, "info");
|
|
159
|
+
const broken = await probe.run(apiKey, allModels, {
|
|
160
|
+
onBroken: (ids) => {
|
|
161
|
+
ctx.ui.notify(
|
|
162
|
+
`Found ${ids.length} broken models (auto-hidden):\n${ids.join("\n")}`,
|
|
163
|
+
"warning",
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
if (broken.length === 0) {
|
|
168
|
+
ctx.ui.notify("All SambaNova models are accessible ✅", "info");
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Lazy auto-probe on first session_start
|
|
174
|
+
pi.on(
|
|
175
|
+
"session_start",
|
|
176
|
+
wrapSessionStartHandler(
|
|
177
|
+
`${PROVIDER_SAMBANOVA}-auto-probe`,
|
|
178
|
+
probe.autoProbeHandler(apiKey, freeModels),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
115
181
|
}
|
|
@@ -41,12 +41,15 @@ import {
|
|
|
41
41
|
PROVIDER_TOGETHER,
|
|
42
42
|
} from "../../constants.ts";
|
|
43
43
|
import { createLogger } from "../../lib/logger.ts";
|
|
44
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
44
45
|
import {
|
|
45
46
|
getProxyModelCompat,
|
|
46
47
|
isLikelyReasoningModel,
|
|
47
48
|
} from "../../lib/provider-compat.ts";
|
|
49
|
+
import { createProviderProbe } from "../../lib/provider-probe.ts";
|
|
48
50
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
49
|
-
import {
|
|
51
|
+
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
52
|
+
import { fetchWithRetry, fetchWithTimeout } from "../../lib/util.ts";
|
|
50
53
|
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
51
54
|
|
|
52
55
|
const _logger = createLogger("together");
|
|
@@ -98,7 +101,7 @@ async function fetchTogetherModels(
|
|
|
98
101
|
|
|
99
102
|
_logger.info(`[together] Fetched ${models.length} models`);
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
const mapped = models
|
|
102
105
|
.filter((m) => m.type === "chat" && m.id && !m.id.includes("embed"))
|
|
103
106
|
.map((m): ProviderModelConfig => {
|
|
104
107
|
const name = m.display_name || m.id.split("/").pop() || m.id;
|
|
@@ -126,6 +129,10 @@ async function fetchTogetherModels(
|
|
|
126
129
|
_pricingKnown: m.pricing !== undefined,
|
|
127
130
|
} as ProviderModelConfig & { _pricingKnown?: boolean };
|
|
128
131
|
});
|
|
132
|
+
|
|
133
|
+
return await safeEnrichModelsWithModelsDev(mapped, {
|
|
134
|
+
providerId: PROVIDER_TOGETHER,
|
|
135
|
+
});
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
// =============================================================================
|
|
@@ -191,4 +198,64 @@ export default async function togetherProvider(pi: ExtensionAPI) {
|
|
|
191
198
|
|
|
192
199
|
// Initial registration — show all models (trial credit provider)
|
|
193
200
|
reRegister(stored.all);
|
|
201
|
+
|
|
202
|
+
// ── Probe support ──────────────────────────────────────────────
|
|
203
|
+
const probe = createProviderProbe({
|
|
204
|
+
providerId: PROVIDER_TOGETHER,
|
|
205
|
+
probeModel: async (_apiKey: string, modelId: string) => {
|
|
206
|
+
try {
|
|
207
|
+
const response = await fetchWithTimeout(
|
|
208
|
+
`${BASE_URL_TOGETHER}/chat/completions`,
|
|
209
|
+
{
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: {
|
|
212
|
+
Authorization: `Bearer ${apiKey}`,
|
|
213
|
+
"Content-Type": "application/json",
|
|
214
|
+
"User-Agent": "pi-free-providers",
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
model: modelId,
|
|
218
|
+
messages: [{ role: "user", content: "hi" }],
|
|
219
|
+
max_tokens: 1,
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
10_000,
|
|
223
|
+
);
|
|
224
|
+
if (response.status === 404 || response.status >= 500) return "broken";
|
|
225
|
+
if (response.status === 429) return "ok";
|
|
226
|
+
if (response.ok) return "ok";
|
|
227
|
+
return "ok";
|
|
228
|
+
} catch {
|
|
229
|
+
return "unknown";
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Probe command
|
|
235
|
+
pi.registerCommand(`probe-${PROVIDER_TOGETHER}`, {
|
|
236
|
+
description: "Test all Together AI models for availability",
|
|
237
|
+
handler: async (_args, ctx) => {
|
|
238
|
+
ctx.ui.notify(`Probing ${allModels.length} Together AI models…`, "info");
|
|
239
|
+
const broken = await probe.run(apiKey, allModels, {
|
|
240
|
+
onBroken: (ids) => {
|
|
241
|
+
ctx.ui.notify(
|
|
242
|
+
`Found ${ids.length} broken models (auto-hidden):\n${ids.join("\n")}`,
|
|
243
|
+
"warning",
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
if (broken.length === 0) {
|
|
248
|
+
ctx.ui.notify("All Together AI models are accessible ✅", "info");
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Lazy auto-probe on first session_start
|
|
254
|
+
pi.on(
|
|
255
|
+
"session_start",
|
|
256
|
+
wrapSessionStartHandler(
|
|
257
|
+
`${PROVIDER_TOGETHER}-auto-probe`,
|
|
258
|
+
probe.autoProbeHandler(apiKey, freeModels),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
194
261
|
}
|