pi-free 2.0.13 → 2.0.14

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.
@@ -39,6 +39,10 @@ import {
39
39
  loadProviderCache,
40
40
  saveProviderCache,
41
41
  } from "../../lib/provider-cache.ts";
42
+ import {
43
+ getModelsDueForProbe,
44
+ recordModelProbeResults,
45
+ } from "../../lib/probe-cache.ts";
42
46
  import { registerWithGlobalToggle } from "../../lib/registry.ts";
43
47
  import { fetchWithRetry, fetchWithTimeout } from "../../lib/util.ts";
44
48
  import { createReRegister, enhanceWithCI } from "../../provider-helper.ts";
@@ -379,6 +383,80 @@ async function fetchAllModels(apiKey: string): Promise<ProviderModelConfig[]> {
379
383
  return applyHidden(models, PROVIDER_OLLAMA);
380
384
  }
381
385
 
386
+ async function runOllamaProbe(
387
+ apiKey: string,
388
+ modelsToTest: ProviderModelConfig[],
389
+ applyModels: (models: ProviderModelConfig[]) => void,
390
+ options: { useCache?: boolean } = {},
391
+ ): Promise<string[]> {
392
+ const modelIdsToProbe = options.useCache
393
+ ? new Set(
394
+ getModelsDueForProbe(
395
+ PROVIDER_OLLAMA,
396
+ modelsToTest.map((m) => m.id),
397
+ ),
398
+ )
399
+ : undefined;
400
+ const probeCandidates = modelIdsToProbe
401
+ ? modelsToTest.filter((m) => modelIdsToProbe.has(m.id))
402
+ : modelsToTest;
403
+
404
+ if (probeCandidates.length === 0) {
405
+ _logger.info("Auto-probe: Ollama probe cache is fresh");
406
+ return [];
407
+ }
408
+
409
+ const notFound: string[] = [];
410
+ const cacheableResults: Array<{ modelId: string; status: "ok" | "broken" }> =
411
+ [];
412
+ const batchSize = 5;
413
+
414
+ for (let i = 0; i < probeCandidates.length; i += batchSize) {
415
+ const batch = probeCandidates.slice(i, i + batchSize);
416
+ const results = await Promise.all(
417
+ batch.map(async (m) => {
418
+ const status = await probeOllamaModel(apiKey, m.id);
419
+ return { id: m.id, status };
420
+ }),
421
+ );
422
+ for (const r of results) {
423
+ if (r.status === "broken") notFound.push(r.id);
424
+ if (r.status !== "unknown") {
425
+ cacheableResults.push({ modelId: r.id, status: r.status });
426
+ }
427
+ }
428
+ }
429
+
430
+ recordModelProbeResults(PROVIDER_OLLAMA, cacheableResults);
431
+
432
+ if (notFound.length === 0) {
433
+ _logger.info("Auto-probe: all checked Ollama models are accessible");
434
+ return [];
435
+ }
436
+
437
+ // Auto-hide 403 models in config (provider-scoped)
438
+ const config = loadConfigFile();
439
+ const existingHidden = new Set(config.hidden_models ?? []);
440
+ for (const id of notFound) existingHidden.add(`${PROVIDER_OLLAMA}/${id}`);
441
+ saveConfig({
442
+ hidden_models: Array.from(existingHidden),
443
+ });
444
+
445
+ // Re-fetch and re-register so hidden models disappear immediately
446
+ try {
447
+ const fresh = await fetchAllModels(apiKey);
448
+ saveProviderCache(PROVIDER_OLLAMA, fresh);
449
+ applyModels(fresh);
450
+ } catch {
451
+ // If refresh fails, keep current models. The next refresh/probe will retry.
452
+ }
453
+
454
+ _logger.info(
455
+ `Auto-probe: found ${notFound.length} broken Ollama models (auto-hidden)`,
456
+ );
457
+ return notFound;
458
+ }
459
+
382
460
  // =============================================================================
383
461
  // Extension Entry Point
384
462
  // =============================================================================
@@ -411,7 +489,7 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
411
489
 
412
490
  // ── Register immediately with cached/fallback models ────────────
413
491
  const freeModels = allModels;
414
- let stored = { free: freeModels, all: allModels };
492
+ const stored = { free: freeModels, all: allModels };
415
493
  const hasKey = true;
416
494
 
417
495
  const reRegister = createReRegister(pi, {
@@ -419,6 +497,12 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
419
497
  baseUrl: BASE_URL_OLLAMA,
420
498
  apiKey,
421
499
  });
500
+ const applyModelList = (models: ProviderModelConfig[]) => {
501
+ allModels = models;
502
+ stored.free = models;
503
+ stored.all = models;
504
+ reRegister(models);
505
+ };
422
506
 
