pi-free 2.0.1 → 2.0.2
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 +106 -2
- package/README.md +548 -393
- package/config.ts +22 -2
- package/index.ts +148 -148
- package/lib/built-in-toggle.ts +23 -42
- package/lib/toggle-state.ts +86 -0
- package/lib/util.ts +256 -256
- package/package.json +1 -1
- package/provider-failover/benchmark-lookup.ts +637 -247
- package/provider-helper.ts +261 -260
- package/providers/cline/cline-models.ts +129 -128
- package/providers/cline/cline.ts +284 -298
- package/providers/cloudflare/cloudflare.ts +292 -134
- package/providers/dynamic-built-in/index.ts +2 -1
- package/providers/kilo/kilo-models.ts +2 -1
- package/providers/modal/modal.ts +47 -44
- package/providers/nvidia/nvidia.ts +296 -33
- package/providers/ollama/ollama.ts +280 -172
package/config.ts
CHANGED
|
@@ -254,10 +254,30 @@ export function getOpenrouterApiKey(): string | undefined {
|
|
|
254
254
|
// Hidden models (re-reads config on every call)
|
|
255
255
|
// =============================================================================
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Apply hidden models filter with provider scoping.
|
|
259
|
+
* Hidden models can be specified as:
|
|
260
|
+
* - "model-id" (global, applies to all providers - deprecated)
|
|
261
|
+
* - "provider/model-id" (provider-specific, preferred)
|
|
262
|
+
*/
|
|
263
|
+
export function applyHidden<T extends { id: string }>(
|
|
264
|
+
models: T[],
|
|
265
|
+
providerId?: string,
|
|
266
|
+
): T[] {
|
|
258
267
|
const hidden = new Set(loadConfigFile().hidden_models ?? []);
|
|
259
268
|
if (hidden.size === 0) return models;
|
|
260
|
-
|
|
269
|
+
|
|
270
|
+
return models.filter((m) => {
|
|
271
|
+
// Check provider-scoped ID (preferred format: "provider/model-id")
|
|
272
|
+
if (providerId && hidden.has(`${providerId}/${m.id}`)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
// Check global ID (legacy format, still supported for backward compat)
|
|
276
|
+
if (hidden.has(m.id)) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
});
|
|
261
281
|
}
|
|
262
282
|
|
|
263
283
|
// =============================================================================
|
package/index.ts
CHANGED
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi-Free Providers Index
|
|
3
|
-
*
|
|
4
|
-
* Provides free model filtering for ALL providers (built-in + extension)
|
|
5
|
-
* plus unique free/paid providers not covered by pi's built-in providers.
|
|
6
|
-
*
|
|
7
|
-
* Unique providers:
|
|
8
|
-
* - Kilo: OAuth-based free models
|
|
9
|
-
* - Cline: Cline bot integration
|
|
10
|
-
* - NVIDIA: NVIDIA NIM hosting (free tier available)
|
|
11
|
-
* - Ollama Cloud: Ollama's cloud-hosted models with usage-based free tier
|
|
12
|
-
* - Qwen: OAuth-based Qwen access (deprecated)
|
|
13
|
-
* - Modal: Modal Labs hosting
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
17
|
-
import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
|
|
18
|
-
import { createLogger } from "./lib/logger.ts";
|
|
19
|
-
import {
|
|
20
|
-
applyGlobalFilter,
|
|
21
|
-
getGlobalFreeOnly,
|
|
22
|
-
getProviderRegistry,
|
|
23
|
-
isFreeModel,
|
|
24
|
-
registerWithGlobalToggle,
|
|
25
|
-
} from "./lib/registry.ts";
|
|
26
|
-
// Import unique provider extensions (only providers NOT built into pi)
|
|
27
|
-
import cline from "./providers/cline/cline.ts";
|
|
28
|
-
import cloudflare from "./providers/cloudflare/cloudflare.ts";
|
|
29
|
-
import kilo from "./providers/kilo/kilo.ts";
|
|
30
|
-
import modal from "./providers/modal/modal.ts";
|
|
31
|
-
import nvidia from "./providers/nvidia/nvidia.ts";
|
|
32
|
-
import ollama from "./providers/ollama/ollama.ts";
|
|
33
|
-
import qwen from "./providers/qwen/qwen.ts";
|
|
34
|
-
|
|
35
|
-
const _logger = createLogger("pi-free");
|
|
36
|
-
|
|
37
|
-
// =============================================================================
|
|
38
|
-
// Global Commands
|
|
39
|
-
// =============================================================================
|
|
40
|
-
|
|
41
|
-
function setupGlobalCommands(pi: ExtensionAPI) {
|
|
42
|
-
// /free-providers - Show free model counts by provider
|
|
43
|
-
pi.registerCommand("free-providers", {
|
|
44
|
-
description: "Show free/paid model counts for all pi-free providers",
|
|
45
|
-
handler: async (_args, ctx) => {
|
|
46
|
-
const lines = ["📊 Pi-Free Providers:", ""];
|
|
47
|
-
const registry = getProviderRegistry();
|
|
48
|
-
|
|
49
|
-
// Providers known to not expose pricing via API (all models show as "free")
|
|
50
|
-
// OpenRouter and OpenCode expose actual pricing
|
|
51
|
-
const noPricingApi = new Set([
|
|
52
|
-
"mistral",
|
|
53
|
-
"xai",
|
|
54
|
-
"huggingface",
|
|
55
|
-
"groq",
|
|
56
|
-
"cerebras",
|
|
57
|
-
]);
|
|
58
|
-
// Freemium providers - all models share a free tier quota
|
|
59
|
-
const freemiumProviders = new Set(["nvidia"]);
|
|
60
|
-
|
|
61
|
-
for (const [id, entry] of registry) {
|
|
62
|
-
const free = entry.stored.free.length;
|
|
63
|
-
const all = entry.stored.all.length || free;
|
|
64
|
-
const indicator = entry.hasKey ? "🔑" : "🆓";
|
|
65
|
-
const paid = all - free;
|
|
66
|
-
|
|
67
|
-
if (freemiumProviders.has(id)) {
|
|
68
|
-
// Freemium: all models share a free tier (e.g., 1,000 reqs/month)
|
|
69
|
-
lines.push(`${indicator} ${id}: ${all} models (freemium)`);
|
|
70
|
-
} else if (noPricingApi.has(id)) {
|
|
71
|
-
// Provider doesn't expose pricing - can't determine free vs paid
|
|
72
|
-
lines.push(
|
|
73
|
-
`${indicator} ${id}: ${all} models (pricing not exposed by API)`,
|
|
74
|
-
);
|
|
75
|
-
} else if (paid === 0 && free > 0) {
|
|
76
|
-
// All models are actually free
|
|
77
|
-
lines.push(`${indicator} ${id}: ${free} free models`);
|
|
78
|
-
} else {
|
|
79
|
-
// Mix of free and paid
|
|
80
|
-
lines.push(
|
|
81
|
-
`${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (registry.size === 0) {
|
|
87
|
-
lines.push("(No providers registered yet)");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// =============================================================================
|
|
96
|
-
// Main Entry Point
|
|
97
|
-
// =============================================================================
|
|
98
|
-
|
|
99
|
-
export default async function (pi: ExtensionAPI) {
|
|
100
|
-
const globalFreeOnly = getGlobalFreeOnly();
|
|
101
|
-
_logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
|
|
102
|
-
|
|
103
|
-
// Setup global commands first
|
|
104
|
-
setupGlobalCommands(pi);
|
|
105
|
-
|
|
106
|
-
// Load all unique providers
|
|
107
|
-
// Each provider will register itself with the global toggle system
|
|
108
|
-
await Promise.allSettled([
|
|
109
|
-
cloudflare(pi),
|
|
110
|
-
modal(pi),
|
|
111
|
-
nvidia(pi),
|
|
112
|
-
kilo(pi),
|
|
113
|
-
ollama(pi),
|
|
114
|
-
// Qwen is deprecated
|
|
115
|
-
qwen(pi).catch((err) => {
|
|
116
|
-
_logger.warn("[pi-free] Qwen provider failed to load (deprecated)", err);
|
|
117
|
-
}),
|
|
118
|
-
cline(pi),
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
// Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face, OpenRouter)
|
|
122
|
-
// These only activate if the user has configured API keys (OpenRouter works without key too)
|
|
123
|
-
const { setupDynamicBuiltInProviders } = await import(
|
|
124
|
-
"./providers/dynamic-built-in/index.ts"
|
|
125
|
-
);
|
|
126
|
-
await setupDynamicBuiltInProviders(pi);
|
|
127
|
-
|
|
128
|
-
// Setup toggles for pi's built-in providers (e.g., OpenCode)
|
|
129
|
-
setupBuiltInProviderToggles(pi);
|
|
130
|
-
|
|
131
|
-
// Apply initial global filter if free-only mode is enabled
|
|
132
|
-
if (globalFreeOnly) {
|
|
133
|
-
_logger.info("[pi-free] Applying initial free-only filter");
|
|
134
|
-
await applyGlobalFilter(pi, true);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const registry = getProviderRegistry();
|
|
138
|
-
_logger.info(`[pi-free] Loaded with ${registry.size} providers`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Re-export registry helpers so consumers don't need deep imports
|
|
142
|
-
export {
|
|
143
|
-
applyGlobalFilter,
|
|
144
|
-
getGlobalFreeOnly,
|
|
145
|
-
getProviderRegistry,
|
|
146
|
-
isFreeModel,
|
|
147
|
-
registerWithGlobalToggle,
|
|
148
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Pi-Free Providers Index
|
|
3
|
+
*
|
|
4
|
+
* Provides free model filtering for ALL providers (built-in + extension)
|
|
5
|
+
* plus unique free/paid providers not covered by pi's built-in providers.
|
|
6
|
+
*
|
|
7
|
+
* Unique providers:
|
|
8
|
+
* - Kilo: OAuth-based free models
|
|
9
|
+
* - Cline: Cline bot integration
|
|
10
|
+
* - NVIDIA: NVIDIA NIM hosting (free tier available)
|
|
11
|
+
* - Ollama Cloud: Ollama's cloud-hosted models with usage-based free tier
|
|
12
|
+
* - Qwen: OAuth-based Qwen access (deprecated)
|
|
13
|
+
* - Modal: Modal Labs hosting
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import { setupBuiltInProviderToggles } from "./lib/built-in-toggle.ts";
|
|
18
|
+
import { createLogger } from "./lib/logger.ts";
|
|
19
|
+
import {
|
|
20
|
+
applyGlobalFilter,
|
|
21
|
+
getGlobalFreeOnly,
|
|
22
|
+
getProviderRegistry,
|
|
23
|
+
isFreeModel,
|
|
24
|
+
registerWithGlobalToggle,
|
|
25
|
+
} from "./lib/registry.ts";
|
|
26
|
+
// Import unique provider extensions (only providers NOT built into pi)
|
|
27
|
+
import cline from "./providers/cline/cline.ts";
|
|
28
|
+
import cloudflare from "./providers/cloudflare/cloudflare.ts";
|
|
29
|
+
import kilo from "./providers/kilo/kilo.ts";
|
|
30
|
+
import modal from "./providers/modal/modal.ts";
|
|
31
|
+
import nvidia from "./providers/nvidia/nvidia.ts";
|
|
32
|
+
import ollama from "./providers/ollama/ollama.ts";
|
|
33
|
+
import qwen from "./providers/qwen/qwen.ts";
|
|
34
|
+
|
|
35
|
+
const _logger = createLogger("pi-free");
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Global Commands
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
function setupGlobalCommands(pi: ExtensionAPI) {
|
|
42
|
+
// /free-providers - Show free model counts by provider
|
|
43
|
+
pi.registerCommand("free-providers", {
|
|
44
|
+
description: "Show free/paid model counts for all pi-free providers",
|
|
45
|
+
handler: async (_args, ctx) => {
|
|
46
|
+
const lines = ["📊 Pi-Free Providers:", ""];
|
|
47
|
+
const registry = getProviderRegistry();
|
|
48
|
+
|
|
49
|
+
// Providers known to not expose pricing via API (all models show as "free")
|
|
50
|
+
// OpenRouter and OpenCode expose actual pricing
|
|
51
|
+
const noPricingApi = new Set([
|
|
52
|
+
"mistral",
|
|
53
|
+
"xai",
|
|
54
|
+
"huggingface",
|
|
55
|
+
"groq",
|
|
56
|
+
"cerebras",
|
|
57
|
+
]);
|
|
58
|
+
// Freemium providers - all models share a free tier quota
|
|
59
|
+
const freemiumProviders = new Set(["nvidia"]);
|
|
60
|
+
|
|
61
|
+
for (const [id, entry] of registry) {
|
|
62
|
+
const free = entry.stored.free.length;
|
|
63
|
+
const all = entry.stored.all.length || free;
|
|
64
|
+
const indicator = entry.hasKey ? "🔑" : "🆓";
|
|
65
|
+
const paid = all - free;
|
|
66
|
+
|
|
67
|
+
if (freemiumProviders.has(id)) {
|
|
68
|
+
// Freemium: all models share a free tier (e.g., 1,000 reqs/month)
|
|
69
|
+
lines.push(`${indicator} ${id}: ${all} models (freemium)`);
|
|
70
|
+
} else if (noPricingApi.has(id)) {
|
|
71
|
+
// Provider doesn't expose pricing - can't determine free vs paid
|
|
72
|
+
lines.push(
|
|
73
|
+
`${indicator} ${id}: ${all} models (pricing not exposed by API)`,
|
|
74
|
+
);
|
|
75
|
+
} else if (paid === 0 && free > 0) {
|
|
76
|
+
// All models are actually free
|
|
77
|
+
lines.push(`${indicator} ${id}: ${free} free models`);
|
|
78
|
+
} else {
|
|
79
|
+
// Mix of free and paid
|
|
80
|
+
lines.push(
|
|
81
|
+
`${indicator} ${id}: ${free} free / ${paid} paid (${all} total)`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (registry.size === 0) {
|
|
87
|
+
lines.push("(No providers registered yet)");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Main Entry Point
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export default async function (pi: ExtensionAPI) {
|
|
100
|
+
const globalFreeOnly = getGlobalFreeOnly();
|
|
101
|
+
_logger.info(`[pi-free] Initializing (global free-only: ${globalFreeOnly})`);
|
|
102
|
+
|
|
103
|
+
// Setup global commands first
|
|
104
|
+
setupGlobalCommands(pi);
|
|
105
|
+
|
|
106
|
+
// Load all unique providers
|
|
107
|
+
// Each provider will register itself with the global toggle system
|
|
108
|
+
await Promise.allSettled([
|
|
109
|
+
cloudflare(pi),
|
|
110
|
+
modal(pi),
|
|
111
|
+
nvidia(pi),
|
|
112
|
+
kilo(pi),
|
|
113
|
+
ollama(pi),
|
|
114
|
+
// Qwen is deprecated
|
|
115
|
+
qwen(pi).catch((err) => {
|
|
116
|
+
_logger.warn("[pi-free] Qwen provider failed to load (deprecated)", err);
|
|
117
|
+
}),
|
|
118
|
+
cline(pi),
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
// Setup dynamic built-in providers (Mistral, Groq, Cerebras, xAI, Hugging Face, OpenRouter)
|
|
122
|
+
// These only activate if the user has configured API keys (OpenRouter works without key too)
|
|
123
|
+
const { setupDynamicBuiltInProviders } = await import(
|
|
124
|
+
"./providers/dynamic-built-in/index.ts"
|
|
125
|
+
);
|
|
126
|
+
await setupDynamicBuiltInProviders(pi);
|
|
127
|
+
|
|
128
|
+
// Setup toggles for pi's built-in providers (e.g., OpenCode)
|
|
129
|
+
setupBuiltInProviderToggles(pi);
|
|
130
|
+
|
|
131
|
+
// Apply initial global filter if free-only mode is enabled
|
|
132
|
+
if (globalFreeOnly) {
|
|
133
|
+
_logger.info("[pi-free] Applying initial free-only filter");
|
|
134
|
+
await applyGlobalFilter(pi, true);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const registry = getProviderRegistry();
|
|
138
|
+
_logger.info(`[pi-free] Loaded with ${registry.size} providers`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Re-export registry helpers so consumers don't need deep imports
|
|
142
|
+
export {
|
|
143
|
+
applyGlobalFilter,
|
|
144
|
+
getGlobalFreeOnly,
|
|
145
|
+
getProviderRegistry,
|
|
146
|
+
isFreeModel,
|
|
147
|
+
registerWithGlobalToggle,
|
|
148
|
+
};
|
package/lib/built-in-toggle.ts
CHANGED
|
@@ -16,17 +16,10 @@ import type {
|
|
|
16
16
|
ExtensionAPI,
|
|
17
17
|
ProviderModelConfig,
|
|
18
18
|
} from "@mariozechner/pi-coding-agent";
|
|
19
|
-
import {
|
|
20
|
-
getOpencodeShowPaid,
|
|
21
|
-
getOpenrouterShowPaid,
|
|
22
|
-
saveConfig,
|
|
23
|
-
} from "../config.ts";
|
|
19
|
+
import { getOpencodeShowPaid, getOpenrouterShowPaid } from "../config.ts";
|
|
24
20
|
import { createLogger } from "./logger.ts";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
isFreeModel,
|
|
28
|
-
registerWithGlobalToggle,
|
|
29
|
-
} from "./registry.ts";
|
|
21
|
+
import { isFreeModel, registerWithGlobalToggle } from "./registry.ts";
|
|
22
|
+
import { createToggleState } from "./toggle-state.ts";
|
|
30
23
|
|
|
31
24
|
const _logger = createLogger("built-in-toggle");
|
|
32
25
|
|
|
@@ -49,10 +42,9 @@ const BUILT_IN_TOGGLE_PROVIDERS: BuiltInToggleConfig[] = [
|
|
|
49
42
|
// =============================================================================
|
|
50
43
|
|
|
51
44
|
interface BuiltInProviderState {
|
|
52
|
-
free: ProviderModelConfig[];
|
|
53
|
-
all: ProviderModelConfig[];
|
|
45
|
+
stored: { free: ProviderModelConfig[]; all: ProviderModelConfig[] };
|
|
54
46
|
reRegister: (models: ProviderModelConfig[]) => void;
|
|
55
|
-
|
|
47
|
+
toggleState: ReturnType<typeof createToggleState<ProviderModelConfig>>;
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
const providerStates = new Map<string, BuiltInProviderState>();
|
|
@@ -102,35 +94,29 @@ export function setupBuiltInProviderToggles(pi: ExtensionAPI): void {
|
|
|
102
94
|
});
|
|
103
95
|
};
|
|
104
96
|
|
|
97
|
+
const stored = { free: freeModels, all: allModels };
|
|
98
|
+
const toggleState = createToggleState<ProviderModelConfig>({
|
|
99
|
+
providerId: config.id,
|
|
100
|
+
initialShowPaid: config.getShowPaid(),
|
|
101
|
+
initialModels: stored,
|
|
102
|
+
});
|
|
103
|
+
|
|
105
104
|
providerStates.set(config.id, {
|
|
106
|
-
|
|
107
|
-
all: allModels,
|
|
105
|
+
stored,
|
|
108
106
|
reRegister,
|
|
109
|
-
|
|
107
|
+
toggleState,
|
|
110
108
|
});
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
registerWithGlobalToggle(
|
|
114
|
-
config.id,
|
|
115
|
-
{ free: freeModels, all: allModels },
|
|
116
|
-
reRegister,
|
|
117
|
-
true,
|
|
118
|
-
);
|
|
110
|
+
registerWithGlobalToggle(config.id, stored, reRegister, true);
|
|
119
111
|
|
|
120
112
|
_logger.info(
|
|
121
113
|
`[built-in-toggle] ${config.id}: captured ${allModels.length} models (${freeModels.length} free)`,
|
|
122
114
|
);
|
|
123
115
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
reRegister(freeModels);
|
|
129
|
-
_logger.info(
|
|
130
|
-
`[built-in-toggle] ${config.id}: applied free-only filter`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
116
|
+
const applied = toggleState.applyCurrent(reRegister);
|
|
117
|
+
_logger.info(
|
|
118
|
+
`[built-in-toggle] ${config.id}: applied ${applied.mode} mode with ${applied.models.length} models`,
|
|
119
|
+
);
|
|
134
120
|
}
|
|
135
121
|
});
|
|
136
122
|
}
|
|
@@ -156,21 +142,16 @@ function registerToggleCommand(
|
|
|
156
142
|
return;
|
|
157
143
|
}
|
|
158
144
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// Persist preference
|
|
162
|
-
saveConfig({ [`${config.id}_show_paid`]: state.showPaid });
|
|
145
|
+
const applied = state.toggleState.toggle(state.reRegister);
|
|
163
146
|
|
|
164
|
-
if (
|
|
165
|
-
state.reRegister(state.all);
|
|
147
|
+
if (applied.mode === "all") {
|
|
166
148
|
ctx.ui.notify(
|
|
167
|
-
`${config.id}: showing all ${state.all.length} models`,
|
|
149
|
+
`${config.id}: showing all ${state.stored.all.length} models`,
|
|
168
150
|
"info",
|
|
169
151
|
);
|
|
170
152
|
} else {
|
|
171
|
-
state.reRegister(state.free);
|
|
172
153
|
ctx.ui.notify(
|
|
173
|
-
`${config.id}: showing ${state.free.length} free models`,
|
|
154
|
+
`${config.id}: showing ${state.stored.free.length} free models`,
|
|
174
155
|
"info",
|
|
175
156
|
);
|
|
176
157
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { saveConfig } from "../config.ts";
|
|
2
|
+
|
|
3
|
+
export type ToggleMode = "free" | "all";
|
|
4
|
+
|
|
5
|
+
export interface ToggleModelStore<T> {
|
|
6
|
+
free: T[];
|
|
7
|
+
all: T[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CreateToggleStateOptions<T> {
|
|
11
|
+
providerId: string;
|
|
12
|
+
initialShowPaid: boolean;
|
|
13
|
+
save?: typeof saveConfig;
|
|
14
|
+
initialModels?: ToggleModelStore<T>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ToggleResult<T> {
|
|
18
|
+
mode: ToggleMode;
|
|
19
|
+
models: T[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createToggleState<T>({
|
|
23
|
+
providerId,
|
|
24
|
+
initialShowPaid,
|
|
25
|
+
save = saveConfig,
|
|
26
|
+
initialModels,
|
|
27
|
+
}: CreateToggleStateOptions<T>) {
|
|
28
|
+
let stored: ToggleModelStore<T> = initialModels ?? { free: [], all: [] };
|
|
29
|
+
let currentMode: ToggleMode = initialShowPaid ? "all" : "free";
|
|
30
|
+
|
|
31
|
+
function resolveMode(mode: ToggleMode): ToggleResult<T> {
|
|
32
|
+
if (mode === "all") {
|
|
33
|
+
if (stored.all.length > 0) {
|
|
34
|
+
return { mode: "all", models: stored.all };
|
|
35
|
+
}
|
|
36
|
+
return { mode: "free", models: stored.free };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (stored.free.length > 0) {
|
|
40
|
+
return { mode: "free", models: stored.free };
|
|
41
|
+
}
|
|
42
|
+
return { mode: "all", models: stored.all };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function persist(mode: ToggleMode): void {
|
|
46
|
+
save({ [`${providerId}_show_paid`]: mode === "all" });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function applyMode(
|
|
50
|
+
mode: ToggleMode,
|
|
51
|
+
apply?: (models: T[]) => void,
|
|
52
|
+
): ToggleResult<T> {
|
|
53
|
+
const resolved = resolveMode(mode);
|
|
54
|
+
currentMode = resolved.mode;
|
|
55
|
+
if (apply) apply(resolved.models);
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
setModels(next: ToggleModelStore<T>): ToggleModelStore<T> {
|
|
61
|
+
stored = next;
|
|
62
|
+
const resolved = resolveMode(currentMode);
|
|
63
|
+
currentMode = resolved.mode;
|
|
64
|
+
return stored;
|
|
65
|
+
},
|
|
66
|
+
getStored(): ToggleModelStore<T> {
|
|
67
|
+
return stored;
|
|
68
|
+
},
|
|
69
|
+
getCurrentMode(): ToggleMode {
|
|
70
|
+
return currentMode;
|
|
71
|
+
},
|
|
72
|
+
getCurrentModels(): T[] {
|
|
73
|
+
return resolveMode(currentMode).models;
|
|
74
|
+
},
|
|
75
|
+
applyCurrent(apply?: (models: T[]) => void): ToggleResult<T> {
|
|
76
|
+
return applyMode(currentMode, apply);
|
|
77
|
+
},
|
|
78
|
+
applyMode,
|
|
79
|
+
toggle(apply?: (models: T[]) => void): ToggleResult<T> {
|
|
80
|
+
const nextMode = currentMode === "all" ? "free" : "all";
|
|
81
|
+
const resolved = applyMode(nextMode, apply);
|
|
82
|
+
persist(resolved.mode);
|
|
83
|
+
return resolved;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|