pi-free 2.0.2 → 2.0.4

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/index.ts CHANGED
@@ -1,148 +1,175 @@
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
- * - Ollama Cloud: Ollama's cloud-hosted models with usage-based free tier
12
- * - Qwen: OAuth-based Qwen access (deprecated)
13
- * - Modal: Modal Labs hosting
14
- */
15
-
16
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
17
- import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
18
- import { createLogger } from "./lib/logger.ts";
19
- import {
20
- applyGlobalFilter,
21
- getGlobalFreeOnly,
22
- getProviderRegistry,
23
- isFreeModel,
24
- registerWithGlobalToggle,
25
- } from "./lib/registry.ts";
26
- // Import unique provider extensions (only providers NOT built into pi)
27
- import cline from "./providers/cline/cline.ts";
28
- import cloudflare from "./providers/cloudflare/cloudflare.ts";
29
- import kilo from "./providers/kilo/kilo.ts";
30
- import modal from "./providers/modal/modal.ts";
31
- import nvidia from "./providers/nvidia/nvidia.ts";
32
- import ollama from "./providers/ollama/ollama.ts";
33
- import qwen from "./providers/qwen/qwen.ts";
34
-
35
- const _logger = createLogger("pi-free");
36
-
37
- // =============================================================================
38
- // Global Commands
39
- // =============================================================================
40
-
41
- function setupGlobalCommands(pi: ExtensionAPI) {
42
- // /free-providers - Show free model counts by provider
43
- pi.registerCommand("free-providers", {
44
- description: "Show free/paid model counts for all pi-free providers",
45
- handler: async (_args, ctx) => {
46
- const lines = ["📊 Pi-Free Providers:", ""];
47
- const registry = getProviderRegistry();
48
-
49
- // Providers known to not expose pricing via API (all models show as "free")
50
- // OpenRouter and OpenCode expose actual pricing
51
- const noPricingApi = new Set([
52
- "mistral",
53
- "xai",
54
- "huggingface",
55
- "groq",
56
- "cerebras",
57
- ]);
58
- // Freemium providers - all models share a free tier quota
59
- const freemiumProviders = new Set(["nvidia"]);
60
-
61
- for (const [id, entry] of registry) {
62
- const free = entry.stored.free.length;
63
- const all = entry.stored.all.length || free;
64
- const indicator = entry.hasKey ? "🔑" : "🆓";
65
- const paid = all - free;
66
-
67
- if (freemiumProviders.has(id)) {
68
- // Freemium: all models share a free tier (e.g., 1,000 reqs/month)
69
- lines.push(`${indicator} ${id}: ${all} models (freemium)`);
70
- } else if (noPricingApi.has(id)) {
71
- // Provider doesn't expose pricing - can't determine free vs paid
72
- lines.push(
73
- `${indicator} ${id}: ${all} models (pricing not exposed by API)`,
74
- );
75
- } else if (paid === 0 && free > 0) {
76
- // All models are actually free
77
- lines.push(`${indicator} ${id}: ${free} free models`);
78
- } else {
79
- // Mix of free and paid
80
- lines.push(
81
- `${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
82
- );
83
- }
84
- }
85
-
86
- if (registry.size === 0) {
87
- lines.push("(No providers registered yet)");
88
- }
89
-
90
- ctx.ui.notify(lines.join("\n"), "info");
91
- },
92
- });
93
- }
94
-
95
- // =============================================================================
96
- // Main Entry Point
97
- // =============================================================================
98
-
99
- export default async function (pi: ExtensionAPI) {
100
- const globalFreeOnly = getGlobalFreeOnly();
101
- _logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
102
-
103
- // Setup global commands first
104
- setupGlobalCommands(pi);
105
-
106
- // Load all unique providers
107
- // Each provider will register itself with the global toggle system
108
- await Promise.allSettled([
109
- cloudflare(pi),
110
- modal(pi),
111
- nvidia(pi),
112
- kilo(pi),
113
- ollama(pi),
114
- // Qwen is deprecated
115
- qwen(pi).catch((err) => {
116
- _logger.warn("[pi-free] Qwen provider failed to load (deprecated)", err);
117
- }),
118
- cline(pi),
119
- ]);
120
-
121
- // Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face, OpenRouter)
122
- // These only activate if the user has configured API keys (OpenRouter works without key too)
123
- const { setupDynamicBuiltInProviders } = await import(
124
- "./providers/dynamic-built-in/index.ts"
125
- );
126
- await setupDynamicBuiltInProviders(pi);
127
-
128
- // Setup toggles for pi's built-in providers (e.g., OpenCode)
129
- setupBuiltInProviderToggles(pi);
130
-
131
- // Apply initial global filter if free-only mode is enabled
132
- if (globalFreeOnly) {
133
- _logger.info("[pi-free] Applying initial free-only filter");
134
- await applyGlobalFilter(pi, true);
135
- }
136
-
137
- const registry = getProviderRegistry();
138
- _logger.info(`[pi-free] Loaded with ${registry.size} providers`);
139
- }
140
-
141
- // Re-export registry helpers so consumers don't need deep imports
142
- export {
143
- applyGlobalFilter,
144
- getGlobalFreeOnly,
145
- getProviderRegistry,
146
- isFreeModel,
147
- registerWithGlobalToggle,
148
- };
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
+ * - Ollama Cloud: Ollama's cloud-hosted models with usage-based free tier
12
+ * - ZenMux: Unified AI API gateway with 200+ models
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
+ import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
17
+ import { createLogger } from "./lib/logger.ts";
18
+ import {
19
+ applyGlobalFilter,
20
+ getGlobalFreeOnly,
21
+ getProviderRegistry,
22
+ isFreeModel,
23
+ registerWithGlobalToggle,
24
+ } from "./lib/registry.ts";
25
+ // Import unique provider extensions (only providers NOT built into pi)
26
+ import cline from "./providers/cline/cline.ts";
27
+ import crofai from "./providers/crofai/crofai.ts";
28
+ import kilo from "./providers/kilo/kilo.ts";
29
+ import nvidia from "./providers/nvidia/nvidia.ts";
30
+ import ollama from "./providers/ollama/ollama.ts";
31
+ import zenmux from "./providers/zenmux/zenmux.ts";
32
+
33
+ const _logger = createLogger("pi-free");
34
+
35
+ // =============================================================================
36
+ // Global Commands
37
+ // =============================================================================
38
+
39
+ function setupGlobalCommands(pi: ExtensionAPI) {
40
+ // /toggle-free - Global free-only mode toggle
41
+ pi.registerCommand("toggle-free", {
42
+ description: "Toggle global free-only mode for all providers",
43
+ handler: async (_args, ctx) => {
44
+ const current = getGlobalFreeOnly();
45
+ const next = !current;
46
+ applyGlobalFilter(pi, next);
47
+
48
+ const registry = getProviderRegistry();
49
+ const providerCount = registry.size;
50
+
51
+ if (next) {
52
+ const totalFree = [...registry.values()].reduce(
53
+ (sum, e) => sum + e.stored.free.length,
54
+ 0,
55
+ );
56
+ ctx.ui.notify(
57
+ `Free-only mode: ON (${totalFree} free models across ${providerCount} providers)`,
58
+ "info",
59
+ );
60
+ } else {
61
+ const totalAll = [...registry.values()].reduce(
62
+ (sum, e) => sum + (e.stored.all.length || e.stored.free.length),
63
+ 0,
64
+ );
65
+ ctx.ui.notify(
66
+ `Free-only mode: OFF (all ${totalAll} models visible across ${providerCount} providers)`,
67
+ "info",
68
+ );
69
+ }
70
+ },
71
+ });
72
+
73
+ // /free-providers - Show free model counts by provider
74
+ pi.registerCommand("free-providers", {
75
+ description: "Show free/paid model counts for all pi-free providers",
76
+ handler: async (_args, ctx) => {
77
+ const lines = ["📊 Pi-Free Providers:", ""];
78
+ const registry = getProviderRegistry();
79
+
80
+ // Providers known to not expose pricing via API (all models show as "free")
81
+ // OpenRouter and OpenCode expose actual pricing
82
+ const noPricingApi = new Set([
83
+ "mistral",
84
+ "xai",
85
+ "huggingface",
86
+ "groq",
87
+ "cerebras",
88
+ ]);
89
+ // Freemium providers - all models share a free tier quota
90
+ const freemiumProviders = new Set(["nvidia"]);
91
+
92
+ for (const [id, entry] of registry) {
93
+ const free = entry.stored.free.length;
94
+ const all = entry.stored.all.length || free;
95
+ const indicator = entry.hasKey ? "🔑" : "🆓";
96
+ const paid = all - free;
97
+
98
+ if (freemiumProviders.has(id)) {
99
+ // Freemium: all models share a free tier (e.g., 1,000 reqs/month)
100
+ lines.push(`${indicator} ${id}: ${all} models (freemium)`);
101
+ } else if (noPricingApi.has(id)) {
102
+ // Provider doesn't expose pricing - can't determine free vs paid
103
+ lines.push(
104
+ `${indicator} ${id}: ${all} models (pricing not exposed by API)`,
105
+ );
106
+ } else if (paid === 0 && free > 0) {
107
+ // All models are actually free
108
+ lines.push(`${indicator} ${id}: ${free} free models`);
109
+ } else {
110
+ // Mix of free and paid
111
+ lines.push(
112
+ `${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
113
+ );
114
+ }
115
+ }
116
+
117
+ if (registry.size === 0) {
118
+ lines.push("(No providers registered yet)");
119
+ }
120
+
121
+ ctx.ui.notify(lines.join("\n"), "info");
122
+ },
123
+ });
124
+ }
125
+
126
+ // =============================================================================
127
+ // Main Entry Point
128
+ // =============================================================================
129
+
130
+ export default async function (pi: ExtensionAPI) {
131
+ const globalFreeOnly = getGlobalFreeOnly();
132
+ _logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
133
+
134
+ // Setup global commands first
135
+ setupGlobalCommands(pi);
136
+
137
+ // Load all unique providers
138
+ // Each provider will register itself with the global toggle system
139
+ await Promise.allSettled([
140
+ nvidia(pi),
141
+ kilo(pi),
142
+ ollama(pi),
143
+ cline(pi),
144
+ zenmux(pi),
145
+ crofai(pi),
146
+ ]);
147
+
148
+ // Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face)
149
+ // These only activate if the user has configured API keys (OpenRouter works without key too)
150
+ const { setupDynamicBuiltInProviders } = await import(
151
+ "./providers/dynamic-built-in/index.ts"
152
+ );
153
+ await setupDynamicBuiltInProviders(pi);
154
+
155
+ // Setup toggles for pi's built-in providers (e.g., OpenCode)
156
+ setupBuiltInProviderToggles(pi);
157
+
158
+ // Apply initial global filter if free-only mode is enabled
159
+ if (globalFreeOnly) {
160
+ _logger.info("[pi-free] Applying initial free-only filter");
161
+ await applyGlobalFilter(pi, true);
162
+ }
163
+
164
+ const registry = getProviderRegistry();
165
+ _logger.info(`[pi-free] Loaded with ${registry.size} providers`);
166
+ }
167
+
168
+ // Re-export registry helpers so consumers don't need deep imports
169
+ export {
170
+ applyGlobalFilter,
171
+ getGlobalFreeOnly,
172
+ getProviderRegistry,
173
+ isFreeModel,
174
+ registerWithGlobalToggle,
175
+ };
@@ -60,6 +60,7 @@ export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
60
60
  for (const config of BUILT_IN_TOGGLE_PROVIDERS) {
61
61
  registerToggleCommand(pi, config);
62
62
  }
63
+ setupStatusBar(pi);
63
64
  commandsRegistered = true;
64
65
  }
