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.
- package/CHANGELOG.md +107 -1
- package/README.md +95 -46
- package/config.ts +165 -120
- package/constants.ts +22 -61
- package/index.ts +186 -0
- package/lib/json-persistence.ts +11 -10
- package/lib/logger.ts +2 -2
- package/lib/model-enhancer.ts +20 -20
- package/lib/open-browser.ts +41 -0
- package/lib/provider-cache.ts +106 -0
- package/lib/registry.ts +144 -0
- package/package.json +67 -82
- package/provider-factory.ts +25 -41
- package/provider-failover/benchmark-lookup.ts +247 -0
- package/provider-failover/benchmarks-chunk-0.ts +2010 -0
- package/provider-failover/benchmarks-chunk-1.ts +1988 -0
- package/provider-failover/benchmarks-chunk-2.ts +2010 -0
- package/provider-failover/benchmarks-chunk-3.ts +2010 -0
- package/provider-failover/benchmarks-chunk-4.ts +1969 -0
- package/provider-failover/hardcoded-benchmarks.ts +22 -10025
- package/provider-helper.ts +38 -37
- package/providers/{cline-auth.ts → cline/cline-auth.ts} +2 -2
- package/providers/cline/cline-models.ts +128 -0
- package/providers/{cline.ts → cline/cline.ts} +300 -257
- package/providers/cloudflare/cloudflare.ts +368 -0
- package/providers/dynamic-built-in/index.ts +513 -0
- package/providers/{kilo-auth.ts → kilo/kilo-auth.ts} +3 -20
- package/providers/{kilo-models.ts → kilo/kilo-models.ts} +2 -2
- package/providers/kilo/kilo.ts +235 -0
- package/providers/{modal.ts → modal/modal.ts} +4 -3
- package/providers/{nvidia.ts → nvidia/nvidia.ts} +152 -113
- package/providers/ollama/ollama.ts +172 -0
- package/providers/opencode-session.ts +34 -34
- package/providers/{qwen-auth.ts → qwen/qwen-auth.ts} +24 -40
- package/providers/{qwen-models.ts → qwen/qwen-models.ts} +101 -95
- package/providers/qwen/qwen.ts +202 -0
- package/provider-failover/auto-switch.ts +0 -350
- package/provider-failover/errors.ts +0 -275
- package/provider-failover/index.ts +0 -238
- package/providers/cline-models.ts +0 -77
- package/providers/factory.ts +0 -125
- package/providers/fireworks.ts +0 -49
- package/providers/go.ts +0 -216
- package/providers/kilo.ts +0 -146
- package/providers/mistral.ts +0 -144
- package/providers/ollama.ts +0 -113
- package/providers/openrouter.ts +0 -175
- package/providers/qwen.ts +0 -127
- package/providers/zen.ts +0 -371
- package/usage/commands.ts +0 -17
- package/usage/cumulative.ts +0 -193
- package/usage/formatters.ts +0 -115
- package/usage/index.ts +0 -46
- package/usage/limits.ts +0 -148
- package/usage/metrics.ts +0 -222
- package/usage/sessions.ts +0 -355
- package/usage/store.ts +0 -99
- package/usage/tracking.ts +0 -329
- package/usage/types.ts +0 -26
- package/usage/widget.ts +0 -90
- package/widget/data.ts +0 -113
- package/widget/format.ts +0 -26
- 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
|
-
}
|
package/providers/factory.ts
DELETED
|
@@ -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
|
-
}
|