pi-free 2.0.4 → 2.0.6
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/README.md +495 -495
- package/banner.svg +132 -0
- package/index.ts +1 -1
- package/lib/model-detection.ts +176 -139
- package/lib/open-browser.ts +31 -4
- package/lib/registry.ts +28 -21
- package/lib/util.ts +351 -256
- package/package.json +2 -1
- package/provider-failover/benchmark-lookup.ts +702 -637
- package/providers/cline/cline.ts +27 -10
- package/providers/dynamic-built-in/index.ts +3 -1
- package/providers/nvidia/nvidia.ts +48 -50
- package/providers/qwen/qwen.ts +47 -49
- package/scripts/check-extensions.mjs +18 -4
package/providers/cline/cline.ts
CHANGED
|
@@ -138,19 +138,24 @@ function extractTaskBody(content: unknown): string {
|
|
|
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
|
// =============================================================================
|
|
@@ -58,7 +58,9 @@ interface FetchModelsOptions {
|
|
|
58
58
|
async function fetchModelsFromEndpoint(
|
|
59
59
|
opts: FetchModelsOptions,
|
|
60
60
|
): Promise<ProviderModelConfig[]> {
|
|
61
|
-
|
|
61
|
+
let cleanBase = opts.baseUrl;
|
|
62
|
+
while (cleanBase.endsWith("/")) cleanBase = cleanBase.slice(0, -1);
|
|
63
|
+
const url = `${cleanBase}/models`;
|
|
62
64
|
const headers: Record<string, string> = {
|
|
63
65
|
Accept: "application/json",
|
|
64
66
|
Authorization: `Bearer ${opts.apiKey}`,
|
|
@@ -170,39 +170,35 @@ function inferModelFromId(id: string): ModelsDevModel | null {
|
|
|
170
170
|
// Fetch + map
|
|
171
171
|
// =============================================================================
|
|
172
172
|
|
|
173
|
-
async function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
`${BASE_URL_NVIDIA}/models`,
|
|
182
|
-
{
|
|
183
|
-
headers: {
|
|
184
|
-
Authorization: `Bearer ${apiKey}`,
|
|
185
|
-
"User-Agent": "pi-free-providers",
|
|
186
|
-
},
|
|
173
|
+
async function fetchNvidiaApiModelIds(apiKey: string): Promise<Set<string>> {
|
|
174
|
+
try {
|
|
175
|
+
const response = await fetchWithRetry(
|
|
176
|
+
`${BASE_URL_NVIDIA}/models`,
|
|
177
|
+
{
|
|
178
|
+
headers: {
|
|
179
|
+
Authorization: `Bearer ${apiKey}`,
|
|
180
|
+
"User-Agent": "pi-free-providers",
|
|
187
181
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
182
|
+
},
|
|
183
|
+
3,
|
|
184
|
+
1000,
|
|
185
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
186
|
+
);
|
|
187
|
+
if (response.ok) {
|
|
188
|
+
const json = (await response.json()) as {
|
|
189
|
+
data?: Array<{ id: string }>;
|
|
190
|
+
};
|
|
191
|
+
if (json.data) {
|
|
192
|
+
return new Set(json.data.map((m) => m.id));
|
|
199
193
|
}
|
|
200
|
-
} catch (error) {
|
|
201
|
-
console.error("[nvidia] Failed to fetch models from NVIDIA API", error);
|
|
202
194
|
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error("[nvidia] Failed to fetch models from NVIDIA API", error);
|
|
203
197
|
}
|
|
198
|
+
return new Set();
|
|
199
|
+
}
|
|
204
200
|
|
|
205
|
-
|
|
201
|
+
async function fetchModelsDevMetadata(): Promise<Map<string, ModelsDevModel>> {
|
|
206
202
|
const devModels = new Map<string, ModelsDevModel>();
|
|
207
203
|
try {
|
|
208
204
|
const response = await fetchWithRetry(
|
|
@@ -226,6 +222,27 @@ async function fetchNvidiaModels(
|
|
|
226
222
|
} catch (error) {
|
|
227
223
|
console.error("[nvidia] Failed to fetch models.dev", error);
|
|
228
224
|
}
|
|
225
|
+
return devModels;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isChatModel(m: ModelsDevModel): boolean {
|
|
229
|
+
const modalities = m.modalities;
|
|
230
|
+
if (!modalities) return true;
|
|
231
|
+
const output = modalities.output ?? [];
|
|
232
|
+
const input = modalities.input ?? [];
|
|
233
|
+
return output.includes("text") && input.includes("text");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function fetchNvidiaModels(
|
|
237
|
+
apiKey?: string,
|
|
238
|
+
): Promise<ProviderModelConfig[]> {
|
|
239
|
+
// ── 1. Query NVIDIA's actual API (source of truth) ─────────────────
|
|
240
|
+
const apiModelIds = apiKey
|
|
241
|
+
? await fetchNvidiaApiModelIds(apiKey)
|
|
242
|
+
: new Set<string>();
|
|
243
|
+
|
|
244
|
+
// ── 2. Fetch models.dev for rich metadata (cost, limits, etc.) ─────
|
|
245
|
+
const devModels = await fetchModelsDevMetadata();
|
|
229
246
|
|
|
230
247
|
// ── 3. Build unified list (NVIDIA API wins; fallback to models.dev) ─
|
|
231
248
|
const modelIds =
|
|
@@ -233,30 +250,11 @@ async function fetchNvidiaModels(
|
|
|
233
250
|
|
|
234
251
|
const result = applyHidden(
|
|
235
252
|
modelIds
|
|
236
|
-
.map((id) =>
|
|
237
|
-
const dev = devModels.get(id);
|
|
238
|
-
if (dev) return dev;
|
|
239
|
-
return inferModelFromId(id);
|
|
240
|
-
})
|
|
253
|
+
.map((id) => devModels.get(id) ?? inferModelFromId(id))
|
|
241
254
|
.filter((m): m is ModelsDevModel => m !== null)
|
|
242
255
|
.filter((m) => isUsableModel(m.id, NVIDIA_MIN_SIZE_B))
|
|
243
|
-
.filter(
|
|
244
|
-
|
|
245
|
-
if (modalities) {
|
|
246
|
-
const output = modalities.output ?? [];
|
|
247
|
-
const input = modalities.input ?? [];
|
|
248
|
-
if (!output.includes("text")) return false;
|
|
249
|
-
if (!input.includes("text")) return false;
|
|
250
|
-
}
|
|
251
|
-
return true;
|
|
252
|
-
})
|
|
253
|
-
// Filter out known 404 models (listed but not provisioned for chat)
|
|
254
|
-
.filter((m) => {
|
|
255
|
-
if (NVIDIA_KNOWN_404_MODELS.has(m.id)) {
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
return true;
|
|
259
|
-
})
|
|
256
|
+
.filter(isChatModel)
|
|
257
|
+
.filter((m) => !NVIDIA_KNOWN_404_MODELS.has(m.id))
|
|
260
258
|
// NVIDIA is freemium — all models are usable with free credits.
|
|
261
259
|
// No cost filtering applied.
|
|
262
260
|
.map(
|
package/providers/qwen/qwen.ts
CHANGED
|
@@ -10,18 +10,18 @@
|
|
|
10
10
|
* 1,000 free API calls/day — run /login qwen to authenticate.~~
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type {
|
|
13
|
+
import type { Api, Model, OAuthCredentials } from "@mariozechner/pi-ai";
|
|
14
14
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
15
15
|
import { PROVIDER_QWEN, URL_QWEN_TOS } from "../../constants.ts";
|
|
16
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
17
|
+
import { logWarning } from "../../lib/util.ts";
|
|
16
18
|
import {
|
|
19
|
+
createReRegister,
|
|
17
20
|
enhanceWithCI,
|
|
18
21
|
type StoredModels,
|
|
19
22
|
setupProvider,
|
|
20
|
-
createReRegister,
|
|
21
23
|
} from "../../provider-helper.ts";
|
|
22
|
-
import {
|
|
23
|
-
import { createLogger } from "../../lib/logger.ts";
|
|
24
|
-
import { loginQwen, refreshQwenToken, getQwenBaseUrl } from "./qwen-auth.ts";
|
|
24
|
+
import { getQwenBaseUrl, loginQwen, refreshQwenToken } from "./qwen-auth.ts";
|
|
25
25
|
import { fetchQwenModels } from "./qwen-models.ts";
|
|
26
26
|
|
|
27
27
|
// =============================================================================
|
|
@@ -61,10 +61,12 @@ const DASHSCOPE_HEADERS = {
|
|
|
61
61
|
|
|
62
62
|
export default async function (pi: ExtensionAPI) {
|
|
63
63
|
// DEPRECATION WARNING
|
|
64
|
-
_logger.warn(
|
|
64
|
+
_logger.warn(
|
|
65
|
+
"Qwen provider is deprecated. The 1,000 req/day free tier is no longer available.",
|
|
66
|
+
);
|
|
65
67
|
|
|
66
68
|
// Fetch static free-tier models
|
|
67
|
-
|
|
69
|
+
const models = await fetchQwenModels().catch((err) => {
|
|
68
70
|
logWarning("qwen", "Failed to load models at startup", err);
|
|
69
71
|
return [];
|
|
70
72
|
});
|
|
@@ -148,55 +150,51 @@ export default async function (pi: ExtensionAPI) {
|
|
|
148
150
|
// getApiKey() call will trigger a token refresh via refreshToken().
|
|
149
151
|
// This mirrors qwen-code's executeWithCredentialManagement() retry logic.
|
|
150
152
|
//
|
|
153
|
+
function isQwenAuthError(msg: {
|
|
154
|
+
role?: string;
|
|
155
|
+
errorMessage?: string;
|
|
156
|
+
}): boolean {
|
|
157
|
+
if (msg.role !== "assistant" || !msg.errorMessage) return false;
|
|
158
|
+
const errLower = msg.errorMessage.toLowerCase();
|
|
159
|
+
return AUTH_ERROR_PATTERNS.some((p) => errLower.includes(p));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function forceExpireQwenToken(
|
|
163
|
+
ctx: any,
|
|
164
|
+
msg: { errorMessage?: string },
|
|
165
|
+
): void {
|
|
166
|
+
_logger.warn("Qwen auth error detected, force-expiring token for refresh", {
|
|
167
|
+
error: (msg.errorMessage ?? "").slice(0, 100),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const authStorage = (ctx as any).modelRegistry?.authStorage;
|
|
172
|
+
if (authStorage) {
|
|
173
|
+
const cred = authStorage.get(PROVIDER_QWEN);
|
|
174
|
+
if (cred?.type === "oauth" && cred.expires > Date.now()) {
|
|
175
|
+
authStorage.set(PROVIDER_QWEN, { ...cred, expires: 0 });
|
|
176
|
+
_logger.info(
|
|
177
|
+
"Qwen token force-expired; will refresh on next request",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
ctx.ui.notify("Qwen: auth error detected, refreshing token…", "warning");
|
|
182
|
+
} catch (e) {
|
|
183
|
+
_logger.warn("Failed to force-expire Qwen token", {
|
|
184
|
+
error: e instanceof Error ? e.message : String(e),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
151
189
|
pi.on("turn_end", async (event, ctx) => {
|
|
152
190
|
if (ctx.model?.provider !== PROVIDER_QWEN) return;
|
|
153
|
-
// NOTE: Request counting removed - usage tracking was deleted in refactor
|
|
154
191
|
|
|
155
192
|
const msg = (
|
|
156
193
|
event as { message?: { role?: string; errorMessage?: string } }
|
|
157
194
|
).message;
|
|
158
195
|
|
|
159
|
-
if (msg
|
|
160
|
-
|
|
161
|
-
const isAuthError = AUTH_ERROR_PATTERNS.some((p) =>
|
|
162
|
-
errLower.includes(p),
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
if (isAuthError) {
|
|
166
|
-
_logger.warn(
|
|
167
|
-
"Qwen auth error detected, force-expiring token for refresh",
|
|
168
|
-
{ error: msg.errorMessage.slice(0, 100) },
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
// Force-expire the stored credential so the next getApiKey() call
|
|
172
|
-
// triggers refreshQwenToken(). The credential object in auth.json
|
|
173
|
-
// is updated with expires = 0 (already past).
|
|
174
|
-
try {
|
|
175
|
-
const authStorage =
|
|
176
|
-
(ctx as any).modelRegistry?.authStorage;
|
|
177
|
-
if (authStorage) {
|
|
178
|
-
const cred = authStorage.get(PROVIDER_QWEN);
|
|
179
|
-
if (cred?.type === "oauth" && cred.expires > Date.now()) {
|
|
180
|
-
// Set expiry to 0 to force refresh on next request
|
|
181
|
-
authStorage.set(PROVIDER_QWEN, {
|
|
182
|
-
...cred,
|
|
183
|
-
expires: 0,
|
|
184
|
-
});
|
|
185
|
-
_logger.info(
|
|
186
|
-
"Qwen token force-expired; will refresh on next request",
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
ctx.ui.notify(
|
|
191
|
-
"Qwen: auth error detected, refreshing token…",
|
|
192
|
-
"warning",
|
|
193
|
-
);
|
|
194
|
-
} catch (e) {
|
|
195
|
-
_logger.warn("Failed to force-expire Qwen token", {
|
|
196
|
-
error: e instanceof Error ? e.message : String(e),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
196
|
+
if (msg && isQwenAuthError(msg)) {
|
|
197
|
+
forceExpireQwenToken(ctx, msg);
|
|
200
198
|
}
|
|
201
199
|
});
|
|
202
200
|
}
|
|
@@ -7,17 +7,31 @@
|
|
|
7
7
|
* node scripts/check-extensions.mjs <dir> # from installed location
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
|
|
14
14
|
const installDir = resolve(process.argv[2] ?? ".");
|
|
15
15
|
const fromSource = process.argv[2] == null;
|
|
16
16
|
|
|
17
|
+
/** Resolve npm to an absolute path to avoid S4036 PATH-lookup flags. */
|
|
18
|
+
function resolveNpm() {
|
|
19
|
+
for (const p of [
|
|
20
|
+
"/usr/bin/npm",
|
|
21
|
+
"/usr/local/bin/npm",
|
|
22
|
+
process.platform === "win32" ? "C:\\Program Files\\nodejs\\npm.cmd" : "",
|
|
23
|
+
]) {
|
|
24
|
+
if (p && existsSync(p)) return p;
|
|
25
|
+
}
|
|
26
|
+
return "npm"; // fallback
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
function getFiles() {
|
|
18
30
|
if (fromSource) {
|
|
19
|
-
// Use npm pack --dry-run
|
|
20
|
-
const out =
|
|
31
|
+
// Use npm pack --dry-run with an absolute executable path.
|
|
32
|
+
const out = execFileSync(resolveNpm(), ["pack", "--dry-run"], {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
});
|
|
21
35
|
return out
|
|
22
36
|
.split("\n")
|
|
23
37
|
.map((l) => l.match(/npm notice \S+\s+(.+)/)?.[1]?.trim())
|