65
66
 
@@ -79,7 +80,9 @@ export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
79
80
  if (providerModels.length === 0) continue;
80
81
 
81
82
  const allModels = providerModels.map(modelToProviderConfig);
82
- const freeModels = allModels.filter(isFreeModel);
83
+ const freeModels = allModels.filter((m) =>
84
+ isFreeModel({ ...m, provider: config.id }, allModels),
85
+ );
83
86
 
84
87
  const baseUrl = providerModels[0].baseUrl;
85
88
  const api = providerModels[0].api;
@@ -178,6 +181,42 @@ function modelToProviderConfig(m: Model<Api>): ProviderModelConfig {
178
181
  };
179
182
  }
180
183
 
184
+ // =============================================================================
185
+ // Status bar for provider selection
186
+ // =============================================================================
187
+
188
+ function setupStatusBar(pi: ExtensionAPI): void {
189
+ pi.on("model_select", (_event, ctx) => {
190
+ const selected = _event.model?.provider;
191
+
192
+ // Clear status for all built-in toggle providers
193
+ for (const config of BUILT_IN_TOGGLE_PROVIDERS) {
194
+ if (selected !== config.id) {
195
+ ctx.ui.setStatus(`${config.id}-status`, undefined);
196
+ }
197
+ }
198
+
199
+ if (!selected) return;
200
+
201
+ const state = providerStates.get(selected);
202
+ if (!state) return;
203
+
204
+ const free = state.stored.free.length;
205
+ const total = state.stored.all.length;
206
+ const paid = total - free;
207
+ const mode = state.toggleState.getCurrentMode();
208
+ let status: string;
209
+ if (paid === 0) {
210
+ status = `${selected}: ${free} free models`;
211
+ } else if (mode === "all") {
212
+ status = `${selected}: ${total} models (free + paid)`;
213
+ } else {
214
+ status = `${selected}: ${free} free \u00b7 ${paid} paid`;
215
+ }
216
+ ctx.ui.setStatus(`${selected}-status`, status);
217
+ });
218
+ }
219
+
181
220
  function getApiKeyEnvForProvider(providerId: string): string {
182
221
  const envMap: Record<string, string> = {
183
222
  opencode: "OPENCODE_API_KEY",
@@ -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/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
- }
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
+ }
@@ -25,7 +25,7 @@ export function openBrowser(url: string): void {
25
25
  "-NoProfile",
26
26
  "-NonInteractive",
27
27
  "-Command",
28
- `Start-Process "${url.replace(/"/g, '\\"')}"`,
28
+ `Start-Process "${url.replace(/[\\"]/g, "\\$&")}"`,
29
29
  ],
