pi-free 2.0.2 → 2.0.5

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/lib/util.ts CHANGED
@@ -1,256 +1,262 @@
1
- import { createLogger } from "./logger.ts";
2
- import type { ProviderModelConfig } from "./types.ts";
3
-
4
- const _logger = createLogger("util");
5
-
6
- // =============================================================================
7
- // Shared Utilities
8
- // =============================================================================
9
-
10
- /**
11
- * Log a warning message for provider operations
12
- */
13
- export function logWarning(
14
- provider: string,
15
- message: string,
16
- error?: unknown,
17
- ): void {
18
- _logger.warn(
19
- `[${provider}] ${message}`,
20
- error ? { error: String(error) } : undefined,
21
- );
22
- }
23
-
24
- /**
25
- * Fetch with timeout using AbortController
26
- */
27
- export async function fetchWithTimeout(
28
- url: string,
29
- options: RequestInit,
30
- timeoutMs = 30000,
31
- ): Promise<Response> {
32
- const controller = new AbortController();
33
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
34
-
35
- try {
36
- const response = await fetch(url, {
37
- ...options,
38
- signal: controller.signal,
39
- });
40
- return response;
41
- } finally {
42
- clearTimeout(timeoutId);
43
- }
44
- }
45
-
46
- /**
47
- * Fetch with retry logic and timeout
48
- */
49
- export async function fetchWithRetry(
50
- url: string,
51
- options: RequestInit,
52
- retries = 3,
53
- delayMs = 1000,
54
- timeoutMs = 30000,
55
- ): Promise<Response> {
56
- let lastError: unknown;
57
-
58
- for (let i = 0; i < retries; i++) {
59
- try {
60
- const response = await fetchWithTimeout(url, options, timeoutMs);
61
- if (response.ok) return response;
62
-
63
- // If it's a rate limit, throw immediately
64
- if (response.status === 429) {
65
- throw new Error(`Rate limited (429)`);
66
- }
67
-
68
- // For server errors, retry
69
- if (response.status >= 500) {
70
- lastError = new Error(`Server error ${response.status}`);
71
- if (i < retries - 1) {
72
- await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
73
- continue;
74
- }
75
- // Last retry exhausted - throw the error
76
- throw lastError;
77
- }
78
-
79
- return response; // Return non-ok but non-retryable responses
80
- } catch (error) {
81
- lastError = error;
82
- if (i < retries - 1) {
83
- await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
84
- }
85
- }
86
- }
87
-
88
- throw lastError;
89
- }
90
-
91
- // =============================================================================
92
- // Shared API Response Parsing
93
- // =============================================================================
94
-
95
- /**
96
- * Parse and validate model list API response
97
- * Shared between Kilo, OpenRouter, and other providers
98
- */
99
- export async function parseModelResponse<T>(
100
- response: Response,
101
- providerName: string,
102
- ): Promise<{ data: T[] }> {
103
- if (!response.ok) {
104
- throw new Error(
105
- `Failed to fetch ${providerName} models: ${response.status} ${response.statusText}`,
106
- );
107
- }
108
-
109
- const json = (await response.json()) as { data?: T[] };
110
-
111
- if (!json.data || !Array.isArray(json.data)) {
112
- throw new Error(
113
- `Invalid ${providerName} models response: missing data array`,
114
- );
115
- }
116
-
117
- return { data: json.data };
118
- }
119
-
120
- // =============================================================================
121
- // Model Filtering Utilities
122
- // =============================================================================
123
-
124
- // Models known to be small (no "Xb" in their ID) that should be filtered.
125
- // Updated as new small free models appear on OpenRouter/Kilo.
126
- const KNOWN_SMALL_MODELS: ReadonlySet<string> = new Set([
127
- // Microsoft Phi models (1.5B–14B)
128
- "microsoft/phi-3-mini-128k-instruct",
129
- "microsoft/phi-3-mini-4k-instruct",
130
- "microsoft/phi-3-small-128k-instruct",
131
- "microsoft/phi-3-small-8k-instruct",
132
- "microsoft/phi-3-medium-128k-instruct",
133
- "microsoft/phi-3-medium-4k-instruct",
134
- "microsoft/phi-3.5-mini-instruct",
135
- "microsoft/phi-4-mini-instruct",
136
- "microsoft/phi-4-mini-reasoning",
137
- "microsoft/phi-4-reasoning-plus",
138
- // OpenChat (7B)
139
- "openchat/openchat-3.5-0106",
140
- "openchat/openchat-3.5-1210",
141
- // Mistral 7B variants
142
- "mistralai/mistral-7b-instruct-v0.1",
143
- "mistralai/mistral-7b-instruct-v0.2",
144
- "mistralai/mistral-7b-instruct-v0.3",
145
- // Gemma small variants
146
- "google/gemma-2b-it",
147
- "google/gemma-1.1-2b-it",
148
- // DeepSeek small variants
149
- "deepseek/deepseek-r1-distill-qwen-1.5b",
150
- "deepseek/deepseek-r1-distill-llama-8b",
151
- "deepseek/deepseek-r1-distill-qwen-7b",
152
- "deepseek/deepseek-r1-distill-qwen-14b",
153
- // Stripe Hyena (2.7B)
154
- "togethercomputer/stripedhy-2.7b",
155
- // TinyLlama
156
- "tinyllama/tinyllama-1.1b-chat-v1.0",
157
- ]);
158
-
159
- /**
160
- * Check if model is usable based on size constraints and naming.
161
- * Extracts model size from ID (e.g., "llama-3-70b" -> 70) and compares to minSizeB.
162
- * Falls back to a blocklist for models that don't encode size in the name.
163
- */
164
- export function isUsableModel(modelId: string, minSizeB?: number): boolean {
165
- // Filter out models that are likely test or debug models
166
- if (modelId.includes("test") || modelId.includes("debug")) {
167
- return false;
168
- }
169
-
170
- // Filter by minimum size if specified
171
- if (minSizeB !== undefined) {
172
- // Known-small blocklist (models without "Xb" in the name)
173
- // Strip :free suffix used by OpenRouter/Kilo
174
- const baseId = modelId.replace(/:free$/, "");
175
- if (KNOWN_SMALL_MODELS.has(baseId)) return false;
176
-
177
- // Check Mixture-of-Experts models first (e.g., "8x22b" = 176b total)
178
- const moeMatch = modelId.match(/(\d+)x(\d+(?:\.\d+)?)b/i);
179
- if (moeMatch) {
180
- const experts = Number.parseInt(moeMatch[1], 10);
181
- const expertSize = Number.parseFloat(moeMatch[2]);
182
- if (experts * expertSize < minSizeB) return false;
183
- return true; // MoE model passed size check
184
- }
185
-
186
- // Standard model size (e.g., "70b", "8b")
187
- const sizeMatch = modelId.match(/(\d+(?:\.\d+)?)b(?!\w)/i);
188
- if (sizeMatch) {
189
- const modelSize = Number.parseFloat(sizeMatch[1]);
190
- if (modelSize < minSizeB) return false;
191
- }
192
- }
193
-
194
- return true;
195
- }
196
-
197
- // =============================================================================
198
- // Model Name Cleaning
199
- // =============================================================================
200
-
201
- /**
202
- * Strip provider prefix from model names.
203
- * OpenRouter/Kilo return names like "Provider : Model Name" or "Provider / Model Name".
204
- * We only want the model name part.
205
- */
206
- export function cleanModelName(name: string): string {
207
- // Handle patterns like "Provider : Model Name" or "Provider / Model Name"
208
- // Match colon or slash separator with optional surrounding whitespace
209
- const separatorMatch = name.match(/^[^:]+\s*[:/]\s*(.+)$/);
210
- if (separatorMatch) {
211
- return separatorMatch[1].trim();
212
- }
213
- return name.trim();
214
- }
215
-
216
- // =============================================================================
217
- // Model Mapping
218
- // =============================================================================
219
-
220
- /**
221
- * Map OpenRouter/Kilo API model to ProviderModelConfig
222
- * Shared between OpenRouter and Kilo providers
223
- */
224
- export function mapOpenRouterModel(m: {
225
- id: string;
226
- name: string;
227
- context_length?: number;
228
- max_completion_tokens?: number | null;
229
- top_provider?: { max_completion_tokens?: number | null };
230
- pricing?: { prompt?: string | null; completion?: string | null };
231
- architecture?: {
232
- input_modalities?: string[] | null;
233
- output_modalities?: string[] | null;
234
- };
235
- }): ProviderModelConfig {
236
- const promptPrice = parseFloat(m.pricing?.prompt ?? "0");
237
- const completionPrice = parseFloat(m.pricing?.completion ?? "0");
238
-
239
- return {
240
- id: m.id,
241
- name: cleanModelName(m.name),
242
- reasoning: false, // OpenRouter doesn't expose reasoning flag directly
243
- input: m.architecture?.input_modalities?.includes("image")
244
- ? (["text", "image"] as const)
245
- : (["text"] as const),
246
- cost: {
247
- input: promptPrice,
248
- output: completionPrice,
249
- cacheRead: 0,
250
- cacheWrite: 0,
251
- },
252
- contextWindow: m.context_length ?? 4096,
253
- maxTokens:
254
- m.max_completion_tokens ?? m.top_provider?.max_completion_tokens ?? 4096,
255
- };
256
- }
1
+ import { createLogger } from "./logger.ts";
2
+ import type { ProviderModelConfig } from "./types.ts";
3
+
4
+ const _logger = createLogger("util");
5
+
6
+ // =============================================================================
7
+ // Shared Utilities
8
+ // =============================================================================
9
+
10
+ /**
11
+ * Log a warning message for provider operations
12
+ */
13
+ export function logWarning(
14
+ provider: string,
15
+ message: string,
16
+ error?: unknown,
17
+ ): void {
18
+ _logger.warn(
19
+ `[${provider}] ${message}`,
20
+ error ? { error: String(error) } : undefined,
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Fetch with timeout using AbortController
26
+ */
27
+ export async function fetchWithTimeout(
28
+ url: string,
29
+ options: RequestInit,
30
+ timeoutMs = 30000,
31
+ ): Promise<Response> {
32
+ const controller = new AbortController();
33
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
34
+
35
+ try {
36
+ const response = await fetch(url, {
37
+ ...options,
38
+ signal: controller.signal,
39
+ });
40
+ return response;
41
+ } finally {
42
+ clearTimeout(timeoutId);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Fetch with retry logic and timeout
48
+ */
49
+ export async function fetchWithRetry(
50
+ url: string,
51
+ options: RequestInit,
52
+ retries = 3,
53
+ delayMs = 1000,
54
+ timeoutMs = 30000,
55
+ ): Promise<Response> {
56
+ let lastError: unknown;
57
+
58
+ for (let i = 0; i < retries; i++) {
59
+ try {
60
+ const response = await fetchWithTimeout(url, options, timeoutMs);
61
+ if (response.ok) return response;
62
+
63
+ // If it's a rate limit, throw immediately
64
+ if (response.status === 429) {
65
+ throw new Error(`Rate limited (429)`);
66
+ }
67
+
68
+ // For server errors, retry
69
+ if (response.status >= 500) {
70
+ lastError = new Error(`Server error ${response.status}`);
71
+ if (i < retries - 1) {
72
+ await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
73
+ continue;
74
+ }
75
+ // Last retry exhausted - throw the error
76
+ throw lastError;
77
+ }
78
+
79
+ return response; // Return non-ok but non-retryable responses
80
+ } catch (error) {
81
+ lastError = error;
82
+ if (i < retries - 1) {
83
+ await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
84
+ }
85
+ }
86
+ }
87
+
88
+ throw lastError;
89
+ }
90
+
91
+ // =============================================================================
92
+ // Shared API Response Parsing
93
+ // =============================================================================
94
+
95
+ /**
96
+ * Parse and validate model list API response
97
+ * Shared between Kilo, OpenRouter, and other providers
98
+ */
99
+ export async function parseModelResponse<T>(
100
+ response: Response,
101
+ providerName: string,
102
+ ): Promise<{ data: T[] }> {
103
+ if (!response.ok) {
104
+ throw new Error(
105
+ `Failed to fetch ${providerName} models: ${response.status} ${response.statusText}`,
106
+ );
107
+ }
108
+
109
+ const json = (await response.json()) as { data?: T[] };
110
+
111
+ if (!json.data || !Array.isArray(json.data)) {
112
+ throw new Error(
113
+ `Invalid ${providerName} models response: missing data array`,
114
+ );
115
+ }
116
+
117
+ return { data: json.data };
118
+ }
119
+
120
+ // =============================================================================
121
+ // Model Filtering Utilities
122
+ // =============================================================================
123
+
124
+ // Models known to be small (no "Xb" in their ID) that should be filtered.
125
+ // Updated as new small free models appear on OpenRouter/Kilo.
126
+ const KNOWN_SMALL_MODELS: ReadonlySet<string> = new Set([
127
+ // Microsoft Phi models (1.5B–14B)
128
+ "microsoft/phi-3-mini-128k-instruct",
129
+ "microsoft/phi-3-mini-4k-instruct",
130
+ "microsoft/phi-3-small-128k-instruct",
131
+ "microsoft/phi-3-small-8k-instruct",
132
+ "microsoft/phi-3-medium-128k-instruct",
133
+ "microsoft/phi-3-medium-4k-instruct",
134
+ "microsoft/phi-3.5-mini-instruct",
135
+ "microsoft/phi-4-mini-instruct",
136
+ "microsoft/phi-4-mini-reasoning",
137
+ "microsoft/phi-4-reasoning-plus",
138
+ // OpenChat (7B)
139
+ "openchat/openchat-3.5-0106",
140
+ "openchat/openchat-3.5-1210",
141
+ // Mistral 7B variants
142
+ "mistralai/mistral-7b-instruct-v0.1",
143
+ "mistralai/mistral-7b-instruct-v0.2",
144
+ "mistralai/mistral-7b-instruct-v0.3",
145
+ // Gemma small variants
146
+ "google/gemma-2b-it",
147
+ "google/gemma-1.1-2b-it",
148
+ // DeepSeek small variants
149
+ "deepseek/deepseek-r1-distill-qwen-1.5b",
150
+ "deepseek/deepseek-r1-distill-llama-8b",
151
+ "deepseek/deepseek-r1-distill-qwen-7b",
152
+ "deepseek/deepseek-r1-distill-qwen-14b",
153
+ // Stripe Hyena (2.7B)
154
+ "togethercomputer/stripedhy-2.7b",
155
+ // TinyLlama
156
+ "tinyllama/tinyllama-1.1b-chat-v1.0",
157
+ ]);
158
+
159
+ /**
160
+ * Check if model is usable based on size constraints and naming.
161
+ * Extracts model size from ID (e.g., "llama-3-70b" -> 70) and compares to minSizeB.
162
+ * Falls back to a blocklist for models that don't encode size in the name.
163
+ */
164
+ export function isUsableModel(modelId: string, minSizeB?: number): boolean {
165
+ // Filter out models that are likely test or debug models
166
+ if (modelId.includes("test") || modelId.includes("debug")) {
167
+ return false;
168
+ }
169
+
170
+ // Filter by minimum size if specified
171
+ if (minSizeB !== undefined) {
172
+ // Known-small blocklist (models without "Xb" in the name)
173
+ // Strip :free suffix used by OpenRouter/Kilo
174
+ const baseId = modelId.replace(/:free$/, "");
175
+ if (KNOWN_SMALL_MODELS.has(baseId)) return false;
176
+
177
+ // Check Mixture-of-Experts models first (e.g., "8x22b" = 176b total)
178
+ const moeMatch = modelId.match(/(\d+)x(\d+(?:\.\d+)?)b/i);
179
+ if (moeMatch) {
180
+ const experts = Number.parseInt(moeMatch[1], 10);
181
+ const expertSize = Number.parseFloat(moeMatch[2]);
182
+ if (experts * expertSize < minSizeB) return false;
183
+ return true; // MoE model passed size check
184
+ }
185
+
186
+ // Standard model size (e.g., "70b", "8b")
187
+ const sizeMatch = modelId.match(/(\d+(?:\.\d+)?)b(?!\w)/i);
188
+ if (sizeMatch) {
189
+ const modelSize = Number.parseFloat(sizeMatch[1]);
190
+ if (modelSize < minSizeB) return false;
191
+ }
192
+ }
193
+
194
+ return true;
195
+ }
196
+
197
+ // =============================================================================
198
+ // Model Name Cleaning
199
+ // =============================================================================
200
+
201
+ /**
202
+ * Strip provider prefix from model names.
203
+ * OpenRouter/Kilo return names like "Provider : Model Name" or "Provider / Model Name".
204
+ * We only want the model name part.
205
+ */
206
+ export function cleanModelName(name: string): string {
207
+ // Handle patterns like "Provider : Model Name" or "Provider / Model Name"
208
+ const colonIdx = name.indexOf(":");
209
+ const slashIdx = name.indexOf("/");
210
+ const idx =
211
+ colonIdx === -1
212
+ ? slashIdx
213
+ : slashIdx === -1
214
+ ? colonIdx
215
+ : Math.min(colonIdx, slashIdx);
216
+ if (idx > 0) {
217
+ return name.slice(idx + 1).trim();
218
+ }
219
+ return name.trim();
220
+ }
221
+
222
+ // =============================================================================
223
+ // Model Mapping
224
+ // =============================================================================
225
+
226
+ /**
227
+ * Map OpenRouter/Kilo API model to ProviderModelConfig
228
+ * Shared between OpenRouter and Kilo providers
229
+ */
230
+ export function mapOpenRouterModel(m: {
231
+ id: string;
232
+ name: string;
233
+ context_length?: number;
234
+ max_completion_tokens?: number | null;
235
+ top_provider?: { max_completion_tokens?: number | null };
236
+ pricing?: { prompt?: string | null; completion?: string | null };
237
+ architecture?: {
238
+ input_modalities?: string[] | null;
239
+ output_modalities?: string[] | null;
240
+ };
241
+ }): ProviderModelConfig {
242
+ const promptPrice = parseFloat(m.pricing?.prompt ?? "0");
243
+ const completionPrice = parseFloat(m.pricing?.completion ?? "0");
244
+
245
+ return {
246
+ id: m.id,
247
+ name: cleanModelName(m.name),
248
+ reasoning: false, // OpenRouter doesn't expose reasoning flag directly
249
+ input: m.architecture?.input_modalities?.includes("image")
250
+ ? (["text", "image"] as const)
251
+ : (["text"] as const),
252
+ cost: {
253
+ input: promptPrice,
254
+ output: completionPrice,
255
+ cacheRead: 0,
256
+ cacheWrite: 0,
257
+ },
258
+ contextWindow: m.context_length ?? 4096,
259
+ maxTokens:
260
+ m.max_completion_tokens ?? m.top_provider?.max_completion_tokens ?? 4096,
261
+ };
262
+ }
package/package.json CHANGED
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "pi-free",
3
- "version": "2.0.2",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
- "description": "AIO free models for PI: Kilo, Cline, Nvidia, Ollama Cloud and others",
5
+ "description": "AI model providers for Pi with free model filtering. Shows only $0 cost models by default. Supports Kilo (free OAuth), Cline (free), NVIDIA (freemium), ZenMux, CrofAI, Ollama Cloud, and more.",
6
6
  "keywords": [
7
7
  "pi-package",
8
8
  "pi-extension",
9
9
  "free-models",
10
+ "paid-models",
10
11
  "model-filter",
11
12
  "nvidia-nim",
12
13
  "kilo",
13
14
  "cline",
14
- "qwen",
15
- "qwen-oauth",
16
- "modal"
15
+ "zenmux",
16
+ "crofai",
17
+ "ollama-cloud"
17
18
  ],
18
19
  "license": "MIT",
19
20
  "author": "Apostolos Mantzaris",
@@ -35,11 +36,11 @@
35
36
  "provider-failover/**/*.ts",
36
37
  "config.ts",
37
38
  "constants.ts",
38
- "provider-factory.ts",
39
39
  "provider-helper.ts",
40
40
  "README.md",
41
41
  "LICENSE",
42
42
  "CHANGELOG.md",
43
+ "banner.svg",
43
44
  "scripts/check-extensions.mjs"
44
45
  ],
45
46
  "scripts": {
@@ -54,10 +55,10 @@
54
55
  "@mariozechner/pi-tui": "*"
55
56
  },
56
57
  "devDependencies": {
57
- "@vitest/ui": "^1.0.0",
58
+ "@vitest/ui": "^4.1.5",
58
59
  "tsx": "^4.0.0",
59
60
  "typescript": "^6.0.2",
60
- "vitest": "^1.0.0"
61
+ "vitest": "^4.1.5"
61
62
  },
62
63
  "pi": {
63
64
  "extensions": [