pi-free 2.0.2 → 2.0.5
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 +84 -12
- package/README.md +44 -97
- package/banner.svg +132 -0
- package/config.ts +24 -52
- package/constants.ts +6 -0
- package/index.ts +175 -148
- package/lib/built-in-toggle.ts +40 -1
- package/lib/model-detection.ts +176 -139
- package/lib/model-enhancer.ts +20 -20
- package/lib/open-browser.ts +1 -1
- package/lib/provider-compat.ts +46 -0
- package/lib/registry.ts +200 -144
- package/lib/types.ts +101 -108
- package/lib/util.ts +262 -256
- package/package.json +9 -8
- package/provider-failover/benchmark-lookup.ts +191 -140
- package/provider-helper.ts +19 -1
- package/providers/cline/cline-auth.ts +473 -473
- package/providers/cline/cline.ts +58 -14
- package/providers/crofai/crofai.ts +170 -0
- package/providers/dynamic-built-in/index.ts +260 -308
- package/providers/kilo/kilo-auth.ts +155 -155
- package/providers/kilo/kilo.ts +263 -235
- package/providers/nvidia/nvidia.ts +474 -415
- package/providers/ollama/ollama.ts +295 -280
- package/providers/opencode-session.ts +3 -4
- package/providers/qwen/qwen-models.ts +101 -101
- package/providers/qwen/qwen.ts +47 -49
- package/providers/zenmux/zenmux.ts +176 -0
- package/scripts/check-extensions.mjs +71 -55
- package/provider-factory.ts +0 -207
- package/providers/cloudflare/cloudflare.ts +0 -526
- package/providers/modal/modal.ts +0 -47
package/providers/cline/cline.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
|
|
18
18
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
19
19
|
import { getClineShowPaid } from "../../config.ts";
|
|
20
20
|
import { BASE_URL_CLINE, PROVIDER_CLINE } from "../../constants.ts";
|
|
21
|
-
import { registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
21
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
22
22
|
import { createToggleState } from "../../lib/toggle-state.ts";
|
|
23
23
|
import { logWarning } from "../../lib/util.ts";
|
|
24
24
|
import { enhanceWithCI } from "../../provider-helper.ts";
|
|
@@ -132,25 +132,30 @@ function extractTaskBody(content: unknown): string {
|
|
|
132
132
|
if (!Array.isArray(content)) return "";
|
|
133
133
|
for (const p of content as any[]) {
|
|
134
134
|
if (p?.type !== "text" || typeof p?.text !== "string") continue;
|
|
135
|
-
const m = p.text.match(/<task
|
|
135
|
+
const m = p.text.match(/<task>([\s\S]*?)<\/task>/);
|
|
136
136
|
if (m?.[1]) return m[1].trim();
|
|
137
137
|
}
|
|
138
138
|
return "";
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
function findLastClineWrappedMessage(messages: any[]): {
|
|
142
|
+
index: number;
|
|
143
|
+
transcript: string;
|
|
144
|
+
} {
|
|
144
145
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
145
146
|
if (messages[i]?.role !== "user") continue;
|
|
146
147
|
if (!isClineWrapped(messages[i]?.content)) continue;
|
|
147
|
-
|
|
148
|
-
baseTranscript = extractTaskBody(messages[i].content);
|
|
149
|
-
break;
|
|
148
|
+
return { index: i, transcript: extractTaskBody(messages[i].content) };
|
|
150
149
|
}
|
|
150
|
+
return { index: -1, transcript: "" };
|
|
151
|
+
}
|
|
151
152
|
|
|
153
|
+
function buildTranscriptParts(
|
|
154
|
+
messages: any[],
|
|
155
|
+
startIdx: number,
|
|
156
|
+
baseTranscript: string,
|
|
157
|
+
): string[] {
|
|
152
158
|
const parts: string[] = baseTranscript ? [baseTranscript] : [];
|
|
153
|
-
const startIdx = lastWrappedIdx >= 0 ? lastWrappedIdx + 1 : 0;
|
|
154
159
|
|
|
155
160
|
for (let i = startIdx; i < messages.length; i++) {
|
|
156
161
|
const msg = messages[i];
|
|
@@ -167,9 +172,10 @@ function shapeMessagesForCline(messages: any[]): any[] {
|
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
return parts;
|
|
176
|
+
}
|
|
172
177
|
|
|
178
|
+
function buildCollapsedMessage(messages: any[], transcript: string): any[] {
|
|
173
179
|
const collapsed: any[] = [];
|
|
174
180
|
const systemMsg = messages.find((m: any) => m?.role === "system");
|
|
175
181
|
if (systemMsg) {
|
|
@@ -182,13 +188,24 @@ function shapeMessagesForCline(messages: any[]): any[] {
|
|
|
182
188
|
content: [
|
|
183
189
|
{ type: "text", text: `<task>\n${transcript}\n</task>` },
|
|
184
190
|
{ type: "text", text: TASK_PROGRESS_BLOCK },
|
|
185
|
-
{ type: "text", text:
|
|
191
|
+
{ type: "text", text: buildEnvironmentDetails() },
|
|
186
192
|
],
|
|
187
193
|
});
|
|
188
194
|
|
|
189
195
|
return collapsed;
|
|
190
196
|
}
|
|
191
197
|
|
|
198
|
+
function shapeMessagesForCline(messages: any[]): any[] {
|
|
199
|
+
const { index: lastWrappedIdx, transcript: baseTranscript } =
|
|
200
|
+
findLastClineWrappedMessage(messages);
|
|
201
|
+
|
|
202
|
+
const startIdx = lastWrappedIdx >= 0 ? lastWrappedIdx + 1 : 0;
|
|
203
|
+
const parts = buildTranscriptParts(messages, startIdx, baseTranscript);
|
|
204
|
+
const transcript = parts.join("\n\n").trim() || "(no conversation yet)";
|
|
205
|
+
|
|
206
|
+
return buildCollapsedMessage(messages, transcript);
|
|
207
|
+
}
|
|
208
|
+
|
|
192
209
|
// =============================================================================
|
|
193
210
|
// Extension entry point
|
|
194
211
|
// =============================================================================
|
|
@@ -198,7 +215,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
198
215
|
logWarning("cline", "Failed to fetch models at startup", err);
|
|
199
216
|
return [];
|
|
200
217
|
});
|
|
201
|
-
let freeModels = allModels.filter((m) =>
|
|
218
|
+
let freeModels = allModels.filter((m) =>
|
|
219
|
+
isFreeModel({ ...m, provider: PROVIDER_CLINE }, allModels),
|
|
220
|
+
);
|
|
202
221
|
const stored = { free: freeModels, all: allModels };
|
|
203
222
|
const toggleState = createToggleState({
|
|
204
223
|
providerId: PROVIDER_CLINE,
|
|
@@ -246,6 +265,29 @@ export default async function (pi: ExtensionAPI) {
|
|
|
246
265
|
},
|
|
247
266
|
});
|
|
248
267
|
|
|
268
|
+
// ── Status bar for provider selection ─────────────────────────
|
|
269
|
+
|
|
270
|
+
pi.on("model_select", (_event, ctx) => {
|
|
271
|
+
if (_event.model?.provider !== PROVIDER_CLINE) {
|
|
272
|
+
ctx.ui.setStatus(`${PROVIDER_CLINE}-status`, undefined);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const free = stored.free.length;
|
|
277
|
+
const total = stored.all.length;
|
|
278
|
+
const paid = total - free;
|
|
279
|
+
const mode = toggleState.getCurrentMode();
|
|
280
|
+
let status: string;
|
|
281
|
+
if (paid === 0) {
|
|
282
|
+
status = `cline: ${free} free models`;
|
|
283
|
+
} else if (mode === "all") {
|
|
284
|
+
status = `cline: ${total} models (free + paid)`;
|
|
285
|
+
} else {
|
|
286
|
+
status = `cline: ${free} free \u00b7 ${paid} paid`;
|
|
287
|
+
}
|
|
288
|
+
ctx.ui.setStatus(`${PROVIDER_CLINE}-status`, status);
|
|
289
|
+
});
|
|
290
|
+
|
|
249
291
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
250
292
|
if (ctx.model?.provider !== PROVIDER_CLINE) return;
|
|
251
293
|
_currentTaskId = generateUlid();
|
|
@@ -263,7 +305,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
263
305
|
const fresh = await fetchClineModels(false);
|
|
264
306
|
if (fresh.length > 0) {
|
|
265
307
|
allModels = fresh;
|
|
266
|
-
freeModels = allModels.filter((m) =>
|
|
308
|
+
freeModels = allModels.filter((m) =>
|
|
309
|
+
isFreeModel({ ...m, provider: PROVIDER_CLINE }, allModels),
|
|
310
|
+
);
|
|
267
311
|
stored.all = allModels;
|
|
268
312
|
stored.free = freeModels;
|
|
269
313
|
toggleState.setModels(stored);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrofAI Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides access to CrofAI API - OpenAI-compatible LLM inference service.
|
|
5
|
+
*
|
|
6
|
+
* Setup:
|
|
7
|
+
* 1. Get API key from https://ai.nahcrof.com
|
|
8
|
+
* 2. Set CROFAI_API_KEY env var or add to ~/.pi/free.json
|
|
9
|
+
*
|
|
10
|
+
* Responds to global free-only filter.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* pi install git:github.com/apmantza/pi-free
|
|
14
|
+
* # Set CROFAI_API_KEY env var
|
|
15
|
+
* # Models appear in /model selector
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ExtensionAPI,
|
|
20
|
+
ProviderModelConfig,
|
|
21
|
+
} from "@mariozechner/pi-coding-agent";
|
|
22
|
+
import { getCrofaiApiKey, getCrofaiShowPaid } from "../../config.ts";
|
|
23
|
+
import {
|
|
24
|
+
BASE_URL_CROFAI,
|
|
25
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
26
|
+
PROVIDER_CROFAI,
|
|
27
|
+
} from "../../constants.ts";
|
|
28
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
29
|
+
import {
|
|
30
|
+
getProxyModelCompat,
|
|
31
|
+
isLikelyReasoningModel,
|
|
32
|
+
} from "../../lib/provider-compat.ts";
|
|
33
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
34
|
+
import { fetchWithRetry } from "../../lib/util.ts";
|
|
35
|
+
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
36
|
+
|
|
37
|
+
const _logger = createLogger("crofai");
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Fetch CrofAI models
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
interface CrofaiModel {
|
|
44
|
+
id: string;
|
|
45
|
+
object?: string;
|
|
46
|
+
created?: number;
|
|
47
|
+
owned_by?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fetchCrofaiModels(
|
|
51
|
+
apiKey: string,
|
|
52
|
+
): Promise<ProviderModelConfig[]> {
|
|
53
|
+
_logger.info("[crofai] Fetching models from CrofAI API...");
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetchWithRetry(
|
|
57
|
+
`${BASE_URL_CROFAI}/models`,
|
|
58
|
+
{
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${apiKey}`,
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
3,
|
|
65
|
+
1000,
|
|
66
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`CrofAI API error: ${response.status}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = (await response.json()) as { data?: CrofaiModel[] };
|
|
74
|
+
const models = data.data ?? [];
|
|
75
|
+
|
|
76
|
+
_logger.info(`[crofai] Fetched ${models.length} models`);
|
|
77
|
+
|
|
78
|
+
return models
|
|
79
|
+
.filter((m) => m.id) // Filter out any empty entries
|
|
80
|
+
.map((m) => {
|
|
81
|
+
const name = m.id.split("/").pop() || m.id;
|
|
82
|
+
return {
|
|
83
|
+
id: m.id,
|
|
84
|
+
name,
|
|
85
|
+
reasoning: isLikelyReasoningModel({ id: m.id, name }),
|
|
86
|
+
input: ["text"],
|
|
87
|
+
cost: {
|
|
88
|
+
input: 0, // CrofAI doesn't expose pricing via API
|
|
89
|
+
output: 0,
|
|
90
|
+
cacheRead: 0,
|
|
91
|
+
cacheWrite: 0,
|
|
92
|
+
},
|
|
93
|
+
contextWindow: 128000, // Default, varies by model
|
|
94
|
+
maxTokens: 4096,
|
|
95
|
+
compat: getProxyModelCompat({ id: m.id, name }),
|
|
96
|
+
} satisfies ProviderModelConfig;
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
_logger.error("[crofai] Failed to fetch models:", {
|
|
100
|
+
error: error instanceof Error ? error.message : String(error),
|
|
101
|
+
});
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Extension Entry Point
|
|
108
|
+
// =============================================================================
|
|
109
|
+
|
|
110
|
+
export default async function crofaiProvider(pi: ExtensionAPI) {
|
|
111
|
+
const apiKey = getCrofaiApiKey();
|
|
112
|
+
|
|
113
|
+
if (!apiKey) {
|
|
114
|
+
_logger.info(
|
|
115
|
+
"[crofai] Skipping - CROFAI_API_KEY not set (env var or ~/.pi/free.json)",
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fetch models
|
|
121
|
+
const allModels = await fetchCrofaiModels(apiKey);
|
|
122
|
+
|
|
123
|
+
if (allModels.length === 0) {
|
|
124
|
+
_logger.warn("[crofai] No models available");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Use isFreeModel with allModels for proper detection
|
|
129
|
+
// CrofAI doesn't expose pricing (all costs are $0), so Route B will be used:
|
|
130
|
+
// FREE only if "free" in name
|
|
131
|
+
const freeModels = allModels.filter((m) =>
|
|
132
|
+
isFreeModel({ ...m, provider: PROVIDER_CROFAI }, allModels),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const stored = { free: freeModels, all: allModels };
|
|
136
|
+
|
|
137
|
+
_logger.info(
|
|
138
|
+
`[crofai] Registered ${allModels.length} models (${freeModels.length} free)`,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Create re-register function
|
|
142
|
+
const reRegister = createReRegister(pi, {
|
|
143
|
+
providerId: PROVIDER_CROFAI,
|
|
144
|
+
baseUrl: BASE_URL_CROFAI,
|
|
145
|
+
apiKey,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Register with global toggle
|
|
149
|
+
registerWithGlobalToggle(PROVIDER_CROFAI, stored, reRegister, true);
|
|
150
|
+
|
|
151
|
+
// Setup provider with toggle command
|
|
152
|
+
setupProvider(
|
|
153
|
+
pi,
|
|
154
|
+
{
|
|
155
|
+
providerId: PROVIDER_CROFAI,
|
|
156
|
+
initialShowPaid: getCrofaiShowPaid(),
|
|
157
|
+
reRegister: (models, _stored) => {
|
|
158
|
+
if (_stored) {
|
|
159
|
+
stored.free = _stored.free;
|
|
160
|
+
stored.all = _stored.all;
|
|
161
|
+
}
|
|
162
|
+
reRegister(models);
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
stored,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Initial registration
|
|
169
|
+
reRegister(freeModels);
|
|
170
|
+
}
|