30
30
  { detached: true, shell: false, windowsHide: true },
31
31
  ).unref();
@@ -0,0 +1,46 @@
1
+ import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
2
+
3
+ export interface ProviderModelIdentity {
4
+ id: string;
5
+ name?: string;
6
+ }
7
+
8
+ export const DEEPSEEK_PROXY_COMPAT: NonNullable<ProviderModelConfig["compat"]> =
9
+ {
10
+ supportsStore: false,
11
+ supportsDeveloperRole: false,
12
+ supportsReasoningEffort: true,
13
+ requiresReasoningContentOnAssistantMessages: true,
14
+ thinkingFormat: "deepseek",
15
+ };
16
+
17
+ export function isDeepSeekModel(model: ProviderModelIdentity): boolean {
18
+ const haystack = `${model.id} ${model.name ?? ""}`.toLowerCase();
19
+ return haystack.includes("deepseek");
20
+ }
21
+
22
+ export function isLikelyReasoningModel(model: ProviderModelIdentity): boolean {
23
+ const haystack = `${model.id} ${model.name ?? ""}`.toLowerCase();
24
+ return (
25
+ isDeepSeekModel(model) ||
26
+ haystack.includes("thinking") ||
27
+ haystack.includes("reasoning") ||
28
+ haystack.includes("reasoner") ||
29
+ haystack.includes("r1") ||
30
+ haystack.includes("qwq")
31
+ );
32
+ }
33
+
34
+ /**
35
+ * For gateway/proxy providers that mask the upstream DeepSeek base URL,
36
+ * add explicit compat so pi-ai preserves and replays reasoning_content.
37
+ */
38
+ export function getProxyModelCompat(
39
+ model: ProviderModelIdentity,
40
+ ): ProviderModelConfig["compat"] | undefined {
41
+ if (isDeepSeekModel(model)) {
42
+ return DEEPSEEK_PROXY_COMPAT;
43
+ }
44
+
45
+ return undefined;
46
+ }