pi-free 1.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/.github/workflows/update-benchmarks.yml +67 -0
- package/.pi/skills/pi-extension-dev/SKILL.md +155 -0
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/config.ts +224 -0
- package/constants.ts +110 -0
- package/docs/free-tier-limits.md +213 -0
- package/docs/model-hopping.md +214 -0
- package/docs/plans/file-reorganization.md +172 -0
- package/docs/plans/package-json-fix.md +143 -0
- package/docs/provider-failover-plan.md +279 -0
- package/lib/json-persistence.ts +102 -0
- package/lib/logger.ts +94 -0
- package/lib/model-enhancer.ts +20 -0
- package/lib/types.ts +108 -0
- package/lib/util.ts +256 -0
- package/package.json +52 -0
- package/provider-factory.ts +221 -0
- package/provider-failover/errors.ts +275 -0
- package/provider-failover/hardcoded-benchmarks.ts +9889 -0
- package/provider-failover/index.ts +194 -0
- package/provider-helper.ts +336 -0
- package/providers/cline-auth.ts +473 -0
- package/providers/cline-models.ts +77 -0
- package/providers/cline.ts +257 -0
- package/providers/factory.ts +125 -0
- package/providers/fireworks.ts +49 -0
- package/providers/kilo-auth.ts +172 -0
- package/providers/kilo-models.ts +26 -0
- package/providers/kilo.ts +144 -0
- package/providers/mistral.ts +144 -0
- package/providers/model-fetcher.ts +138 -0
- package/providers/nvidia.ts +97 -0
- package/providers/ollama.ts +113 -0
- package/providers/openrouter.ts +175 -0
- package/providers/zen.ts +416 -0
- package/scripts/update-benchmarks.ts +255 -0
- package/tests/cline.test.ts +149 -0
- package/tests/errors.test.ts +139 -0
- package/tests/failover.test.ts +94 -0
- package/tests/fireworks.test.ts +148 -0
- package/tests/free-tier-limits.test.ts +191 -0
- package/tests/json-persistence.test.ts +105 -0
- package/tests/kilo.test.ts +186 -0
- package/tests/mistral.test.ts +138 -0
- package/tests/nvidia.test.ts +55 -0
- package/tests/ollama.test.ts +261 -0
- package/tests/openrouter.test.ts +192 -0
- package/tests/usage-tracking.test.ts +150 -0
- package/tests/util.test.ts +413 -0
- package/tests/zen.test.ts +180 -0
- package/todo.md +153 -0
- package/tsconfig.json +26 -0
- package/usage/commands.ts +17 -0
- package/usage/cumulative.ts +193 -0
- package/usage/formatters.ts +131 -0
- package/usage/index.ts +46 -0
- package/usage/limits.ts +166 -0
- package/usage/metrics.ts +222 -0
- package/usage/sessions.ts +355 -0
- package/usage/store.ts +99 -0
- package/usage/tracking.ts +329 -0
- package/usage/widget.ts +90 -0
- package/vitest.config.ts +20 -0
- package/widget/data.ts +113 -0
- package/widget/format.ts +26 -0
- package/widget/render.ts +117 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main provider failover handler
|
|
3
|
+
* Coordinates error detection and provider switching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { createLogger } from "../lib/logger.ts";
|
|
8
|
+
import {
|
|
9
|
+
type ClassifiedError,
|
|
10
|
+
classifyError,
|
|
11
|
+
logErrorClassification,
|
|
12
|
+
} from "./errors.js";
|
|
13
|
+
|
|
14
|
+
const _logger = createLogger("failover");
|
|
15
|
+
|
|
16
|
+
export interface FailoverConfig {
|
|
17
|
+
// Provider identifier (e.g., "kilo", "openrouter")
|
|
18
|
+
provider: string;
|
|
19
|
+
|
|
20
|
+
// Whether this provider is in paid mode
|
|
21
|
+
isPaidMode: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FailoverResult {
|
|
25
|
+
action: "retry" | "fail";
|
|
26
|
+
message: string;
|
|
27
|
+
shouldRetry: boolean;
|
|
28
|
+
retryDelayMs?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Track consecutive failures per provider
|
|
32
|
+
const failureCounts = new Map<string, number>();
|
|
33
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handle provider error with smart failover logic
|
|
37
|
+
*/
|
|
38
|
+
export async function handleProviderError(
|
|
39
|
+
error: unknown,
|
|
40
|
+
config: FailoverConfig,
|
|
41
|
+
_pi: ExtensionAPI,
|
|
42
|
+
ctx: {
|
|
43
|
+
ui: {
|
|
44
|
+
notify: (message: string, type: "info" | "warning" | "error") => void;
|
|
45
|
+
};
|
|
46
|
+
session?: { id?: string };
|
|
47
|
+
},
|
|
48
|
+
): Promise<FailoverResult> {
|
|
49
|
+
const { provider, isPaidMode } = config;
|
|
50
|
+
|
|
51
|
+
// Classify the error
|
|
52
|
+
const classified = classifyError(error);
|
|
53
|
+
logErrorClassification(error, classified);
|
|
54
|
+
|
|
55
|
+
// Track failures
|
|
56
|
+
const failureKey = `${provider}`;
|
|
57
|
+
const currentFailures = (failureCounts.get(failureKey) ?? 0) + 1;
|
|
58
|
+
failureCounts.set(failureKey, currentFailures);
|
|
59
|
+
|
|
60
|
+
// Check for too many consecutive failures
|
|
61
|
+
if (currentFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
62
|
+
_logger.info(`${provider} has ${currentFailures} consecutive failures`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
switch (classified.type) {
|
|
66
|
+
case "rate_limit":
|
|
67
|
+
return handleRateLimit(classified, provider, isPaidMode, ctx);
|
|
68
|
+
|
|
69
|
+
case "capacity":
|
|
70
|
+
return handleCapacityError(classified, provider);
|
|
71
|
+
|
|
72
|
+
case "auth":
|
|
73
|
+
return handleAuthError(classified, provider);
|
|
74
|
+
|
|
75
|
+
case "network":
|
|
76
|
+
return handleNetworkError(classified, provider);
|
|
77
|
+
|
|
78
|
+
default:
|
|
79
|
+
return handleUnknownError(classified, provider);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle rate limit (429) error
|
|
85
|
+
*/
|
|
86
|
+
function handleRateLimit(
|
|
87
|
+
classified: ClassifiedError,
|
|
88
|
+
provider: string,
|
|
89
|
+
isPaidMode: boolean,
|
|
90
|
+
_ctx: {
|
|
91
|
+
ui: {
|
|
92
|
+
notify: (message: string, type: "info" | "warning" | "error") => void;
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
): FailoverResult {
|
|
96
|
+
_logger.info(`Rate limit on ${provider}`, { isPaidMode });
|
|
97
|
+
|
|
98
|
+
const waitTime = Math.round((classified.retryAfterMs ?? 60000) / 1000);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
action: "fail",
|
|
102
|
+
message: `Rate limit on ${provider}. Wait ${waitTime}s or switch providers manually with /model.`,
|
|
103
|
+
shouldRetry: false,
|
|
104
|
+
retryDelayMs: classified.retryAfterMs,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle capacity error (provider overloaded)
|
|
110
|
+
*/
|
|
111
|
+
function handleCapacityError(
|
|
112
|
+
classified: ClassifiedError,
|
|
113
|
+
provider: string,
|
|
114
|
+
): FailoverResult {
|
|
115
|
+
_logger.info(`Capacity error on ${provider}`);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
action: "retry",
|
|
119
|
+
message: `${provider} is at capacity. Try again in ${Math.round((classified.retryAfterMs ?? 30000) / 1000)}s or switch providers.`,
|
|
120
|
+
shouldRetry: true,
|
|
121
|
+
retryDelayMs: classified.retryAfterMs ?? 30000,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handle authentication error
|
|
127
|
+
*/
|
|
128
|
+
function handleAuthError(
|
|
129
|
+
_classified: ClassifiedError,
|
|
130
|
+
provider: string,
|
|
131
|
+
): FailoverResult {
|
|
132
|
+
_logger.info(`Auth error on ${provider}`);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
action: "fail",
|
|
136
|
+
message: `Authentication failed for ${provider}. Check your API key with /login ${provider} or set ${provider.toUpperCase()}_API_KEY.`,
|
|
137
|
+
shouldRetry: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Handle network error
|
|
143
|
+
*/
|
|
144
|
+
function handleNetworkError(
|
|
145
|
+
classified: ClassifiedError,
|
|
146
|
+
provider: string,
|
|
147
|
+
): FailoverResult {
|
|
148
|
+
_logger.info(`Network error on ${provider}`);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
action: "retry",
|
|
152
|
+
message: `Network error connecting to ${provider}. Retrying...`,
|
|
153
|
+
shouldRetry: true,
|
|
154
|
+
retryDelayMs: classified.retryAfterMs ?? 5000,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle unknown/unclassified error
|
|
160
|
+
*/
|
|
161
|
+
function handleUnknownError(
|
|
162
|
+
classified: ClassifiedError,
|
|
163
|
+
provider: string,
|
|
164
|
+
): FailoverResult {
|
|
165
|
+
_logger.info(`Unknown error on ${provider}`, { message: classified.message });
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
action: classified.retryable ? "retry" : "fail",
|
|
169
|
+
message: `Error from ${provider}: ${classified.message.slice(0, 100)}`,
|
|
170
|
+
shouldRetry: classified.retryable,
|
|
171
|
+
retryDelayMs: classified.retryAfterMs ?? 10000,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Reset failure count for a provider (call on successful request)
|
|
177
|
+
*/
|
|
178
|
+
export function resetFailureCount(provider: string): void {
|
|
179
|
+
failureCounts.delete(provider);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get current failure count for a provider
|
|
184
|
+
*/
|
|
185
|
+
export function getFailureCount(provider: string): number {
|
|
186
|
+
return failureCounts.get(provider) ?? 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if provider should be considered exhausted
|
|
191
|
+
*/
|
|
192
|
+
export function isProviderExhausted(provider: string): boolean {
|
|
193
|
+
return getFailureCount(provider) >= MAX_CONSECUTIVE_FAILURES;
|
|
194
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared provider setup helpers for pi-free-providers.
|
|
3
|
+
* Extracts the common boilerplate pattern repeated across providers:
|
|
4
|
+
* - /{provider}-toggle command to switch between free/paid models
|
|
5
|
+
* - model_select handler (clear status for other providers)
|
|
6
|
+
* - turn_end handler (increment request count, handle errors)
|
|
7
|
+
* - before_agent_start handler (one-time ToS notice)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ExtensionAPI,
|
|
12
|
+
ProviderModelConfig,
|
|
13
|
+
} from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import { saveConfig } from "./config.ts";
|
|
15
|
+
import { createLogger } from "./lib/logger.ts";
|
|
16
|
+
import { enhanceModelNameWithCodingIndex } from "./provider-failover/hardcoded-benchmarks.js";
|
|
17
|
+
import {
|
|
18
|
+
handleProviderError,
|
|
19
|
+
isProviderExhausted,
|
|
20
|
+
resetFailureCount,
|
|
21
|
+
} from "./provider-failover/index.js";
|
|
22
|
+
import { incrementRequestCount } from "./usage/metrics.ts";
|
|
23
|
+
import { incrementModelRequestCount } from "./usage/tracking.ts";
|
|
24
|
+
|
|
25
|
+
const _logger = createLogger("provider-helper");
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Types
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export interface ProviderSetupConfig {
|
|
32
|
+
/** Provider identifier (e.g., "kilo", "openrouter"). */
|
|
33
|
+
providerId: string;
|
|
34
|
+
/** Terms of service URL. If set, shows a one-time notice on first free use. */
|
|
35
|
+
tosUrl?: string;
|
|
36
|
+
/** When true, suppresses the "free models / set API key" ToS notice. */
|
|
37
|
+
hasKey?: boolean;
|
|
38
|
+
/** Initial mode - auto-detected from config at startup. */
|
|
39
|
+
initialShowPaid?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Called by /{provider}-toggle command to re-register
|
|
42
|
+
* the provider with the given model set.
|
|
43
|
+
*/
|
|
44
|
+
reRegister: (models: ProviderModelConfig[], stored: StoredModels) => void;
|
|
45
|
+
/** Optional custom error handler. Return true if handled. */
|
|
46
|
+
onError?: (
|
|
47
|
+
error: unknown,
|
|
48
|
+
ctx: {
|
|
49
|
+
ui: { notify: (m: string, t: "info" | "warning" | "error") => void };
|
|
50
|
+
},
|
|
51
|
+
) => Promise<boolean>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface StoredModels {
|
|
55
|
+
free: ProviderModelConfig[];
|
|
56
|
+
all: ProviderModelConfig[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// Provider Registration Helpers
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
export interface OpenAICompatibleConfig {
|
|
64
|
+
/** Provider identifier (e.g., "nvidia", "fireworks") */
|
|
65
|
+
providerId: string;
|
|
66
|
+
/** Base URL for the API */
|
|
67
|
+
baseUrl: string;
|
|
68
|
+
/** Environment variable name for the API key */
|
|
69
|
+
apiKey: string;
|
|
70
|
+
/** Additional headers to include */
|
|
71
|
+
headers?: Record<string, string>;
|
|
72
|
+
/** OAuth configuration (optional) */
|
|
73
|
+
oauth?: {
|
|
74
|
+
name: string;
|
|
75
|
+
login: (callbacks: unknown) => Promise<unknown>;
|
|
76
|
+
refreshToken?: (cred: unknown) => Promise<unknown>;
|
|
77
|
+
getApiKey?: (cred: unknown) => string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Enhance all model names with Coding Index scores
|
|
83
|
+
* Use this for direct provider registration (not through setupProvider)
|
|
84
|
+
*/
|
|
85
|
+
export function enhanceWithCI(
|
|
86
|
+
models: ProviderModelConfig[],
|
|
87
|
+
): ProviderModelConfig[] {
|
|
88
|
+
return models.map((m) => ({
|
|
89
|
+
...m,
|
|
90
|
+
name: enhanceModelNameWithCodingIndex(m.name, m.id),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Register an OpenAI-compatible provider with standard headers.
|
|
96
|
+
* Reduces boilerplate across providers that use the OpenAI API format.
|
|
97
|
+
*/
|
|
98
|
+
export function registerOpenAICompatible(
|
|
99
|
+
pi: ExtensionAPI,
|
|
100
|
+
config: OpenAICompatibleConfig,
|
|
101
|
+
models: ProviderModelConfig[],
|
|
102
|
+
): void {
|
|
103
|
+
const { providerId, baseUrl, apiKey, headers, oauth } = config;
|
|
104
|
+
|
|
105
|
+
pi.registerProvider(providerId, {
|
|
106
|
+
baseUrl,
|
|
107
|
+
apiKey,
|
|
108
|
+
api: "openai-completions" as const,
|
|
109
|
+
headers: {
|
|
110
|
+
"User-Agent": "pi-free-providers",
|
|
111
|
+
...headers,
|
|
112
|
+
},
|
|
113
|
+
models: enhanceWithCI(models),
|
|
114
|
+
...(oauth && { oauth: oauth as any }),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create a reRegister function for use with setupProvider.
|
|
120
|
+
* Returns a function that re-registers the provider with new models.
|
|
121
|
+
*/
|
|
122
|
+
export function createReRegister(
|
|
123
|
+
pi: ExtensionAPI,
|
|
124
|
+
config: OpenAICompatibleConfig,
|
|
125
|
+
): (models: ProviderModelConfig[]) => void {
|
|
126
|
+
return (models: ProviderModelConfig[]) => {
|
|
127
|
+
registerOpenAICompatible(pi, config, models);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a reRegister function that uses ctx.modelRegistry.registerProvider.
|
|
133
|
+
* Used by providers that need to register with runtime context (session_start handlers).
|
|
134
|
+
*/
|
|
135
|
+
export function createCtxReRegister(
|
|
136
|
+
ctx: {
|
|
137
|
+
modelRegistry: { registerProvider: (id: string, config: unknown) => void };
|
|
138
|
+
},
|
|
139
|
+
config: OpenAICompatibleConfig,
|
|
140
|
+
): (models: ProviderModelConfig[]) => void {
|
|
141
|
+
const { providerId, baseUrl, apiKey, headers, oauth } = config;
|
|
142
|
+
|
|
143
|
+
return (models: ProviderModelConfig[]) => {
|
|
144
|
+
ctx.modelRegistry.registerProvider(providerId, {
|
|
145
|
+
baseUrl,
|
|
146
|
+
apiKey,
|
|
147
|
+
api: "openai-completions" as const,
|
|
148
|
+
headers: {
|
|
149
|
+
"User-Agent": "pi-free-providers",
|
|
150
|
+
...headers,
|
|
151
|
+
},
|
|
152
|
+
models: enhanceWithCI(models),
|
|
153
|
+
...(oauth && { oauth: oauth as any }),
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the config key name for a provider's show_paid setting.
|
|
160
|
+
*/
|
|
161
|
+
function getShowPaidConfigKey(providerId: string): string {
|
|
162
|
+
return `${providerId}_show_paid`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function setupProvider(
|
|
166
|
+
pi: ExtensionAPI,
|
|
167
|
+
config: ProviderSetupConfig,
|
|
168
|
+
stored: StoredModels,
|
|
169
|
+
): void {
|
|
170
|
+
const { providerId, tosUrl, initialShowPaid = false } = config;
|
|
171
|
+
|
|
172
|
+
// Track current mode (synced with config)
|
|
173
|
+
let currentShowPaid = initialShowPaid;
|
|
174
|
+
|
|
175
|
+
// Wrap reRegister to automatically add CI scores to all models
|
|
176
|
+
const reRegister = (models: ProviderModelConfig[], _s: StoredModels) => {
|
|
177
|
+
const enhanced = enhanceWithCI(models);
|
|
178
|
+
config.reRegister(enhanced, _s);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ── Single toggle command ──────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
pi.registerCommand(`${providerId}-toggle`, {
|
|
184
|
+
description: `Toggle between free and all ${providerId} models`,
|
|
185
|
+
handler: async (_args, ctx) => {
|
|
186
|
+
// Toggle the mode
|
|
187
|
+
currentShowPaid = !currentShowPaid;
|
|
188
|
+
|
|
189
|
+
// Persist to config file
|
|
190
|
+
const configKey = getShowPaidConfigKey(providerId);
|
|
191
|
+
saveConfig({ [configKey]: currentShowPaid });
|
|
192
|
+
|
|
193
|
+
// Re-register with appropriate model set
|
|
194
|
+
if (currentShowPaid) {
|
|
195
|
+
if (stored.all.length === 0) {
|
|
196
|
+
ctx.ui.notify("No models available", "warning");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
reRegister(stored.all, stored);
|
|
200
|
+
ctx.ui.notify(
|
|
201
|
+
`${providerId}: showing all ${stored.all.length} models (including paid)`,
|
|
202
|
+
"info",
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
if (stored.free.length === 0) {
|
|
206
|
+
ctx.ui.notify("No free models loaded", "warning");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
reRegister(stored.free, stored);
|
|
210
|
+
ctx.ui.notify(
|
|
211
|
+
`${providerId}: showing ${stored.free.length} free models`,
|
|
212
|
+
"info",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── Clear status when another provider is selected ───────────────────
|
|
219
|
+
|
|
220
|
+
pi.on("model_select", (_event, ctx) => {
|
|
221
|
+
if (_event.model?.provider !== providerId) {
|
|
222
|
+
ctx.ui.setStatus(`${providerId}-status`, undefined);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ── Track request count, reset failure count, handle errors ──────────
|
|
227
|
+
|
|
228
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
229
|
+
if (ctx.model?.provider !== providerId) return;
|
|
230
|
+
|
|
231
|
+
const msg = (
|
|
232
|
+
event as { message?: { role?: string; errorMessage?: string } }
|
|
233
|
+
).message;
|
|
234
|
+
|
|
235
|
+
// Check for errors in the assistant message
|
|
236
|
+
if (msg?.role === "assistant" && msg.errorMessage) {
|
|
237
|
+
const errorMsg = msg.errorMessage;
|
|
238
|
+
_logger.info("Error detected", {
|
|
239
|
+
provider: providerId,
|
|
240
|
+
error: errorMsg.slice(0, 100),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Use custom error handler if provided
|
|
244
|
+
if (config.onError) {
|
|
245
|
+
const handled = await config.onError(errorMsg, ctx);
|
|
246
|
+
if (handled) return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Use default failover handler
|
|
250
|
+
const result = await handleProviderError(
|
|
251
|
+
errorMsg,
|
|
252
|
+
{
|
|
253
|
+
provider: providerId,
|
|
254
|
+
isPaidMode: currentShowPaid,
|
|
255
|
+
},
|
|
256
|
+
pi,
|
|
257
|
+
ctx as {
|
|
258
|
+
ui: {
|
|
259
|
+
notify: (m: string, t: "info" | "warning" | "error") => void;
|
|
260
|
+
};
|
|
261
|
+
session?: { id?: string };
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Show notification based on result
|
|
266
|
+
if (result.action === "retry") {
|
|
267
|
+
ctx.ui.notify(result.message, "warning");
|
|
268
|
+
if (isProviderExhausted(providerId)) {
|
|
269
|
+
ctx.ui.setStatus(
|
|
270
|
+
`${providerId}-status`,
|
|
271
|
+
ctx.ui.theme.fg("dim", "⚠️ Rate limited - consider switching"),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
} else if (result.action === "fail") {
|
|
275
|
+
ctx.ui.notify(result.message, "error");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Don't reset failure count on error
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Success - reset failure count and increment metrics
|
|
283
|
+
incrementRequestCount(providerId);
|
|
284
|
+
|
|
285
|
+
// Track per-model usage if we have a model selected
|
|
286
|
+
const modelId = ctx.model?.id;
|
|
287
|
+
if (modelId) {
|
|
288
|
+
// Extract token usage from the event if available
|
|
289
|
+
const msg = (
|
|
290
|
+
event as {
|
|
291
|
+
message?: {
|
|
292
|
+
usage?: {
|
|
293
|
+
input?: number;
|
|
294
|
+
output?: number;
|
|
295
|
+
cacheRead?: number;
|
|
296
|
+
cacheWrite?: number;
|
|
297
|
+
cost?: { total?: number };
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
).message;
|
|
302
|
+
const tokensIn = msg?.usage?.input ?? 0;
|
|
303
|
+
const tokensOut = msg?.usage?.output ?? 0;
|
|
304
|
+
const cacheRead = msg?.usage?.cacheRead ?? 0;
|
|
305
|
+
const cacheWrite = msg?.usage?.cacheWrite ?? 0;
|
|
306
|
+
const cost = msg?.usage?.cost?.total ?? 0;
|
|
307
|
+
incrementModelRequestCount(
|
|
308
|
+
providerId,
|
|
309
|
+
modelId,
|
|
310
|
+
tokensIn,
|
|
311
|
+
tokensOut,
|
|
312
|
+
cacheRead,
|
|
313
|
+
cacheWrite,
|
|
314
|
+
cost,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
resetFailureCount(providerId);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── ToS notice on first use ────────────────────────────────
|
|
322
|
+
if (tosUrl) {
|
|
323
|
+
let tosShown = false;
|
|
324
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
325
|
+
if (tosShown || ctx.model?.provider !== providerId) return;
|
|
326
|
+
tosShown = true;
|
|
327
|
+
if (config.hasKey) return;
|
|
328
|
+
const cred = ctx.modelRegistry.authStorage.get(providerId);
|
|
329
|
+
if (cred?.type === "oauth") return;
|
|
330
|
+
ctx.ui.notify(
|
|
331
|
+
`Using ${providerId} free models. Set API key for paid access. Terms: ${tosUrl}`,
|
|
332
|
+
"info",
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|