pi-bansos 0.1.3 → 0.1.4
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/extensions/index.ts +107 -217
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -23,38 +23,59 @@ const PORT = Number(process.env.BANSOS_PORT) || 18080;
|
|
|
23
23
|
const HOST = "127.0.0.1";
|
|
24
24
|
const API = `${UPSTREAM}/v1`;
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const DEFAULT_CONTEXT = 128_000;
|
|
39
|
-
const DEFAULT_MAX_TOKENS = 16_384;
|
|
40
|
-
|
|
41
|
-
// Known free models that don't have -free suffix
|
|
42
|
-
const KNOWN_FREE = new Set(["big-pickle"]);
|
|
43
|
-
|
|
44
|
-
// Models to exclude (promos ended, known broken)
|
|
45
|
-
const EXCLUDE = new Set(["minimax-m3-free", "qwen3.6-plus-free"]);
|
|
46
|
-
|
|
47
|
-
// ── Security: Whitelists & Constants ───────────────────────────────
|
|
26
|
+
// ── Hardcoded Free Models ──────────────────────────────────────────
|
|
27
|
+
// Update this list manually when models change.
|
|
28
|
+
// On startup, each model is tested for availability.
|
|
29
|
+
interface ModelDef {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
reasoning: boolean;
|
|
33
|
+
contextWindow: number;
|
|
34
|
+
maxTokens: number;
|
|
35
|
+
}
|
|
48
36
|
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
const KNOWN_MODELS: ModelDef[] = [
|
|
38
|
+
{
|
|
39
|
+
id: "deepseek-v4-flash-free",
|
|
40
|
+
name: "DeepSeek V4 Flash",
|
|
41
|
+
reasoning: true,
|
|
42
|
+
contextWindow: 128_000,
|
|
43
|
+
maxTokens: 16_384,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "mimo-v2.5-free",
|
|
47
|
+
name: "Mimo V2.5",
|
|
48
|
+
reasoning: false,
|
|
49
|
+
contextWindow: 128_000,
|
|
50
|
+
maxTokens: 16_384,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "nemotron-3-ultra-free",
|
|
54
|
+
name: "Nemotron 3 Ultra",
|
|
55
|
+
reasoning: true,
|
|
56
|
+
contextWindow: 128_000,
|
|
57
|
+
maxTokens: 16_384,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "north-mini-code-free",
|
|
61
|
+
name: "North Mini Code",
|
|
62
|
+
reasoning: true,
|
|
63
|
+
contextWindow: 128_000,
|
|
64
|
+
maxTokens: 16_384,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "big-pickle",
|
|
68
|
+
name: "Big Pickle",
|
|
69
|
+
reasoning: true,
|
|
70
|
+
contextWindow: 128_000,
|
|
71
|
+
maxTokens: 16_384,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// ── Whitelists ─────────────────────────────────────────────────────
|
|
51
76
|
const ALLOWED_PATH_PATTERN = /^\/v1\/[a-zA-Z0-9/_.,\-?&=]*$/;
|
|
52
77
|
const PATH_TRAVERSAL_PATTERN = /\.\./;
|
|
53
|
-
|
|
54
|
-
/** Only allow safe HTTP methods (A03: prevents CONNECT tunneling) */
|
|
55
78
|
const ALLOWED_METHODS = new Set(["GET", "POST", "OPTIONS", "HEAD"]);
|
|
56
|
-
|
|
57
|
-
/** Headers that must never be forwarded upstream (A04: header injection) */
|
|
58
79
|
const STRIP_HEADERS = new Set([
|
|
59
80
|
"authorization",
|
|
60
81
|
"host",
|
|
@@ -70,33 +91,28 @@ const STRIP_HEADERS = new Set([
|
|
|
70
91
|
"proxy-authorization",
|
|
71
92
|
]);
|
|
72
93
|
|
|
73
|
-
// ──
|
|
74
|
-
|
|
94
|
+
// ── Logger ─────────────────────────────────────────────────────────
|
|
75
95
|
type LogLevel = "info" | "warn" | "error" | "audit";
|
|
76
96
|
|
|
77
97
|
function log(level: LogLevel, message: string, meta?: Record<string, unknown>) {
|
|
78
|
-
const
|
|
79
|
-
const prefix = `[bansos] [${timestamp}] [${level.toUpperCase()}]`;
|
|
98
|
+
const ts = new Date().toISOString();
|
|
80
99
|
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
100
|
+
const line = `[bansos] [${ts}] [${level.toUpperCase()}] ${message}${metaStr}`;
|
|
81
101
|
if (level === "error") {
|
|
82
|
-
console.error(
|
|
102
|
+
console.error(line);
|
|
83
103
|
} else {
|
|
84
|
-
console.log(
|
|
104
|
+
console.log(line);
|
|
85
105
|
}
|
|
86
106
|
}
|
|
87
107
|
|
|
88
|
-
// ──
|
|
89
|
-
|
|
108
|
+
// ── Rate Limiter ───────────────────────────────────────────────────
|
|
90
109
|
interface RateLimitEntry {
|
|
91
110
|
count: number;
|
|
92
111
|
resetAt: number;
|
|
93
112
|
}
|
|
94
|
-
|
|
95
113
|
const rateLimitMap = new Map<string, RateLimitEntry>();
|
|
96
|
-
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
// Cleanup stale entries every 5 minutes
|
|
114
|
+
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
115
|
+
const RATE_LIMIT_MAX = 120;
|
|
100
116
|
let cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
101
117
|
|
|
102
118
|
function startCleanupInterval() {
|
|
@@ -104,9 +120,7 @@ function startCleanupInterval() {
|
|
|
104
120
|
cleanupInterval = setInterval(() => {
|
|
105
121
|
const now = Date.now();
|
|
106
122
|
for (const [key, entry] of rateLimitMap) {
|
|
107
|
-
if (entry.resetAt <= now)
|
|
108
|
-
rateLimitMap.delete(key);
|
|
109
|
-
}
|
|
123
|
+
if (entry.resetAt <= now) rateLimitMap.delete(key);
|
|
110
124
|
}
|
|
111
125
|
}, 300_000);
|
|
112
126
|
}
|
|
@@ -114,166 +128,46 @@ function startCleanupInterval() {
|
|
|
114
128
|
function checkRateLimit(ip: string): boolean {
|
|
115
129
|
const now = Date.now();
|
|
116
130
|
const entry = rateLimitMap.get(ip);
|
|
117
|
-
|
|
118
131
|
if (!entry || entry.resetAt <= now) {
|
|
119
132
|
rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
120
133
|
return true;
|
|
121
134
|
}
|
|
122
|
-
|
|
123
|
-
if (entry.count >= RATE_LIMIT_MAX_REQUESTS) {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
135
|
+
if (entry.count >= RATE_LIMIT_MAX) return false;
|
|
127
136
|
entry.count++;
|
|
128
137
|
return true;
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
// ──
|
|
132
|
-
|
|
133
|
-
interface ModelCache {
|
|
134
|
-
models: ZenModel[];
|
|
135
|
-
fetchedAt: number;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
let modelCache: ModelCache | null = null;
|
|
139
|
-
|
|
140
|
-
interface ZenModel {
|
|
141
|
-
id: string;
|
|
142
|
-
name: string;
|
|
143
|
-
reasoning: boolean;
|
|
144
|
-
contextWindow: number;
|
|
145
|
-
maxTokens: number;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ── Fetch & verify free models ─────────────────────────────────────
|
|
149
|
-
async function fetchModels(): Promise<ZenModel[]> {
|
|
150
|
-
// Check cache first (A08: integrity - serve cached data within TTL)
|
|
151
|
-
if (modelCache && Date.now() - modelCache.fetchedAt < MODEL_CACHE_TTL_MS) {
|
|
152
|
-
return modelCache.models;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let res: Response;
|
|
140
|
+
// ── Health Check ───────────────────────────────────────────────────
|
|
141
|
+
async function checkModelAlive(id: string): Promise<boolean> {
|
|
156
142
|
try {
|
|
157
|
-
res = await fetch(`${API}/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
const res = await fetch(`${API}/chat/completions`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "content-type": "application/json" },
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
model: id,
|
|
148
|
+
messages: [{ role: "user", content: "hi" }],
|
|
149
|
+
max_tokens: 1,
|
|
150
|
+
stream: false,
|
|
151
|
+
}),
|
|
152
|
+
signal: AbortSignal.timeout(10_000),
|
|
161
153
|
});
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
throw err;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!res.ok) {
|
|
170
|
-
log("error", `models fetch failed`, { status: res.status });
|
|
171
|
-
if (modelCache) {
|
|
172
|
-
return modelCache.models;
|
|
173
|
-
}
|
|
174
|
-
throw new Error(`models fetch failed: ${res.status}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const data = (await res.json()) as { data: Array<{ id: string }> };
|
|
178
|
-
|
|
179
|
-
// Validate response structure (A08: data integrity)
|
|
180
|
-
if (!Array.isArray(data?.data)) {
|
|
181
|
-
log("error", "invalid response structure from upstream");
|
|
182
|
-
if (modelCache) return modelCache.models;
|
|
183
|
-
throw new Error("invalid models response");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const candidates = data.data
|
|
187
|
-
.map((m) => m.id)
|
|
188
|
-
.filter(
|
|
189
|
-
(id) =>
|
|
190
|
-
typeof id === "string" &&
|
|
191
|
-
id.length > 0 &&
|
|
192
|
-
id.length < 128 && // sanity check
|
|
193
|
-
!EXCLUDE.has(id) &&
|
|
194
|
-
(id.endsWith("-free") || KNOWN_FREE.has(id)),
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
const verified: ZenModel[] = [];
|
|
198
|
-
for (const id of candidates) {
|
|
199
|
-
try {
|
|
200
|
-
const ok = await testModel(id);
|
|
201
|
-
if (ok) {
|
|
202
|
-
verified.push({
|
|
203
|
-
id,
|
|
204
|
-
name: formatName(id),
|
|
205
|
-
reasoning: id.includes("nemotron") || id.includes("mimo"),
|
|
206
|
-
contextWindow: CONTEXT[id] ?? DEFAULT_CONTEXT,
|
|
207
|
-
maxTokens: DEFAULT_MAX_TOKENS,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
} catch {
|
|
211
|
-
// Model test error — skip silently
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Update cache (A08: integrity)
|
|
216
|
-
modelCache = { models: verified, fetchedAt: Date.now() };
|
|
217
|
-
|
|
218
|
-
return verified;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ── Test if model is free ──────────────────────────────────────────
|
|
222
|
-
async function testModel(id: string): Promise<boolean> {
|
|
223
|
-
// Validate model ID format (A03: injection prevention)
|
|
224
|
-
if (!/^[a-zA-Z0-9._-]+$/.test(id) || id.length > 128) {
|
|
225
|
-
log("warn", `invalid model ID format: ${id}`);
|
|
154
|
+
// Model is alive if API responds (even with error)
|
|
155
|
+
return res.ok || res.status === 400 || res.status === 429;
|
|
156
|
+
} catch {
|
|
226
157
|
return false;
|
|
227
158
|
}
|
|
228
|
-
|
|
229
|
-
const res = await fetch(`${API}/chat/completions`, {
|
|
230
|
-
method: "POST",
|
|
231
|
-
headers: { "content-type": "application/json" },
|
|
232
|
-
body: JSON.stringify({
|
|
233
|
-
model: id,
|
|
234
|
-
messages: [{ role: "user", content: "1" }],
|
|
235
|
-
max_tokens: 1,
|
|
236
|
-
stream: false,
|
|
237
|
-
}),
|
|
238
|
-
});
|
|
239
|
-
if (!res.ok) return false;
|
|
240
|
-
const data = (await res.json()) as { cost?: string };
|
|
241
|
-
return !data.cost || data.cost === "0" || data.cost === "0.0";
|
|
242
159
|
}
|
|
243
160
|
|
|
244
|
-
// ── Pretty name ────────────────────────────────────────────────────
|
|
245
|
-
function formatName(id: string): string {
|
|
246
|
-
return id
|
|
247
|
-
.replace(/-free$/, "")
|
|
248
|
-
.replace(/-/g, " ")
|
|
249
|
-
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ── Security: Extract client IP from socket ────────────────────────
|
|
253
161
|
function getClientIP(req: http.IncomingMessage): string {
|
|
254
162
|
const addr = req.socket.remoteAddress;
|
|
255
163
|
if (!addr) return "unknown";
|
|
256
|
-
|
|
257
|
-
if (addr.startsWith("::ffff:")) return addr.slice(7);
|
|
258
|
-
return addr;
|
|
164
|
+
return addr.startsWith("::ffff:") ? addr.slice(7) : addr;
|
|
259
165
|
}
|
|
260
166
|
|
|
261
|
-
// ── Security: Validate and sanitize path (A03/A10) ─────────────────
|
|
262
167
|
function validatePath(rawUrl: string): URL | null {
|
|
263
|
-
// Remove leading slashes and normalize
|
|
264
168
|
const cleaned = rawUrl.replace(/^\/+/, "");
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!ALLOWED_PATH_PATTERN.test(`/${cleaned}`)) {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Reject path traversal (..) — A03: prevents directory traversal
|
|
272
|
-
if (PATH_TRAVERSAL_PATTERN.test(cleaned)) {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Prevent double-encoding tricks
|
|
169
|
+
if (!ALLOWED_PATH_PATTERN.test(`/${cleaned}`)) return null;
|
|
170
|
+
if (PATH_TRAVERSAL_PATTERN.test(cleaned)) return null;
|
|
277
171
|
try {
|
|
278
172
|
const decoded = decodeURIComponent(cleaned);
|
|
279
173
|
if (decoded !== cleaned && !ALLOWED_PATH_PATTERN.test(`/${decoded}`)) {
|
|
@@ -282,7 +176,6 @@ function validatePath(rawUrl: string): URL | null {
|
|
|
282
176
|
} catch {
|
|
283
177
|
return null;
|
|
284
178
|
}
|
|
285
|
-
|
|
286
179
|
try {
|
|
287
180
|
return new URL(cleaned, `${UPSTREAM}/`);
|
|
288
181
|
} catch {
|
|
@@ -290,46 +183,36 @@ function validatePath(rawUrl: string): URL | null {
|
|
|
290
183
|
}
|
|
291
184
|
}
|
|
292
185
|
|
|
293
|
-
// ── Security: Sanitize headers for upstream (A04) ──────────────────
|
|
294
186
|
function sanitizeHeaders(
|
|
295
187
|
incoming: http.IncomingHttpHeaders,
|
|
296
188
|
targetHost: string,
|
|
297
189
|
): Record<string, string> {
|
|
298
190
|
const sanitized: Record<string, string> = {};
|
|
299
|
-
|
|
300
191
|
for (const [key, value] of Object.entries(incoming)) {
|
|
301
192
|
const lower = key.toLowerCase();
|
|
302
|
-
|
|
303
|
-
// Skip stripped headers
|
|
304
193
|
if (STRIP_HEADERS.has(lower)) continue;
|
|
305
|
-
|
|
306
|
-
// Skip pseudo-headers
|
|
307
194
|
if (lower.startsWith(":")) continue;
|
|
308
|
-
|
|
309
|
-
// Only forward safe headers
|
|
310
195
|
if (typeof value === "string") {
|
|
311
196
|
sanitized[lower] = value;
|
|
312
197
|
} else if (Array.isArray(value)) {
|
|
313
198
|
sanitized[lower] = value.join(", ");
|
|
314
199
|
}
|
|
315
200
|
}
|
|
316
|
-
|
|
317
|
-
// Set required headers explicitly
|
|
318
201
|
sanitized.host = targetHost;
|
|
319
202
|
sanitized["accept-encoding"] = "identity";
|
|
320
203
|
sanitized.connection = "close";
|
|
321
|
-
|
|
322
204
|
return sanitized;
|
|
323
205
|
}
|
|
324
206
|
|
|
325
207
|
// ── Start local proxy ──────────────────────────────────────────────
|
|
326
208
|
function startProxy(overridePort?: number): http.Server {
|
|
327
209
|
const effectivePort = overridePort ?? PORT;
|
|
210
|
+
|
|
328
211
|
const server = http.createServer((req, res) => {
|
|
329
212
|
const clientIP = getClientIP(req);
|
|
330
213
|
const startTime = Date.now();
|
|
331
214
|
|
|
332
|
-
//
|
|
215
|
+
// Rate limiting
|
|
333
216
|
if (!checkRateLimit(clientIP)) {
|
|
334
217
|
log("warn", "rate limit exceeded", { ip: clientIP });
|
|
335
218
|
res.writeHead(429, { "content-type": "application/json" });
|
|
@@ -337,7 +220,7 @@ function startProxy(overridePort?: number): http.Server {
|
|
|
337
220
|
return;
|
|
338
221
|
}
|
|
339
222
|
|
|
340
|
-
//
|
|
223
|
+
// Method whitelist
|
|
341
224
|
if (!ALLOWED_METHODS.has(req.method ?? "")) {
|
|
342
225
|
log("audit", "method not allowed", {
|
|
343
226
|
ip: clientIP,
|
|
@@ -349,7 +232,7 @@ function startProxy(overridePort?: number): http.Server {
|
|
|
349
232
|
return;
|
|
350
233
|
}
|
|
351
234
|
|
|
352
|
-
//
|
|
235
|
+
// CORS preflight
|
|
353
236
|
if (req.method === "OPTIONS") {
|
|
354
237
|
res.writeHead(204, {
|
|
355
238
|
"access-control-allow-origin": "http://localhost",
|
|
@@ -360,7 +243,7 @@ function startProxy(overridePort?: number): http.Server {
|
|
|
360
243
|
return;
|
|
361
244
|
}
|
|
362
245
|
|
|
363
|
-
//
|
|
246
|
+
// Validate path
|
|
364
247
|
const target = validatePath(req.url ?? "/");
|
|
365
248
|
if (!target) {
|
|
366
249
|
log("audit", "invalid path rejected", {
|
|
@@ -373,7 +256,7 @@ function startProxy(overridePort?: number): http.Server {
|
|
|
373
256
|
return;
|
|
374
257
|
}
|
|
375
258
|
|
|
376
|
-
//
|
|
259
|
+
// Sanitize headers
|
|
377
260
|
const fwd = sanitizeHeaders(req.headers, target.hostname);
|
|
378
261
|
|
|
379
262
|
const proxy = https.request(
|
|
@@ -386,8 +269,6 @@ function startProxy(overridePort?: number): http.Server {
|
|
|
386
269
|
},
|
|
387
270
|
(upstream) => {
|
|
388
271
|
const outHeaders: Record<string, string> = {};
|
|
389
|
-
|
|
390
|
-
// Forward safe response headers only
|
|
391
272
|
const safeResponseHeaders = [
|
|
392
273
|
"content-type",
|
|
393
274
|
"cache-control",
|
|
@@ -399,11 +280,8 @@ function startProxy(overridePort?: number): http.Server {
|
|
|
399
280
|
outHeaders[h] = val;
|
|
400
281
|
}
|
|
401
282
|
}
|
|
402
|
-
|
|
403
|
-
// Add security headers
|
|
404
283
|
outHeaders["x-content-type-options"] = "nosniff";
|
|
405
284
|
outHeaders["x-frame-options"] = "DENY";
|
|
406
|
-
|
|
407
285
|
res.writeHead(upstream.statusCode ?? 502, outHeaders);
|
|
408
286
|
upstream.pipe(res);
|
|
409
287
|
},
|
|
@@ -423,13 +301,12 @@ function startProxy(overridePort?: number): http.Server {
|
|
|
423
301
|
res.end(JSON.stringify({ error: "upstream error" }));
|
|
424
302
|
});
|
|
425
303
|
|
|
426
|
-
//
|
|
304
|
+
// Per-request timeout
|
|
427
305
|
proxy.setTimeout(30_000, () => {
|
|
428
306
|
proxy.destroy(new Error("request timeout"));
|
|
429
307
|
});
|
|
430
308
|
|
|
431
|
-
// Handle client abort
|
|
432
|
-
// NOT when the readable stream ends normally (fixes 502 on every request)
|
|
309
|
+
// Handle client abort
|
|
433
310
|
req.on("aborted", () => {
|
|
434
311
|
if (!proxy.destroyed) {
|
|
435
312
|
proxy.destroy();
|
|
@@ -450,12 +327,10 @@ function startProxy(overridePort?: number): http.Server {
|
|
|
450
327
|
log("error", "server error", { code: err.code, message: err.message });
|
|
451
328
|
});
|
|
452
329
|
|
|
453
|
-
// A09: Log server start
|
|
454
330
|
server.listen(effectivePort, HOST);
|
|
455
331
|
log("info", `proxy listening on http://${HOST}:${effectivePort}`);
|
|
456
332
|
|
|
457
333
|
startCleanupInterval();
|
|
458
|
-
|
|
459
334
|
return server;
|
|
460
335
|
}
|
|
461
336
|
|
|
@@ -465,25 +340,39 @@ export default async function (pi: ExtensionAPI) {
|
|
|
465
340
|
|
|
466
341
|
const server = startProxy();
|
|
467
342
|
|
|
468
|
-
log("info",
|
|
469
|
-
|
|
343
|
+
log("info", `checking ${KNOWN_MODELS.length} model(s) availability...`);
|
|
344
|
+
|
|
345
|
+
// Health check each model in parallel
|
|
346
|
+
const aliveChecks = await Promise.all(
|
|
347
|
+
KNOWN_MODELS.map(async (model) => {
|
|
348
|
+
const alive = await checkModelAlive(model.id);
|
|
349
|
+
if (alive) {
|
|
350
|
+
log("info", `✓ ${model.id} is alive`);
|
|
351
|
+
} else {
|
|
352
|
+
log("warn", `✗ ${model.id} is dead — skipping`);
|
|
353
|
+
}
|
|
354
|
+
return { ...model, alive };
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const aliveModels = aliveChecks.filter((m) => m.alive);
|
|
470
359
|
|
|
471
|
-
if (
|
|
472
|
-
log("warn", "no
|
|
360
|
+
if (aliveModels.length === 0) {
|
|
361
|
+
log("warn", "no alive models found — extension inactive");
|
|
473
362
|
return;
|
|
474
363
|
}
|
|
475
364
|
|
|
476
365
|
log(
|
|
477
366
|
"info",
|
|
478
|
-
`${
|
|
367
|
+
`${aliveModels.length} model(s) registered: ${aliveModels.map((m) => m.id).join(", ")}`,
|
|
479
368
|
);
|
|
480
369
|
|
|
481
370
|
pi.registerProvider("bansos", {
|
|
482
371
|
baseUrl: `http://${HOST}:${PORT}/v1`,
|
|
483
372
|
apiKey: "placeholder",
|
|
484
373
|
api: "openai-completions",
|
|
485
|
-
compat: { supportsDeveloperRole:
|
|
486
|
-
models:
|
|
374
|
+
compat: { supportsDeveloperRole: false, supportsReasoningEffort: true },
|
|
375
|
+
models: aliveModels.map((m) => ({
|
|
487
376
|
id: m.id,
|
|
488
377
|
name: m.name,
|
|
489
378
|
reasoning: m.reasoning,
|
|
@@ -491,6 +380,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
491
380
|
contextWindow: m.contextWindow,
|
|
492
381
|
maxTokens: m.maxTokens,
|
|
493
382
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
383
|
+
compat: { supportsDeveloperRole: false, supportsReasoningEffort: true },
|
|
494
384
|
})),
|
|
495
385
|
});
|
|
496
386
|
|