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
@@ -1,275 +0,0 @@
1
- /**
2
- * Error classification for provider failover
3
- * Detects 429 rate limits, capacity errors, and other provider-specific errors
4
- */
5
-
6
- import { createLogger } from "../lib/logger.ts";
7
- import { getFreeTierUsage, getLimitWarning } from "../usage/limits.ts";
8
-
9
- const _logger = createLogger("failover");
10
-
11
- export type ErrorType =
12
- | "rate_limit" // 429, quota exceeded
13
- | "capacity" // No capacity, overloaded
14
- | "auth" // Invalid key, unauthorized
15
- | "network" // Timeout, connection error
16
- | "unknown"; // Unclassified
17
-
18
- export interface ClassifiedError {
19
- type: ErrorType;
20
- provider?: string;
21
- model?: string;
22
- statusCode?: number;
23
- message: string;
24
- retryable: boolean;
25
- retryAfterMs?: number; // Server-suggested retry delay
26
- }
27
-
28
- // Pattern matching for various provider error messages
29
- const RATE_LIMIT_PATTERNS = [
30
- /429/i,
31
- /rate.?limit/i,
32
- /too.?many.?requests/i,
33
- /quota.*exceeded/i,
34
- /insufficient.*quota/i,
35
- /billing.*quota/i,
36
- /limit.*exceeded/i,
37
- /throttled/i,
38
- /ratelimit/i,
39
- ];
40
-
41
- const CAPACITY_PATTERNS = [
42
- /no.*capacity/i,
43
- /overloaded/i,
44
- /engine.*overloaded/i,
45
- /temporarily.*unavailable/i,
46
- /service.*unavailable/i,
47
- /503/i,
48
- /529/i, // Cloudflare origin is overloaded
49
- /busy/i,
50
- ];
51
-
52
- const AUTH_PATTERNS = [
53
- /401/i,
54
- /403/i,
55
- /unauthorized/i,
56
- /invalid.*key/i,
57
- /invalid.*token/i,
58
- /authentication/i,
59
- /api.*key.*invalid/i,
60
- /key.*not.*valid/i,
61
- /invalid.*api.*key/i,
62
- /invalid.*auth/i,
63
- ];
64
-
65
- const NETWORK_PATTERNS = [
66
- /timeout/i,
67
- /etimedout/i,
68
- /enetunreach/i,
69
- /econnreset/i,
70
- /connection.*refused/i,
71
- /fetch.*failed/i,
72
- /network.*error/i,
73
- /abort/i,
74
- /signal/i,
75
- ];
76
-
77
- /**
78
- * Extract HTTP status code from error object or message
79
- */
80
- function extractStatusCode(error: unknown): number | undefined {
81
- // Check for statusCode property
82
- if (
83
- typeof error === "object" &&
84
- error !== null &&
85
- "statusCode" in error &&
86
- typeof error.statusCode === "number"
87
- ) {
88
- return error.statusCode;
89
- }
90
-
91
- // Check for status property
92
- if (
93
- typeof error === "object" &&
94
- error !== null &&
95
- "status" in error &&
96
- typeof error.status === "number"
97
- ) {
98
- return error.status;
99
- }
100
-
101
- // Extract from message
102
- const message = String(error);
103
- const match = message.match(/\b(\d{3})\b/);
104
- if (match) {
105
- const code = Number.parseInt(match[1], 10);
106
- if (code >= 400 && code < 600) return code;
107
- }
108
-
109
- return undefined;
110
- }
111
-
112
- /**
113
- * Extract retry-after hint from error
114
- */
115
- function extractRetryAfter(error: unknown): number | undefined {
116
- const message = String(error);
117
-
118
- // Look for "retry after X seconds/minutes"
119
- const secondsMatch = message.match(/retry.?after\s+(\d+)\s*s/i);
120
- if (secondsMatch) {
121
- return Number.parseInt(secondsMatch[1], 10) * 1000;
122
- }
123
-
124
- const minutesMatch = message.match(/retry.?after\s+(\d+)\s*m/i);
125
- if (minutesMatch) {
126
- return Number.parseInt(minutesMatch[1], 10) * 60 * 1000;
127
- }
128
-
129
- // Check for retry_after property
130
- if (
131
- typeof error === "object" &&
132
- error !== null &&
133
- "retry_after" in error &&
134
- typeof error.retry_after === "number"
135
- ) {
136
- return error.retry_after * 1000;
137
- }
138
-
139
- return undefined;
140
- }
141
-
142
- /**
143
- * Classify an error to determine if it's a 429/capacity issue
144
- */
145
- export function classifyError(error: unknown): ClassifiedError {
146
- const message = String(error);
147
- const statusCode = extractStatusCode(error);
148
- const retryAfterMs = extractRetryAfter(error);
149
-
150
- // Check status code first
151
- if (statusCode === 429) {
152
- return {
153
- type: "rate_limit",
154
- statusCode,
155
- message,
156
- retryable: true,
157
- retryAfterMs: retryAfterMs ?? 60000, // Default 1 min
158
- };
159
- }
160
-
161
- if (statusCode === 503 || statusCode === 529) {
162
- return {
163
- type: "capacity",
164
- statusCode,
165
- message,
166
- retryable: true,
167
- retryAfterMs: retryAfterMs ?? 30000, // Default 30 sec
168
- };
169
- }
170
-
171
- if (statusCode === 401 || statusCode === 403) {
172
- return {
173
- type: "auth",
174
- statusCode,
175
- message,
176
- retryable: false,
177
- };
178
- }
179
-
180
- // Check patterns in message
181
- if (RATE_LIMIT_PATTERNS.some((p) => p.test(message))) {
182
- return {
183
- type: "rate_limit",
184
- statusCode,
185
- message,
186
- retryable: true,
187
- retryAfterMs: retryAfterMs ?? 60000,
188
- };
189
- }
190
-
191
- if (CAPACITY_PATTERNS.some((p) => p.test(message))) {
192
- return {
193
- type: "capacity",
194
- statusCode,
195
- message,
196
- retryable: true,
197
- retryAfterMs: retryAfterMs ?? 30000,
198
- };
199
- }
200
-
201
- if (AUTH_PATTERNS.some((p) => p.test(message))) {
202
- return {
203
- type: "auth",
204
- statusCode,
205
- message,
206
- retryable: false,
207
- };
208
- }
209
-
210
- if (NETWORK_PATTERNS.some((p) => p.test(message))) {
211
- return {
212
- type: "network",
213
- statusCode,
214
- message,
215
- retryable: true,
216
- retryAfterMs: 5000, // Short retry for network
217
- };
218
- }
219
-
220
- // Unknown error - assume retryable but with caution
221
- return {
222
- type: "unknown",
223
- statusCode,
224
- message,
225
- retryable: statusCode ? statusCode >= 500 : true,
226
- retryAfterMs: 10000,
227
- };
228
- }
229
-
230
- /**
231
- * Check if error is specifically a rate limit (429)
232
- */
233
- export function isRateLimit(error: unknown): boolean {
234
- return classifyError(error).type === "rate_limit";
235
- }
236
-
237
- /**
238
- * Check if error is capacity-related (provider overloaded)
239
- */
240
- export function isCapacityError(error: unknown): boolean {
241
- return classifyError(error).type === "capacity";
242
- }
243
-
244
- /**
245
- * Log error classification for debugging
246
- */
247
- export function logErrorClassification(
248
- _error: unknown,
249
- classified: ClassifiedError,
250
- ): void {
251
- _logger.info(`Error classified: ${classified.type}`, {
252
- statusCode: classified.statusCode,
253
- retryable: classified.retryable,
254
- retryAfterMs: classified.retryAfterMs,
255
- message: classified.message.slice(0, 100),
256
- });
257
- }
258
-
259
- /**
260
- * Log free tier usage when rate limit occurs
261
- * Helps users understand their quota consumption
262
- */
263
- export function logFreeTierUsage(provider: string): void {
264
- const usage = getFreeTierUsage(provider);
265
- const warning = getLimitWarning(provider);
266
-
267
- if (warning) {
268
- _logger.warn(`Free tier warning: ${warning}`, { provider });
269
- } else {
270
- _logger.info(`${provider} usage`, {
271
- requestsToday: usage.requestsToday,
272
- limit: usage.limit.description,
273
- });
274
- }
275
- }
@@ -1,238 +0,0 @@
1
- /**
2
- * Main provider failover handler
3
- * Coordinates error detection and provider switching
4
- */
5
-
6
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
7
- import { createLogger } from "../lib/logger.ts";
8
- import {
9
- type ClassifiedError,
10
- classifyError,
11
- logErrorClassification,
12
- } from "./errors.ts";
13
- import { autoFailover, findFallbackModel, type AutoSwitchConfig } from "./auto-switch.ts";
14
-
15
- export type { AutoSwitchConfig } from "./auto-switch.ts";
16
-
17
- const _logger = createLogger("failover");
18
-
19
- export interface FailoverConfig {
20
- // Provider identifier (e.g., "kilo", "openrouter")
21
- provider: string;
22
-
23
- // Whether this provider is in paid mode
24
- isPaidMode: boolean;
25
-
26
- // Auto-switch configuration
27
- autoSwitch?: Partial<AutoSwitchConfig>;
28
- }
29
-
30
- export interface FailoverResult {
31
- action: "retry" | "fail" | "switch";
32
- message: string;
33
- shouldRetry: boolean;
34
- retryDelayMs?: number;
35
- /** The model to switch to */
36
- switchToModel?: string;
37
- }
38
-
39
- // Track consecutive failures per provider
40
- const failureCounts = new Map<string, number>();
41
- const MAX_CONSECUTIVE_FAILURES = 3;
42
-
43
- /**
44
- * Handle provider error with smart failover logic
45
- */
46
- export async function handleProviderError(
47
- error: unknown,
48
- config: FailoverConfig,
49
- pi: ExtensionAPI,
50
- ctx: {
51
- ui: {
52
- notify: (message: string, type: "info" | "warning" | "error") => void;
53
- };
54
- model?: { provider?: string; id?: string };
55
- session?: { id?: string };
56
- },
57
- ): Promise<FailoverResult> {
58
- const { provider, isPaidMode, autoSwitch } = config;
59
-
60
- // Classify the error
61
- const classified = classifyError(error);
62
- logErrorClassification(error, classified);
63
-
64
- // Track failures
65
- const failureKey = `${provider}`;
66
- const currentFailures = (failureCounts.get(failureKey) ?? 0) + 1;
67
- failureCounts.set(failureKey, currentFailures);
68
-
69
- // Check for too many consecutive failures
70
- if (currentFailures >= MAX_CONSECUTIVE_FAILURES) {
71
- _logger.info(`${provider} has ${currentFailures} consecutive failures`);
72
- }
73
-
74
- switch (classified.type) {
75
- case "rate_limit":
76
- return handleRateLimit(classified, provider, isPaidMode, ctx, pi, autoSwitch);
77
-
78
- case "capacity":
79
- return handleCapacityError(classified, provider, ctx, pi, autoSwitch);
80
-
81
- case "auth":
82
- return handleAuthError(classified, provider);
83
-
84
- case "network":
85
- return handleNetworkError(classified, provider);
86
-
87
- default:
88
- return handleUnknownError(classified, provider);
89
- }
90
- }
91
-
92
- /**
93
- * Handle rate limit (429) error
94
- */
95
- function handleRateLimit(
96
- classified: ClassifiedError,
97
- provider: string,
98
- isPaidMode: boolean,
99
- _ctx: {
100
- ui: {
101
- notify: (message: string, type: "info" | "warning" | "error") => void;
102
- };
103
- model?: { provider?: string; id?: string };
104
- },
105
- _pi: ExtensionAPI,
106
- autoSwitchConfig?: Partial<AutoSwitchConfig>,
107
- ): FailoverResult {
108
- _logger.info(`Rate limit on ${provider}`, { isPaidMode, model: _ctx.model });
109
-
110
- const waitTime = Math.round((classified.retryAfterMs ?? 60000) / 1000);
111
-
112
- // Auto-switch is enabled by default unless explicitly disabled
113
- if (autoSwitchConfig?.enabled !== false && _ctx.model?.id) {
114
- _logger.info("Attempting auto-switch for rate limit");
115
- // Note: Actual switching happens in provider-helper.ts turn_end handler
116
- // This just signals that a switch is possible
117
- return {
118
- action: "switch",
119
- message: `Rate limit on ${provider}. Auto-switching to another provider...`,
120
- shouldRetry: false,
121
- retryDelayMs: classified.retryAfterMs,
122
- };
123
- }
124
-
125
- return {
126
- action: "fail",
127
- message: `Rate limit on ${provider}. Wait ${waitTime}s or switch providers manually with /model.`,
128
- shouldRetry: false,
129
- retryDelayMs: classified.retryAfterMs,
130
- };
131
- }
132
-
133
- /**
134
- * Handle capacity error (provider overloaded)
135
- */
136
- function handleCapacityError(
137
- classified: ClassifiedError,
138
- provider: string,
139
- _ctx: {
140
- ui: {
141
- notify: (message: string, type: "info" | "warning" | "error") => void;
142
- };
143
- model?: { provider?: string; id?: string };
144
- },
145
- _pi: ExtensionAPI,
146
- autoSwitchConfig?: Partial<AutoSwitchConfig>,
147
- ): FailoverResult {
148
- _logger.info(`Capacity error on ${provider}`, { model: _ctx.model });
149
-
150
- // Auto-switch is enabled by default unless explicitly disabled
151
- if (autoSwitchConfig?.enabled !== false && _ctx.model?.id) {
152
- _logger.info("Attempting auto-switch for capacity error");
153
- return {
154
- action: "switch",
155
- message: `${provider} is at capacity. Auto-switching to another provider...`,
156
- shouldRetry: false,
157
- retryDelayMs: classified.retryAfterMs ?? 30000,
158
- };
159
- }
160
-
161
- return {
162
- action: "retry",
163
- message: `${provider} is at capacity. Try again in ${Math.round((classified.retryAfterMs ?? 30000) / 1000)}s or switch providers.`,
164
- shouldRetry: true,
165
- retryDelayMs: classified.retryAfterMs ?? 30000,
166
- };
167
- }
168
-
169
- /**
170
- * Handle authentication error
171
- */
172
- function handleAuthError(
173
- _classified: ClassifiedError,
174
- provider: string,
175
- ): FailoverResult {
176
- _logger.info(`Auth error on ${provider}`);
177
-
178
- return {
179
- action: "fail",
180
- message: `Authentication failed for ${provider}. Check your API key with /login ${provider} or set ${provider.toUpperCase()}_API_KEY.`,
181
- shouldRetry: false,
182
- };
183
- }
184
-
185
- /**
186
- * Handle network error
187
- */
188
- function handleNetworkError(
189
- classified: ClassifiedError,
190
- provider: string,
191
- ): FailoverResult {
192
- _logger.info(`Network error on ${provider}`);
193
-
194
- return {
195
- action: "retry",
196
- message: `Network error connecting to ${provider}. Retrying...`,
197
- shouldRetry: true,
198
- retryDelayMs: classified.retryAfterMs ?? 5000,
199
- };
200
- }
201
-
202
- /**
203
- * Handle unknown/unclassified error
204
- */
205
- function handleUnknownError(
206
- classified: ClassifiedError,
207
- provider: string,
208
- ): FailoverResult {
209
- _logger.info(`Unknown error on ${provider}`, { message: classified.message });
210
-
211
- return {
212
- action: classified.retryable ? "retry" : "fail",
213
- message: `Error from ${provider}: ${classified.message.slice(0, 100)}`,
214
- shouldRetry: classified.retryable,
215
- retryDelayMs: classified.retryAfterMs ?? 10000,
216
- };
217
- }
218
-
219
- /**
220
- * Reset failure count for a provider (call on successful request)
221
- */
222
- export function resetFailureCount(provider: string): void {
223
- failureCounts.delete(provider);
224
- }
225
-
226
- /**
227
- * Get current failure count for a provider
228
- */
229
- export function getFailureCount(provider: string): number {
230
- return failureCounts.get(provider) ?? 0;
231
- }
232
-
233
- /**
234
- * Check if provider should be considered exhausted
235
- */
236
- export function isProviderExhausted(provider: string): boolean {
237
- return getFailureCount(provider) >= MAX_CONSECUTIVE_FAILURES;
238
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * Cline free model fetching.
3
- *
4
- * Fetches zero-cost models from OpenRouter (Cline's gateway).
5
- */
6
-
7
- import { applyHidden } from "../config.ts";
8
- import {
9
- BASE_URL_OPENROUTER,
10
- DEFAULT_FETCH_TIMEOUT_MS,
11
- DEFAULT_MIN_SIZE_B,
12
- } from "../constants.ts";
13
- import type { ProviderModelConfig } from "../lib/types.ts";
14
- import { cleanModelName, fetchWithRetry, isUsableModel } from "../lib/util.ts";
15
-
16
- interface OpenRouterRaw {
17
- id: string;
18
- name: string;
19
- context_length?: number;
20
- supported_parameters?: string[];
21
- architecture?: { input_modalities?: string[]; output_modalities?: string[] };
22
- top_provider?: { max_completion_tokens?: number | null };
23
- pricing?: { prompt?: string; completion?: string };
24
- }
25
-
26
- function extractNameFromId(id: string): string {
27
- const part = id.split("/")[1] ?? id;
28
- return part
29
- .split(/[-_]/)
30
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
31
- .join(" ");
32
- }
33
-
34
- export async function fetchClineModels(): Promise<ProviderModelConfig[]> {
35
- const response = await fetchWithRetry(
36
- `${BASE_URL_OPENROUTER}/models`,
37
- {},
38
- 3,
39
- 1000,
40
- DEFAULT_FETCH_TIMEOUT_MS,
41
- );
42
-
43
- if (!response.ok)
44
- throw new Error(`Failed to fetch OpenRouter models: ${response.status}`);
45
-
46
- const json = (await response.json()) as { data?: OpenRouterRaw[] };
47
- const freeModels = (json.data ?? []).filter(
48
- (m) => m.pricing?.prompt === "0" && m.pricing?.completion === "0",
49
- );
50
-
51
- const models: ProviderModelConfig[] = [];
52
- for (const info of freeModels) {
53
- if (!isUsableModel(info.id, DEFAULT_MIN_SIZE_B)) continue;
54
-
55
- const isReasoning = !!(
56
- info.supported_parameters?.includes("include_reasoning") ||
57
- info.supported_parameters?.includes("reasoning")
58
- );
59
- const hasImage =
60
- info.architecture?.input_modalities?.includes("image") ?? false;
61
-
62
- const cleanName = info.name
63
- ? cleanModelName(info.name)
64
- : extractNameFromId(info.id);
65
- models.push({
66
- id: info.id,
67
- name: `${cleanName} (Cline)`,
68
- reasoning: isReasoning,
69
- input: hasImage ? ["text", "image"] : ["text"],
70
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
71
- contextWindow: info.context_length ?? 128_000,
72
- maxTokens: info.top_provider?.max_completion_tokens ?? 8_192,
73
- });
74
- }
75
-
76
- return applyHidden(models);
77
- }
@@ -1,125 +0,0 @@
1
- /**
2
- * Generic OpenAI-compatible provider factory
3
- *
4
- * Creates provider extensions for any OpenAI-compatible API endpoint.
5
- * Used to easily add new providers without duplicating boilerplate.
6
- */
7
-
8
- import type {
9
- ExtensionAPI,
10
- ProviderModelConfig,
11
- } from "@mariozechner/pi-coding-agent";
12
- import { createLogger } from "../lib/logger.ts";
13
- import {
14
- createReRegister,
15
- type StoredModels,
16
- setupProvider,
17
- } from "../provider-helper.ts";
18
-
19
- export interface OpenAIProviderConfig {
20
- /** Unique provider identifier (e.g., "groq", "together") */
21
- providerId: string;
22
- /** Environment variable name for the API key */
23
- apiKeyEnvVar: string;
24
- /** API base URL */
25
- baseUrl: string;
26
- /** Human-readable name for the provider */
27
- displayName: string;
28
- /** Website URL for users to get an API key */
29
- keyWebsite: string;
30
- /** Whether this provider has a free tier */
31
- hasFreeTier: boolean;
32
- /** Hardcoded models (when models.dev doesn't have data) */
33
- models: ProviderModelConfig[];
34
- /** Additional headers to include in requests */
35
- headers?: Record<string, string>;
36
- /** Whether to show a ToS notice */
37
- showTosNotice?: boolean;
38
- /** Terms of service URL */
39
- tosUrl?: string;
40
- }
41
-
42
- /**
43
- * Create an OpenAI-compatible provider extension
44
- */
45
- export function createOpenAIProvider(config: OpenAIProviderConfig) {
46
- const {
47
- providerId,
48
- apiKeyEnvVar,
49
- baseUrl,
50
- displayName,
51
- keyWebsite,
52
- hasFreeTier,
53
- models: hardcodedModels,
54
- headers = {},
55
- showTosNotice = false,
56
- tosUrl,
57
- } = config;
58
-
59
- const _logger = createLogger(providerId);
60
-
61
- return async (pi: ExtensionAPI) => {
62
- // Get API key from environment or config
63
- const apiKey = process.env[apiKeyEnvVar];
64
-
65
- // Inject into process.env so Pi's apiKey lookup finds it
66
- if (apiKey) {
67
- process.env[apiKeyEnvVar] = apiKey;
68
- } else if (!hasFreeTier) {
69
- _logger.warn(
70
- `No API key found — set ${apiKeyEnvVar} or add ${apiKeyEnvVar.toLowerCase()}_api_key to ~/.pi/free.json. Get a key at ${keyWebsite}`,
71
- );
72
- return;
73
- }
74
-
75
- // Filter to free models if no API key
76
- let models = hardcodedModels;
77
- if (!apiKey && hasFreeTier) {
78
- models = models.filter((m) => (m.cost?.input ?? 0) === 0);
79
- }
80
-
81
- if (models.length === 0 && !hasFreeTier) {
82
- _logger.warn(`No models available for ${displayName}`);
83
- return;
84
- }
85
-
86
- // Shared model storage
87
- const stored: StoredModels = { free: models, all: models };
88
-
89
- // Register provider
90
- pi.registerProvider(providerId, {
91
- baseUrl,
92
- apiKey: apiKeyEnvVar,
93
- api: "openai-completions" as const,
94
- headers: {
95
- "User-Agent": "pi-free-providers",
96
- ...headers,
97
- },
98
- models,
99
- });
100
-
101
- // Wire up shared boilerplate
102
- const reRegister = createReRegister(pi, {
103
- providerId,
104
- baseUrl,
105
- apiKey: apiKeyEnvVar,
106
- headers,
107
- });
108
-
109
- setupProvider(
110
- pi,
111
- {
112
- providerId,
113
- tosUrl: showTosNotice ? tosUrl : undefined,
114
- hasKey: !!apiKey,
115
- initialShowPaid: !!apiKey, // If they have a key, show all by default
116
- reRegister: (m) => {
117
- stored.free = m;
118
- stored.all = m;
119
- reRegister(m);
120
- },
121
- },
122
- stored,
123
- );
124
- };
125
- }