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