pi-free 2.0.10 → 2.0.12
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 +40 -0
- package/README.md +594 -588
- package/banner.svg +1 -1
- package/config.ts +500 -349
- package/constants.ts +108 -106
- package/index.ts +244 -242
- package/lib/built-in-toggle.ts +91 -58
- package/lib/registry.ts +40 -16
- package/lib/util.ts +525 -524
- package/package.json +2 -4
- package/providers/cline/cline-models.ts +3 -10
- package/providers/crofai/crofai.ts +194 -190
- package/providers/deepinfra/deepinfra.ts +208 -206
- package/providers/dynamic-built-in/index.ts +104 -31
- package/providers/model-fetcher.ts +2 -13
- package/providers/novita/novita.ts +205 -0
- package/providers/nvidia/nvidia.ts +4 -6
- package/providers/ollama/ollama.ts +610 -610
- package/providers/ollama/thinking-levels.ts +96 -96
- package/providers/sambanova/sambanova.ts +8 -2
- package/providers/together/together.ts +194 -197
- package/providers/zenmux/zenmux.ts +196 -194
- package/banner.jpg +0 -0
- package/banner.png +0 -0
package/lib/built-in-toggle.ts
CHANGED
|
@@ -18,7 +18,11 @@ import type {
|
|
|
18
18
|
} from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { getOpencodeShowPaid, getOpenrouterShowPaid } from "../config.ts";
|
|
20
20
|
import { createLogger } from "./logger.ts";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
getProviderRegistry,
|
|
23
|
+
isFreeModel,
|
|
24
|
+
registerWithGlobalToggle,
|
|
25
|
+
} from "./registry.ts";
|
|
22
26
|
import { createToggleState } from "./toggle-state.ts";
|
|
23
27
|
|
|
24
28
|
const _logger = createLogger("built-in-toggle");
|
|
@@ -55,68 +59,38 @@ let commandsRegistered = false;
|
|
|
55
59
|
// =============================================================================
|
|
56
60
|
|
|
57
61
|
export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
|
|
62
|
+
const activeConfigs = BUILT_IN_TOGGLE_PROVIDERS.filter(
|
|
63
|
+
(config) => !getProviderRegistry().has(config.id),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (activeConfigs.length === 0) {
|
|
67
|
+
_logger.info(
|
|
68
|
+
"[built-in-toggle] OpenCode/OpenRouter already registered dynamically; skipping fallback capture",
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
// Register toggle commands once (available even before models load)
|
|
59
74
|
if (!commandsRegistered) {
|
|
60
|
-
for (const config of
|
|
75
|
+
for (const config of activeConfigs) {
|
|
61
76
|
registerToggleCommand(pi, config);
|
|
62
77
|
}
|
|
63
|
-
setupStatusBar(pi);
|
|
78
|
+
setupStatusBar(pi, activeConfigs);
|
|
64
79
|
commandsRegistered = true;
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
// Capture built-in models on session start and apply initial filter
|
|
68
83
|
pi.on("session_start", async (_event, ctx) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
for (const config of BUILT_IN_TOGGLE_PROVIDERS) {
|
|
84
|
+
for (const config of activeConfigs) {
|
|
72
85
|
if (providerStates.has(config.id)) {
|
|
73
|
-
// Already captured
|
|
86
|
+
// Already captured — skip to avoid re-registering
|
|
74
87
|
continue;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
if (providerModels.length === 0) continue;
|
|
81
|
-
|
|
82
|
-
const allModels = providerModels.map(modelToProviderConfig);
|
|
83
|
-
const freeModels = allModels.filter((m) =>
|
|
84
|
-
isFreeModel({ ...m, provider: config.id }, allModels),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const baseUrl = providerModels[0].baseUrl;
|
|
88
|
-
const api = providerModels[0].api;
|
|
89
|
-
const apiKeyEnv = getApiKeyEnvForProvider(config.id);
|
|
90
|
-
|
|
91
|
-
const reRegister = (models: ProviderModelConfig[]) => {
|
|
92
|
-
pi.registerProvider(config.id, {
|
|
93
|
-
baseUrl,
|
|
94
|
-
apiKey: apiKeyEnv,
|
|
95
|
-
api,
|
|
96
|
-
models,
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const stored = { free: freeModels, all: allModels };
|
|
101
|
-
const toggleState = createToggleState<ProviderModelConfig>({
|
|
102
|
-
providerId: config.id,
|
|
103
|
-
initialShowPaid: config.getShowPaid(),
|
|
104
|
-
initialModels: stored,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
providerStates.set(config.id, {
|
|
108
|
-
stored,
|
|
109
|
-
reRegister,
|
|
110
|
-
toggleState,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
registerWithGlobalToggle(config.id, stored, reRegister, true);
|
|
90
|
+
const state = tryCaptureProvider(pi, config, ctx);
|
|
91
|
+
if (!state) continue;
|
|
114
92
|
|
|
115
|
-
|
|
116
|
-
`[built-in-toggle] ${config.id}: captured ${allModels.length} models (${freeModels.length} free)`,
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const applied = toggleState.applyCurrent(reRegister);
|
|
93
|
+
const applied = state.toggleState.applyCurrent(state.reRegister);
|
|
120
94
|
_logger.info(
|
|
121
95
|
`[built-in-toggle] ${config.id}: applied ${applied.mode} mode with ${applied.models.length} models`,
|
|
122
96
|
);
|
|
@@ -124,6 +98,58 @@ export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
|
|
|
124
98
|
});
|
|
125
99
|
}
|
|
126
100
|
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// On-demand model capture (called by toggle command when state is missing)
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
function tryCaptureProvider(
|
|
106
|
+
pi: ExtensionAPI,
|
|
107
|
+
config: BuiltInToggleConfig,
|
|
108
|
+
ctx: any,
|
|
109
|
+
): BuiltInProviderState | undefined {
|
|
110
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
111
|
+
const providerModels = available.filter(
|
|
112
|
+
(m: Model<Api>) => m.provider === config.id,
|
|
113
|
+
);
|
|
114
|
+
if (providerModels.length === 0) return undefined;
|
|
115
|
+
|
|
116
|
+
const allModels = providerModels.map(modelToProviderConfig);
|
|
117
|
+
const freeModels = allModels.filter((m: ProviderModelConfig) =>
|
|
118
|
+
isFreeModel({ ...m, provider: config.id }, allModels),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const baseUrl = providerModels[0].baseUrl;
|
|
122
|
+
const api = providerModels[0].api;
|
|
123
|
+
const apiKeyEnv = getApiKeyEnvForProvider(config.id);
|
|
124
|
+
|
|
125
|
+
const reRegister = (models: ProviderModelConfig[]) => {
|
|
126
|
+
pi.registerProvider(config.id, {
|
|
127
|
+
baseUrl,
|
|
128
|
+
apiKey: apiKeyEnv,
|
|
129
|
+
api,
|
|
130
|
+
models,
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const stored = { free: freeModels, all: allModels };
|
|
135
|
+
const toggleState = createToggleState<ProviderModelConfig>({
|
|
136
|
+
providerId: config.id,
|
|
137
|
+
initialShowPaid: config.getShowPaid(),
|
|
138
|
+
initialModels: stored,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const state: BuiltInProviderState = { stored, reRegister, toggleState };
|
|
142
|
+
providerStates.set(config.id, state);
|
|
143
|
+
|
|
144
|
+
registerWithGlobalToggle(config.id, stored, reRegister, true);
|
|
145
|
+
|
|
146
|
+
_logger.info(
|
|
147
|
+
`[built-in-toggle] ${config.id}: captured ${allModels.length} models (${freeModels.length} free)`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return state;
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
// =============================================================================
|
|
128
154
|
// Per-provider toggle command
|
|
129
155
|
// =============================================================================
|
|
@@ -136,13 +162,17 @@ function registerToggleCommand(
|
|
|
136
162
|
pi.registerCommand(commandName, {
|
|
137
163
|
description: `Toggle free/paid ${config.id} models`,
|
|
138
164
|
handler: async (_args, ctx) => {
|
|
139
|
-
|
|
165
|
+
let state = providerStates.get(config.id);
|
|
140
166
|
if (!state) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
// Models may have loaded after session_start — try on-demand capture
|
|
168
|
+
state = tryCaptureProvider(pi, config, ctx);
|
|
169
|
+
if (!state) {
|
|
170
|
+
ctx.ui.notify(
|
|
171
|
+
`${config.id}: models not loaded yet. Start a session first.`,
|
|
172
|
+
"warning",
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
146
176
|
}
|
|
147
177
|
|
|
148
178
|
const applied = state.toggleState.toggle(state.reRegister);
|
|
@@ -185,12 +215,15 @@ function modelToProviderConfig(m: Model<Api>): ProviderModelConfig {
|
|
|
185
215
|
// Status bar for provider selection
|
|
186
216
|
// =============================================================================
|
|
187
217
|
|
|
188
|
-
function setupStatusBar(
|
|
218
|
+
function setupStatusBar(
|
|
219
|
+
pi: ExtensionAPI,
|
|
220
|
+
configs: BuiltInToggleConfig[],
|
|
221
|
+
): void {
|
|
189
222
|
pi.on("model_select", (_event, ctx) => {
|
|
190
223
|
const selected = _event.model?.provider;
|
|
191
224
|
|
|
192
|
-
// Clear status for all built-in
|
|
193
|
-
for (const config of
|
|
225
|
+
// Clear status for all fallback-captured built-in providers
|
|
226
|
+
for (const config of configs) {
|
|
194
227
|
if (selected !== config.id) {
|
|
195
228
|
ctx.ui.setStatus(`${config.id}-status`, undefined);
|
|
196
229
|
}
|
package/lib/registry.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ExtensionAPI,
|
|
10
10
|
ProviderModelConfig,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { getFreeOnly, saveConfig } from "../config.ts";
|
|
12
|
+
import { getFreeOnly, getProviderShowPaid, saveConfig } from "../config.ts";
|
|
13
13
|
import { createLogger } from "./logger.ts";
|
|
14
14
|
|
|
15
15
|
const _logger = createLogger("pi-free");
|
|
@@ -82,7 +82,7 @@ function detectPricingExposed(allModels: ProviderModelConfig[]): boolean {
|
|
|
82
82
|
* @returns true if the model is definitively free per the provider's API
|
|
83
83
|
*/
|
|
84
84
|
export function isFreeModel(
|
|
85
|
-
model: ProviderModelConfig & { provider?: string },
|
|
85
|
+
model: ProviderModelConfig & { provider?: string; _pricingKnown?: boolean },
|
|
86
86
|
allModels?: ProviderModelConfig[],
|
|
87
87
|
): boolean {
|
|
88
88
|
return isFreeModelInternal(model, allModels);
|
|
@@ -90,7 +90,7 @@ export function isFreeModel(
|
|
|
90
90
|
|
|
91
91
|
// Internal implementation to work around TypeScript filter callback issues
|
|
92
92
|
function isFreeModelInternal(
|
|
93
|
-
model: ProviderModelConfig & { provider?: string },
|
|
93
|
+
model: ProviderModelConfig & { provider?: string; _pricingKnown?: boolean },
|
|
94
94
|
allModels: ProviderModelConfig[] | undefined,
|
|
95
95
|
): boolean {
|
|
96
96
|
// Determine if pricing is exposed
|
|
@@ -106,12 +106,20 @@ function isFreeModelInternal(
|
|
|
106
106
|
pricingExposed = true;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// Route A: Pricing-exposed providers
|
|
110
|
-
// Model is free if EITHER cost is zero OR name contains "free"
|
|
109
|
+
// Route A: Pricing-exposed providers
|
|
110
|
+
// Model is free if EITHER cost is zero OR name contains "free".
|
|
111
|
+
// BUT: when _pricingKnown is explicitly false (API returned no pricing data),
|
|
112
|
+
// cost values are untrustworthy defaults — fall back to name-only detection.
|
|
111
113
|
if (pricingExposed) {
|
|
112
114
|
const isZeroCost =
|
|
113
115
|
(model.cost?.input ?? 0) === 0 && (model.cost?.output ?? 0) === 0;
|
|
114
116
|
const hasFreeInName = model.name.toLowerCase().includes("free");
|
|
117
|
+
|
|
118
|
+
// Pricing missing for this specific model — only trust name-based signal
|
|
119
|
+
if (model._pricingKnown === false) {
|
|
120
|
+
return hasFreeInName;
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
return isZeroCost || hasFreeInName;
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -156,12 +164,32 @@ export function getProviderRegistry(): ReadonlyMap<string, ProviderEntry> {
|
|
|
156
164
|
// Global filter application
|
|
157
165
|
// =============================================================================
|
|
158
166
|
|
|
167
|
+
function showAllForProvider(providerId: string, entry: ProviderEntry): void {
|
|
168
|
+
const allModels =
|
|
169
|
+
entry.stored.all.length > 0 ? entry.stored.all : entry.stored.free;
|
|
170
|
+
if (allModels.length > 0) {
|
|
171
|
+
entry.reRegister(allModels);
|
|
172
|
+
_logger.info(
|
|
173
|
+
`[pi-free] ${providerId}: showing all ${allModels.length} models`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
function applyFilterToProvider(
|
|
160
179
|
providerId: string,
|
|
161
180
|
entry: ProviderEntry,
|
|
162
181
|
freeOnly: boolean,
|
|
182
|
+
force: boolean,
|
|
163
183
|
): void {
|
|
164
184
|
if (freeOnly) {
|
|
185
|
+
if (!force && getProviderShowPaid(providerId)) {
|
|
186
|
+
showAllForProvider(providerId, entry);
|
|
187
|
+
_logger.info(
|
|
188
|
+
`[pi-free] ${providerId}: preserved persisted all-models toggle`,
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
165
193
|
if (entry.stored.free.length > 0) {
|
|
166
194
|
entry.reRegister(entry.stored.free);
|
|
167
195
|
_logger.info(
|
|
@@ -171,25 +199,21 @@ function applyFilterToProvider(
|
|
|
171
199
|
_logger.warn(`[pi-free] ${providerId}: no free models available`);
|
|
172
200
|
}
|
|
173
201
|
} else {
|
|
174
|
-
|
|
175
|
-
const allModels =
|
|
176
|
-
entry.stored.all.length > 0 ? entry.stored.all : entry.stored.free;
|
|
177
|
-
if (allModels.length > 0) {
|
|
178
|
-
entry.reRegister(allModels);
|
|
179
|
-
_logger.info(
|
|
180
|
-
`[pi-free] ${providerId}: showing all ${allModels.length} models`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
202
|
+
showAllForProvider(providerId, entry);
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
|
|
186
|
-
export function applyGlobalFilter(
|
|
206
|
+
export function applyGlobalFilter(
|
|
207
|
+
_pi: ExtensionAPI,
|
|
208
|
+
freeOnly: boolean,
|
|
209
|
+
options: { force?: boolean } = {},
|
|
210
|
+
): void {
|
|
187
211
|
globalFreeOnly = freeOnly;
|
|
188
212
|
saveConfig({ free_only: freeOnly });
|
|
189
213
|
|
|
190
214
|
for (const [providerId, entry] of providerRegistry) {
|
|
191
215
|
try {
|
|
192
|
-
applyFilterToProvider(providerId, entry, freeOnly);
|
|
216
|
+
applyFilterToProvider(providerId, entry, freeOnly, options.force === true);
|
|
193
217
|
} catch (err) {
|
|
194
218
|
_logger.error(
|
|
195
219
|
`[pi-free] Failed to apply filter to ${providerId}`,
|