pi-free 1.0.8 → 2.0.0

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/README.md +95 -46
  3. package/config.ts +165 -120
  4. package/constants.ts +22 -61
  5. package/index.ts +186 -0
  6. package/lib/json-persistence.ts +11 -10
  7. package/lib/logger.ts +2 -2
  8. package/lib/model-enhancer.ts +20 -20
  9. package/lib/open-browser.ts +41 -0
  10. package/lib/provider-cache.ts +106 -0
  11. package/lib/registry.ts +144 -0
  12. package/package.json +67 -82
  13. package/provider-factory.ts +25 -41
  14. package/provider-failover/benchmark-lookup.ts +247 -0
  15. package/provider-failover/benchmarks-chunk-0.ts +2010 -0
  16. package/provider-failover/benchmarks-chunk-1.ts +1988 -0
  17. package/provider-failover/benchmarks-chunk-2.ts +2010 -0
  18. package/provider-failover/benchmarks-chunk-3.ts +2010 -0
  19. package/provider-failover/benchmarks-chunk-4.ts +1969 -0
  20. package/provider-failover/hardcoded-benchmarks.ts +22 -10025
  21. package/provider-helper.ts +38 -37
  22. package/providers/{cline-auth.ts → cline/cline-auth.ts} +2 -2
  23. package/providers/cline/cline-models.ts +128 -0
  24. package/providers/{cline.ts → cline/cline.ts} +300 -257
  25. package/providers/cloudflare/cloudflare.ts +368 -0
  26. package/providers/dynamic-built-in/index.ts +513 -0
  27. package/providers/{kilo-auth.ts → kilo/kilo-auth.ts} +3 -20
  28. package/providers/{kilo-models.ts → kilo/kilo-models.ts} +2 -2
  29. package/providers/kilo/kilo.ts +235 -0
  30. package/providers/{modal.ts → modal/modal.ts} +4 -3
  31. package/providers/{nvidia.ts → nvidia/nvidia.ts} +152 -113
  32. package/providers/ollama/ollama.ts +172 -0
  33. package/providers/opencode-session.ts +34 -34
  34. package/providers/{qwen-auth.ts → qwen/qwen-auth.ts} +24 -40
  35. package/providers/{qwen-models.ts → qwen/qwen-models.ts} +101 -95
  36. package/providers/qwen/qwen.ts +202 -0
  37. package/provider-failover/auto-switch.ts +0 -350
  38. package/provider-failover/errors.ts +0 -275
  39. package/provider-failover/index.ts +0 -238
  40. package/providers/cline-models.ts +0 -77
  41. package/providers/factory.ts +0 -125
  42. package/providers/fireworks.ts +0 -49
  43. package/providers/go.ts +0 -216
  44. package/providers/kilo.ts +0 -146
  45. package/providers/mistral.ts +0 -144
  46. package/providers/ollama.ts +0 -113
  47. package/providers/openrouter.ts +0 -175
  48. package/providers/qwen.ts +0 -127
  49. package/providers/zen.ts +0 -371
  50. package/usage/commands.ts +0 -17
  51. package/usage/cumulative.ts +0 -193
  52. package/usage/formatters.ts +0 -115
  53. package/usage/index.ts +0 -46
  54. package/usage/limits.ts +0 -148
  55. package/usage/metrics.ts +0 -222
  56. package/usage/sessions.ts +0 -355
  57. package/usage/store.ts +0 -99
  58. package/usage/tracking.ts +0 -329
  59. package/usage/types.ts +0 -26
  60. package/usage/widget.ts +0 -90
  61. package/widget/data.ts +0 -113
  62. package/widget/format.ts +0 -26
  63. package/widget/render.ts +0 -117
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Qwen OAuth Provider Extension
3
+ *
4
+ * @deprecated This provider is deprecated. Qwen no longer offers the 1,000 free API calls/day tier.
5
+ * The provider remains functional for existing authenticated users but new free tier registrations
6
+ * are not supported. Consider using other free providers like Kilo, Cline, or NVIDIA instead.
7
+ *
8
+ * Original description (now outdated):
9
+ * ~~Provides free access to Qwen 3.6 Plus via OAuth device flow.
10
+ * 1,000 free API calls/day — run /login qwen to authenticate.~~
11
+ */
12
+
13
+ import type { OAuthCredentials, Model, Api } from "@mariozechner/pi-ai";
14
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
+ import { PROVIDER_QWEN, URL_QWEN_TOS } from "../../constants.ts";
16
+ import {
17
+ enhanceWithCI,
18
+ type StoredModels,
19
+ setupProvider,
20
+ createReRegister,
21
+ } from "../../provider-helper.ts";
22
+ import { logWarning } from "../../lib/util.ts";
23
+ import { createLogger } from "../../lib/logger.ts";
24
+ import { loginQwen, refreshQwenToken, getQwenBaseUrl } from "./qwen-auth.ts";
25
+ import { fetchQwenModels } from "./qwen-models.ts";
26
+
27
+ // =============================================================================
28
+ // 401 detection patterns
29
+ // =============================================================================
30
+
31
+ /** Patterns that indicate an auth failure requiring token refresh. */
32
+ const AUTH_ERROR_PATTERNS = [
33
+ "invalid access token",
34
+ "token expired",
35
+ "401",
36
+ "unauthorized",
37
+ "authentication",
38
+ ] as const;
39
+
40
+ const _logger = createLogger("qwen");
41
+
42
+ // =============================================================================
43
+ // Constants
44
+ // =============================================================================
45
+
46
+ // Mirrors qwen-code's DEFAULT_QWEN_BASE_URL (used when resource_url is absent).
47
+ const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
48
+
49
+ // Headers required by DashScope's OpenAI-compatible API for OAuth tokens.
50
+ // Replicates DashScopeOpenAICompatibleProvider.buildHeaders() from qwen-code.
51
+ const DASHSCOPE_HEADERS = {
52
+ "X-DashScope-AuthType": "qwen-oauth",
53
+ "X-DashScope-CacheControl": "enable",
54
+ "X-DashScope-UserAgent": "QwenCode/0.0.5 (pi-free)",
55
+ "Client-Code": "QwenCode",
56
+ };
57
+
58
+ // =============================================================================
59
+ // Extension entry point
60
+ // =============================================================================
61
+
62
+ export default async function (pi: ExtensionAPI) {
63
+ // DEPRECATION WARNING
64
+ _logger.warn("Qwen provider is deprecated. The 1,000 req/day free tier is no longer available.");
65
+
66
+ // Fetch static free-tier models
67
+ let models = await fetchQwenModels().catch((err) => {
68
+ logWarning("qwen", "Failed to load models at startup", err);
69
+ return [];
70
+ });
71
+
72
+ if (models.length === 0) {
73
+ logWarning("qwen", "No models available, skipping provider");
74
+ return;
75
+ }
76
+
77
+ const stored: StoredModels = { free: models, all: models };
78
+
79
+ // OAuth config for Qwen
80
+ const oauthConfig = {
81
+ name: "Qwen",
82
+ login: loginQwen,
83
+ refreshToken: refreshQwenToken,
84
+ getApiKey: (cred: OAuthCredentials) => cred.access,
85
+ modifyModels: (models: Model<Api>[], cred: OAuthCredentials) => {
86
+ // Mirror qwen-code: resolve baseUrl from resource_url per-credential.
87
+ // Chinese accounts → dashscope.aliyuncs.com/v1
88
+ // International accounts → portal.qwen.ai/v1 (or custom endpoint)
89
+ const baseUrl = getQwenBaseUrl(cred);
90
+ _logger.info("Qwen OAuth modifyModels called", {
91
+ baseUrl,
92
+ resource_url: cred.resource_url,
93
+ modelCount: models.length,
94
+ });
95
+ if (baseUrl === DEFAULT_BASE_URL) return models;
96
+ // modifyModels receives ALL models across providers — only patch Qwen ones.
97
+ const nonQwen = models.filter((m) => m.provider !== PROVIDER_QWEN);
98
+ const qwen = models
99
+ .filter((m) => m.provider === PROVIDER_QWEN)
100
+ .map((m) => ({ ...m, baseUrl }));
101
+ return [...nonQwen, ...qwen];
102
+ },
103
+ };
104
+
105
+ // Register provider with OpenAI-compatible API
106
+ function registerProvider(m = models) {
107
+ pi.registerProvider(PROVIDER_QWEN, {
108
+ baseUrl: DEFAULT_BASE_URL,
109
+ apiKey: "QWEN_API_KEY",
110
+ api: "openai-completions" as const,
111
+ headers: {
112
+ "User-Agent": "pi-free",
113
+ ...DASHSCOPE_HEADERS,
114
+ },
115
+ models: enhanceWithCI(m),
116
+ oauth: oauthConfig,
117
+ });
118
+ }
119
+
120
+ registerProvider();
121
+
122
+ // Wire up shared boilerplate (commands, model_select, turn_end, ToS)
123
+ const reRegister = createReRegister(pi, {
124
+ providerId: PROVIDER_QWEN,
125
+ baseUrl: DEFAULT_BASE_URL,
126
+ apiKey: "QWEN_API_KEY",
127
+ oauth: oauthConfig as any,
128
+ });
129
+
130
+ setupProvider(
131
+ pi,
132
+ {
133
+ providerId: PROVIDER_QWEN,
134
+ tosUrl: URL_QWEN_TOS,
135
+ initialShowPaid: false,
136
+ reRegister: (m) => {
137
+ reRegister(m);
138
+ },
139
+ },
140
+ stored,
141
+ );
142
+
143
+ // Request counting + 401 auth-error detection with forced token refresh.
144
+ //
145
+ // When Qwen returns a 401 / "invalid access token" error, the stored token
146
+ // may have been revoked server-side even though its expiry hasn't been reached.
147
+ // We force-expire the credential in auth storage so that pi-core's next
148
+ // getApiKey() call will trigger a token refresh via refreshToken().
149
+ // This mirrors qwen-code's executeWithCredentialManagement() retry logic.
150
+ //
151
+ pi.on("turn_end", async (event, ctx) => {
152
+ if (ctx.model?.provider !== PROVIDER_QWEN) return;
153
+ // NOTE: Request counting removed - usage tracking was deleted in refactor
154
+
155
+ const msg = (
156
+ event as { message?: { role?: string; errorMessage?: string } }
157
+ ).message;
158
+
159
+ if (msg?.role === "assistant" && msg.errorMessage) {
160
+ const errLower = msg.errorMessage.toLowerCase();
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
+ }
200
+ }
201
+ });
202
+ }
@@ -1,350 +0,0 @@
1
- /**
2
- * Auto-switch failover for pi-free-providers.
3
- *
4
- * When a provider hits a 429 or capacity error, this module finds
5
- * an equivalent or similar model from another provider and switches to it.
6
- *
7
- * Strategy:
8
- * 1. Extract the base model name/family from the failed model
9
- * 2. Search all available models for the same model (different provider)
10
- * 3. If not found, find a similar model in the same family
11
- * 4. If not found, find any free model with similar capability
12
- */
13
-
14
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
15
- import type { Model } from "@mariozechner/pi-ai";
16
- import { createLogger } from "../lib/logger.ts";
17
- import {
18
- detectModelFamily,
19
- normalizeModelName,
20
- toModelInfo,
21
- type ModelInfo,
22
- } from "../lib/model-detection.ts";
23
- import { getHardcodedScore } from "./hardcoded-benchmarks.ts";
24
-
25
- const _logger = createLogger("auto-switch");
26
-
27
- export interface AutoSwitchConfig {
28
- /** Whether to enable auto-switching (can be disabled by user) */
29
- enabled: boolean;
30
- /** Maximum CI score degradation allowed (e.g., 10 = can drop up to 10 points) */
31
- maxCIScoreDrop: number;
32
- /** Provider priority for fallback (preferred first) */
33
- providerPriority: string[];
34
- }
35
-
36
- const DEFAULT_CONFIG: AutoSwitchConfig = {
37
- enabled: true,
38
- maxCIScoreDrop: 15,
39
- providerPriority: ["zen", "go", "kilo", "openrouter", "nvidia", "fireworks", "mistral", "ollama", "cline"],
40
- };
41
-
42
- export interface AutoSwitchResult {
43
- success: boolean;
44
- switched: boolean;
45
- message: string;
46
- fallbackModel?: ModelInfo;
47
- }
48
-
49
- interface CandidateModel {
50
- model: Model<any>;
51
- modelInfo: ModelInfo;
52
- ciScore: number;
53
- normalizedName: string;
54
- family: string;
55
- }
56
-
57
- /**
58
- * Find a fallback model when the current provider fails.
59
- *
60
- * Priority order:
61
- * 1. Same model name from different provider (best match)
62
- * 2. Same model family, prefer free models
63
- * 3. Any free model with similar CI score
64
- */
65
- export async function findFallbackModel(
66
- failedModel: Model<any>,
67
- availableModels: Model<any>[],
68
- config: Partial<AutoSwitchConfig> = {},
69
- ): Promise<CandidateModel | null> {
70
- const fullConfig = { ...DEFAULT_CONFIG, ...config };
71
-
72
- // Convert to ModelInfo for internal processing
73
- const failedModelInfo = toModelInfo(failedModel);
74
- const failedFamily = detectModelFamily(failedModelInfo);
75
- const failedNormalizedName = normalizeModelName(failedModelInfo.name || failedModelInfo.id);
76
- const failedCIScore = getHardcodedScore(failedModel.name || "", failedModel.id) ?? 20;
77
-
78
- _logger.info("Finding fallback model", {
79
- failedModel: failedModel.id,
80
- failedProvider: failedModel.provider,
81
- failedFamily: failedFamily?.familyId,
82
- failedNormalizedName: failedNormalizedName,
83
- failedCIScore,
84
- });
85
-
86
- // Build candidate list
87
- const candidates: CandidateModel[] = [];
88
-
89
- for (const candidate of availableModels) {
90
- // Skip the same provider
91
- if (candidate.provider === failedModel.provider) continue;
92
-
93
- // Skip if no auth configured for this provider
94
- // (We'll assume available models have auth, but check anyway)
95
- if (!candidate.baseUrl) continue;
96
-
97
- const modelInfo = toModelInfo(candidate);
98
- const family = detectModelFamily(modelInfo);
99
- const normalizedName = normalizeModelName(modelInfo.name || modelInfo.id);
100
- const ciScore = getHardcodedScore(candidate.name || "", candidate.id) ?? 20;
101
-
102
- candidates.push({
103
- model: candidate,
104
- modelInfo,
105
- ciScore,
106
- normalizedName,
107
- family: family?.familyId ?? "other",
108
- });
109
- }
110
-
111
- if (candidates.length === 0) {
112
- _logger.info("No candidate models found");
113
- return null;
114
- }
115
-
116
- // Priority 1: Same model name (different provider)
117
- // e.g., "minimax-m2.5" on zen → "minimax-m2.5" on openrouter
118
- const sameName = candidates.find(
119
- (c) => c.normalizedName === failedNormalizedName && c.model.provider !== failedModel.provider,
120
- );
121
- if (sameName) {
122
- _logger.info("Found exact model match", {
123
- provider: sameName.model.provider,
124
- model: sameName.model.id,
125
- });
126
- return sameName;
127
- }
128
-
129
- // Priority 2: Same model family, prefer free models
130
- const sameFamily = candidates
131
- .filter((c) => c.family === failedFamily?.familyId && c.model.provider !== failedModel.provider)
132
- .sort((a, b) => {
133
- // Prefer free models
134
- if (a.modelInfo.isFree !== b.modelInfo.isFree) {
135
- return a.modelInfo.isFree ? -1 : 1;
136
- }
137
- // Then by CI score
138
- return b.ciScore - a.ciScore;
139
- });
140
-
141
- if (sameFamily.length > 0) {
142
- const best = sameFamily[0]!;
143
- _logger.info("Found same family model", {
144
- provider: best.model.provider,
145
- model: best.model.id,
146
- family: best.family,
147
- isFree: best.modelInfo.isFree,
148
- ciScore: best.ciScore,
149
- });
150
- return best;
151
- }
152
-
153
- // Priority 3: Any free model with similar CI score
154
- // Check CI score degradation limit
155
- const freeCandidates = candidates
156
- .filter((c) => {
157
- // Must be free
158
- if (!c.modelInfo.isFree) return false;
159
- // Must not drop CI score too much
160
- const ciDrop = failedCIScore - c.ciScore;
161
- return ciDrop <= fullConfig.maxCIScoreDrop;
162
- })
163
- .sort((a, b) => {
164
- // Sort by CI score (closest to failed model first)
165
- return b.ciScore - a.ciScore;
166
- });
167
-
168
- if (freeCandidates.length > 0) {
169
- const best = freeCandidates[0]!;
170
- _logger.info("Found free fallback model", {
171
- provider: best.model.provider,
172
- model: best.model.id,
173
- ciScore: best.ciScore,
174
- ciDrop: failedCIScore - best.ciScore,
175
- });
176
- return best;
177
- }
178
-
179
- // Priority 4: Any free model (no CI limit)
180
- const anyFree = candidates
181
- .filter((c) => c.modelInfo.isFree)
182
- .sort((a, b) => b.ciScore - a.ciScore);
183
-
184
- if (anyFree.length > 0) {
185
- const best = anyFree[0]!;
186
- _logger.info("Found any free fallback", {
187
- provider: best.model.provider,
188
- model: best.model.id,
189
- ciScore: best.ciScore,
190
- });
191
- return best;
192
- }
193
-
194
- // Priority 5: Any model with similar CI score
195
- const similarCI = candidates
196
- .filter((c) => {
197
- const ciDrop = failedCIScore - c.ciScore;
198
- return ciDrop <= fullConfig.maxCIScoreDrop;
199
- })
200
- .sort((a, b) => {
201
- // Prefer providers in priority order
202
- const aPriority = fullConfig.providerPriority.indexOf(a.model.provider);
203
- const bPriority = fullConfig.providerPriority.indexOf(b.model.provider);
204
- if (aPriority !== bPriority && aPriority >= 0 && bPriority >= 0) {
205
- return aPriority - bPriority;
206
- }
207
- // Then by CI score
208
- return b.ciScore - a.ciScore;
209
- });
210
-
211
- if (similarCI.length > 0) {
212
- const best = similarCI[0]!;
213
- _logger.info("Found similar CI fallback", {
214
- provider: best.model.provider,
215
- model: best.model.id,
216
- ciScore: best.ciScore,
217
- });
218
- return best;
219
- }
220
-
221
- // Last resort: any model, prefer by provider priority
222
- const anyModel = candidates.sort((a, b) => {
223
- const aPriority = fullConfig.providerPriority.indexOf(a.model.provider);
224
- const bPriority = fullConfig.providerPriority.indexOf(b.model.provider);
225
- if (aPriority !== bPriority && aPriority >= 0 && bPriority >= 0) {
226
- return aPriority - bPriority;
227
- }
228
- return b.ciScore - a.ciScore;
229
- });
230
-
231
- if (anyModel.length > 0) {
232
- const best = anyModel[0]!;
233
- _logger.info("Found any fallback", {
234
- provider: best.model.provider,
235
- model: best.model.id,
236
- ciScore: best.ciScore,
237
- });
238
- return best;
239
- }
240
-
241
- return null;
242
- }
243
-
244
- /**
245
- * Perform automatic failover when a provider hits an error.
246
- * Returns the result of the switch attempt.
247
- */
248
- export async function autoFailover(
249
- _errorMessage: string,
250
- failedModel: Model<any>,
251
- pi: ExtensionAPI,
252
- ctx: ExtensionContext,
253
- config: Partial<AutoSwitchConfig> = {},
254
- ): Promise<AutoSwitchResult> {
255
- const fullConfig = { ...DEFAULT_CONFIG, ...config };
256
- if (!fullConfig.enabled) {
257
- return {
258
- success: false,
259
- switched: false,
260
- message: "Auto-switch disabled",
261
- };
262
- }
263
-
264
- // Get all available models
265
- const availableModels = ctx.modelRegistry.getAvailable();
266
-
267
- if (availableModels.length === 0) {
268
- return {
269
- success: false,
270
- switched: false,
271
- message: "No alternative models available",
272
- };
273
- }
274
-
275
- // Find fallback model
276
- const fallback = await findFallbackModel(
277
- failedModel,
278
- availableModels,
279
- fullConfig,
280
- );
281
-
282
- if (!fallback) {
283
- return {
284
- success: false,
285
- switched: false,
286
- message: `No fallback model found for ${failedModel.provider}/${failedModel.id}`,
287
- };
288
- }
289
-
290
- // Attempt to switch
291
- const success = await pi.setModel(fallback.model);
292
-
293
- if (success) {
294
- const freeStatus = fallback.modelInfo.isFree ? " (free)" : "";
295
- return {
296
- success: true,
297
- switched: true,
298
- message: `Switched from ${failedModel.provider} to ${fallback.model.provider}/${fallback.model.id}${freeStatus}`,
299
- fallbackModel: fallback.modelInfo,
300
- };
301
- } else {
302
- return {
303
- success: false,
304
- switched: false,
305
- message: `Failed to switch to ${fallback.model.provider}/${fallback.model.id} (no API key?)`,
306
- fallbackModel: fallback.modelInfo,
307
- };
308
- }
309
- }
310
-
311
- /**
312
- * Check if a model is available from multiple providers
313
- */
314
- export function getModelAvailability(
315
- modelId: string,
316
- availableModels: Model<any>[],
317
- ): string[] {
318
- const normalizedName = normalizeModelName(modelId);
319
-
320
- return availableModels
321
- .filter((m) => {
322
- const mNormalized = normalizeModelName(m.name || m.id);
323
- return mNormalized === normalizedName;
324
- })
325
- .map((m) => m.provider);
326
- }
327
-
328
- /**
329
- * Get a summary of available models grouped by family
330
- */
331
- export function getModelAvailabilitySummary(
332
- availableModels: Model<any>[],
333
- ): Map<string, string[]> {
334
- const families = new Map<string, string[]>();
335
-
336
- for (const model of availableModels) {
337
- const modelInfo = toModelInfo(model);
338
- const family = detectModelFamily(modelInfo);
339
-
340
- if (!family) continue;
341
-
342
- const existing = families.get(family.familyId) ?? [];
343
- if (!existing.includes(model.provider)) {
344
- existing.push(model.provider);
345
- }
346
- families.set(family.familyId, existing);
347
- }
348
-
349
- return families;
350
- }