pi-free 1.0.8 → 2.0.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/README.md +95 -46
  3. package/config.ts +165 -120
  4. package/constants.ts +22 -61
  5. package/index.ts +186 -0
  6. package/lib/json-persistence.ts +11 -10
  7. package/lib/logger.ts +2 -2
  8. package/lib/model-enhancer.ts +20 -20
  9. package/lib/open-browser.ts +41 -0
  10. package/lib/provider-cache.ts +106 -0
  11. package/lib/registry.ts +144 -0
  12. package/package.json +67 -82
  13. package/provider-factory.ts +25 -41
  14. package/provider-failover/benchmark-lookup.ts +247 -0
  15. package/provider-failover/benchmarks-chunk-0.ts +2010 -0
  16. package/provider-failover/benchmarks-chunk-1.ts +1988 -0
  17. package/provider-failover/benchmarks-chunk-2.ts +2010 -0
  18. package/provider-failover/benchmarks-chunk-3.ts +2010 -0
  19. package/provider-failover/benchmarks-chunk-4.ts +1969 -0
  20. package/provider-failover/hardcoded-benchmarks.ts +22 -10025
  21. package/provider-helper.ts +38 -37
  22. package/providers/{cline-auth.ts → cline/cline-auth.ts} +2 -2
  23. package/providers/cline/cline-models.ts +128 -0
  24. package/providers/{cline.ts → cline/cline.ts} +300 -257
  25. package/providers/cloudflare/cloudflare.ts +368 -0
  26. package/providers/dynamic-built-in/index.ts +513 -0
  27. package/providers/{kilo-auth.ts → kilo/kilo-auth.ts} +3 -20
  28. package/providers/{kilo-models.ts → kilo/kilo-models.ts} +2 -2
  29. package/providers/kilo/kilo.ts +235 -0
  30. package/providers/{modal.ts → modal/modal.ts} +4 -3
  31. package/providers/{nvidia.ts → nvidia/nvidia.ts} +152 -113
  32. package/providers/ollama/ollama.ts +172 -0
  33. package/providers/opencode-session.ts +34 -34
  34. package/providers/{qwen-auth.ts → qwen/qwen-auth.ts} +24 -40
  35. package/providers/{qwen-models.ts → qwen/qwen-models.ts} +101 -95
  36. package/providers/qwen/qwen.ts +202 -0
  37. package/provider-failover/auto-switch.ts +0 -350
  38. package/provider-failover/errors.ts +0 -275
  39. package/provider-failover/index.ts +0 -238
  40. package/providers/cline-models.ts +0 -77
  41. package/providers/factory.ts +0 -125
  42. package/providers/fireworks.ts +0 -49
  43. package/providers/go.ts +0 -216
  44. package/providers/kilo.ts +0 -146
  45. package/providers/mistral.ts +0 -144
  46. package/providers/ollama.ts +0 -113
  47. package/providers/openrouter.ts +0 -175
  48. package/providers/qwen.ts +0 -127
  49. package/providers/zen.ts +0 -371
  50. package/usage/commands.ts +0 -17
  51. package/usage/cumulative.ts +0 -193
  52. package/usage/formatters.ts +0 -115
  53. package/usage/index.ts +0 -46
  54. package/usage/limits.ts +0 -148
  55. package/usage/metrics.ts +0 -222
  56. package/usage/sessions.ts +0 -355
  57. package/usage/store.ts +0 -99
  58. package/usage/tracking.ts +0 -329
  59. package/usage/types.ts +0 -26
  60. package/usage/widget.ts +0 -90
  61. package/widget/data.ts +0 -113
  62. package/widget/format.ts +0 -26
  63. package/widget/render.ts +0 -117
