pi-free 2.0.0 → 2.0.1

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.
@@ -1,260 +1,260 @@
1
- /**
2
- * Shared provider setup helpers for pi-free-providers.
3
- * Extracts the common boilerplate pattern repeated across providers:
4
- * - /{provider}-toggle command to switch between free/paid models
5
- * - model_select handler (clear status for other providers)
6
- * - turn_end handler (provider-specific error hook)
7
- * - before_agent_start handler (one-time ToS notice)
8
- */
9
-
10
- import type {
11
- ExtensionAPI,
12
- ProviderModelConfig,
13
- } from "@mariozechner/pi-coding-agent";
14
- import { saveConfig } from "./config.ts";
15
- import { createLogger } from "./lib/logger.ts";
16
- import { enhanceModelNameWithCodingIndex } from "./provider-failover/benchmark-lookup.ts";
17
-
18
- const _logger = createLogger("provider-helper");
19
-
20
- // =============================================================================
21
- // Types
22
- // =============================================================================
23
-
24
- export interface ProviderSetupConfig {
25
- /** Provider identifier (e.g., "kilo", "openrouter"). */
26
- providerId: string;
27
- /** Terms of service URL. If set, shows a one-time notice on first free use. */
28
- tosUrl?: string;
29
- /** When true, suppresses the "free models / set API key" ToS notice. */
30
- hasKey?: boolean;
31
- /** Initial mode - auto-detected from config at startup. */
32
- initialShowPaid?: boolean;
33
- /**
34
- * Called by /{provider}-toggle command to re-register
35
- * the provider with the given model set.
36
- */
37
- reRegister: (models: ProviderModelConfig[], stored: StoredModels) => void;
38
- /** Optional custom error handler. Return true if handled. */
39
- onError?: (
40
- error: unknown,
41
- ctx: {
42
- ui: { notify: (m: string, t: "info" | "warning" | "error") => void };
43
- },
44
- ) => Promise<boolean>;
45
- /** When true, skips creating the /{provider}-toggle command. Useful for providers with only one model. */
46
- skipToggle?: boolean;
47
- }
48
-
49
- export interface StoredModels {
50
- free: ProviderModelConfig[];
51
- all: ProviderModelConfig[];
52
- }
53
-
54
- // =============================================================================
55
- // Provider Registration Helpers
56
- // =============================================================================
57
-
58
- export interface OpenAICompatibleConfig {
59
- /** Provider identifier (e.g., "nvidia", "modal") */
60
- providerId: string;
61
- /** Base URL for the API */
62
- baseUrl: string;
63
- /** Environment variable name for the API key */
64
- apiKey: string;
65
- /** Additional headers to include */
66
- headers?: Record<string, string>;
67
- /** OAuth configuration (optional) */
68
- oauth?: {
69
- name: string;
70
- login: (callbacks: unknown) => Promise<unknown>;
71
- refreshToken?: (cred: unknown) => Promise<unknown>;
72
- getApiKey?: (cred: unknown) => string;
73
- };
74
- }
75
-
76
- /**
77
- * Enhance all model names with Coding Index scores
78
- * Use this for direct provider registration (not through setupProvider)
79
- */
80
- export function enhanceWithCI(
81
- models: ProviderModelConfig[],
82
- ): ProviderModelConfig[] {
83
- return models.map((m) => ({
84
- ...m,
85
- name: enhanceModelNameWithCodingIndex(m.name, m.id),
86
- }));
87
- }
88
-
89
- /**
90
- * Register an OpenAI-compatible provider with standard headers.
91
- * Reduces boilerplate across providers that use the OpenAI API format.
92
- */
93
- export function registerOpenAICompatible(
94
- pi: ExtensionAPI,
95
- config: OpenAICompatibleConfig,
96
- models: ProviderModelConfig[],
97
- ): void {
98
- const { providerId, baseUrl, apiKey, headers, oauth } = config;
99
-
100
- pi.registerProvider(providerId, {
101
- baseUrl,
102
- apiKey,
103
- api: "openai-completions" as const,
104
- headers: {
105
- "User-Agent": "pi-free-providers",
106
- ...headers,
107
- },
108
- models: enhanceWithCI(models),
109
- ...(oauth && { oauth: oauth as any }),
110
- });
111
- }
112
-
113
- /**
114
- * Create a reRegister function for use with setupProvider.
115
- * Returns a function that re-registers the provider with new models.
116
- */
117
- export function createReRegister(
118
- pi: ExtensionAPI,
119
- config: OpenAICompatibleConfig,
120
- ): (models: ProviderModelConfig[]) => void {
121
- return (models: ProviderModelConfig[]) => {
122
- registerOpenAICompatible(pi, config, models);
123
- };
124
- }
125
-
126
- /**
127
- * Create a reRegister function that uses ctx.modelRegistry.registerProvider.
128
- * Used by providers that need to register with runtime context (session_start handlers).
129
- */
130
- export function createCtxReRegister(
131
- ctx: {
132
- modelRegistry: { registerProvider: (id: string, config: unknown) => void };
133
- },
134
- config: OpenAICompatibleConfig,
135
- ): (models: ProviderModelConfig[]) => void {
136
- const { providerId, baseUrl, apiKey, headers, oauth } = config;
137
-
138
- return (models: ProviderModelConfig[]) => {
139
- ctx.modelRegistry.registerProvider(providerId, {
140
- baseUrl,
141
- apiKey,
142
- api: "openai-completions" as const,
143
- headers: {
144
- "User-Agent": "pi-free-providers",
145
- ...headers,
146
- },
147
- models: enhanceWithCI(models),
148
- ...(oauth && { oauth: oauth as any }),
149
- });
150
- };
151
- }
152
-
153
- /**
154
- * Get the config key name for a provider's show_paid setting.
155
- */
156
- function getShowPaidConfigKey(providerId: string): string {
157
- return `${providerId}_show_paid`;
158
- }
159
-
160
- export function setupProvider(
161
- pi: ExtensionAPI,
162
- config: ProviderSetupConfig,
163
- stored: StoredModels,
164
- ): void {
165
- const { providerId, tosUrl, initialShowPaid = false } = config;
166
-
167
- // Track current mode (synced with config)
168
- let currentShowPaid = initialShowPaid;
169
-
170
- // Wrap reRegister to automatically add CI scores to all models
171
- const reRegister = (models: ProviderModelConfig[], _s: StoredModels) => {
172
- const enhanced = enhanceWithCI(models);
173
- config.reRegister(enhanced, _s);
174
- };
175
-
176
- // ── Single toggle command (skip if requested) ──────────────────────
177
-
178
- if (!config.skipToggle) {
179
- pi.registerCommand(`${providerId}-toggle`, {
180
- description: `Toggle between free and all ${providerId} models`,
181
- handler: async (_args, ctx) => {
182
- // Toggle the mode
183
- currentShowPaid = !currentShowPaid;
184
-
185
- // Persist to config file
186
- const configKey = getShowPaidConfigKey(providerId);
187
- saveConfig({ [configKey]: currentShowPaid });
188
-
189
- // Re-register with appropriate model set
190
- if (currentShowPaid) {
191
- if (stored.all.length === 0) {
192
- ctx.ui.notify("No models available", "warning");
193
- return;
194
- }
195
- reRegister(stored.all, stored);
196
- ctx.ui.notify(
197
- `${providerId}: showing all ${stored.all.length} models (including paid)`,
198
- "info",
199
- );
200
- } else {
201
- if (stored.free.length === 0) {
202
- ctx.ui.notify("No free models loaded", "warning");
203
- return;
204
- }
205
- reRegister(stored.free, stored);
206
- ctx.ui.notify(
207
- `${providerId}: showing ${stored.free.length} free models`,
208
- "info",
209
- );
210
- }
211
- },
212
- });
213
- }
214
-
215
- // ── Clear status when another provider is selected ───────────────────
216
-
217
- pi.on("model_select", (_event, ctx) => {
218
- if (_event.model?.provider !== providerId) {
219
- ctx.ui.setStatus(`${providerId}-status`, undefined);
220
- }
221
- });
222
-
223
- // ── Error handling / usage tracking are temporarily deprecated ─────────
224
-
225
- pi.on("turn_end", async (event, ctx) => {
226
- if (ctx.model?.provider !== providerId) return;
227
-
228
- const msg = (
229
- event as { message?: { role?: string; errorMessage?: string } }
230
- ).message;
231
-
232
- if (msg?.role === "assistant" && msg.errorMessage) {
233
- _logger.info("Provider error (auto model hopping disabled)", {
234
- provider: providerId,
235
- error: msg.errorMessage.slice(0, 100),
236
- });
237
-
238
- // Keep custom handlers working for provider-specific logic.
239
- if (config.onError) {
240
- await config.onError(msg.errorMessage, ctx);
241
- }
242
- }
243
- });
244
-
245
- // ── ToS notice on first use ────────────────────────────────
246
- if (tosUrl) {
247
- let tosShown = false;
248
- pi.on("model_select", async (_event, ctx) => {
249
- if (tosShown || ctx.model?.provider !== providerId) return;
250
- tosShown = true;
251
- if (config.hasKey) return;
252
- const cred = ctx.modelRegistry.authStorage.get(providerId);
253
- if (cred?.type === "oauth") return;
254
- ctx.ui.notify(
255
- `Using ${providerId} free models. Set API key for paid access. Terms: ${tosUrl}`,
256
- "info",
257
- );
258
- });
259
- }
260
- }
1
+ /**
2
+ * Shared provider setup helpers for pi-free-providers.
3
+ * Extracts the common boilerplate pattern repeated across providers:
4
+ * - toggle-{provider} command to switch between free/paid models
5
+ * - model_select handler (clear status for other providers)
6
+ * - turn_end handler (provider-specific error hook)
7
+ * - before_agent_start handler (one-time ToS notice)
8
+ */
9
+
10
+ import type {
11
+ ExtensionAPI,
12
+ ProviderModelConfig,
13
+ } from "@mariozechner/pi-coding-agent";
14
+ import { saveConfig } from "./config.ts";
15
+ import { createLogger } from "./lib/logger.ts";
16
+ import { enhanceModelNameWithCodingIndex } from "./provider-failover/benchmark-lookup.ts";
17
+
18
+ const _logger = createLogger("provider-helper");
19
+
20
+ // =============================================================================
21
+ // Types
22
+ // =============================================================================
23
+
24
+ export interface ProviderSetupConfig {
25
+ /** Provider identifier (e.g., "kilo", "openrouter"). */
26
+ providerId: string;
27
+ /** Terms of service URL. If set, shows a one-time notice on first free use. */
28
+ tosUrl?: string;
29
+ /** When true, suppresses the "free models / set API key" ToS notice. */
30
+ hasKey?: boolean;
31
+ /** Initial mode - auto-detected from config at startup. */
32
+ initialShowPaid?: boolean;
33
+ /**
34
+ * Called by toggle-{provider} command to re-register
35
+ * the provider with the given model set.
36
+ */
37
+ reRegister: (models: ProviderModelConfig[], stored: StoredModels) => void;
38
+ /** Optional custom error handler. Return true if handled. */
39
+ onError?: (
40
+ error: unknown,
41
+ ctx: {
42
+ ui: { notify: (m: string, t: "info" | "warning" | "error") => void };
43
+ },
44
+ ) => Promise<boolean>;
45
+ /** When true, skips creating the toggle-{provider} command. Useful for providers with only one model. */
46
+ skipToggle?: boolean;
47
+ }
48
+
49
+ export interface StoredModels {
50
+ free: ProviderModelConfig[];
51
+ all: ProviderModelConfig[];
52
+ }
53
+
54
+ // =============================================================================
55
+ // Provider Registration Helpers
56
+ // =============================================================================
57
+
58
+ export interface OpenAICompatibleConfig {
59
+ /** Provider identifier (e.g., "nvidia", "modal") */
60
+ providerId: string;
61
+ /** Base URL for the API */
62
+ baseUrl: string;
63
+ /** Environment variable name for the API key */
64
+ apiKey: string;
65
+ /** Additional headers to include */
66
+ headers?: Record<string, string>;
67
+ /** OAuth configuration (optional) */
68
+ oauth?: {
69
+ name: string;
70
+ login: (callbacks: unknown) => Promise<unknown>;
71
+ refreshToken?: (cred: unknown) => Promise<unknown>;
72
+ getApiKey?: (cred: unknown) => string;
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Enhance all model names with Coding Index scores
78
+ * Use this for direct provider registration (not through setupProvider)
79
+ */
80
+ export function enhanceWithCI(
81
+ models: ProviderModelConfig[],
82
+ ): ProviderModelConfig[] {
83
+ return models.map((m) => ({
84
+ ...m,
85
+ name: enhanceModelNameWithCodingIndex(m.name, m.id),
86
+ }));
87
+ }
88
+
89
+ /**
90
+ * Register an OpenAI-compatible provider with standard headers.
91
+ * Reduces boilerplate across providers that use the OpenAI API format.
92
+ */
93
+ export function registerOpenAICompatible(
94
+ pi: ExtensionAPI,
95
+ config: OpenAICompatibleConfig,
96
+ models: ProviderModelConfig[],
97
+ ): void {
98
+ const { providerId, baseUrl, apiKey, headers, oauth } = config;
99
+
100
+ pi.registerProvider(providerId, {
101
+ baseUrl,
102
+ apiKey,
103
+ api: "openai-completions" as const,
104
+ headers: {
105
+ "User-Agent": "pi-free-providers",
106
+ ...headers,
107
+ },
108
+ models: enhanceWithCI(models),
109
+ ...(oauth && { oauth: oauth as any }),
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Create a reRegister function for use with setupProvider.
115
+ * Returns a function that re-registers the provider with new models.
116
+ */
117
+ export function createReRegister(
118
+ pi: ExtensionAPI,
119
+ config: OpenAICompatibleConfig,
120
+ ): (models: ProviderModelConfig[]) => void {
121
+ return (models: ProviderModelConfig[]) => {
122
+ registerOpenAICompatible(pi, config, models);
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Create a reRegister function that uses ctx.modelRegistry.registerProvider.
128
+ * Used by providers that need to register with runtime context (session_start handlers).
129
+ */
130
+ export function createCtxReRegister(
131
+ ctx: {
132
+ modelRegistry: { registerProvider: (id: string, config: unknown) => void };
133
+ },
134
+ config: OpenAICompatibleConfig,
135
+ ): (models: ProviderModelConfig[]) => void {
136
+ const { providerId, baseUrl, apiKey, headers, oauth } = config;
137
+
138
+ return (models: ProviderModelConfig[]) => {
139
+ ctx.modelRegistry.registerProvider(providerId, {
140
+ baseUrl,
141
+ apiKey,
142
+ api: "openai-completions" as const,
143
+ headers: {
144
+ "User-Agent": "pi-free-providers",
145
+ ...headers,
146
+ },
147
+ models: enhanceWithCI(models),
148
+ ...(oauth && { oauth: oauth as any }),
149
+ });
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Get the config key name for a provider's show_paid setting.
155
+ */
156
+ function getShowPaidConfigKey(providerId: string): string {
157
+ return `${providerId}_show_paid`;
158
+ }
159
+
160
+ export function setupProvider(
161
+ pi: ExtensionAPI,
162
+ config: ProviderSetupConfig,
163
+ stored: StoredModels,
164
+ ): void {
165
+ const { providerId, tosUrl, initialShowPaid = false } = config;
166
+
167
+ // Track current mode (synced with config)
168
+ let currentShowPaid = initialShowPaid;
169
+
170
+ // Wrap reRegister to automatically add CI scores to all models
171
+ const reRegister = (models: ProviderModelConfig[], _s: StoredModels) => {
172
+ const enhanced = enhanceWithCI(models);
173
+ config.reRegister(enhanced, _s);
174
+ };
175
+
176
+ // ── Single toggle command (skip if requested) ──────────────────────
177
+
178
+ if (!config.skipToggle) {
179
+ pi.registerCommand(`toggle-${providerId}`, {
180
+ description: `Toggle between free and all ${providerId} models`,
181
+ handler: async (_args, ctx) => {
182
+ // Toggle the mode
183
+ currentShowPaid = !currentShowPaid;
184
+
185
+ // Persist to config file
186
+ const configKey = getShowPaidConfigKey(providerId);
187
+ saveConfig({ [configKey]: currentShowPaid });
188
+
189
+ // Re-register with appropriate model set
190
+ if (currentShowPaid) {
191
+ if (stored.all.length === 0) {
192
+ ctx.ui.notify("No models available", "warning");
193
+ return;
194
+ }
195
+ reRegister(stored.all, stored);
196
+ ctx.ui.notify(
197
+ `${providerId}: showing all ${stored.all.length} models (including paid)`,
198
+ "info",
199
+ );
200
+ } else {
201
+ if (stored.free.length === 0) {
202
+ ctx.ui.notify("No free models loaded", "warning");
203
+ return;
204
+ }
205
+ reRegister(stored.free, stored);
206
+ ctx.ui.notify(
207
+ `${providerId}: showing ${stored.free.length} free models`,
208
+ "info",
209
+ );
210
+ }
211
+ },
212
+ });
213
+ }
214
+
215
+ // ── Clear status when another provider is selected ───────────────────
216
+
217
+ pi.on("model_select", (_event, ctx) => {
218
+ if (_event.model?.provider !== providerId) {
219
+ ctx.ui.setStatus(`${providerId}-status`, undefined);
220
+ }
221
+ });
222
+
223
+ // ── Error handling / usage tracking are temporarily deprecated ─────────
224
+
225
+ pi.on("turn_end", async (event, ctx) => {
226
+ if (ctx.model?.provider !== providerId) return;
227
+
228
+ const msg = (
229
+ event as { message?: { role?: string; errorMessage?: string } }
230
+ ).message;
231
+
232
+ if (msg?.role === "assistant" && msg.errorMessage) {
233
+ _logger.info("Provider error (auto model hopping disabled)", {
234
+ provider: providerId,
235
+ error: msg.errorMessage.slice(0, 100),
236
+ });
237
+
238
+ // Keep custom handlers working for provider-specific logic.
239
+ if (config.onError) {
240
+ await config.onError(msg.errorMessage, ctx);
241
+ }
242
+ }
243
+ });
244
+
245
+ // ── ToS notice on first use ────────────────────────────────
246
+ if (tosUrl) {
247
+ let tosShown = false;
248
+ pi.on("model_select", async (_event, ctx) => {
249
+ if (tosShown || ctx.model?.provider !== providerId) return;
250
+ tosShown = true;
251
+ if (config.hasKey) return;
252
+ const cred = ctx.modelRegistry.authStorage.get(providerId);
253
+ if (cred?.type === "oauth") return;
254
+ ctx.ui.notify(
255
+ `Using ${providerId} free models. Set API key for paid access. Terms: ${tosUrl}`,
256
+ "info",
257
+ );
258
+ });
259
+ }
260
+ }
@@ -2,7 +2,7 @@
2
2
  * Cline model fetching.
3
3
  *
4
4
  * Fetches ALL models from OpenRouter (Cline's gateway).
5
- * Free/paid filtering is handled by the global /free toggle.
5
+ * Free/paid filtering is handled by the global free-only filter.
6
6
  */
7
7
 
8
8
  import { applyHidden } from "../../config.ts";
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Auth flow based on pi-cline's proven implementation.
9
9
  *
10
- * Responds to global /free toggle (though Cline only provides free models without auth).
10
+ * Responds to global free-only filter (though Cline only provides free models without auth).
11
11
  *
12
12
  * Usage:
13
13
  * pi install git:github.com/apmantza/pi-free
@@ -193,13 +193,13 @@ function shapeMessagesForCline(messages: any[]): any[] {
193
193
 
194
194
  export default async function (pi: ExtensionAPI) {
195
195
  // Fetch ALL models from OpenRouter (free and paid)
196
- // The global /free toggle will filter based on cost.input
196
+ // The global free-only filter will filter based on cost.input
197
197
  let allModels = await fetchClineModels(false).catch((err) => {
198
198
  logWarning("cline", "Failed to fetch models at startup", err);
199
199
  return [];
200
200
  });
201
201
 
202
- // Also fetch free-only list for the global toggle's free filter
202
+ // Also fetch free-only list for the global free-only filter
203
203
  let freeModels = allModels.filter((m) => m.cost.input === 0);
204
204
 
205
205
  // Create re-register function for global toggle
@@ -230,10 +230,9 @@ export default async function (pi: ExtensionAPI) {
230
230
  // Initial registration with all models
231
231
  reRegister(allModels);
232
232
 
233
- // Per-provider toggle command (works independently of global /free)
233
+ // Per-provider toggle command
234
234
  let showPaidModels = false;
235
- let currentModels = allModels;
236
- pi.registerCommand("cline-toggle", {
235
+ pi.registerCommand("toggle-cline", {
237
236
  description: "Toggle between free and all Cline models",
238
237
  handler: async (_args, ctx) => {
239
238
  showPaidModels = !showPaidModels;
@@ -242,7 +241,6 @@ export default async function (pi: ExtensionAPI) {
242
241
  const modelsToShow =
243
242
  showPaidModels && allModels.length > 0 ? allModels : freeModels;
244
243
 
245
- currentModels = modelsToShow;
246
244
  reRegister(modelsToShow);
247
245
 
248
246
  const freeCount = freeModels.length;