pi-free 1.0.9 → 2.0.1

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 +138 -0
  2. package/README.md +393 -367
  3. package/config.ts +170 -121
  4. package/constants.ts +23 -61
  5. package/index.ts +148 -0
  6. package/lib/built-in-toggle.ts +206 -0
  7. package/lib/json-persistence.ts +11 -10
  8. package/lib/logger.ts +2 -2
  9. package/lib/model-enhancer.ts +20 -20
  10. package/lib/open-browser.ts +41 -0
  11. package/lib/provider-cache.ts +106 -0
  12. package/lib/registry.ts +144 -0
  13. package/package.json +8 -23
  14. package/provider-factory.ts +25 -41
  15. package/provider-failover/benchmark-lookup.ts +247 -0
  16. package/provider-failover/benchmarks-chunk-0.ts +2010 -0
  17. package/provider-failover/benchmarks-chunk-1.ts +1988 -0
  18. package/provider-failover/benchmarks-chunk-2.ts +2010 -0
  19. package/provider-failover/benchmarks-chunk-3.ts +2010 -0
  20. package/provider-failover/benchmarks-chunk-4.ts +1969 -0
  21. package/provider-failover/hardcoded-benchmarks.ts +22 -10025
  22. package/provider-helper.ts +260 -259
  23. package/providers/{cline-auth.ts → cline/cline-auth.ts} +2 -2
  24. package/providers/cline/cline-models.ts +128 -0
  25. package/providers/{cline.ts → cline/cline.ts} +298 -257
  26. package/providers/cloudflare/cloudflare.ts +368 -0
  27. package/providers/dynamic-built-in/index.ts +432 -0
  28. package/providers/{kilo-auth.ts → kilo/kilo-auth.ts} +3 -20
  29. package/providers/{kilo-models.ts → kilo/kilo-models.ts} +2 -2
  30. package/providers/kilo/kilo.ts +235 -0
  31. package/providers/{modal.ts → modal/modal.ts} +4 -3
  32. package/providers/{nvidia.ts → nvidia/nvidia.ts} +152 -113
  33. package/providers/ollama/ollama.ts +172 -0
  34. package/providers/opencode-session.ts +34 -34
  35. package/providers/{qwen-auth.ts → qwen/qwen-auth.ts} +24 -40
  36. package/providers/{qwen-models.ts → qwen/qwen-models.ts} +101 -95
  37. package/providers/{qwen.ts → qwen/qwen.ts} +83 -13
  38. package/provider-failover/auto-switch.ts +0 -350
  39. package/provider-failover/errors.ts +0 -275
  40. package/provider-failover/index.ts +0 -238
  41. package/providers/cline-models.ts +0 -77
  42. package/providers/factory.ts +0 -125
  43. package/providers/fireworks.ts +0 -49
  44. package/providers/go.ts +0 -216
  45. package/providers/kilo.ts +0 -146
  46. package/providers/mistral.ts +0 -144
  47. package/providers/ollama.ts +0 -113
  48. package/providers/openrouter.ts +0 -175
  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
@@ -2,7 +2,7 @@
2
2
  * Provider Factory
3
3
  *
4
4
  * Extracts the common boilerplate pattern repeated across providers:
5
- * - API key check and env injection
5
+ * - API key check
6
6
  * - SHOW_PAID flag check
7
7
  * - Model fetching with error handling
8
8
  * - Provider registration
@@ -23,22 +23,16 @@ import type {
23
23
  ProviderModelConfig,
24
24
  } from "@mariozechner/pi-coding-agent";
25
25
  import {
26
- FIREWORKS_API_KEY,
27
- FIREWORKS_SHOW_PAID,
28
- MISTRAL_API_KEY,
29
- MISTRAL_SHOW_PAID,
30
- NVIDIA_API_KEY,
31
- NVIDIA_SHOW_PAID,
32
- OLLAMA_API_KEY,
33
- OLLAMA_SHOW_PAID,
34
- OPENCODE_API_KEY,
35
- ZEN_SHOW_PAID,
36
- MODAL_API_KEY,
26
+ getModalApiKey,
27
+ getNvidiaApiKey,
28
+ getNvidiaShowPaid,
29
+ getOpencodeApiKey,
37
30
  } from "./config.ts";
38
31
  import { createLogger } from "./lib/logger.ts";
39
32
  import { logWarning } from "./lib/util.ts";
