pi-free 2.1.1 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +32 -4
- package/banner.svg +1 -1
- package/config.ts +667 -629
- package/constants.ts +4 -0
- package/index.ts +407 -378
- package/lib/probe-cache.ts +8 -0
- package/lib/provider-probe.ts +15 -0
- package/package.json +5 -5
- package/providers/bai/bai.ts +232 -0
- package/providers/cline/cline-xml-bridge.ts +44 -15
- package/providers/dynamic-built-in/index.ts +12 -0
- package/providers/ollama/ollama.ts +13 -0
- package/providers/routeway/routeway.ts +10 -0
package/lib/probe-cache.ts
CHANGED
|
@@ -60,6 +60,14 @@ export function getModelsDueForProbe(
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export function areAllModelsFresh(
|
|
64
|
+
providerId: string,
|
|
65
|
+
modelIds: string[],
|
|
66
|
+
ttlMs = DEFAULT_PROBE_TTL_MS,
|
|
67
|
+
): boolean {
|
|
68
|
+
return getModelsDueForProbe(providerId, modelIds, ttlMs).length === 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
export async function recordModelProbeResults(
|
|
64
72
|
providerId: string,
|
|
65
73
|
results: ModelProbeResult[],
|
package/lib/provider-probe.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
|
24
24
|
import { updateConfig } from "../config.ts";
|
|
25
25
|
import { createLogger } from "./logger.ts";
|
|
26
26
|
import {
|
|
27
|
+
areAllModelsFresh,
|
|
27
28
|
getModelsDueForProbe,
|
|
28
29
|
recordModelProbeResults,
|
|
29
30
|
type ModelProbeResult,
|
|
@@ -175,6 +176,20 @@ export function createProviderProbe(
|
|
|
175
176
|
return () => {
|
|
176
177
|
if (done) return;
|
|
177
178
|
done = true;
|
|
179
|
+
|
|
180
|
+
// Skip scheduling entirely if every model was probed recently.
|
|
181
|
+
// Without this check the handler fires on every session_start and
|
|
182
|
+
// only then discovers the cache is fresh inside run().
|
|
183
|
+
if (
|
|
184
|
+
areAllModelsFresh(
|
|
185
|
+
providerId,
|
|
186
|
+
models.map((m) => m.id),
|
|
187
|
+
)
|
|
188
|
+
) {
|
|
189
|
+
_logger.info(`[probe] ${providerId}: auto-probe cache is fresh`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
_logger.info(`[probe] Starting lazy auto-probe for ${providerId}...`);
|
|
179
194
|
run(apiKey, models, { useCache: true }).catch((err) => {
|
|
180
195
|
_logger.warn(`[probe] ${providerId}: auto-probe failed`, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-free",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI model providers for Pi with free model filtering and dynamic model fetching",
|
|
6
6
|
"keywords": [
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
"smoke:cline": "tsx scripts/smoke-cline-xml-bridge.ts"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@earendil-works/pi-ai": "
|
|
59
|
-
"@earendil-works/pi-coding-agent": "
|
|
60
|
-
"@earendil-works/pi-tui": "
|
|
58
|
+
"@earendil-works/pi-ai": "^0.79.8",
|
|
59
|
+
"@earendil-works/pi-coding-agent": "^0.79.8",
|
|
60
|
+
"@earendil-works/pi-tui": "^0.79.8"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@vitest/ui": "^4.1.5",
|
|
64
64
|
"tsx": "^4.0.0",
|
|
65
|
-
"typescript": "^6.0.
|
|
65
|
+
"typescript": "^6.0.3",
|
|
66
66
|
"vitest": "^4.1.5"
|
|
67
67
|
},
|
|
68
68
|
"pi": {
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B.AI Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* B.AI (https://b.ai) is an OpenAI-compatible LLM gateway providing access
|
|
5
|
+
* to many models (OpenAI, Anthropic, Google, DeepSeek, Qwen, GLM, Kimi).
|
|
6
|
+
*
|
|
7
|
+
* API: https://api.b.ai/v1
|
|
8
|
+
* Models: /v1/models
|
|
9
|
+
* Chat: /v1/chat/completions
|
|
10
|
+
*
|
|
11
|
+
* Pricing is not exposed via the /v1/models endpoint, so all models
|
|
12
|
+
* default to cost=0. The `isFreeModel` Route B detection (name contains
|
|
13
|
+
* "free") is therefore used. As a result, with `free_only: true` no b.ai
|
|
14
|
+
* models will be visible until you run `/toggle-bai` to enable paid models.
|
|
15
|
+
*
|
|
16
|
+
* A small set of known-promotional models are hardcoded as known-free so
|
|
17
|
+
* they remain visible even when free-only mode is on (mirrors the
|
|
18
|
+
* TokenRouter approach for `MiniMax-M3`).
|
|
19
|
+
*
|
|
20
|
+
* Setup:
|
|
21
|
+
* BAI_API_KEY=sk-...
|
|
22
|
+
* # or add bai_api_key to ~/.pi/free.json
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
ExtensionAPI,
|
|
27
|
+
ProviderModelConfig,
|
|
28
|
+
} from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { getBaiApiKey, getBaiShowPaid, applyHidden } from "../../config.ts";
|
|
30
|
+
import {
|
|
31
|
+
BASE_URL_BAI,
|
|
32
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
33
|
+
PROVIDER_BAI,
|
|
34
|
+
} from "../../constants.ts";
|
|
35
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
36
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
37
|
+
import {
|
|
38
|
+
getProxyModelCompat,
|
|
39
|
+
isLikelyReasoningModel,
|
|
40
|
+
} from "../../lib/provider-compat.ts";
|
|
41
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
42
|
+
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
43
|
+
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
44
|
+
|
|
45
|
+
const _logger = createLogger("bai");
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Known Free Models
|
|
49
|
+
// B.AI doesn't expose pricing via /v1/models, so known-free models are
|
|
50
|
+
// hardcoded. The site currently advertises `MiniMax-M3` as a limited-time
|
|
51
|
+
// free promotional model; we hardcode that alias and any future `:free`
|
|
52
|
+
// suffixed IDs (catches dynamic promotional additions).
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
const BAI_KNOWN_FREE_MODELS = new Set(["minimax-m3", "MiniMax-M3"]);
|
|
56
|
+
|
|
57
|
+
function isBaiKnownFree(modelId: string): boolean {
|
|
58
|
+
if (BAI_KNOWN_FREE_MODELS.has(modelId)) return true;
|
|
59
|
+
// Catch any future `:free` suffixed model the gateway advertises
|
|
60
|
+
return modelId.toLowerCase().endsWith(":free");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Types
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
interface BaiModel {
|
|
68
|
+
id: string;
|
|
69
|
+
object?: string;
|
|
70
|
+
created?: number;
|
|
71
|
+
owned_by?: string;
|
|
72
|
+
supported_endpoint_types?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Helpers
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
/** Text-capable chat endpoints (excludes image/video/audio-only types) */
|
|
80
|
+
const CHAT_ENDPOINT_TYPES = new Set([
|
|
81
|
+
"openai",
|
|
82
|
+
"openai-response",
|
|
83
|
+
"anthropic",
|
|
84
|
+
"anthropic-compatible",
|
|
85
|
+
"gemini",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
function isTextChatModel(model: BaiModel): boolean {
|
|
89
|
+
const endpoints = model.supported_endpoint_types ?? [];
|
|
90
|
+
if (endpoints.length === 0) {
|
|
91
|
+
// No endpoint info — assume text chat (matches TokenRouter fallback)
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return endpoints.some((t) => CHAT_ENDPOINT_TYPES.has(t));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mapBaiModel(model: BaiModel): ProviderModelConfig & {
|
|
98
|
+
_pricingKnown?: boolean;
|
|
99
|
+
_freeKnown?: boolean;
|
|
100
|
+
_isFree?: boolean;
|
|
101
|
+
} {
|
|
102
|
+
const name = cleanModelName(model.id);
|
|
103
|
+
const reasoning = isLikelyReasoningModel({ id: model.id, name });
|
|
104
|
+
const isKnownFree = isBaiKnownFree(model.id);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
id: model.id,
|
|
108
|
+
name,
|
|
109
|
+
reasoning,
|
|
110
|
+
input: ["text"],
|
|
111
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
112
|
+
contextWindow: 128_000,
|
|
113
|
+
maxTokens: 16_384,
|
|
114
|
+
compat: getProxyModelCompat({ id: model.id, name }),
|
|
115
|
+
// Known-free models bypass name-based detection entirely
|
|
116
|
+
_freeKnown: isKnownFree,
|
|
117
|
+
_isFree: isKnownFree,
|
|
118
|
+
// Non-free models signal no pricing data (name-based detection only)
|
|
119
|
+
_pricingKnown: false,
|
|
120
|
+
} as ProviderModelConfig & {
|
|
121
|
+
_pricingKnown?: boolean;
|
|
122
|
+
_freeKnown?: boolean;
|
|
123
|
+
_isFree?: boolean;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// Fetch Models
|
|
129
|
+
// =============================================================================
|
|
130
|
+
|
|
131
|
+
async function fetchBaiModels(apiKey: string): Promise<ProviderModelConfig[]> {
|
|
132
|
+
_logger.info("[bai] Fetching models from B.AI API...");
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetchWithRetry(
|
|
136
|
+
`${BASE_URL_BAI}/models`,
|
|
137
|
+
{
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: `Bearer ${apiKey}`,
|
|
140
|
+
Accept: "application/json",
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
3,
|
|
145
|
+
1000,
|
|
146
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`B.AI API error: ${response.status}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const json = (await response.json()) as { data?: BaiModel[] };
|
|
154
|
+
const models = (json.data ?? []).filter(isTextChatModel);
|
|
155
|
+
|
|
156
|
+
_logger.info(`[bai] Fetched ${models.length} text chat models`);
|
|
157
|
+
const enriched = await safeEnrichModelsWithModelsDev(
|
|
158
|
+
models.map(mapBaiModel),
|
|
159
|
+
{ providerId: PROVIDER_BAI },
|
|
160
|
+
);
|
|
161
|
+
return applyHidden(enriched, PROVIDER_BAI);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
_logger.error("[bai] Failed to fetch models", {
|
|
164
|
+
error: error instanceof Error ? error.message : String(error),
|
|
165
|
+
});
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Extension Entry Point
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
export default async function baiProvider(pi: ExtensionAPI) {
|
|
175
|
+
const apiKey = getBaiApiKey();
|
|
176
|
+
|
|
177
|
+
if (!apiKey) {
|
|
178
|
+
_logger.info(
|
|
179
|
+
"[bai] Skipping — BAI_API_KEY not set. Sign up at https://b.ai/",
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const allModels = await fetchBaiModels(apiKey);
|
|
185
|
+
|
|
186
|
+
if (allModels.length === 0) {
|
|
187
|
+
_logger.warn("[bai] No text chat models available");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Use isFreeModel with allModels for proper detection
|
|
192
|
+
// B.AI doesn't expose pricing, so Route B (name-based) applies:
|
|
193
|
+
// FREE if name contains "free" OR _isFree is true (known-free hardcoded).
|
|
194
|
+
const freeModels = allModels.filter((m) =>
|
|
195
|
+
isFreeModel({ ...m, provider: PROVIDER_BAI }, allModels),
|
|
196
|
+
);
|
|
197
|
+
const stored = { free: freeModels, all: allModels };
|
|
198
|
+
|
|
199
|
+
_logger.info(
|
|
200
|
+
`[bai] Registered ${allModels.length} models (${freeModels.length} free)`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const reRegister = createReRegister(pi, {
|
|
204
|
+
providerId: PROVIDER_BAI,
|
|
205
|
+
baseUrl: BASE_URL_BAI,
|
|
206
|
+
apiKey,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
registerWithGlobalToggle(PROVIDER_BAI, stored, reRegister, true);
|
|
210
|
+
|
|
211
|
+
setupProvider(
|
|
212
|
+
pi,
|
|
213
|
+
{
|
|
214
|
+
providerId: PROVIDER_BAI,
|
|
215
|
+
initialShowPaid: getBaiShowPaid(),
|
|
216
|
+
tosUrl: "https://b.ai/",
|
|
217
|
+
reRegister: (models, _stored) => {
|
|
218
|
+
if (_stored) {
|
|
219
|
+
stored.free = _stored.free;
|
|
220
|
+
stored.all = _stored.all;
|
|
221
|
+
}
|
|
222
|
+
reRegister(models);
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
stored,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const showPaid = getBaiShowPaid();
|
|
229
|
+
const initialModels =
|
|
230
|
+
showPaid && stored.all.length > 0 ? stored.all : freeModels;
|
|
231
|
+
reRegister(initialModels);
|
|
232
|
+
}
|
|
@@ -315,10 +315,25 @@ function replaceInFileBridge(tool?: Tool): ToolBridge {
|
|
|
315
315
|
description:
|
|
316
316
|
tool?.description ?? "Edit a file using Cline SEARCH/REPLACE blocks",
|
|
317
317
|
parameters: ["path", "diff"],
|
|
318
|
-
toRuntimeArgs: (args) =>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
318
|
+
toRuntimeArgs: (args) => {
|
|
319
|
+
// Pi native <edit> form sends <edits>[{oldText,newText},...]</edits>
|
|
320
|
+
// as JSON. Cline <replace_in_file> form uses SEARCH/REPLACE <diff>.
|
|
321
|
+
if (Array.isArray(args.edits)) {
|
|
322
|
+
return {
|
|
323
|
+
path: stringArg(args, "path"),
|
|
324
|
+
edits: args.edits
|
|
325
|
+
.map((edit) => ({
|
|
326
|
+
oldText: stringArg(edit as Record<string, unknown>, "oldText"),
|
|
327
|
+
newText: stringArg(edit as Record<string, unknown>, "newText"),
|
|
328
|
+
}))
|
|
329
|
+
.filter((edit) => edit.oldText || edit.newText),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
path: stringArg(args, "path"),
|
|
334
|
+
edits: parseSearchReplaceBlocks(stringArg(args, "diff")),
|
|
335
|
+
};
|
|
336
|
+
},
|
|
322
337
|
fromRuntimeArgs: (args) => {
|
|
323
338
|
const edits = Array.isArray(args.edits)
|
|
324
339
|
? args.edits
|
|
@@ -949,10 +964,20 @@ function parseXmlToolCalls(
|
|
|
949
964
|
rawText: string,
|
|
950
965
|
tools: Tool[] | undefined,
|
|
951
966
|
): ParsedToolCalls {
|
|
967
|
+
const bridges = getParseToolBridges(tools);
|
|
952
968
|
const bridgeByRemoteName = new Map(
|
|
953
|
-
|
|
969
|
+
bridges.map((bridge) => [bridge.remoteName, bridge]),
|
|
970
|
+
);
|
|
971
|
+
// Some Cline/MiMo variants use the Pi runtime tool name (e.g. <edit>,
|
|
972
|
+
// <write>) instead of the Cline XML name (<replace_in_file>, <write_to_file>).
|
|
973
|
+
// Register runtime names as aliases so both forms are recognised.
|
|
974
|
+
const bridgeByName = new Map(
|
|
975
|
+
bridges.flatMap((bridge) => [
|
|
976
|
+
[bridge.remoteName, bridge],
|
|
977
|
+
[bridge.runtimeName, bridge],
|
|
978
|
+
]),
|
|
954
979
|
);
|
|
955
|
-
const toolNames = new Set(
|
|
980
|
+
const toolNames = new Set(bridgeByName.keys());
|
|
956
981
|
|
|
957
982
|
// Extract <function=name> Pi SDK tool calls directly (no Cline XML intermediate)
|
|
958
983
|
const fnResult = extractFunctionTagToolCalls(rawText, bridgeByRemoteName);
|
|
@@ -972,7 +997,9 @@ function parseXmlToolCalls(
|
|
|
972
997
|
while (cursor < sourceText.length) {
|
|
973
998
|
const next = findNextToolStart(sourceText, toolNames, cursor);
|
|
974
999
|
if (!next) break;
|
|
975
|
-
const
|
|
1000
|
+
const bridge = bridgeByName.get(next.name);
|
|
1001
|
+
const remoteName = bridge?.remoteName ?? next.name;
|
|
1002
|
+
const closeTag = `</${remoteName}>`;
|
|
976
1003
|
const closeStart = sourceText.indexOf(
|
|
977
1004
|
closeTag,
|
|
978
1005
|
next.index + next.openTag.length,
|
|
@@ -980,11 +1007,10 @@ function parseXmlToolCalls(
|
|
|
980
1007
|
pushTextFragment(textParts, sourceText.slice(cursor, next.index));
|
|
981
1008
|
const blockEnd = closeStart === -1 ? sourceText.length : closeStart;
|
|
982
1009
|
const block = sourceText.slice(next.index + next.openTag.length, blockEnd);
|
|
983
|
-
const bridge = bridgeByRemoteName.get(next.name);
|
|
984
1010
|
const remoteArgs = parseToolArguments(block);
|
|
985
1011
|
const writeRuntimeName = getWriteRuntimeToolName(tools);
|
|
986
1012
|
const heredocWrite =
|
|
987
|
-
|
|
1013
|
+
remoteName === "execute_command" && writeRuntimeName
|
|
988
1014
|
? parseCatHeredocWriteCommand(stringArg(remoteArgs, "command"))
|
|
989
1015
|
: undefined;
|
|
990
1016
|
if (heredocWrite && writeRuntimeName) {
|
|
@@ -1003,7 +1029,10 @@ function parseXmlToolCalls(
|
|
|
1003
1029
|
}
|
|
1004
1030
|
|
|
1005
1031
|
pushTextFragment(textParts, sourceText.slice(cursor));
|
|
1006
|
-
return {
|
|
1032
|
+
return {
|
|
1033
|
+
text: textParts.join("\n\n").trim(),
|
|
1034
|
+
toolCalls: [...fnResult.toolCalls, ...toolCalls],
|
|
1035
|
+
};
|
|
1007
1036
|
}
|
|
1008
1037
|
|
|
1009
1038
|
function parseReasoningHiddenToolCalls(
|
|
@@ -1393,7 +1422,10 @@ export function streamClineXml(
|
|
|
1393
1422
|
thinking,
|
|
1394
1423
|
currentContext.tools,
|
|
1395
1424
|
);
|
|
1396
|
-
const parsed = parseXmlToolCalls(
|
|
1425
|
+
const parsed = parseXmlToolCalls(
|
|
1426
|
+
extractedThinking.text,
|
|
1427
|
+
currentContext.tools,
|
|
1428
|
+
);
|
|
1397
1429
|
output = prepareClineXmlOutput(
|
|
1398
1430
|
parsed.text,
|
|
1399
1431
|
extractedThinking.thinking,
|
|
@@ -1404,10 +1436,7 @@ export function streamClineXml(
|
|
|
1404
1436
|
// Reasoning-only response: MiMo stopped without producing visible
|
|
1405
1437
|
// text or tool calls. Auto-retry once with a "continue" nudge
|
|
1406
1438
|
// instead of showing a dead-end error to the user.
|
|
1407
|
-
if (
|
|
1408
|
-
output.visibleText === INTERNAL_ONLY_RESPONSE &&
|
|
1409
|
-
attempt === 0
|
|
1410
|
-
) {
|
|
1439
|
+
if (output.visibleText === INTERNAL_ONLY_RESPONSE && attempt === 0) {
|
|
1411
1440
|
currentContext = {
|
|
1412
1441
|
...context,
|
|
1413
1442
|
messages: [
|
|
@@ -45,6 +45,7 @@ import { createLogger } from "../../lib/logger.ts";
|
|
|
45
45
|
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
46
46
|
import { getProxyModelCompat } from "../../lib/provider-compat.ts";
|
|
47
47
|
import {
|
|
48
|
+
areAllModelsFresh,
|
|
48
49
|
getModelsDueForProbe,
|
|
49
50
|
recordModelProbeResults,
|
|
50
51
|
} from "../../lib/probe-cache.ts";
|
|
@@ -605,6 +606,17 @@ async function registerProvider(
|
|
|
605
606
|
wrapSessionStartHandler(`${config.providerId}-auto-probe`, async () => {
|
|
606
607
|
if (_autoProbeDone) return;
|
|
607
608
|
_autoProbeDone = true;
|
|
609
|
+
if (
|
|
610
|
+
areAllModelsFresh(
|
|
611
|
+
config.providerId,
|
|
612
|
+
stored.free.map((m) => m.id),
|
|
613
|
+
)
|
|
614
|
+
) {
|
|
615
|
+
_logger.info(
|
|
616
|
+
`[probe] ${config.providerId}: auto-probe cache is fresh`,
|
|
617
|
+
);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
608
620
|
_logger.info(
|
|
609
621
|
`Starting lazy auto-probe of ${config.providerId} free models...`,
|
|
610
622
|
);
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from "../../lib/provider-cache.ts";
|
|
44
44
|
import { wrapSessionStartHandler } from "../../lib/session-start-metrics.ts";
|
|
45
45
|
import {
|
|
46
|
+
areAllModelsFresh,
|
|
46
47
|
getModelsDueForProbe,
|
|
47
48
|
recordModelProbeResults,
|
|
48
49
|
} from "../../lib/probe-cache.ts";
|
|
@@ -592,6 +593,18 @@ export default async function ollamaProvider(pi: ExtensionAPI) {
|
|
|
592
593
|
});
|
|
593
594
|
|
|
594
595
|
const runProbeInBackground = (models: ProviderModelConfig[]) => {
|
|
596
|
+
// Skip scheduling entirely if every model was probed recently.
|
|
597
|
+
// Without this check the probe runs on every session_start and
|
|
598
|
+
// only then discovers the cache is fresh inside runOllamaProbe.
|
|
599
|
+
if (
|
|
600
|
+
areAllModelsFresh(
|
|
601
|
+
PROVIDER_OLLAMA,
|
|
602
|
+
models.map((m) => m.id),
|
|
603
|
+
)
|
|
604
|
+
) {
|
|
605
|
+
_logger.info("Auto-probe: Ollama probe cache is fresh");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
595
608
|
runOllamaProbe(apiKey, models, applyModelList, { useCache: true }).catch(
|
|
596
609
|
(error) => {
|
|
597
610
|
_logger.warn("Auto-probe failed", {
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
isLikelyReasoningModel,
|
|
38
38
|
} from "../../lib/provider-compat.ts";
|
|
39
39
|
import {
|
|
40
|
+
areAllModelsFresh,
|
|
40
41
|
getModelsDueForProbe,
|
|
41
42
|
recordModelProbeResults,
|
|
42
43
|
} from "../../lib/probe-cache.ts";
|
|
@@ -350,6 +351,15 @@ export default async function routewayProvider(pi: ExtensionAPI) {
|
|
|
350
351
|
wrapSessionStartHandler("routeway", async () => {
|
|
351
352
|
if (_autoProbeDone || !apiKey) return;
|
|
352
353
|
_autoProbeDone = true;
|
|
354
|
+
if (
|
|
355
|
+
areAllModelsFresh(
|
|
356
|
+
PROVIDER_ROUTEWAY,
|
|
357
|
+
allModels.map((m) => m.id),
|
|
358
|
+
)
|
|
359
|
+
) {
|
|
360
|
+
_logger.info("Auto-probe: Routeway probe cache is fresh");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
353
363
|
_logger.info("Starting lazy auto-probe of Routeway models...");
|
|
354
364
|
runRoutewayProbe(apiKey, allModels, stored, reRegister, {
|
|
355
365
|
useCache: true,
|