pi-free 1.0.9 → 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.
- package/CHANGELOG.md +138 -0
- package/README.md +393 -367
- package/config.ts +170 -121
- package/constants.ts +23 -61
- package/index.ts +148 -0
- package/lib/built-in-toggle.ts +206 -0
- package/lib/json-persistence.ts +11 -10
- package/lib/logger.ts +2 -2
- package/lib/model-enhancer.ts +20 -20
- package/lib/open-browser.ts +41 -0
- package/lib/provider-cache.ts +106 -0
- package/lib/registry.ts +144 -0
- package/package.json +8 -23
- package/provider-factory.ts +25 -41
- package/provider-failover/benchmark-lookup.ts +247 -0
- package/provider-failover/benchmarks-chunk-0.ts +2010 -0
- package/provider-failover/benchmarks-chunk-1.ts +1988 -0
- package/provider-failover/benchmarks-chunk-2.ts +2010 -0
- package/provider-failover/benchmarks-chunk-3.ts +2010 -0
- package/provider-failover/benchmarks-chunk-4.ts +1969 -0
- package/provider-failover/hardcoded-benchmarks.ts +22 -10025
- package/provider-helper.ts +260 -259
- package/providers/{cline-auth.ts → cline/cline-auth.ts} +2 -2
- package/providers/cline/cline-models.ts +128 -0
- package/providers/{cline.ts → cline/cline.ts} +298 -257
- package/providers/cloudflare/cloudflare.ts +368 -0
- package/providers/dynamic-built-in/index.ts +432 -0
- package/providers/{kilo-auth.ts → kilo/kilo-auth.ts} +3 -20
- package/providers/{kilo-models.ts → kilo/kilo-models.ts} +2 -2
- package/providers/kilo/kilo.ts +235 -0
- package/providers/{modal.ts → modal/modal.ts} +4 -3
- package/providers/{nvidia.ts → nvidia/nvidia.ts} +152 -113
- package/providers/ollama/ollama.ts +172 -0
- package/providers/opencode-session.ts +34 -34
- package/providers/{qwen-auth.ts → qwen/qwen-auth.ts} +24 -40
- package/providers/{qwen-models.ts → qwen/qwen-models.ts} +101 -95
- package/providers/{qwen.ts → qwen/qwen.ts} +83 -13
- package/provider-failover/auto-switch.ts +0 -350
- package/provider-failover/errors.ts +0 -275
- package/provider-failover/index.ts +0 -238
- package/providers/cline-models.ts +0 -77
- package/providers/factory.ts +0 -125
- package/providers/fireworks.ts +0 -49
- package/providers/go.ts +0 -216
- package/providers/kilo.ts +0 -146
- package/providers/mistral.ts +0 -144
- package/providers/ollama.ts +0 -113
- package/providers/openrouter.ts +0 -175
- package/providers/zen.ts +0 -371
- package/usage/commands.ts +0 -17
- package/usage/cumulative.ts +0 -193
- package/usage/formatters.ts +0 -115
- package/usage/index.ts +0 -46
- package/usage/limits.ts +0 -148
- package/usage/metrics.ts +0 -222
- package/usage/sessions.ts +0 -355
- package/usage/store.ts +0 -99
- package/usage/tracking.ts +0 -329
- package/usage/types.ts +0 -26
- package/usage/widget.ts +0 -90
- package/widget/data.ts +0 -113
- package/widget/format.ts +0 -26
- package/widget/render.ts +0 -117
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Provider Toggle Support
|
|
3
|
+
*
|
|
4
|
+
* Captures pi's built-in providers after session start and enables
|
|
5
|
+
* free/paid toggling for them via the global registry.
|
|
6
|
+
*
|
|
7
|
+
* Currently supports:
|
|
8
|
+
* - opencode (OpenCode / Zen gateway)
|
|
9
|
+
* - openrouter (OpenRouter)
|
|
10
|
+
*
|
|
11
|
+
* Usage: /toggle-opencode
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
15
|
+
import type {
|
|
16
|
+
ExtensionAPI,
|
|
17
|
+
ProviderModelConfig,
|
|
18
|
+
} from "@mariozechner/pi-coding-agent";
|
|
19
|
+
import {
|
|
20
|
+
getOpencodeShowPaid,
|
|
21
|
+
getOpenrouterShowPaid,
|
|
22
|
+
saveConfig,
|
|
23
|
+
} from "../config.ts";
|
|
24
|
+
import { createLogger } from "./logger.ts";
|
|
25
|
+
import {
|
|
26
|
+
getGlobalFreeOnly,
|
|
27
|
+
isFreeModel,
|
|
28
|
+
registerWithGlobalToggle,
|
|
29
|
+
} from "./registry.ts";
|
|
30
|
+
|
|
31
|
+
const _logger = createLogger("built-in-toggle");
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Configuration
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
interface BuiltInToggleConfig {
|
|
38
|
+
id: string;
|
|
39
|
+
getShowPaid: () => boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const BUILT_IN_TOGGLE_PROVIDERS: BuiltInToggleConfig[] = [
|
|
43
|
+
{ id: "opencode", getShowPaid: getOpencodeShowPaid },
|
|
44
|
+
{ id: "openrouter", getShowPaid: getOpenrouterShowPaid },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// State
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
interface BuiltInProviderState {
|
|
52
|
+
free: ProviderModelConfig[];
|
|
53
|
+
all: ProviderModelConfig[];
|
|
54
|
+
reRegister: (models: ProviderModelConfig[]) => void;
|
|
55
|
+
showPaid: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const providerStates = new Map<string, BuiltInProviderState>();
|
|
59
|
+
let commandsRegistered = false;
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Setup
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
|
|
66
|
+
// Register toggle commands once (available even before models load)
|
|
67
|
+
if (!commandsRegistered) {
|
|
68
|
+
for (const config of BUILT_IN_TOGGLE_PROVIDERS) {
|
|
69
|
+
registerToggleCommand(pi, config);
|
|
70
|
+
}
|
|
71
|
+
commandsRegistered = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Capture built-in models on session start and apply initial filter
|
|
75
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
76
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
77
|
+
|
|
78
|
+
for (const config of BUILT_IN_TOGGLE_PROVIDERS) {
|
|
79
|
+
if (providerStates.has(config.id)) {
|
|
80
|
+
// Already captured this session — skip to avoid re-registering
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const providerModels = available.filter(
|
|
85
|
+
(m: Model<Api>) => m.provider === config.id,
|
|
86
|
+
);
|
|
87
|
+
if (providerModels.length === 0) continue;
|
|
88
|
+
|
|
89
|
+
const allModels = providerModels.map(modelToProviderConfig);
|
|
90
|
+
const freeModels = allModels.filter(isFreeModel);
|
|
91
|
+
|
|
92
|
+
const baseUrl = providerModels[0].baseUrl;
|
|
93
|
+
const api = providerModels[0].api;
|
|
94
|
+
const apiKeyEnv = getApiKeyEnvForProvider(config.id);
|
|
95
|
+
|
|
96
|
+
const reRegister = (models: ProviderModelConfig[]) => {
|
|
97
|
+
pi.registerProvider(config.id, {
|
|
98
|
+
baseUrl,
|
|
99
|
+
apiKey: apiKeyEnv,
|
|
100
|
+
api,
|
|
101
|
+
models,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
providerStates.set(config.id, {
|
|
106
|
+
free: freeModels,
|
|
107
|
+
all: allModels,
|
|
108
|
+
reRegister,
|
|
109
|
+
showPaid: config.getShowPaid(),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Register with global free-only filter
|
|
113
|
+
registerWithGlobalToggle(
|
|
114
|
+
config.id,
|
|
115
|
+
{ free: freeModels, all: allModels },
|
|
116
|
+
reRegister,
|
|
117
|
+
true,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
_logger.info(
|
|
121
|
+
`[built-in-toggle] ${config.id}: captured ${allModels.length} models (${freeModels.length} free)`,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Respect global free-only setting at capture time
|
|
125
|
+
if (!getGlobalFreeOnly() && !config.getShowPaid()) {
|
|
126
|
+
// Default: show free only (same as other pi-free providers)
|
|
127
|
+
if (freeModels.length > 0) {
|
|
128
|
+
reRegister(freeModels);
|
|
129
|
+
_logger.info(
|
|
130
|
+
`[built-in-toggle] ${config.id}: applied free-only filter`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Per-provider toggle command
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
function registerToggleCommand(
|
|
143
|
+
pi: ExtensionAPI,
|
|
144
|
+
config: BuiltInToggleConfig,
|
|
145
|
+
): void {
|
|
146
|
+
const commandName = `toggle-${config.id}`;
|
|
147
|
+
pi.registerCommand(commandName, {
|
|
148
|
+
description: `Toggle free/paid ${config.id} models`,
|
|
149
|
+
handler: async (_args, ctx) => {
|
|
150
|
+
const state = providerStates.get(config.id);
|
|
151
|
+
if (!state) {
|
|
152
|
+
ctx.ui.notify(
|
|
153
|
+
`${config.id}: models not loaded yet. Start a session first.`,
|
|
154
|
+
"warning",
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
state.showPaid = !state.showPaid;
|
|
160
|
+
|
|
161
|
+
// Persist preference
|
|
162
|
+
saveConfig({ [`${config.id}_show_paid`]: state.showPaid });
|
|
163
|
+
|
|
164
|
+
if (state.showPaid) {
|
|
165
|
+
state.reRegister(state.all);
|
|
166
|
+
ctx.ui.notify(
|
|
167
|
+
`${config.id}: showing all ${state.all.length} models`,
|
|
168
|
+
"info",
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
state.reRegister(state.free);
|
|
172
|
+
ctx.ui.notify(
|
|
173
|
+
`${config.id}: showing ${state.free.length} free models`,
|
|
174
|
+
"info",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// Helpers
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
function modelToProviderConfig(m: Model<Api>): ProviderModelConfig {
|
|
186
|
+
return {
|
|
187
|
+
id: m.id,
|
|
188
|
+
name: m.name,
|
|
189
|
+
api: m.api,
|
|
190
|
+
reasoning: m.reasoning,
|
|
191
|
+
input: m.input,
|
|
192
|
+
cost: m.cost,
|
|
193
|
+
contextWindow: m.contextWindow,
|
|
194
|
+
maxTokens: m.maxTokens,
|
|
195
|
+
headers: m.headers,
|
|
196
|
+
compat: (m as any).compat,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getApiKeyEnvForProvider(providerId: string): string {
|
|
201
|
+
const envMap: Record<string, string> = {
|
|
202
|
+
opencode: "OPENCODE_API_KEY",
|
|
203
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
204
|
+
};
|
|
205
|
+
return envMap[providerId] || `${providerId.toUpperCase()}_API_KEY`;
|
|
206
|
+
}
|
package/lib/json-persistence.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error("Failed to write to log file:", err);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
package/lib/model-enhancer.ts
CHANGED
|
@@ -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/
|
|
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
|
+
}
|
package/lib/registry.ts
ADDED
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-free",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AIO
|
|
5
|
+
"description": "AIO free models for PI: Kilo, Cline, Nvidia, Ollama Cloud and others",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
9
9
|
"free-models",
|
|
10
|
-
"
|
|
11
|
-
"openrouter",
|
|
10
|
+
"model-filter",
|
|
12
11
|
"nvidia-nim",
|
|
13
12
|
"kilo",
|
|
14
13
|
"cline",
|
|
15
|
-
"ollama",
|
|
16
|
-
"mistral",
|
|
17
|
-
"fireworks",
|
|
18
14
|
"qwen",
|
|
19
|
-
"qwen-oauth"
|
|
15
|
+
"qwen-oauth",
|
|
16
|
+
"modal"
|
|
20
17
|
],
|
|
21
18
|
"license": "MIT",
|
|
22
19
|
"author": "Apostolos Mantzaris",
|
|
@@ -32,11 +29,10 @@
|
|
|
32
29
|
"node": ">=20.0.0"
|
|
33
30
|
},
|
|
34
31
|
"files": [
|
|
32
|
+
"index.ts",
|
|
35
33
|
"providers/**/*.ts",
|
|
36
34
|
"lib/**/*.ts",
|
|
37
|
-
"usage/**/*.ts",
|
|
38
35
|
"provider-failover/**/*.ts",
|
|
39
|
-
"widget/**/*.ts",
|
|
40
36
|
"config.ts",
|
|
41
37
|
"constants.ts",
|
|
42
38
|
"provider-factory.ts",
|
|
@@ -55,8 +51,7 @@
|
|
|
55
51
|
"peerDependencies": {
|
|
56
52
|
"@mariozechner/pi-ai": "*",
|
|
57
53
|
"@mariozechner/pi-coding-agent": "*",
|
|
58
|
-
"@mariozechner/pi-tui": "*"
|
|
59
|
-
"@sinclair/typebox": "*"
|
|
54
|
+
"@mariozechner/pi-tui": "*"
|
|
60
55
|
},
|
|
61
56
|
"devDependencies": {
|
|
62
57
|
"@vitest/ui": "^1.0.0",
|
|
@@ -66,17 +61,7 @@
|
|
|
66
61
|
},
|
|
67
62
|
"pi": {
|
|
68
63
|
"extensions": [
|
|
69
|
-
"./
|
|
70
|
-
"./providers/zen.ts",
|
|
71
|
-
"./providers/go.ts",
|
|
72
|
-
"./providers/openrouter.ts",
|
|
73
|
-
"./providers/nvidia.ts",
|
|
74
|
-
"./providers/cline.ts",
|
|
75
|
-
"./providers/fireworks.ts",
|
|
76
|
-
"./providers/mistral.ts",
|
|
77
|
-
"./providers/ollama.ts",
|
|
78
|
-
"./providers/qwen.ts",
|
|
79
|
-
"./providers/modal.ts"
|
|
64
|
+
"./index.ts"
|
|
80
65
|
]
|
|
81
66
|
}
|
|
82
67
|
}
|