40
33
  import {
41
34
  createReRegister,
35
+ enhanceWithCI,
42
36
  type StoredModels,
43
37
  setupProvider,
44
38
  } from "./provider-helper.ts";
@@ -50,7 +44,7 @@ const _logger = createLogger("provider-factory");
50
44
  // =============================================================================
51
45
 
52
46
  export interface ProviderDefinition {
53
- /** Provider identifier (e.g., "nvidia", "fireworks") */
47
+ /** Provider identifier (e.g., "nvidia", "modal") */
54
48
  providerId: string;
55
49
  /** Base URL for the API */
56
50
  baseUrl: string;
@@ -66,6 +60,8 @@ export interface ProviderDefinition {
66
60
  tosUrl?: string;
67
61
  /** Whether this provider has a free tier (free + paid models). Default: false */
68
62
  hasFreeTier?: boolean;
63
+ /** Whether to skip creating a toggle command (e.g., for single-model providers). Default: false */
64
+ skipToggle?: boolean;
69
65
  /** Additional headers to include in requests */
70
66
  extraHeaders?: Record<string, string>;
71
67
  /** Optional hook to modify request payload before sending */
@@ -80,22 +76,14 @@ export interface ProviderDefinition {
80
76
  // Config value getters (dynamic lookup)
81
77
  // =============================================================================
82
78
 
83
- // Map config key names to their values
84
79
  const API_KEY_GETTERS: Record<string, () => string | undefined> = {
85
- nvidia_api_key: () => NVIDIA_API_KEY,
86
- fireworks_api_key: () => FIREWORKS_API_KEY,
87
- ollama_api_key: () => OLLAMA_API_KEY,
88
- mistral_api_key: () => MISTRAL_API_KEY,
89
- opencode_api_key: () => OPENCODE_API_KEY,
90
- modal_api_key: () => MODAL_API_KEY,
80
+ nvidia_api_key: getNvidiaApiKey,
81
+ opencode_api_key: getOpencodeApiKey,
82
+ modal_api_key: getModalApiKey,
91
83
  };
92
84
 
93
85
  const SHOW_PAID_GETTERS: Record<string, () => boolean> = {
94
- NVIDIA_SHOW_PAID: () => NVIDIA_SHOW_PAID,
95
- FIREWORKS_SHOW_PAID: () => FIREWORKS_SHOW_PAID,
96
- OLLAMA_SHOW_PAID: () => OLLAMA_SHOW_PAID,
97
- MISTRAL_SHOW_PAID: () => MISTRAL_SHOW_PAID,
98
- ZEN_SHOW_PAID: () => ZEN_SHOW_PAID,
86
+ NVIDIA_SHOW_PAID: getNvidiaShowPaid,
99
87
  };
100
88
 
101
89
  // =============================================================================
@@ -106,7 +94,7 @@ const SHOW_PAID_GETTERS: Record<string, () => boolean> = {
106
94
  * Create a provider with minimal boilerplate.
107
95
  *
108
96
  * Handles:
109
- * - API key check and env injection
97
+ * - API key check
110
98
  * - SHOW_PAID flag check (if applicable)
111
99
  * - Model fetching with error handling
112
100
  * - Provider registration with OpenAI-compatible API
@@ -126,12 +114,7 @@ export async function createProvider(
126
114
  }
127
115
  const apiKey = getApiKey();
128
116
 
129
- // 2. Inject into process.env so Pi's apiKey lookup finds it
130
- if (apiKey) {
131
- process.env[def.apiKeyEnvVar] = apiKey;
132
- }
133
-
134
- // 3. Check key exists
117
+ // 2. Check key exists
135
118
  if (!apiKey) {
136
119
  _logger.warn(
137
120
  `No API key found — set ${def.apiKeyEnvVar} or add ${def.apiKeyConfigKey} to ~/.pi/free.json`,
@@ -139,7 +122,7 @@ export async function createProvider(
139
122
  return;
140
123
  }
141
124
 
142
- // 4. Check paid flag (if applicable)
125
+ // 3. Check paid flag (if applicable)
143
126
  if (def.showPaidFlag) {
144
127
  const getShowPaid = SHOW_PAID_GETTERS[def.showPaidFlag];
145
128
  if (getShowPaid && !getShowPaid()) {
@@ -150,7 +133,7 @@ export async function createProvider(
150
133
  }
151
134
  }
152
135
 
153
- // 5. Fetch models
136
+ // 4. Fetch models
154
137
  let models: ProviderModelConfig[] = [];
155
138
  try {
156
139
  models = await def.fetchModels();
@@ -164,7 +147,7 @@ export async function createProvider(
164
147
  return;
165
148
  }
166
149
 
167
- // 6. Build storage (free/all or single set)
150
+ // 5. Build storage (free/all or single set)
168
151
  const stored: StoredModels = def.hasFreeTier
169
152
  ? {
170
153
  free: models.filter((m) => (m.cost?.input ?? 0) === 0),
@@ -172,23 +155,23 @@ export async function createProvider(
172
155
  }
173
156
  : { free: models, all: models };
174
157
 
175
- // 7. Register provider
158
+ // 6. Register provider (pass literal key so we don't mutate process.env)
176
159
  pi.registerProvider(def.providerId, {
177
160
  baseUrl: def.baseUrl,
178
- apiKey: def.apiKeyEnvVar,
161
+ apiKey,
179
162
  api: "openai-completions" as const,
180
163
  headers: {
181
164
  "User-Agent": "pi-free-providers",
182
165
  ...def.extraHeaders,
183
166
  },
184
- models,
167
+ models: enhanceWithCI(models),
185
168
  });
186
169
 
187
- // 8. Setup boilerplate
170
+ // 7. Setup boilerplate
188
171
  const config = {
189
172
  providerId: def.providerId,
190
173
  baseUrl: def.baseUrl,
191
- apiKey: def.apiKeyEnvVar,
174
+ apiKey,
192
175
  };
193
176
 
194
177
  const reRegister = createReRegister(pi, config);
@@ -204,11 +187,12 @@ export async function createProvider(
204
187
  stored.all = m;
205
188
  reRegister(m);
206
189
  },
190
+ skipToggle: def.skipToggle,
207
191
  },
208
192
  stored,
209
193
  );
210
194
 
211
- // 9. Optional: before_provider_request hook
195
+ // 8. Optional: before_provider_request hook
212
196
  if (def.beforeProviderRequest) {
213
197
  const hook = def.beforeProviderRequest;
214
198
  (pi.on as (event: string, handler: (e: unknown) => unknown) => void)(
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Benchmark lookup logic — extracted from hardcoded-benchmarks.ts
3
+ * for maintainability (the data file is ~10k lines of JSON-like entries).
4
+ *
5
+ * This module re-exports everything consumers currently import from
6
+ * hardcoded-benchmarks, so you can switch imports to this file without
7
+ * breaking anything.
8
+ */
9
+
10
+ import {
11
+ HARDCODED_BENCHMARKS,
12
+ type HardcodedBenchmark,
13
+ } from "./hardcoded-benchmarks.ts";
14
+
15
+ // Re-export the type and data so callers can migrate imports here
16
+ export { HARDCODED_BENCHMARKS, type HardcodedBenchmark };
17
+
18
+ // =============================================================================
19
+ // Prefix fallback helpers
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Segments that indicate a variant of the same base model
24
+ * (effort level, reasoning mode, date, preview) — NOT a fundamentally different model.
25
+ * Used to filter prefix matches so we don't cross model boundaries
26
+ * (e.g. gpt-4o → gpt-4o-mini is wrong, but gpt-4o → gpt-4o-aug-24 is fine).
27
+ */
28
+ const VARIANT_QUALIFIER_SEGMENTS = new Set([
29
+ "reasoning",
30
+ "non-reasoning",
31
+ "high",
32
+ "low",
33
+ "medium",
34
+ "xhigh",
35
+ "preview",
36
+ "adaptive",
37
+ "fast",
38
+ ]);
39
+
40
+ /**
41
+ * Check if a segment is a variant qualifier rather than a different model identifier.
42
+ * Accepts effort levels, reasoning modes, date codes, size specifiers, and version numbers.
43
+ */
44
+ function isVariantQualifier(segment: string): boolean {
45
+ if (VARIANT_QUALIFIER_SEGMENTS.has(segment)) return true;
46
+ // Date codes like "0528", "20250514"
47
+ if (/^\d{4,8}$/.test(segment)) return true;
48
+ // Month names (from date suffixes like "may-25", "mar-24")
49
+ if (/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)$/.test(segment))
50
+ return true;
51
+ // Size specifiers like "70b", "8b", "a35b", "a3b" (MoE notation)
52
+ if (/^a?\d+(\.\d+)?b$/i.test(segment)) return true;
53
+ // Version numbers like "v3.2", "v2.5", "v1"
54
+ if (/^v\d+(\.\d+)?$/.test(segment)) return true;
55
+ // Two-digit year like "25", "24"
56
+ if (/^\d{2}$/.test(segment)) return true;
57
+ // Special variant suffixes
58
+ if (segment === "speciale" || segment === "chatgpt" || segment === "latest")
59
+ return true;
60
+ return false;
61
+ }
62
+
63
+ /**
64
+ * Normalize model ID by reordering size tokens to match AA convention.
65
+ * Converts "70b-instruct" → "instruct-70b", "405b-chat" → "chat-405b".
66
+ * AA uses instruct-70b order while providers often use 70b-instruct.
67
+ */
68
+ function normalizeSizeTokenOrder(id: string): string {
69
+ return id.replace(/(\d+(?:\.\d+)?b)-(instruct|chat)/gi, "$2-$1");
70
+ }
71
+
72
+ /**
73
+ * Extract the base model ID from a provider model ID.
74
+ * Strips provider prefix ("openai/"), :free suffix, date suffixes, and version suffixes.
75
+ */
76
+ function extractBaseModelId(modelId: string): string {
77
+ return modelId
78
+ .toLowerCase()
79
+ .replace(/^[^/]+\//, "") // Strip provider prefix like "openai/"
80
+ .replace(/:free$/, "") // Strip :free suffix
81
+ .replace(/-\d{8}$/, "") // Strip date suffixes like -20250514
82
+ .replace(/-v\d+(\.\d+)?$/, "") // Strip version suffixes like -v1.1
83
+ .replace(/-\d{3,}$/, "") // Strip numeric suffixes like -001, -2603
84
+ .replace(/-it$/, "") // Strip -it suffix (Gemma convention for "instruct")
85
+ .replace(/-fp\d+$/, "") // Strip -fp8, -fp16 suffixes
86
+ .replace(/-bf\d+$/, "") // Strip -bf16 suffixes
87
+ .trim();
88
+ }
89
+
90
+ /**
91
+ * Find the best benchmark variant by prefix matching.
92
+ * Given a base model ID, finds all benchmark keys that are variants of it
93
+ * (same base model with effort/reasoning/date qualifiers) and returns the
94
+ * variant with the highest codingIndex.
95
+ */
96
+ function findBestVariantByPrefix(baseId: string): HardcodedBenchmark | null {
97
+ const prefixKey = baseId + "-";
98
+ const candidates: { key: string; data: HardcodedBenchmark }[] = [];
99
+
100
+ for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
101
+ string,
102
+ HardcodedBenchmark,
103
+ ][]) {
104
+ // Exact match
105
+ if (key === baseId) {
106
+ if (data.codingIndex !== undefined) return data;
107
+ continue;
108
+ }
109
+
110
+ // Prefix match: key starts with baseId + "-"
111
+ if (key.startsWith(prefixKey)) {
112
+ // Check that the first segment after the prefix is a qualifier
113
+ // (prevents gpt-4o → gpt-4o-mini cross-model matches)
114
+ const remainder = key.slice(prefixKey.length);
115
+ const firstSegment = remainder.split("-")[0]!;
116
+ if (isVariantQualifier(firstSegment)) {
117
+ candidates.push({ key, data });
118
+ }
119
+ }
120
+ }
121
+
122
+ if (candidates.length === 0) return null;
123
+
124
+ // Pick the candidate with the highest codingIndex
125
+ // If tied or no CI, use normalizedScore as tiebreaker
126
+ candidates.sort((a, b) => {
127
+ const ciA = a.data.codingIndex ?? -1;
128
+ const ciB = b.data.codingIndex ?? -1;
129
+ if (ciB !== ciA) return ciB - ciA;
130
+ return (b.data.normalizedScore ?? 0) - (a.data.normalizedScore ?? 0);
131
+ });
132
+
133
+ // Only return if the best candidate has a codingIndex
134
+ if (candidates[0]!.data.codingIndex !== undefined) {
135
+ return candidates[0]!.data;
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ // =============================================================================
142
+ // Main lookup
143
+ // =============================================================================
144
+
145
+ export function findHardcodedBenchmark(
146
+ modelName: string,
147
+ modelId: string,
148
+ ): HardcodedBenchmark | null {
149
+ const search = `${modelName} ${modelId}`.toLowerCase();
150
+
151
+ // 1. Direct lookup — check if any benchmark key is a substring of the search
152
+ for (const [key, data] of Object.entries(HARDCODED_BENCHMARKS) as [
153
+ string,
154
+ HardcodedBenchmark,
155
+ ][]) {
156
+ if (search.includes(key.toLowerCase())) {
157
+ return data;
158
+ }
159
+ }
160
+
161
+ // 2. Variant matching — aliases for models with different naming conventions
162
+ const variants: Record<string, string[]> = {
163
+ "gpt-4o-aug-24": ["gpt-4o", "gpt-4-o"],
164
+ "gpt-4": ["gpt-4", "gpt4"],
165
+ "claude-3.5-sonnet-oct-24": [
166
+ "claude-3.5-sonnet",
167
+ "claude-3-5-sonnet",
168
+ "sonnet-3.5",
169
+ ],
170
+ "claude-3-opus": ["claude-3-opus", "opus-3"],
171
+ "llama-3.1-instruct-405b": [
172
+ "llama-3.1-405b",
173
+ "llama3.1-405b",
174
+ "llama-405b",
175
+ ],
176
+ "llama-3.1-instruct-70b": ["llama-3.1-70b", "llama3.1-70b", "llama-70b"],
177
+ "gemini-1.5-pro": ["gemini-1.5-pro", "gemini1.5-pro", "gemini-pro-1.5"],
178
+ "qwen2.5-instruct-72b": ["qwen2.5-72b", "qwen-2.5-72b"],
179
+ "deepseek-v3.2-non-reasoning": [
180
+ "deepseek-v3",
181
+ "deepseekv3",
182
+ "deepseek-chat",
183
+ ],
184
+ "mimo-v2-pro": ["mimo-v2-pro", "mimo-v2-pro-free", "mimo-pro"],
185
+ "mimo-v2-omni": ["mimo-v2-omni", "mimo-v2-omni-free", "mimo-omni"],
186
+ "mimo-v2-flash": ["mimo-v2-flash", "mimo-v2-flash-free", "mimo-flash"],
187
+ "big-pickle": ["big-pickle", "bigpickle"],
188
+ "minimax-m2.5": ["minimax-m2.5", "minimax-m2.5-free", "minimax-m25"],
189
+ "nvidia-nemotron-3-super-120b-a12b-reasoning": [
190
+ "nemotron-3-super",
191
+ "nemotron-3-super-free",
192
+ "nemotron-super",
193
+ "nemotron-3",
194
+ ],
195
+ };
196
+
197
+ for (const [canonical, names] of Object.entries(variants)) {
198
+ if (names.some((n) => search.includes(n.toLowerCase()))) {
199
+ return HARDCODED_BENCHMARKS[canonical] || null;
200
+ }
201
+ }
202
+
203
+ // 3. Prefix fallback — extract base model ID and find best variant
204
+ // Handles cases where benchmark keys have variant suffixes
205
+ // (reasoning/non-reasoning, effort levels, dates) that the model ID lacks
206
+ const baseId = extractBaseModelId(modelId);
207
+ if (baseId) {
208
+ let best = findBestVariantByPrefix(baseId);
209
+ if (best) return best;
210
+
211
+ // 3b. Try with word-order normalization
212
+ // (e.g., llama-3.3-70b-instruct → llama-3.3-instruct-70b)
213
+ const normalizedId = normalizeSizeTokenOrder(baseId);
214
+ if (normalizedId !== baseId) {
215
+ best = findBestVariantByPrefix(normalizedId);
216
+ if (best) return best;
217
+ }
218
+ }
219
+
220
+ return null;
221
+ }
222
+
223
+ /**
224
+ * Get score from hardcoded data
225
+ */
226
+ export function getHardcodedScore(
227
+ modelName: string,
228
+ modelId: string,
229
+ ): number | null {
230
+ const benchmark = findHardcodedBenchmark(modelName, modelId);
231
+ return benchmark?.normalizedScore ?? null;
232
+ }
233
+
234
+ /**
235
+ * Enhance model name with Coding Index score
236
+ * Returns model name with CI score appended if available
237
+ */
238
+ export function enhanceModelNameWithCodingIndex(
239
+ modelName: string,
240
+ modelId: string,
241
+ ): string {
242
+ const benchmark = findHardcodedBenchmark(modelName, modelId);
243
+ if (benchmark?.codingIndex !== undefined) {
244
+ return `${modelName} [CI: ${benchmark.codingIndex.toFixed(1)}]`;
245
+ }
246
+ return modelName;
247
+ }