koishi-plugin-elysia-api-aggregator 0.2.0 → 0.2.2

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/lib/index.cjs CHANGED
@@ -39,7 +39,6 @@ var ModelFetcher = class {
39
39
  __name(this, "ModelFetcher");
40
40
  }
41
41
  async fetchModels(source) {
42
- this.ctx.logger.info(`Fetching models from ${source.name} (${source.platform})`);
43
42
  try {
44
43
  switch (source.platform) {
45
44
  case "openai":
@@ -279,74 +278,113 @@ function apply(ctx, config) {
279
278
  getByType: /* @__PURE__ */ __name((type) => service.getByType(type), "getByType")
280
279
  }
281
280
  };
282
- async function loadModels() {
283
- if (config.debugMode) {
284
- ctx.logger.info("=== loadModels: Starting to load models ===");
285
- } else {
286
- ctx.logger.info("Loading models...");
287
- }
288
- const fetchedModels = [];
289
- for (const source of config.autoFetchSources) {
290
- if (!source.enabled) continue;
291
- if (config.debugMode) {
292
- ctx.logger.info(`loadModels: Fetching from ${source.name}`);
293
- }
294
- const sourceModels = await fetcher.fetchModels(source);
295
- fetchedModels.push(...sourceModels);
281
+ let isLoading = false;
282
+ let pendingReload = false;
283
+ let lastModelsHash = "";
284
+ let lastConfigHash = "";
285
+ const buildConfigHash = /* @__PURE__ */ __name(() => {
286
+ return JSON.stringify({
287
+ autoFetchSources: config.autoFetchSources,
288
+ manualModels: config.manualModels
289
+ });
290
+ }, "buildConfigHash");
291
+ const buildModelsHash = /* @__PURE__ */ __name((models) => {
292
+ const normalized = [...models].map((m) => ({
293
+ id: m.id,
294
+ name: m.name,
295
+ source: m.source,
296
+ sourceName: m.sourceName,
297
+ baseUrl: m.baseUrl,
298
+ platform: m.platform,
299
+ type: m.type,
300
+ maxTokens: m.maxTokens,
301
+ visionCapable: m.visionCapable,
302
+ toolsCapable: m.toolsCapable,
303
+ structuredOutput: m.structuredOutput,
304
+ thinkingMode: m.thinkingMode,
305
+ available: m.available
306
+ })).sort((a, b) => a.id.localeCompare(b.id));
307
+ return JSON.stringify(normalized);
308
+ }, "buildModelsHash");
309
+ async function loadModels(trigger = "config") {
310
+ if (isLoading) {
311
+ pendingReload = true;
296
312
  if (config.debugMode) {
297
- ctx.logger.info(`loadModels: Fetched ${sourceModels.length} models from ${source.name}`);
298
- } else {
299
- ctx.logger.info(`Fetched ${sourceModels.length} models from ${source.name}`);
313
+ ctx.logger.info(`loadModels skipped (already running), queued pending reload (trigger=${trigger})`);
300
314
  }
315
+ return;
301
316
  }
302
- if (config.debugMode) {
303
- ctx.logger.info(`loadModels: Processing ${config.manualModels.length} manual models`);
304
- }
305
- const manualModels = config.manualModels.map((m) => {
306
- if (config.debugMode) {
307
- ctx.logger.info(`loadModels: Adding manual model ${m.id}`);
317
+ isLoading = true;
318
+ const loadStartedAt = Date.now();
319
+ try {
320
+ ctx.logger.info("Loading models...");
321
+ const fetchedModels = [];
322
+ for (const source of config.autoFetchSources) {
323
+ if (!source.enabled) continue;
324
+ const sourceStartedAt = Date.now();
325
+ const sourceModels = await fetcher.fetchModels(source);
326
+ fetchedModels.push(...sourceModels);
327
+ const sourceCostMs = Date.now() - sourceStartedAt;
328
+ ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
329
+ }
330
+ const manualModels = config.manualModels.map((m) => {
331
+ return {
332
+ id: m.id,
333
+ name: m.name,
334
+ source: "manual",
335
+ sourceName: m.sourceName,
336
+ baseUrl: m.baseUrl,
337
+ apiKey: m.apiKey,
338
+ platform: m.platform,
339
+ // 使用默认值
340
+ type: "llm",
341
+ maxTokens: 128e3,
342
+ visionCapable: false,
343
+ toolsCapable: false,
344
+ structuredOutput: false,
345
+ thinkingMode: "both",
346
+ available: true,
347
+ lastChecked: /* @__PURE__ */ new Date()
348
+ };
349
+ });
350
+ const allModels = [...fetchedModels, ...manualModels];
351
+ service.updateModels(allModels);
352
+ const totalCostMs = Date.now() - loadStartedAt;
353
+ ctx.logger.info(`[source] manual: ${manualModels.length} models`);
354
+ const modelsHash = buildModelsHash(allModels);
355
+ if (modelsHash === lastModelsHash) {
356
+ ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms, unchanged)`);
357
+ return;
358
+ }
359
+ lastModelsHash = modelsHash;
360
+ ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms)`);
361
+ ctx.emit("elysia-api/models-updated", [...allModels]);
362
+ } finally {
363
+ isLoading = false;
364
+ if (pendingReload) {
365
+ pendingReload = false;
366
+ void loadModels("pending");
308
367
  }
309
- return {
310
- id: m.id,
311
- name: m.name,
312
- source: "manual",
313
- sourceName: m.sourceName,
314
- baseUrl: m.baseUrl,
315
- apiKey: m.apiKey,
316
- platform: m.platform,
317
- // 使用默认值
318
- type: "llm",
319
- maxTokens: 128e3,
320
- visionCapable: false,
321
- toolsCapable: false,
322
- structuredOutput: false,
323
- thinkingMode: "both",
324
- available: true,
325
- lastChecked: /* @__PURE__ */ new Date()
326
- };
327
- });
328
- const allModels = [...fetchedModels, ...manualModels];
329
- service.updateModels(allModels);
330
- if (config.debugMode) {
331
- ctx.logger.info(`loadModels: Total models loaded: ${allModels.length}`);
332
- ctx.logger.info(`loadModels: Model IDs: ${allModels.map((m) => m.id).join(", ")}`);
333
- ctx.logger.info(`loadModels: ctx.elysiaApi exists: ${ctx.elysiaApi != null}`);
334
- ctx.logger.info(`loadModels: ctx.elysiaApi.models exists: ${ctx.elysiaApi?.models != null}`);
335
- } else {
336
- ctx.logger.info(`Total models loaded: ${allModels.length}`);
337
- }
338
- if (config.debugMode) {
339
- ctx.logger.info(`loadModels: Emitting elysia-api/models-updated event with ${allModels.length} models`);
340
368
  }
341
- ctx.emit("elysia-api/models-updated", [...allModels]);
342
369
  }
343
370
  __name(loadModels, "loadModels");
344
- ctx.on("ready", loadModels);
371
+ lastConfigHash = buildConfigHash();
372
+ ctx.on("ready", () => {
373
+ void loadModels("ready");
374
+ });
345
375
  ctx.on("config", () => {
346
- loadModels();
376
+ const configHash = buildConfigHash();
377
+ if (configHash === lastConfigHash) {
378
+ if (config.debugMode) {
379
+ ctx.logger.info("aggregator: config event ignored (aggregator config unchanged)");
380
+ }
381
+ return;
382
+ }
383
+ lastConfigHash = configHash;
384
+ void loadModels("config");
347
385
  });
348
386
  ctx.command("elysia-api.models.reload", "重新加载模型列表").action(async () => {
349
- await loadModels();
387
+ await loadModels("command");
350
388
  const count = service.getAll().length;
351
389
  return `已加载 ${count} 个模型`;
352
390
  });
package/lib/index.mjs CHANGED
@@ -13,7 +13,6 @@ var ModelFetcher = class {
13
13
  __name(this, "ModelFetcher");
14
14
  }
15
15
  async fetchModels(source) {
16
- this.ctx.logger.info(`Fetching models from ${source.name} (${source.platform})`);
17
16
  try {
18
17
  switch (source.platform) {
19
18
  case "openai":
@@ -253,74 +252,113 @@ function apply(ctx, config) {
253
252
  getByType: /* @__PURE__ */ __name((type) => service.getByType(type), "getByType")
254
253
  }
255
254
  };
256
- async function loadModels() {
257
- if (config.debugMode) {
258
- ctx.logger.info("=== loadModels: Starting to load models ===");
259
- } else {
260
- ctx.logger.info("Loading models...");
261
- }
262
- const fetchedModels = [];
263
- for (const source of config.autoFetchSources) {
264
- if (!source.enabled) continue;
265
- if (config.debugMode) {
266
- ctx.logger.info(`loadModels: Fetching from ${source.name}`);
267
- }
268
- const sourceModels = await fetcher.fetchModels(source);
269
- fetchedModels.push(...sourceModels);
255
+ let isLoading = false;
256
+ let pendingReload = false;
257
+ let lastModelsHash = "";
258
+ let lastConfigHash = "";
259
+ const buildConfigHash = /* @__PURE__ */ __name(() => {
260
+ return JSON.stringify({
261
+ autoFetchSources: config.autoFetchSources,
262
+ manualModels: config.manualModels
263
+ });
264
+ }, "buildConfigHash");
265
+ const buildModelsHash = /* @__PURE__ */ __name((models) => {
266
+ const normalized = [...models].map((m) => ({
267
+ id: m.id,
268
+ name: m.name,
269
+ source: m.source,
270
+ sourceName: m.sourceName,
271
+ baseUrl: m.baseUrl,
272
+ platform: m.platform,
273
+ type: m.type,
274
+ maxTokens: m.maxTokens,
275
+ visionCapable: m.visionCapable,
276
+ toolsCapable: m.toolsCapable,
277
+ structuredOutput: m.structuredOutput,
278
+ thinkingMode: m.thinkingMode,
279
+ available: m.available
280
+ })).sort((a, b) => a.id.localeCompare(b.id));
281
+ return JSON.stringify(normalized);
282
+ }, "buildModelsHash");
283
+ async function loadModels(trigger = "config") {
284
+ if (isLoading) {
285
+ pendingReload = true;
270
286
  if (config.debugMode) {
271
- ctx.logger.info(`loadModels: Fetched ${sourceModels.length} models from ${source.name}`);
272
- } else {
273
- ctx.logger.info(`Fetched ${sourceModels.length} models from ${source.name}`);
287
+ ctx.logger.info(`loadModels skipped (already running), queued pending reload (trigger=${trigger})`);
274
288
  }
289
+ return;
275
290
  }
276
- if (config.debugMode) {
277
- ctx.logger.info(`loadModels: Processing ${config.manualModels.length} manual models`);
278
- }
279
- const manualModels = config.manualModels.map((m) => {
280
- if (config.debugMode) {
281
- ctx.logger.info(`loadModels: Adding manual model ${m.id}`);
291
+ isLoading = true;
292
+ const loadStartedAt = Date.now();
293
+ try {
294
+ ctx.logger.info("Loading models...");
295
+ const fetchedModels = [];
296
+ for (const source of config.autoFetchSources) {
297
+ if (!source.enabled) continue;
298
+ const sourceStartedAt = Date.now();
299
+ const sourceModels = await fetcher.fetchModels(source);
300
+ fetchedModels.push(...sourceModels);
301
+ const sourceCostMs = Date.now() - sourceStartedAt;
302
+ ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
303
+ }
304
+ const manualModels = config.manualModels.map((m) => {
305
+ return {
306
+ id: m.id,
307
+ name: m.name,
308
+ source: "manual",
309
+ sourceName: m.sourceName,
310
+ baseUrl: m.baseUrl,
311
+ apiKey: m.apiKey,
312
+ platform: m.platform,
313
+ // 使用默认值
314
+ type: "llm",
315
+ maxTokens: 128e3,
316
+ visionCapable: false,
317
+ toolsCapable: false,
318
+ structuredOutput: false,
319
+ thinkingMode: "both",
320
+ available: true,
321
+ lastChecked: /* @__PURE__ */ new Date()
322
+ };
323
+ });
324
+ const allModels = [...fetchedModels, ...manualModels];
325
+ service.updateModels(allModels);
326
+ const totalCostMs = Date.now() - loadStartedAt;
327
+ ctx.logger.info(`[source] manual: ${manualModels.length} models`);
328
+ const modelsHash = buildModelsHash(allModels);
329
+ if (modelsHash === lastModelsHash) {
330
+ ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms, unchanged)`);
331
+ return;
332
+ }
333
+ lastModelsHash = modelsHash;
334
+ ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms)`);
335
+ ctx.emit("elysia-api/models-updated", [...allModels]);
336
+ } finally {
337
+ isLoading = false;
338
+ if (pendingReload) {
339
+ pendingReload = false;
340
+ void loadModels("pending");
282
341
  }
283
- return {
284
- id: m.id,
285
- name: m.name,
286
- source: "manual",
287
- sourceName: m.sourceName,
288
- baseUrl: m.baseUrl,
289
- apiKey: m.apiKey,
290
- platform: m.platform,
291
- // 使用默认值
292
- type: "llm",
293
- maxTokens: 128e3,
294
- visionCapable: false,
295
- toolsCapable: false,
296
- structuredOutput: false,
297
- thinkingMode: "both",
298
- available: true,
299
- lastChecked: /* @__PURE__ */ new Date()
300
- };
301
- });
302
- const allModels = [...fetchedModels, ...manualModels];
303
- service.updateModels(allModels);
304
- if (config.debugMode) {
305
- ctx.logger.info(`loadModels: Total models loaded: ${allModels.length}`);
306
- ctx.logger.info(`loadModels: Model IDs: ${allModels.map((m) => m.id).join(", ")}`);
307
- ctx.logger.info(`loadModels: ctx.elysiaApi exists: ${ctx.elysiaApi != null}`);
308
- ctx.logger.info(`loadModels: ctx.elysiaApi.models exists: ${ctx.elysiaApi?.models != null}`);
309
- } else {
310
- ctx.logger.info(`Total models loaded: ${allModels.length}`);
311
- }
312
- if (config.debugMode) {
313
- ctx.logger.info(`loadModels: Emitting elysia-api/models-updated event with ${allModels.length} models`);
314
342
  }
315
- ctx.emit("elysia-api/models-updated", [...allModels]);
316
343
  }
317
344
  __name(loadModels, "loadModels");
318
- ctx.on("ready", loadModels);
345
+ lastConfigHash = buildConfigHash();
346
+ ctx.on("ready", () => {
347
+ void loadModels("ready");
348
+ });
319
349
  ctx.on("config", () => {
320
- loadModels();
350
+ const configHash = buildConfigHash();
351
+ if (configHash === lastConfigHash) {
352
+ if (config.debugMode) {
353
+ ctx.logger.info("aggregator: config event ignored (aggregator config unchanged)");
354
+ }
355
+ return;
356
+ }
357
+ lastConfigHash = configHash;
358
+ void loadModels("config");
321
359
  });
322
360
  ctx.command("elysia-api.models.reload", "重新加载模型列表").action(async () => {
323
- await loadModels();
361
+ await loadModels("command");
324
362
  const count = service.getAll().length;
325
363
  return `已加载 ${count} 个模型`;
326
364
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-elysia-api-aggregator",
3
3
  "description": "Inspired by New-API, the Elysia-API model aggregator plugin allows automatic fetching and manual configuration of available AI models, designed to work with the orchestrator plugin.",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "main": "lib/index.cjs",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",