pi-free 2.0.1 → 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/lib/registry.ts CHANGED
@@ -1,144 +1,193 @@
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
- }
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
+ // =============================================================================
36
+ // Free-model detection
37
+ // =============================================================================
38
+
39
+ /**
40
+ * Detect if a provider exposes actual per-model pricing.
41
+ *
42
+ * Heuristic: If ALL models have cost === 0, the provider likely doesn't expose
43
+ * real pricing (cost was defaulted to 0). If SOME models have cost > 0, the
44
+ * provider definitely exposes pricing.
45
+ *
46
+ * @param allModels - All models from the provider to check
47
+ * @returns true if pricing appears to be exposed (some costs > 0)
48
+ */
49
+ function detectPricingExposed(allModels: ProviderModelConfig[]): boolean {
50
+ if (allModels.length === 0) return false;
51
+
52
+ // If ANY model has cost > 0, pricing is definitely exposed
53
+ return allModels.some(
54
+ (m) => (m.cost?.input ?? 0) > 0 || (m.cost?.output ?? 0) > 0,
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Check if a model is free using adaptive Route A/B logic.
60
+ *
61
+ * **Automatic Detection:**
62
+ * The function detects whether the provider exposes pricing by checking if
63
+ * ALL models have cost === 0. If so, it assumes no pricing is exposed and
64
+ * falls back to name-based detection.
65
+ *
66
+ * **Route A (Pricing-Exposed Providers):** Uses ONLY cost-based detection.
67
+ * - Detected when SOME models have cost > 0
68
+ * - Free = cost.input === 0 && cost.output === 0
69
+ * - No fallback to name-based detection
70
+ *
71
+ * **Route B (Non-Pricing-Exposed Providers):** Uses ONLY name-based detection.
72
+ * - Detected when ALL models have cost === 0 (or no models)
73
+ * - Free = model name contains "free" (case-insensitive)
74
+ * - No cost-based detection (avoids marking freemium as free)
75
+ *
76
+ * This automatic detection handles providers without hardcoding - if a provider
77
+ * shows all models as zero cost, we assume pricing isn't exposed and check
78
+ * model names instead.
79
+ *
80
+ * @param model - The model config to check
81
+ * @param allModels - Optional: all models from the same provider for detection
82
+ * @returns true if the model is definitively free per the provider's API
83
+ */
84
+ export function isFreeModel(
85
+ model: ProviderModelConfig & { provider?: string },
86
+ allModels?: ProviderModelConfig[],
87
+ ): boolean {
88
+ return isFreeModelInternal(model, allModels);
89
+ }
90
+
91
+ // Internal implementation to work around TypeScript filter callback issues
92
+ function isFreeModelInternal(
93
+ model: ProviderModelConfig & { provider?: string },
94
+ allModels: ProviderModelConfig[] | undefined,
95
+ ): boolean {
96
+ // Determine if pricing is exposed
97
+ let pricingExposed: boolean;
98
+
99
+ if (allModels && allModels.length > 0) {
100
+ // Dynamic detection: check if ALL models have cost === 0
101
+ // If all costs are 0, assume pricing is NOT actually exposed
102
+ pricingExposed = detectPricingExposed(allModels);
103
+ } else {
104
+ // No allModels provided - default to cost-based detection
105
+ // This maintains backward compatibility
106
+ pricingExposed = true;
107
+ }
108
+
109
+ // Route A: Pricing-exposed providers - use OR logic
110
+ // Model is free if EITHER cost is zero OR name contains "free"
111
+ if (pricingExposed) {
112
+ const isZeroCost =
113
+ (model.cost?.input ?? 0) === 0 && (model.cost?.output ?? 0) === 0;
114
+ const hasFreeInName = model.name.toLowerCase().includes("free");
115
+ return isZeroCost || hasFreeInName;
116
+ }
117
+
118
+ // Route B: Non-pricing-exposed providers - use ONLY name-based detection
119
+ // This handles providers where all costs are defaulted to 0
120
+ return model.name.toLowerCase().includes("free");
121
+ }
122
+
123
+ // =============================================================================
124
+ // Registration
125
+ // =============================================================================
126
+
127
+ /** Register a provider with the global free/paid toggle system */
128
+ export function registerWithGlobalToggle(
129
+ providerId: string,
130
+ stored: { free: ProviderModelConfig[]; all: ProviderModelConfig[] },
131
+ reRegister: (models: ProviderModelConfig[]) => void,
132
+ hasKey: boolean = false,
133
+ ): void {
134
+ providerRegistry.set(providerId, {
135
+ id: providerId,
136
+ stored,
137
+ reRegister,
138
+ hasKey,
139
+ });
140
+ _logger.info(
141
+ `[pi-free] Registered ${providerId} with global toggle (${stored.free.length} free, ${stored.all.length} total)`,
142
+ );
143
+ }
144
+
145
+ /** Get current global free-only state */
146
+ export function getGlobalFreeOnly(): boolean {
147
+ return globalFreeOnly;
148
+ }
149
+
150
+ /** Access the raw registry (used by /free-providers command) */
151
+ export function getProviderRegistry(): ReadonlyMap<string, ProviderEntry> {
152
+ return providerRegistry;
153
+ }
154
+
155
+ // =============================================================================
156
+ // Global filter application
157
+ // =============================================================================
158
+
159
+ export function applyGlobalFilter(_pi: ExtensionAPI, freeOnly: boolean): void {
160
+ globalFreeOnly = freeOnly;
161
+ saveConfig({ free_only: freeOnly });
162
+
163
+ for (const [providerId, entry] of providerRegistry) {
164
+ try {
165
+ if (freeOnly) {
166
+ // Show only free models
167
+ if (entry.stored.free.length > 0) {
168
+ entry.reRegister(entry.stored.free);
169
+ _logger.info(
170
+ `[pi-free] ${providerId}: filtered to ${entry.stored.free.length} free models`,
171
+ );
172
+ } else {
173
+ _logger.warn(`[pi-free] ${providerId}: no free models available`);
174
+ }
175
+ } else {
176
+ // Show all models (paid + free)
177
+ const allModels =
178
+ entry.stored.all.length > 0 ? entry.stored.all : entry.stored.free;
179
+ if (allModels.length > 0) {
180
+ entry.reRegister(allModels);
181
+ _logger.info(
182
+ `[pi-free] ${providerId}: showing all ${allModels.length} models`,
183
+ );
184
+ }
185
+ }
186
+ } catch (err) {
187
+ _logger.error(
188
+ `[pi-free] Failed to apply filter to ${providerId}`,
189
+ err instanceof Error ? { error: err.message } : { error: String(err) },
190
+ );
191
+ }
192
+ }
193
+ }
@@ -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
+ }
package/lib/types.ts CHANGED
@@ -1,108 +1,101 @@
1
- /**
2
- * Shared types for pi-free-providers.
3
- * Interfaces duplicated across providers consolidated here.
4
- */
5
-
6
- // =============================================================================
7
- // Provider model configuration (matches Pi's ProviderModelConfig)
8
- // =============================================================================
9
-
10
- export interface CostConfig {
11
- input: number;
12
- output: number;
13
- cacheRead: number;
14
- cacheWrite: number;
15
- }
16
-
17
- export interface ProviderModelConfig {
18
- id: string;
19
- name: string;
20
- reasoning: boolean;
21
- input: ("text" | "image")[];
22
- cost: CostConfig;
23
- contextWindow: number;
24
- maxTokens: number;
25
- }
26
-
27
- // =============================================================================
28
- // models.dev schema types
29
- // =============================================================================
30
-
31
- export interface ModelsDevCost {
32
- input: number;
33
- output: number;
34
- cache_read?: number;
35
- cache_write?: number;
36
- }
37
-
38
- export interface ModelsDevLimit {
39
- context: number;
40
- output: number;
41
- }
42
-
43
- export interface ModelsDevModalities {
44
- input?: string[];
45
- output?: string[];
46
- }
47
-
48
- export interface ModelsDevModel {
49
- id: string;
50
- name: string;
51
- reasoning: boolean;
52
- cost?: ModelsDevCost;
53
- limit: ModelsDevLimit;
54
- modalities?: ModelsDevModalities;
55
- }
56
-
57
- export interface ModelsDevProvider {
58
- id: string;
59
- api: string;
60
- models: Record<string, ModelsDevModel>;
61
- }
62
-
63
- // =============================================================================
64
- // OpenRouter API response types
65
- // =============================================================================
66
-
67
- export interface OpenRouterPricing {
68
- prompt?: string | null;
69
- completion?: string | null;
70
- input_cache_write?: string | null;
71
- input_cache_read?: string | null;
72
- }
73
-
74
- export interface OpenRouterArchitecture {
75
- input_modalities?: string[] | null;
76
- output_modalities?: string[] | null;
77
- }
78
-
79
- export interface OpenRouterTopProvider {
80
- max_completion_tokens?: number | null;
81
- }
82
-
83
- export interface OpenRouterModel {
84
- id: string;
85
- name: string;
86
- context_length: number;
87
- max_completion_tokens?: number | null;
88
- pricing?: OpenRouterPricing;
89
- architecture?: OpenRouterArchitecture;
90
- top_provider?: OpenRouterTopProvider;
91
- supported_parameters?: string[];
92
- }
93
-
94
- // =============================================================================
95
- // Zen gateway types
96
- // =============================================================================
97
-
98
- export interface ZenGatewayModel {
99
- id: string;
100
- object?: string;
101
- }
102
-
103
- // Test: LSP should handle new interface
104
- export interface LspTestInterface {
105
- name: string;
106
- value: number;
107
- enabled: boolean;
108
- }
1
+ /**
2
+ * Shared types for pi-free-providers.
3
+ * Interfaces duplicated across providers consolidated here.
4
+ */
5
+
6
+ // =============================================================================
7
+ // Provider model configuration (matches Pi's ProviderModelConfig)
8
+ // =============================================================================
9
+
10
+ export interface CostConfig {
11
+ input: number;
12
+ output: number;
13
+ cacheRead: number;
14
+ cacheWrite: number;
15
+ }
16
+
17
+ export interface ProviderModelConfig {
18
+ id: string;
19
+ name: string;
20
+ reasoning: boolean;
21
+ input: ("text" | "image")[];
22
+ cost: CostConfig;
23
+ contextWindow: number;
24
+ maxTokens: number;
25
+ }
26
+
27
+ // =============================================================================
28
+ // models.dev schema types
29
+ // =============================================================================
30
+
31
+ export interface ModelsDevCost {
32
+ input: number;
33
+ output: number;
34
+ cache_read?: number;
35
+ cache_write?: number;
36
+ }
37
+
38
+ export interface ModelsDevLimit {
39
+ context: number;
40
+ output: number;
41
+ }
42
+
43
+ export interface ModelsDevModalities {
44
+ input?: string[];
45
+ output?: string[];
46
+ }
47
+
48
+ export interface ModelsDevModel {
49
+ id: string;
50
+ name: string;
51
+ reasoning: boolean;
52
+ cost?: ModelsDevCost;
53
+ limit: ModelsDevLimit;
54
+ modalities?: ModelsDevModalities;
55
+ }
56
+
57
+ export interface ModelsDevProvider {
58
+ id: string;
59
+ api: string;
60
+ models: Record<string, ModelsDevModel>;
61
+ }
62
+
63
+ // =============================================================================
64
+ // OpenRouter API response types
65
+ // =============================================================================
66
+
67
+ export interface OpenRouterPricing {
68
+ prompt?: string | null;
69
+ completion?: string | null;
70
+ input_cache_write?: string | null;
71
+ input_cache_read?: string | null;
72
+ }
73
+
74
+ export interface OpenRouterArchitecture {
75
+ input_modalities?: string[] | null;
76
+ output_modalities?: string[] | null;
77
+ }
78
+
79
+ export interface OpenRouterTopProvider {
80
+ max_completion_tokens?: number | null;
81
+ }
82
+
83
+ export interface OpenRouterModel {
84
+ id: string;
85
+ name: string;
86
+ context_length: number;
87
+ max_completion_tokens?: number | null;
88
+ pricing?: OpenRouterPricing;
89
+ architecture?: OpenRouterArchitecture;
90
+ top_provider?: OpenRouterTopProvider;
91
+ supported_parameters?: string[];
92
+ }
93
+
94
+ // =============================================================================
95
+ // Zen gateway types
96
+ // =============================================================================
97
+
98
+ export interface ZenGatewayModel {
99
+ id: string;
100
+ object?: string;
101
+ }