package/index.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Pi-Free Providers Index
3
+ *
4
+ * Provides free model filtering for ALL providers (built-in + extension)
5
+ * plus unique free/paid providers not covered by pi's built-in providers.
6
+ *
7
+ * Unique providers:
8
+ * - Kilo: OAuth-based free models
9
+ * - Cline: Cline bot integration
10
+ * - NVIDIA: NVIDIA NIM hosting (free tier available)
11
+ * - Qwen: OAuth-based Qwen access
12
+ * - Modal: Modal Labs hosting
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
+ import { createLogger } from "./lib/logger.ts";
17
+ import {
18
+ applyGlobalFilter,
19
+ getGlobalFreeOnly,
20
+ getProviderRegistry,
21
+ isFreeModel,
22
+ registerWithGlobalToggle,
23
+ } from "./lib/registry.ts";
24
+ // Import unique provider extensions (only providers NOT built into pi)
25
+ import cline from "./providers/cline/cline.ts";
26
+ import cloudflare from "./providers/cloudflare/cloudflare.ts";
27
+ import kilo from "./providers/kilo/kilo.ts";
28
+ import modal from "./providers/modal/modal.ts";
29
+ import nvidia from "./providers/nvidia/nvidia.ts";
30
+ import ollama from "./providers/ollama/ollama.ts";
31
+ import qwen from "./providers/qwen/qwen.ts";
32
+
33
+ const _logger = createLogger("pi-free");
34
+
35
+ // =============================================================================
36
+ // Global Commands
37
+ // =============================================================================
38
+
39
+ function setupGlobalCommands(pi: ExtensionAPI) {
40
+ // /free - Global toggle for ALL providers
41
+ pi.registerCommand("free", {
42
+ description: "Toggle free-only mode for ALL providers (on/off/status)",
43
+ handler: async (args, ctx) => {
44
+ const arg = args.trim().toLowerCase();
45
+ const registry = getProviderRegistry();
46
+
47
+ if (arg === "on" || arg === "true" || arg === "yes") {
48
+ applyGlobalFilter(pi, true);
49
+ ctx.ui.notify(
50
+ "✓ Free-only mode enabled - paid models hidden for all providers",
51
+ "info",
52
+ );
53
+ } else if (arg === "off" || arg === "false" || arg === "no") {
54
+ applyGlobalFilter(pi, false);
55
+ ctx.ui.notify(
56
+ "✓ Paid models enabled - all models visible for all providers",
57
+ "info",
58
+ );
59
+ } else if (arg === "status" || arg === "" || !arg) {
60
+ const available = await ctx.modelRegistry.getAvailable();
61
+ const freeCount = available.filter(isFreeModel).length;
62
+ const status = getGlobalFreeOnly() ? "enabled" : "disabled";
63
+
64
+ // Count by provider
65
+ const lines = [
66
+ `Free-only mode: ${status}`,
67
+ `${freeCount}/${available.length} models free`,
68
+ "",
69
+ ];
70
+ for (const [id, entry] of registry) {
71
+ const free = entry.stored.free.length;
72
+ const all = entry.stored.all.length || free;
73
+ lines.push(`${id}: ${free}/${all} free`);
74
+ }
75
+
76
+ ctx.ui.notify(lines.join("\n"), "info");
77
+ } else {
78
+ ctx.ui.notify("Usage: /free [on|off|status]", "warning");
79
+ }
80
+ },
81
+ });
82
+
83
+ // /free-providers - Show free model counts by provider
84
+ pi.registerCommand("free-providers", {
85
+ description: "Show free/paid model counts for all pi-free providers",
86
+ handler: async (_args, ctx) => {
87
+ const lines = ["📊 Pi-Free Providers:", ""];
88
+ const registry = getProviderRegistry();
89
+
90
+ // Providers known to not expose pricing via API (all models show as "free")
91
+ // OpenRouter and OpenCode expose actual pricing
92
+ const noPricingApi = new Set([
93
+ "mistral",
94
+ "xai",
95
+ "huggingface",
96
+ "groq",
97
+ "cerebras",
98
+ ]);
99
+ // Freemium providers - all models share a free tier quota
100
+ const freemiumProviders = new Set(["nvidia"]);
101
+
102
+ for (const [id, entry] of registry) {
103
+ const free = entry.stored.free.length;
104
+ const all = entry.stored.all.length || free;
105
+ const indicator = entry.hasKey ? "🔑" : "🆓";
106
+ const paid = all - free;
107
+
108
+ if (freemiumProviders.has(id)) {
109
+ // Freemium: all models share a free tier (e.g., 1,000 reqs/month)
110
+ lines.push(`${indicator} ${id}: ${all} models (freemium)`);
111
+ } else if (noPricingApi.has(id)) {
112
+ // Provider doesn't expose pricing - can't determine free vs paid
113
+ lines.push(
114
+ `${indicator} ${id}: ${all} models (pricing not exposed by API)`,
115
+ );
116
+ } else if (paid === 0 && free > 0) {
117
+ // All models are actually free
118
+ lines.push(`${indicator} ${id}: ${free} free models`);
119
+ } else {
120
+ // Mix of free and paid
121
+ lines.push(
122
+ `${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
123
+ );
124
+ }
125
+ }
126
+
127
+ if (registry.size === 0) {
128
+ lines.push("(No providers registered yet)");
129
+ }
130
+
131
+ ctx.ui.notify(lines.join("\n"), "info");
132
+ },
133
+ });
134
+ }
135
+
136
+ // =============================================================================
137
+ // Main Entry Point
138
+ // =============================================================================
139
+
140
+ export default async function (pi: ExtensionAPI) {
141
+ const globalFreeOnly = getGlobalFreeOnly();
142
+ _logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
143
+
144
+ // Setup global commands first
145
+ setupGlobalCommands(pi);
146
+
147
+ // Load all unique providers
148
+ // Each provider will register itself with the global toggle system
149
+ await Promise.allSettled([
150
+ cloudflare(pi),
151
+ modal(pi),
152
+ nvidia(pi),
153
+ kilo(pi),
154
+ ollama(pi),
155
+ // Qwen is deprecated
156
+ qwen(pi).catch((err) => {
157
+ _logger.warn("[pi-free] Qwen provider failed to load (deprecated)", err);
158
+ }),
159
+ cline(pi),
160
+ ]);
161
+
162
+ // Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face, OpenRouter)
163
+ // These only activate if the user has configured API keys (OpenRouter works without key too)
164
+ const { setupDynamicBuiltInProviders } = await import(
165
+ "./providers/dynamic-built-in/index.ts"
166
+ );
167
+ await setupDynamicBuiltInProviders(pi);
168
+
169
+ // Apply initial global filter if free-only mode is enabled
170
+ if (globalFreeOnly) {
171
+ _logger.info("[pi-free] Applying initial free-only filter");
172
+ await applyGlobalFilter(pi, true);
173
+ }
174
+
175
+ const registry = getProviderRegistry();
176
+ _logger.info(`[pi-free] Loaded with ${registry.size} providers`);
177
+ }
178
+
179
+ // Re-export registry helpers so consumers don't need deep imports
180
+ export {
181
+ applyGlobalFilter,
182
+ getGlobalFreeOnly,
183
+ getProviderRegistry,
184
+ isFreeModel,
185
+ registerWithGlobalToggle,
186
+ };
@@ -5,6 +5,9 @@
5
5
 
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { dirname } from "node:path";
8
+ import { createLogger } from "./logger.ts";
9
+
10
+ const _logger = createLogger("json-persistence");
8
11
 
9
12
  export interface JSONStore<T> {
10
13
  load(): T;
@@ -28,8 +31,7 @@ export function createJSONStore<T extends object>(
28
31
  return cached;
29
32
  }
30
33
  } catch (err) {
31
- // Silently fail and return default
32
- void err;
34
+ _logger.warn("Failed to load JSON store, using default", { filepath, error: err });
33
35
  }
34
36
  cached = defaultValue;
35
37
  return cached;
@@ -44,8 +46,7 @@ export function createJSONStore<T extends object>(
44
46
  }
45
47
  writeFileSync(filepath, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
46
48
  } catch (err) {
47
- // Silently fail - persistence is best-effort
48
- void err;
49
+ _logger.warn("Failed to save JSON store", { filepath, error: err });
49
50
  }
50
51
  }
51
52
 
@@ -71,8 +72,8 @@ export function createJSONLStore<T extends object>(
71
72
  .filter((line) => line.trim())
72
73
  .map((line) => JSON.parse(line) as T);
73
74
  }
74
- } catch {
75
- // Return empty on error
75
+ } catch (err) {
76
+ _logger.warn("Failed to load JSONL store, using empty array", { filepath, error: err });
76
77
  }
77
78
  return [];
78
79
  }
@@ -85,16 +86,16 @@ export function createJSONLStore<T extends object>(
85
86
  }
86
87
  const line = JSON.stringify(entry);
87
88
  writeFileSync(filepath, `${line}\n`, { flag: "a", encoding: "utf-8" });
88
- } catch {
89
- // Silently fail
89
+ } catch (err) {
90
+ _logger.warn("Failed to append to JSONL store", { filepath, error: err });
90
91
  }
91
92
  }
92
93
 
93
94
  function clear(): void {
94
95
  try {
95
96
  writeFileSync(filepath, "", "utf-8");
96
- } catch {
97
- // Silently fail
97
+ } catch (err) {
98
+ _logger.warn("Failed to clear JSONL store", { filepath, error: err });
98
99
  }
99
100
  }
100
101
 
package/lib/logger.ts CHANGED
@@ -63,8 +63,8 @@ function appendToFile(line: string): void {
63
63
  mkdirSync(dir, { recursive: true });
64
64
  }
65
65
  appendFileSync(LOG_PATH, `${line}\n`, "utf8");
66
- } catch {
67
- // Never throw from logger
66
+ } catch (err) {
67
+ console.error("Failed to write to log file:", err);
68
68
  }
69
69
  }
70
70
 
@@ -1,20 +1,20 @@
1
- /**
2
- * Model name enhancement helper
3
- * Adds Coding Index scores to model names for display in /model
4
- */
5
-
6
- import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
7
- import { enhanceModelNameWithCodingIndex } from "../provider-failover/hardcoded-benchmarks.ts";
8
-
9
- /**
10
- * Enhance model names with Coding Index scores
11
- * Use this before registering providers to show CI in /model list
12
- */
13
- export function enhanceModelsWithCodingIndex(
14
- models: ProviderModelConfig[],
15
- ): ProviderModelConfig[] {
16
- return models.map((m) => ({
17
- ...m,
18
- name: enhanceModelNameWithCodingIndex(m.name, m.id),
19
- }));
20
- }
1
+ /**
2
+ * Model name enhancement helper
3
+ * Adds Coding Index scores to model names for display in /model
4
+ */
5
+
6
+ import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
7
+ import { enhanceModelNameWithCodingIndex } from "../provider-failover/benchmark-lookup.ts";
8
+
9
+ /**
10
+ * Enhance model names with Coding Index scores
11
+ * Use this before registering providers to show CI in /model list
12
+ */
13
+ export function enhanceModelsWithCodingIndex(
14
+ models: ProviderModelConfig[],
15
+ ): ProviderModelConfig[] {
16
+ return models.map((m) => ({
17
+ ...m,
18
+ name: enhanceModelNameWithCodingIndex(m.name, m.id),
19
+ }));
20
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Cross-platform browser opener
3
+ *
4
+ * Opens a URL in the user's default browser. Handles URL-unsafe characters
5
+ * on Windows by using PowerShell's Start-Process instead of cmd.exe.
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+
10
+ /**
11
+ * Open a URL in the user's default browser.
12
+ *
13
+ * - Windows: uses PowerShell Start-Process (cmd.exe interprets & as command separator)
14
+ * - macOS: uses `open`
15
+ * - Linux/BSD: uses `xdg-open`
16
+ */
17
+ export function openBrowser(url: string): void {
18
+ try {
19
+ if (process.platform === "win32") {
20
+ // PowerShell's Start-Process treats the URL as a literal string,
21
+ // unlike cmd.exe which interprets & as a command separator.
22
+ spawn(
23
+ "powershell.exe",
24
+ [
25
+ "-NoProfile",
26
+ "-NonInteractive",
27
+ "-Command",
28
+ `Start-Process "${url.replace(/"/g, '\\"')}"`,
29
+ ],
30
+ { detached: true, shell: false, windowsHide: true },
31
+ ).unref();
32
+ } else if (process.platform === "darwin") {
33
+ spawn("open", [url], { detached: true }).unref();
34
+ } else {
35
+ spawn("xdg-open", [url], { detached: true }).unref();
36
+ }
37
+ } catch (err) {
38
+ // Best-effort — browser opening is non-critical
39
+ console.debug("Failed to open browser:", err);
40
+ }
41
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Provider Model Cache
3
+ *
4
+ * Caches provider model lists to disk for faster startup and offline use.
5
+ *
6
+ * Flow:
7
+ * 1. On session_start: fetch fresh models from API, save to cache
8
+ * 2. On extension load: register cached models immediately (shows in --list-models)
9
+ * 3. If API fails: use cached models as fallback
10
+ */
11
+
12
+ import { homedir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { createJSONStore } from "./json-persistence.ts";
15
+ import { createLogger } from "./logger.ts";
16
+ import type { ProviderModelConfig } from "./types.ts";
17
+
18
+ const _logger = createLogger("provider-cache");
19
+
20
+ // =============================================================================
21
+ // Types
22
+ // =============================================================================
23
+
24
+ export interface CachedProviderModels {
25
+ /** Provider ID */
26
+ provider: string;
27
+ /** Cached model list */
28
+ models: ProviderModelConfig[];
29
+ /** When these models were fetched */
30
+ fetchedAt: string; // ISO timestamp
31
+ }
32
+
33
+ interface CacheData {
34
+ providers: Record<string, CachedProviderModels>;
35
+ }
36
+
37
+ // =============================================================================
38
+ // Cache Store
39
+ // =============================================================================
40
+
41
+ const CACHE_FILE = join(homedir(), ".pi", "provider-cache.json");
42
+
43
+ const _cache = createJSONStore<CacheData>(CACHE_FILE, { providers: {} });
44
+
45
+ /**
46
+ * Load cached models for a provider.
47
+ * Returns undefined if no cache exists.
48
+ */
49
+ export function loadProviderCache(
50
+ providerId: string,
51
+ ): ProviderModelConfig[] | undefined {
52
+ const data = _cache.load();
53
+ const cached = data.providers[providerId];
54
+
55
+ if (!cached) {
56
+ return undefined;
57
+ }
58
+
59
+ _logger.debug(`Loaded cached models for ${providerId}`, {
60
+ count: cached.models.length,
61
+ fetchedAt: cached.fetchedAt,
62
+ });
63
+
64
+ return cached.models;
65
+ }
66
+
67
+ /**
68
+ * Save models to cache for a provider.
69
+ */
70
+ export function saveProviderCache(
71
+ providerId: string,
72
+ models: ProviderModelConfig[],
73
+ ): void {
74
+ const data = _cache.load();
75
+
76
+ data.providers[providerId] = {
77
+ provider: providerId,
78
+ models,
79
+ fetchedAt: new Date().toISOString(),
80
+ };
81
+
82
+ _cache.save(data);
83
+
84
+ _logger.debug(`Saved ${models.length} models to cache for ${providerId}`);
85
+ }
86
+
87
+ /**
88
+ * Clear cached models for a provider.
89
+ */
90
+ export function clearProviderCache(providerId: string): void {
91
+ const data = _cache.load();
92
+
93
+ if (data.providers[providerId]) {
94
+ delete data.providers[providerId];
95
+ _cache.save(data);
96
+ _logger.debug(`Cleared cache for ${providerId}`);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Clear all provider caches.
102
+ */
103
+ export function clearAllProviderCaches(): void {
104
+ _cache.save({ providers: {} });
105
+ _logger.debug("Cleared all provider caches");
106
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Global Provider Registry for pi-free.
3
+ *
4
+ * Decoupled from index.ts so providers can import toggle logic
5
+ * without creating a circular dependency.
6
+ */
7
+
8
+ import type {
9
+ ExtensionAPI,
10
+ ProviderModelConfig,
11
+ } from "@mariozechner/pi-coding-agent";
12
+ import { getFreeOnly, saveConfig } from "../config.ts";
13
+ import { createLogger } from "./logger.ts";
14
+
15
+ const _logger = createLogger("pi-free");
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ interface ProviderEntry {
22
+ id: string;
23
+ stored: { free: ProviderModelConfig[]; all: ProviderModelConfig[] };
24
+ reRegister: (models: ProviderModelConfig[]) => void;
25
+ hasKey: boolean;
26
+ }
27
+
28
+ // =============================================================================
29
+ // State
30
+ // =============================================================================
31
+
32
+ const providerRegistry = new Map<string, ProviderEntry>();
33
+ let globalFreeOnly = getFreeOnly();
34
+
35
+ // Providers that expose actual per-model pricing via API
36
+ const PRICING_EXPOSED_PROVIDERS = new Set([
37
+ "openrouter",
38
+ "opencode",
39
+ "kilo",
40
+ "cline",
41
+ ]);
42
+
43
+ // =============================================================================
44
+ // Free-model detection
45
+ // =============================================================================
46
+
47
+ /**
48
+ * Check if a model is free.
49
+ *
50
+ * For providers with pricing APIs: uses cost (input === 0 && output === 0)
51
+ * For providers without pricing: ONLY uses name-based check (name includes "free")
52
+ */
53
+ export function isFreeModel(
54
+ model: ProviderModelConfig & { provider?: string },
55
+ ): boolean {
56
+ const provider = model.provider;
57
+ const hasPricing = provider && PRICING_EXPOSED_PROVIDERS.has(provider);
58
+
59
+ // For providers WITH pricing API: cost-based check
60
+ if (hasPricing) {
61
+ if ((model.cost?.input ?? 0) === 0 && (model.cost?.output ?? 0) === 0) {
62
+ return true;
63
+ }
64
+ }
65
+
66
+ // For providers WITHOUT pricing API: ONLY name-based check
67
+ if (model.name.toLowerCase().includes("free")) {
68
+ return true;
69
+ }
70
+
71
+ return false;
72
+ }
73
+
74
+ // =============================================================================
75
+ // Registration
76
+ // =============================================================================
77
+
78
+ /** Register a provider with the global free/paid toggle system */
79
+ export function registerWithGlobalToggle(
80
+ providerId: string,
81
+ stored: { free: ProviderModelConfig[]; all: ProviderModelConfig[] },
82
+ reRegister: (models: ProviderModelConfig[]) => void,
83
+ hasKey: boolean = false,
84
+ ): void {
85
+ providerRegistry.set(providerId, {
86
+ id: providerId,
87
+ stored,
88
+ reRegister,
89
+ hasKey,
90
+ });
91
+ _logger.info(
92
+ `[pi-free] Registered ${providerId} with global toggle (${stored.free.length} free, ${stored.all.length} total)`,
93
+ );
94
+ }
95
+
96
+ /** Get current global free-only state */
97
+ export function getGlobalFreeOnly(): boolean {
98
+ return globalFreeOnly;
99
+ }
100
+
101
+ /** Access the raw registry (used by /free-providers command) */
102
+ export function getProviderRegistry(): ReadonlyMap<string, ProviderEntry> {
103
+ return providerRegistry;
104
+ }
105
+
106
+ // =============================================================================
107
+ // Global filter application
108
+ // =============================================================================
109
+
110
+ export function applyGlobalFilter(_pi: ExtensionAPI, freeOnly: boolean): void {
111
+ globalFreeOnly = freeOnly;
112
+ saveConfig({ free_only: freeOnly });
113
+
114
+ for (const [providerId, entry] of providerRegistry) {
115
+ try {
116
+ if (freeOnly) {
117
+ // Show only free models
118
+ if (entry.stored.free.length > 0) {
119
+ entry.reRegister(entry.stored.free);
120
+ _logger.info(
121
+ `[pi-free] ${providerId}: filtered to ${entry.stored.free.length} free models`,
122
+ );
123
+ } else {
124
+ _logger.warn(`[pi-free] ${providerId}: no free models available`);
125
+ }
126
+ } else {
127
+ // Show all models (paid + free)
128
+ const allModels =
129
+ entry.stored.all.length > 0 ? entry.stored.all : entry.stored.free;
130
+ if (allModels.length > 0) {
131
+ entry.reRegister(allModels);
132
+ _logger.info(
133
+ `[pi-free] ${providerId}: showing all ${allModels.length} models`,
134
+ );
135
+ }
136
+ }
137
+ } catch (err) {
138
+ _logger.error(
139
+ `[pi-free] Failed to apply filter to ${providerId}`,
140
+ err instanceof Error ? { error: err.message } : { error: String(err) },
141
+ );
142
+ }
143
+ }
144
+ }