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/CHANGELOG.md +84 -12
- package/README.md +44 -97
- package/config.ts +24 -52
- package/constants.ts +6 -0
- package/index.ts +175 -148
- package/lib/built-in-toggle.ts +40 -1
- package/lib/model-enhancer.ts +20 -20
- package/lib/open-browser.ts +1 -1
- package/lib/provider-compat.ts +46 -0
- package/lib/registry.ts +193 -144
- package/lib/types.ts +101 -108
- package/lib/util.ts +256 -256
- package/package.json +8 -8
- package/provider-failover/benchmark-lookup.ts +2 -2
- package/provider-helper.ts +19 -1
- package/providers/cline/cline-auth.ts +473 -473
- package/providers/cline/cline.ts +31 -4
- package/providers/crofai/crofai.ts +170 -0
- package/providers/dynamic-built-in/index.ts +258 -308
- package/providers/kilo/kilo-auth.ts +155 -155
- package/providers/kilo/kilo.ts +263 -235
- package/providers/nvidia/nvidia.ts +476 -415
- package/providers/ollama/ollama.ts +295 -280
- package/providers/opencode-session.ts +3 -4
- package/providers/qwen/qwen-models.ts +101 -101
- package/providers/zenmux/zenmux.ts +176 -0
- package/scripts/check-extensions.mjs +64 -55
- package/provider-factory.ts +0 -207
- package/providers/cloudflare/cloudflare.ts +0 -526
- package/providers/modal/modal.ts +0 -47
package/lib/util.ts
CHANGED
|
@@ -1,256 +1,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
|
-
// 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
|
+
// 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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-free",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"zenmux",
|
|
16
|
+
"crofai",
|
|
17
|
+
"ollama-cloud"
|
|
17
18
|
],
|
|
18
19
|
"license": "MIT",
|
|
19
20
|
"author": "Apostolos Mantzaris",
|
|
@@ -35,7 +36,6 @@
|
|
|
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",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"@mariozechner/pi-tui": "*"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@vitest/ui": "^1.
|
|
57
|
+
"@vitest/ui": "^4.1.5",
|
|
58
58
|
"tsx": "^4.0.0",
|
|
59
59
|
"typescript": "^6.0.2",
|
|
60
|
-
"vitest": "^1.
|
|
60
|
+
"vitest": "^4.1.5"
|
|
61
61
|
},
|
|
62
62
|
"pi": {
|
|
63
63
|
"extensions": [
|
|
@@ -78,7 +78,7 @@ function logDebug(entry: {
|
|
|
78
78
|
entry.codingIndex !== undefined ? entry.codingIndex.toFixed(1) : "",
|
|
79
79
|
entry.details || "",
|
|
80
80
|
]
|
|
81
|
-
.map((f) => f.replace(
|
|
81
|
+
.map((f) => f.replace(/[\\|]/g, "\\$&")) // Escape backslashes and pipes
|
|
82
82
|
.join("|");
|
|
83
83
|
|
|
84
84
|
appendFileSync(LOG_FILE, `${line}\n`);
|
|
@@ -128,7 +128,7 @@ function applyProviderNormalization(
|
|
|
128
128
|
if (provider === "nvidia") {
|
|
129
129
|
// NVIDIA uses prefixes like meta/, mistralai/, microsoft/, qwen/
|
|
130
130
|
const prefixMatch = normalized.match(
|
|
131
|
-
/^(meta|mistralai|microsoft|qwen|nvidia|ibm|google|ai21labs|bigcode|databricks|deepseek-ai|01-ai|adept|aisingapore|baai|
|
|
131
|
+
/^(meta|mistralai|microsoft|qwen|nvidia|ibm|google|ai21labs|bigcode|databricks|deepseek-ai|01-ai|adept|aisingapore|baai|bytedance|luma|stabilityai|fireworks|upstage|voyage|snowflake|recursal|kdan|unity|cloudflare|fblgit|nttdata|dito|nousresearch|espressomodels|ftmsh|huggingface|isolationai|pinglab|functionnetwork|huggingfaceh4|mcw|shutterstock)[^/]*\//,
|
|
132
132
|
);
|
|
133
133
|
if (prefixMatch) {
|
|
134
134
|
normalized = normalized.replace(/^[^/]+\//, "");
|
package/provider-helper.ts
CHANGED
|
@@ -213,12 +213,30 @@ export function setupProvider(
|
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
// ──
|
|
216
|
+
// ── Status bar for selected provider ───────────────────────────
|
|
217
217
|
|
|
218
218
|
pi.on("model_select", (_event, ctx) => {
|
|
219
219
|
if (_event.model?.provider !== providerId) {
|
|
220
220
|
ctx.ui.setStatus(`${providerId}-status`, undefined);
|
|
221
|
+
return;
|
|
221
222
|
}
|
|
223
|
+
|
|
224
|
+
// Build status line for this provider
|
|
225
|
+
const free = stored.free.length;
|
|
226
|
+
const total = stored.all.length || free;
|
|
227
|
+
const paid = total - free;
|
|
228
|
+
let status: string;
|
|
229
|
+
|
|
230
|
+
if (paid === 0) {
|
|
231
|
+
status = `${providerId}: ${free} free models`;
|
|
232
|
+
} else if (currentShowPaid) {
|
|
233
|
+
status = `${providerId}: ${total} models (free + paid)`;
|
|
234
|
+
} else {
|
|
235
|
+
status = `${providerId}: ${free} free · ${paid} paid`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (config.hasKey) status += " 🔑";
|
|
239
|
+
ctx.ui.setStatus(`${providerId}-status`, status);
|
|
222
240
|
});
|
|
223
241
|
|
|
224
242
|
// ── Error handling / usage tracking are temporarily deprecated ─────────
|