pi-free 2.0.15 → 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 +74 -0
- package/README.md +64 -79
- 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 +53 -37
- 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 +7 -1
- 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 +25 -17
- 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 -510
|
@@ -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
|
+
}
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../../constants.ts";
|
|
32
32
|
import { applyHidden } from "../../config.ts";
|
|
33
33
|
import { createLogger } from "../../lib/logger.ts";
|
|
34
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
34
35
|
import {
|
|
35
36
|
getProxyModelCompat,
|
|
36
37
|
isLikelyReasoningModel,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
recordModelProbeResults,
|
|
41
42
|
} from "../../lib/probe-cache.ts";
|
|
42
43
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
44
|
+
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
43
45
|
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
44
46
|
import { fetchWithTimeout } from "../../lib/util.ts";
|
|
45
47
|
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
@@ -156,7 +158,13 @@ async function fetchRoutewayModels(
|
|
|
156
158
|
const models = (json.data ?? []).filter(isChatModel);
|
|
157
159
|
|
|
158
160
|
_logger.info(`[routeway] Fetched ${models.length} chat models`);
|
|
159
|
-
|
|
161
|
+
const enriched = await safeEnrichModelsWithModelsDev(
|
|
162
|
+
models.map(mapRoutewayModel),
|
|
163
|
+
{
|
|
164
|
+
providerId: PROVIDER_ROUTEWAY,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
return applyHidden(enriched, PROVIDER_ROUTEWAY);
|
|
160
168
|
} catch (error) {
|
|
161
169
|
_logger.error("[routeway] Failed to fetch models", {
|
|
162
170
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -255,7 +263,7 @@ async function runRoutewayProbe(
|
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
|
|
258
|
-
recordModelProbeResults(PROVIDER_ROUTEWAY, cacheableResults);
|
|
266
|
+
await recordModelProbeResults(PROVIDER_ROUTEWAY, cacheableResults);
|
|
259
267
|
|
|
260
268
|
if (broken.length === 0) {
|
|
261
269
|
_logger.info("Auto-probe: all checked Routeway models are routable");
|
|
@@ -337,18 +345,21 @@ export default async function routewayProvider(pi: ExtensionAPI) {
|
|
|
337
345
|
|
|
338
346
|
// ── Lazy auto-probe on first session_start ──────────────────────
|
|
339
347
|
let _autoProbeDone = false;
|
|
340
|
-
pi.on(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
});
|
|
349
360
|
});
|
|
350
|
-
})
|
|
351
|
-
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
352
363
|
|
|
353
364
|
// ── Probe command: test all registered models for 5xx ─────────────
|
|
354
365
|
pi.registerCommand("probe-routeway", {
|
|
@@ -361,10 +372,7 @@ export default async function routewayProvider(pi: ExtensionAPI) {
|
|
|
361
372
|
}
|
|
362
373
|
|
|
363
374
|
const modelsToTest = allModels;
|
|
364
|
-
ctx.ui.notify(
|
|
365
|
-
`Probing ${modelsToTest.length} Routeway models…`,
|
|
366
|
-
"info",
|
|
367
|
-
);
|
|
375
|
+
ctx.ui.notify(`Probing ${modelsToTest.length} Routeway models…`, "info");
|
|
368
376
|
|
|
369
377
|
await runRoutewayProbe(apiKey, modelsToTest, stored, reRegister);
|
|
370
378
|
|
|
@@ -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
|
}
|