423
507
  registerWithGlobalToggle(PROVIDER_OLLAMA, stored, reRegister, hasKey);
424
508
 
@@ -460,9 +544,7 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
460
544
  try {
461
545
  const fresh = await fetchAllModels(apiKey!);
462
546
  saveProviderCache(PROVIDER_OLLAMA, fresh);
463
- allModels = fresh;
464
- stored = { free: fresh, all: fresh };
465
- reRegister(fresh);
547
+ applyModelList(fresh);
466
548
  ctx.ui.notify(
467
549
  `Registered ${fresh.length} Ollama Cloud models (refresh complete)`,
468
550
  "info",
@@ -488,47 +570,17 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
488
570
  const modelsToTest = allModels;
489
571
  ctx.ui.notify(`Probing ${modelsToTest.length} Ollama models…`, "info");
490
572
 
491
- const notFound: string[] = [];
492
- const batchSize = 5;
493
-
494
- for (let i = 0; i < modelsToTest.length; i += batchSize) {
495
- const batch = modelsToTest.slice(i, i + batchSize);
496
- const results = await Promise.all(
497
- batch.map(async (m) => {
498
- const ok = await probeOllamaModel(apiKey, m.id);
499
- return { id: m.id, ok };
500
- }),
501
- );
502
- for (const r of results) {
503
- if (!r.ok) notFound.push(r.id);
504
- }
505
- }
573
+ const notFound = await runOllamaProbe(
574
+ apiKey,
575
+ modelsToTest,
576
+ applyModelList,
577
+ );
506
578
 
507
579
  if (notFound.length === 0) {
508
580
  ctx.ui.notify("All Ollama models are accessible ✅", "info");
509
581
  return;
510
582
  }
511
583
 
512
- // Auto-hide 403 models in config (provider-scoped)
513
- const config = loadConfigFile();
514
- const existingHidden = new Set(config.hidden_models ?? []);
515
- for (const id of notFound) existingHidden.add(`${PROVIDER_OLLAMA}/${id}`);
516
- saveConfig({
517
- hidden_models: Array.from(existingHidden),
518
- });
519
-
520
- // Re-fetch and re-register so hidden models disappear immediately
521
- try {
522
- const fresh = await fetchAllModels(apiKey!);
523
- saveProviderCache(PROVIDER_OLLAMA, fresh);
524
- allModels = fresh;
525
- stored = { free: fresh, all: fresh };
526
- reRegister(fresh);
527
- } catch {
528
- // If refresh fails, just re-register current models
529
- reRegister(allModels);
530
- }
531
-
532
584
  ctx.ui.notify(
533
585
  `Found ${notFound.length} broken models (auto-hidden):\n${notFound.join("\n")}`,
534
586
  "warning",
@@ -560,10 +612,15 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
560
612
 
561
613
  try {
562
614
  const fresh = await refreshModels();
563
- allModels = fresh;
564
- stored = { free: fresh, all: fresh };
565
- reRegister(fresh);
615
+ applyModelList(fresh);
566
616
  ctx.ui.notify(`Ollama Cloud: ${fresh.length} models ready`, "info");
617
+ runOllamaProbe(apiKey, fresh, applyModelList, { useCache: true }).catch(
618
+ (error) => {
619
+ _logger.warn("Auto-probe failed", {
620
+ error: error instanceof Error ? error.message : String(error),
621
+ });
622
+ },
623
+ );
567
624
  } catch {
568
625
  // Already logged in refreshModels()
569
626
  }
@@ -576,12 +633,12 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
576
633
 
577
634
  /**
578
635
  * Probe a single Ollama model with a minimal chat request.
579
- * Returns true if the model is accessible (not 403), false if it 403s.
636
+ * Returns "broken" only for deterministic 403s; network errors are unknown.
580
637
  */
581
638
  async function probeOllamaModel(
582
639
  apiKey: string,
583
640
  modelId: string,
584
- ): Promise<boolean> {
641
+ ): Promise<"ok" | "broken" | "unknown"> {
585
642
  try {
586
643
  const response = await fetchWithTimeout(
587
644
  `${BASE_URL_OLLAMA}/chat/completions`,
@@ -602,9 +659,9 @@ async function probeOllamaModel(
602
659
  );
603
660
  // 403 = access denied (model not provisioned)
604
661
  // 200/400/401/etc = at least accessible
605
- return response.status !== 403;
662
+ return response.status === 403 ? "broken" : "ok";
606
663
  } catch {
607
664
  // Network errors / timeouts are not "access denied"
608
- return true;
665
+ return "unknown";
609
666
  }
610
667
  }