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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-free",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "type": "module",
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.",
5
+ "description": "AI model providers for Pi with free model filtering and dynamic model fetching",
6
6
  "keywords": [
7
7
  "pi-package",
8
8
  "pi-extension",
@@ -41,8 +41,6 @@
41
41
  "LICENSE",
42
42
  "CHANGELOG.md",
43
43
  "banner.svg",
44
- "banner.png",
45
- "banner.jpg",
46
44
  "scripts/check-extensions.mjs"
47
45
  ],
48
46
  "scripts": {
@@ -9,15 +9,10 @@ import { applyHidden } from "../../config.ts";
9
9
  import {
10
10
  BASE_URL_OPENROUTER,
11
11
  DEFAULT_FETCH_TIMEOUT_MS,
12
- DEFAULT_MIN_SIZE_B,
13
12
  PROVIDER_CLINE,
14
13
  } from "../../constants.ts";
15
14
  import type { ProviderModelConfig } from "../../lib/types.ts";
16
- import {
17
- cleanModelName,
18
- fetchWithRetry,
19
- isUsableModel,
20
- } from "../../lib/util.ts";
15
+ import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
21
16
 
22
17
  interface OpenRouterRaw {
23
18
  id: string;
@@ -74,10 +69,8 @@ export async function fetchClineModels(
74
69
 
75
70
  const json = (await response.json()) as { data?: OpenRouterRaw[] };
76
71
 
77
- // Filter to usable models (chat-capable, size threshold)
78
- let usableModels = (json.data ?? []).filter((m) =>
79
- isUsableModel(m.id, DEFAULT_MIN_SIZE_B),
80
- );
72
+ // Filter to usable models (chat-capable)
73
+ let usableModels = json.data ?? [];
81
74
 
82
75
  // If freeOnly, filter to free models
83
76
  if (freeOnly) {
@@ -1,190 +1,194 @@
1
- /**
2
- * CrofAI Provider Extension
3
- *
4
- * Provides access to CrofAI API - OpenAI-compatible LLM inference service
5
- * hosting DeepSeek, Qwen, and other open-source models.
6
- *
7
- * NOTE: CrofAI's /v1/models returns per-model context_length, max_completion_tokens,
8
- * name, custom_reasoning, and reasoning_effort. Pricing is per-MILLION tokens.
9
- *
10
- * Setup:
11
- * 1. Get API key from https://ai.nahcrof.com
12
- * 2. Set CROFAI_API_KEY env var or add to ~/.pi/free.json
13
- *
14
- * Usage:
15
- * pi install git:github.com/apmantza/pi-free
16
- * # Set CROFAI_API_KEY env var
17
- * # Models appear in /model selector
18
- */
19
-
20
- import type {
21
- ExtensionAPI,
22
- ProviderModelConfig,
23
- } from "@earendil-works/pi-coding-agent";
24
- import { getCrofaiApiKey, getCrofaiShowPaid } from "../../config.ts";
25
- import {
26
- BASE_URL_CROFAI,
27
- DEFAULT_FETCH_TIMEOUT_MS,
28
- PROVIDER_CROFAI,
29
- } from "../../constants.ts";
30
- import { createLogger } from "../../lib/logger.ts";
31
- import {
32
- getProxyModelCompat,
33
- isLikelyReasoningModel,
34
- } from "../../lib/provider-compat.ts";
35
- import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
36
- import { fetchWithRetry } from "../../lib/util.ts";
37
- import { createReRegister, setupProvider } from "../../provider-helper.ts";
38
-
39
- const _logger = createLogger("crofai");
40
-
41
- // =============================================================================
42
- // Types
43
- // =============================================================================
44
-
45
- interface CrofaiModel {
46
- id: string;
47
- name?: string;
48
- context_length?: number;
49
- max_completion_tokens?: number;
50
- custom_reasoning?: boolean;
51
- reasoning_effort?: boolean;
52
- pricing?: {
53
- prompt?: string;
54
- completion?: string;
55
- cache_prompt?: string;
56
- };
57
- }
58
-
59
- // =============================================================================
60
- // Fetch
61
- // =============================================================================
62
-
63
- function parseCrofaiPrice(priceStr: string | undefined): number {
64
- if (priceStr === undefined) return 0;
65
- const num = Number.parseFloat(priceStr);
66
- if (Number.isNaN(num)) return 0;
67
- // CrofAI pricing is per-MILLION tokens. Divide to get per-token (Pi convention).
68
- return num / 1_000_000;
69
- }
70
-
71
- async function fetchCrofaiModels(
72
- apiKey: string,
73
- ): Promise<ProviderModelConfig[]> {
74
- const response = await fetchWithRetry(
75
- `${BASE_URL_CROFAI}/models`,
76
- {
77
- headers: {
78
- Authorization: `Bearer ${apiKey}`,
79
- "Content-Type": "application/json",
80
- },
81
- },
82
- 3,
83
- 1000,
84
- DEFAULT_FETCH_TIMEOUT_MS,
85
- );
86
-
87
- if (!response.ok) {
88
- throw new Error(
89
- `CrofAI API error: ${response.status} ${response.statusText}`,
90
- );
91
- }
92
-
93
- // CrofAI returns { data: [...] }
94
- const json = (await response.json()) as {
95
- data?: CrofaiModel[];
96
- };
97
- const models = json.data ?? [];
98
-
99
- _logger.info(`[crofai] Fetched ${models.length} models`);
100
-
101
- return models
102
- .filter((m) => m.id)
103
- .map((m): ProviderModelConfig => {
104
- const name = m.name || m.id;
105
- const reasoning =
106
- m.custom_reasoning ?? isLikelyReasoningModel({ id: m.id, name });
107
-
108
- return {
109
- id: m.id,
110
- name,
111
- reasoning,
112
- input: ["text"],
113
- cost: {
114
- input: parseCrofaiPrice(m.pricing?.prompt),
115
- output: parseCrofaiPrice(m.pricing?.completion),
116
- cacheRead: parseCrofaiPrice(m.pricing?.cache_prompt),
117
- cacheWrite: 0,
118
- },
119
- contextWindow: m.context_length ?? 128_000,
120
- maxTokens: m.max_completion_tokens ?? 16_384,
121
- compat: getProxyModelCompat({ id: m.id, name }),
122
- };
123
- });
124
- }
125
-
126
- // =============================================================================
127
- // Extension Entry Point
128
- // =============================================================================
129
-
130
- export default async function crofaiProvider(pi: ExtensionAPI) {
131
- const apiKey = getCrofaiApiKey();
132
-
133
- if (!apiKey) {
134
- _logger.info(
135
- "[crofai] Skipping - CROFAI_API_KEY not set (env var or ~/.pi/free.json)",
136
- );
137
- return;
138
- }
139
-
140
- // Fetch models
141
- const allModels = await fetchCrofaiModels(apiKey);
142
-
143
- if (allModels.length === 0) {
144
- _logger.warn("[crofai] No models available");
145
- return;
146
- }
147
-
148
- const freeModels = allModels.filter((m) =>
149
- isFreeModel({ ...m, provider: PROVIDER_CROFAI }, allModels),
150
- );
151
-
152
- const stored = { free: freeModels, all: allModels };
153
-
154
- _logger.info(
155
- `[crofai] Registered ${allModels.length} models (${freeModels.length} free)`,
156
- );
157
-
158
- // Create re-register function
159
- const reRegister = createReRegister(pi, {
160
- providerId: PROVIDER_CROFAI,
161
- baseUrl: BASE_URL_CROFAI,
162
- apiKey,
163
- });
164
-
165
- // Register with global toggle
166
- registerWithGlobalToggle(PROVIDER_CROFAI, stored, reRegister, true);
167
-
168
- // Setup provider with toggle command
169
- setupProvider(
170
- pi,
171
- {
172
- providerId: PROVIDER_CROFAI,
173
- initialShowPaid: getCrofaiShowPaid(),
174
- reRegister: (models, _stored) => {
175
- if (_stored) {
176
- stored.free = _stored.free;
177
- stored.all = _stored.all;
178
- }
179
- reRegister(models);
180
- },
181
- },
182
- stored,
183
- );
184
-
185
- // Initial registration — respect persisted toggle state
186
- const showPaid = getCrofaiShowPaid();
187
- const initialModels =
188
- showPaid && stored.all.length > 0 ? stored.all : freeModels;
189
- reRegister(initialModels);
190
- }
1
+ /**
2
+ * CrofAI Provider Extension
3
+ *
4
+ * Provides access to CrofAI API - OpenAI-compatible LLM inference service
5
+ * hosting DeepSeek, Qwen, and other open-source models.
6
+ *
7
+ * NOTE: CrofAI's /v1/models returns per-model context_length, max_completion_tokens,
8
+ * name, custom_reasoning, and reasoning_effort. Pricing is per-MILLION tokens.
9
+ *
10
+ * Setup:
11
+ * 1. Get API key from https://ai.nahcrof.com
12
+ * 2. Set CROFAI_API_KEY env var or add to ~/.pi/free.json
13
+ *
14
+ * Usage:
15
+ * pi install git:github.com/apmantza/pi-free
16
+ * # Set CROFAI_API_KEY env var
17
+ * # Models appear in /model selector
18
+ */
19
+
20
+ import type {
21
+ ExtensionAPI,
22
+ ProviderModelConfig,
23
+ } from "@earendil-works/pi-coding-agent";
24
+ import { getCrofaiApiKey, getCrofaiShowPaid } from "../../config.ts";
25
+ import {
26
+ BASE_URL_CROFAI,
27
+ DEFAULT_FETCH_TIMEOUT_MS,
28
+ PROVIDER_CROFAI,
29
+ } from "../../constants.ts";
30
+ import { createLogger } from "../../lib/logger.ts";
31
+ import {
32
+ getProxyModelCompat,
33
+ isLikelyReasoningModel,
34
+ } from "../../lib/provider-compat.ts";
35
+ import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
36
+ import { fetchWithRetry } from "../../lib/util.ts";
37
+ import { createReRegister, setupProvider } from "../../provider-helper.ts";
38
+
39
+ const _logger = createLogger("crofai");
40
+
41
+ // =============================================================================
42
+ // Types
43
+ // =============================================================================
44
+
45
+ interface CrofaiModel {
46
+ id: string;
47
+ name?: string;
48
+ context_length?: number;
49
+ max_completion_tokens?: number;
50
+ custom_reasoning?: boolean;
51
+ reasoning_effort?: boolean;
52
+ pricing?: {
53
+ prompt?: string;
54
+ completion?: string;
55
+ cache_prompt?: string;
56
+ };
57
+ }
58
+
59
+ // =============================================================================
60
+ // Fetch
61
+ // =============================================================================
62
+
63
+ function parseCrofaiPrice(priceStr: string | undefined): number {
64
+ if (priceStr === undefined) return 0;
65
+ const num = Number.parseFloat(priceStr);
66
+ if (Number.isNaN(num)) return 0;
67
+ // CrofAI pricing is per-MILLION tokens. Divide to get per-token (Pi convention).
68
+ return num / 1_000_000;
69
+ }
70
+
71
+ async function fetchCrofaiModels(
72
+ apiKey: string,
73
+ ): Promise<ProviderModelConfig[]> {
74
+ const response = await fetchWithRetry(
75
+ `${BASE_URL_CROFAI}/models`,
76
+ {
77
+ headers: {
78
+ Authorization: `Bearer ${apiKey}`,
79
+ "Content-Type": "application/json",
80
+ },
81
+ },
82
+ 3,
83
+ 1000,
84
+ DEFAULT_FETCH_TIMEOUT_MS,
85
+ );
86
+
87
+ if (!response.ok) {
88
+ throw new Error(
89
+ `CrofAI API error: ${response.status} ${response.statusText}`,
90
+ );
91
+ }
92
+
93
+ // CrofAI returns { data: [...] }
94
+ const json = (await response.json()) as {
95
+ data?: CrofaiModel[];
96
+ };
97
+ const models = json.data ?? [];
98
+
99
+ _logger.info(`[crofai] Fetched ${models.length} models`);
100
+
101
+ return models
102
+ .filter((m) => m.id)
103
+ .map((m): ProviderModelConfig => {
104
+ const name = m.name || m.id;
105
+ const reasoning =
106
+ m.custom_reasoning ?? isLikelyReasoningModel({ id: m.id, name });
107
+
108
+ return {
109
+ id: m.id,
110
+ name,
111
+ reasoning,
112
+ input: ["text"],
113
+ cost: {
114
+ input: parseCrofaiPrice(m.pricing?.prompt),
115
+ output: parseCrofaiPrice(m.pricing?.completion),
116
+ cacheRead: parseCrofaiPrice(m.pricing?.cache_prompt),
117
+ cacheWrite: 0,
118
+ },
119
+ contextWindow: m.context_length ?? 128_000,
120
+ maxTokens: m.max_completion_tokens ?? 16_384,
121
+ compat: getProxyModelCompat({ id: m.id, name }),
122
+ _pricingKnown:
123
+ m.pricing?.prompt !== undefined ||
124
+ m.pricing?.completion !== undefined ||
125
+ m.pricing?.cache_prompt !== undefined,
126
+ } as ProviderModelConfig & { _pricingKnown?: boolean };
127
+ });
128
+ }
129
+
130
+ // =============================================================================
131
+ // Extension Entry Point
132
+ // =============================================================================
133
+
134
+ export default async function crofaiProvider(pi: ExtensionAPI) {
135
+ const apiKey = getCrofaiApiKey();
136
+
137
+ if (!apiKey) {
138
+ _logger.info(
139
+ "[crofai] Skipping - CROFAI_API_KEY not set (env var or ~/.pi/free.json)",
140
+ );
141
+ return;
142
+ }
143
+
144
+ // Fetch models
145
+ const allModels = await fetchCrofaiModels(apiKey);
146
+
147
+ if (allModels.length === 0) {
148
+ _logger.warn("[crofai] No models available");
149
+ return;
150
+ }
151
+
152
+ const freeModels = allModels.filter((m) =>
153
+ isFreeModel({ ...m, provider: PROVIDER_CROFAI }, allModels),
154
+ );
155
+
156
+ const stored = { free: freeModels, all: allModels };
157
+
158
+ _logger.info(
159
+ `[crofai] Registered ${allModels.length} models (${freeModels.length} free)`,
160
+ );
161
+
162
+ // Create re-register function
163
+ const reRegister = createReRegister(pi, {
164
+ providerId: PROVIDER_CROFAI,
165
+ baseUrl: BASE_URL_CROFAI,
166
+ apiKey,
167
+ });
168
+
169
+ // Register with global toggle
170
+ registerWithGlobalToggle(PROVIDER_CROFAI, stored, reRegister, true);
171
+
172
+ // Setup provider with toggle command
173
+ setupProvider(
174
+ pi,
175
+ {
176
+ providerId: PROVIDER_CROFAI,
177
+ initialShowPaid: getCrofaiShowPaid(),
178
+ reRegister: (models, _stored) => {
179
+ if (_stored) {
180
+ stored.free = _stored.free;
181
+ stored.all = _stored.all;
182
+ }
183
+ reRegister(models);
184
+ },
185
+ },
186
+ stored,
187
+ );
188
+
189
+ // Initial registration — respect persisted toggle state
190
+ const showPaid = getCrofaiShowPaid();
191
+ const initialModels =
192
+ showPaid && stored.all.length > 0 ? stored.all : freeModels;
193
+ reRegister(initialModels);
194
+ }