koishi-plugin-elysia-api-aggregator 0.2.3 → 0.2.5

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/config.d.ts CHANGED
@@ -1,9 +1,29 @@
1
1
  import { Schema } from 'koishi';
2
- import { AutoFetchSource, ManualModel } from '@elysia-api/shared';
2
+ import { PlatformType } from '@elysia-api/shared';
3
+ export interface ManualSourceModel {
4
+ id: string;
5
+ name: string;
6
+ }
7
+ export type SourcePlatformType = PlatformType | 'openai-compatible';
8
+ export interface AggregatorSourceConfigBase {
9
+ name: string;
10
+ baseUrl: string;
11
+ apiKey: string;
12
+ platform: SourcePlatformType;
13
+ enabled: boolean;
14
+ }
15
+ export interface AutoFetchAggregatorSource extends AggregatorSourceConfigBase {
16
+ autoFetchModels: true;
17
+ }
18
+ export interface ManualAggregatorSource extends AggregatorSourceConfigBase {
19
+ autoFetchModels: false;
20
+ manualModels: ManualSourceModel[];
21
+ }
22
+ export type AggregatorSourceConfig = AutoFetchAggregatorSource | ManualAggregatorSource;
3
23
  export interface Config {
4
- autoFetchSources: AutoFetchSource[];
5
- manualModels: ManualModel[];
24
+ sources: AggregatorSourceConfig[];
6
25
  debugMode?: boolean;
26
+ verboseLog?: boolean;
7
27
  }
8
28
  export declare const Config: Schema<Config>;
9
29
  export declare const name = "elysia-api-aggregator";
package/lib/index.cjs CHANGED
@@ -56,23 +56,35 @@ var ModelFetcher = class {
56
56
  return [];
57
57
  }
58
58
  }
59
+ normalizeBaseUrl(baseUrl) {
60
+ return baseUrl.replace(/\/+$/, "");
61
+ }
62
+ buildUrl(baseUrl, path) {
63
+ const normalizedBase = this.normalizeBaseUrl(baseUrl);
64
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
65
+ return `${normalizedBase}${normalizedPath}`;
66
+ }
67
+ getModelPlatform(source) {
68
+ return source.platform === "openai-compatible" ? "openai" : source.platform;
69
+ }
59
70
  async fetchOpenAIModels(source) {
60
- const response = await fetch(`${source.baseUrl}/models`, {
61
- headers: { "Authorization": `Bearer ${source.apiKey}` }
71
+ const response = await fetch(this.buildUrl(source.baseUrl, "/models"), {
72
+ headers: { Authorization: `Bearer ${source.apiKey}` }
62
73
  });
63
74
  if (!response.ok) {
64
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
75
+ const raw = await response.text().catch(() => "");
76
+ throw new Error(`HTTP ${response.status}: ${response.statusText}${raw ? ` | ${raw}` : ""}`);
65
77
  }
66
78
  const data = await response.json();
67
- const models = data.data || [];
68
- return models.map((model) => ({
79
+ const models = Array.isArray(data?.data) ? data.data : [];
80
+ return models.filter((model) => typeof model?.id === "string" && model.id.length > 0).map((model) => ({
69
81
  id: `${source.name}:${model.id}`,
70
82
  name: model.id,
71
83
  source: "auto",
72
84
  sourceName: source.name,
73
85
  baseUrl: source.baseUrl,
74
86
  apiKey: source.apiKey,
75
- platform: "openai",
87
+ platform: this.getModelPlatform(source),
76
88
  type: this.inferModelType(model.id),
77
89
  maxTokens: this.inferMaxTokens(model.id),
78
90
  visionCapable: this.hasVisionCapability(model.id),
@@ -84,6 +96,48 @@ var ModelFetcher = class {
84
96
  }));
85
97
  }
86
98
  async fetchClaudeModels(source) {
99
+ try {
100
+ const response = await fetch(this.buildUrl(source.baseUrl, "/models"), {
101
+ headers: {
102
+ "x-api-key": source.apiKey,
103
+ "anthropic-version": "2023-06-01",
104
+ "Content-Type": "application/json"
105
+ }
106
+ });
107
+ if (!response.ok) {
108
+ const raw = await response.text().catch(() => "");
109
+ throw new Error(`HTTP ${response.status}: ${response.statusText}${raw ? ` | ${raw}` : ""}`);
110
+ }
111
+ const data = await response.json();
112
+ const models = Array.isArray(data?.data) ? data.data : [];
113
+ if (!models.length) {
114
+ throw new Error("Claude models API returned empty list");
115
+ }
116
+ return models.filter((model) => typeof model?.id === "string" && model.id.length > 0).map((model) => ({
117
+ id: `${source.name}:${model.id}`,
118
+ name: model.display_name?.trim() || model.id,
119
+ source: "auto",
120
+ sourceName: source.name,
121
+ baseUrl: source.baseUrl,
122
+ apiKey: source.apiKey,
123
+ platform: "claude",
124
+ type: "llm",
125
+ maxTokens: this.inferClaudeMaxTokens(model.id),
126
+ visionCapable: true,
127
+ toolsCapable: true,
128
+ structuredOutput: true,
129
+ thinkingMode: "both",
130
+ available: true,
131
+ lastChecked: /* @__PURE__ */ new Date()
132
+ }));
133
+ } catch (error) {
134
+ this.ctx.logger.warn(
135
+ `[Claude Fetch Fallback] Failed to fetch models from source "${source.name}" (${source.baseUrl}), fallback to built-in Claude model list: ${error}`
136
+ );
137
+ return this.getBuiltInClaudeModels(source);
138
+ }
139
+ }
140
+ getBuiltInClaudeModels(source) {
87
141
  const knownModels = [
88
142
  { id: "claude-3-7-sonnet-20250219", maxTokens: 2e5 },
89
143
  { id: "claude-3-5-sonnet-20241022", maxTokens: 2e5 },
@@ -110,30 +164,35 @@ var ModelFetcher = class {
110
164
  }
111
165
  async fetchGeminiModels(source) {
112
166
  const response = await fetch(
113
- `${source.baseUrl}/v1beta/models?key=${source.apiKey}`
167
+ this.buildUrl(source.baseUrl, `/v1beta/models?key=${encodeURIComponent(source.apiKey)}`)
114
168
  );
115
169
  if (!response.ok) {
116
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
170
+ const raw = await response.text().catch(() => "");
171
+ throw new Error(`HTTP ${response.status}: ${response.statusText}${raw ? ` | ${raw}` : ""}`);
117
172
  }
118
173
  const data = await response.json();
119
- const models = data.models || [];
120
- return models.filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((model) => ({
121
- id: `${source.name}:${model.name}`,
122
- name: model.name,
123
- source: "auto",
124
- sourceName: source.name,
125
- baseUrl: source.baseUrl,
126
- apiKey: source.apiKey,
127
- platform: "gemini",
128
- type: "llm",
129
- maxTokens: this.parseGeminiMaxTokens(model),
130
- visionCapable: true,
131
- toolsCapable: true,
132
- structuredOutput: false,
133
- thinkingMode: "both",
134
- available: true,
135
- lastChecked: /* @__PURE__ */ new Date()
136
- }));
174
+ const models = Array.isArray(data?.models) ? data.models : [];
175
+ return models.filter((m) => m?.supportedGenerationMethods?.includes("generateContent")).map((model) => {
176
+ const rawName = typeof model.name === "string" ? model.name : "";
177
+ const displayName = rawName.replace(/^models\//, "") || rawName;
178
+ return {
179
+ id: `${source.name}:${rawName}`,
180
+ name: displayName,
181
+ source: "auto",
182
+ sourceName: source.name,
183
+ baseUrl: source.baseUrl,
184
+ apiKey: source.apiKey,
185
+ platform: "gemini",
186
+ type: "llm",
187
+ maxTokens: this.parseGeminiMaxTokens(model),
188
+ visionCapable: true,
189
+ toolsCapable: true,
190
+ structuredOutput: false,
191
+ thinkingMode: "both",
192
+ available: true,
193
+ lastChecked: /* @__PURE__ */ new Date()
194
+ };
195
+ });
137
196
  }
138
197
  inferModelType(modelId) {
139
198
  const id = modelId.toLowerCase();
@@ -163,6 +222,13 @@ var ModelFetcher = class {
163
222
  }
164
223
  return 128e3;
165
224
  }
225
+ inferClaudeMaxTokens(modelId) {
226
+ const id = modelId.toLowerCase();
227
+ if (id.includes("claude-3") || id.includes("claude-sonnet") || id.includes("claude-opus") || id.includes("claude-haiku")) {
228
+ return 2e5;
229
+ }
230
+ return 2e5;
231
+ }
166
232
  hasVisionCapability(modelId) {
167
233
  const id = modelId.toLowerCase();
168
234
  return id.includes("vision") || id.includes("gpt-4o") || id.includes("gpt-4-turbo");
@@ -176,52 +242,47 @@ var ModelFetcher = class {
176
242
  return id.includes("gpt-4o") || id.includes("gpt-4-turbo");
177
243
  }
178
244
  parseGeminiMaxTokens(model) {
179
- return model.topK?.outputTokenLimit || 128e3;
245
+ return Number(model?.outputTokenLimit) || Number(model?.inputTokenLimit) || 128e3;
180
246
  }
181
247
  };
182
248
 
183
249
  // src/config.ts
184
250
  var import_koishi = require("koishi");
251
+ var manualSourceModelSchema = import_koishi.Schema.object({
252
+ id: import_koishi.Schema.string().required().description("模型 ID"),
253
+ name: import_koishi.Schema.string().required().description("模型名称")
254
+ });
255
+ var sourceBaseSchema = import_koishi.Schema.object({
256
+ name: import_koishi.Schema.string().required().description("源名称"),
257
+ baseUrl: import_koishi.Schema.string().required().description("API 端点"),
258
+ apiKey: import_koishi.Schema.string().required().role("secret").description("API Key"),
259
+ platform: import_koishi.Schema.union([
260
+ import_koishi.Schema.const("openai").description("OpenAI"),
261
+ import_koishi.Schema.const("claude").description("Claude"),
262
+ import_koishi.Schema.const("gemini").description("Gemini"),
263
+ import_koishi.Schema.const("openai-compatible").description("OpenAI 兼容")
264
+ ]).description("平台类型"),
265
+ enabled: import_koishi.Schema.boolean().default(true).description("启用"),
266
+ autoFetchModels: import_koishi.Schema.boolean().default(true).description("是否自动拉取模型")
267
+ });
268
+ var sourceSchema = import_koishi.Schema.intersect([
269
+ sourceBaseSchema,
270
+ import_koishi.Schema.union([
271
+ import_koishi.Schema.object({}),
272
+ import_koishi.Schema.object({
273
+ autoFetchModels: import_koishi.Schema.const(false).required(),
274
+ manualModels: import_koishi.Schema.array(manualSourceModelSchema).default([]).role("table").description("手动添加模型")
275
+ })
276
+ ])
277
+ ]);
185
278
  var Config = import_koishi.Schema.intersect([
186
- // Auto-fetch sources configuration
187
- import_koishi.Schema.object({
188
- autoFetchSources: import_koishi.Schema.array(
189
- import_koishi.Schema.intersect([
190
- import_koishi.Schema.object({
191
- name: import_koishi.Schema.string().required().description("源名称"),
192
- baseUrl: import_koishi.Schema.string().required().description("API 端点"),
193
- apiKey: import_koishi.Schema.string().required().role("secret").description("API Key"),
194
- platform: import_koishi.Schema.union([
195
- import_koishi.Schema.const("openai").description("OpenAI"),
196
- import_koishi.Schema.const("claude").description("Claude"),
197
- import_koishi.Schema.const("gemini").description("Gemini"),
198
- import_koishi.Schema.const("openai-compatible").description("OpenAI 兼容")
199
- ]).description("平台类型"),
200
- enabled: import_koishi.Schema.boolean().default(true).description("启用")
201
- })
202
- ])
203
- ).role("table").description("自动拉取源")
204
- }),
205
- // Manual models
206
279
  import_koishi.Schema.object({
207
- manualModels: import_koishi.Schema.array(
208
- import_koishi.Schema.object({
209
- id: import_koishi.Schema.string().required().description("模型 ID"),
210
- name: import_koishi.Schema.string().required().description("模型名称"),
211
- sourceName: import_koishi.Schema.string().required().description("源名称"),
212
- baseUrl: import_koishi.Schema.string().required().description("API 端点"),
213
- apiKey: import_koishi.Schema.string().required().role("secret").description("API Key"),
214
- platform: import_koishi.Schema.union([
215
- import_koishi.Schema.const("openai").description("OpenAI"),
216
- import_koishi.Schema.const("claude").description("Claude"),
217
- import_koishi.Schema.const("gemini").description("Gemini")
218
- ]).description("平台类型")
219
- })
220
- ).role("table").description("手动添加的模型")
221
- }),
280
+ sources: import_koishi.Schema.array(sourceSchema).role("list").default([]).description("添加源")
281
+ }).description("模型源配置"),
222
282
  // Debug options
223
283
  import_koishi.Schema.object({
224
- debugMode: import_koishi.Schema.boolean().default(false).description("启用调试日志")
284
+ debugMode: import_koishi.Schema.boolean().default(false).description("启用调试日志"),
285
+ verboseLog: import_koishi.Schema.boolean().default(false).description("启用详细日志(输出配置摘要、hash 与重载判断过程)")
225
286
  }).description("调试选项")
226
287
  ]);
227
288
  var name = "elysia-api-aggregator";
@@ -235,10 +296,16 @@ var usage = `---
235
296
 
236
297
  ### 配置步骤
237
298
 
238
- 1. **自动获取**: 在「自动拉取源」中添加 API 端点和 API Key
239
- 2. **手动添加**: 在「手动添加的模型」中直接配置模型
299
+ 1. **添加源**: 在「添加源」中配置 API 端点、API Key 与平台
300
+ 2. **自动/手动模式**: 保持「是否自动拉取模型」为是,或切换为否后手动填写模型
240
301
  3. **重新加载**: 配置完成后使用 \`elysia-api.models.reload\` 命令生效
241
302
 
303
+ ### 自动拉取说明
304
+
305
+ - OpenAI / OpenAI-compatible:通过模型列表接口自动拉取
306
+ - Gemini:通过 Google Gemini 模型列表接口自动拉取
307
+ - Claude:优先尝试远程拉取,失败时回退到内置模型列表,并打印警告日志
308
+
242
309
  ---
243
310
 
244
311
  `;
@@ -282,11 +349,33 @@ function apply(ctx, config) {
282
349
  let pendingReload = false;
283
350
  let lastModelsHash = "";
284
351
  let lastConfigHash = "";
352
+ const summarizeSources = /* @__PURE__ */ __name((sources) => {
353
+ return sources.map((source) => ({
354
+ name: source.name,
355
+ baseUrl: source.baseUrl,
356
+ platform: source.platform,
357
+ enabled: source.enabled,
358
+ autoFetchModels: source.autoFetchModels,
359
+ manualModelsCount: source.autoFetchModels ? 0 : source.manualModels.length,
360
+ hasApiKey: Boolean(source.apiKey)
361
+ }));
362
+ }, "summarizeSources");
363
+ const verboseLog = /* @__PURE__ */ __name((message, payload) => {
364
+ if (!config.verboseLog) return;
365
+ if (payload === void 0) {
366
+ ctx.logger.info(`[aggregator verbose] ${message}`);
367
+ return;
368
+ }
369
+ ctx.logger.info(`[aggregator verbose] ${message}: ${JSON.stringify(payload)}`);
370
+ }, "verboseLog");
285
371
  const buildConfigHash = /* @__PURE__ */ __name(() => {
286
- return JSON.stringify({
287
- autoFetchSources: config.autoFetchSources,
288
- manualModels: config.manualModels
289
- });
372
+ const normalized = {
373
+ sources: summarizeSources(config.sources)
374
+ };
375
+ const hash = JSON.stringify(normalized);
376
+ verboseLog("buildConfigHash summary", normalized);
377
+ verboseLog("buildConfigHash value", hash);
378
+ return hash;
290
379
  }, "buildConfigHash");
291
380
  const buildModelsHash = /* @__PURE__ */ __name((models) => {
292
381
  const normalized = [...models].map((m) => ({
@@ -306,81 +395,168 @@ function apply(ctx, config) {
306
395
  })).sort((a, b) => a.id.localeCompare(b.id));
307
396
  return JSON.stringify(normalized);
308
397
  }, "buildModelsHash");
398
+ const mapManualSourceModels = /* @__PURE__ */ __name((source) => {
399
+ return source.manualModels.map((m) => ({
400
+ id: `${source.name}:${m.id}`,
401
+ name: m.name,
402
+ source: "manual",
403
+ sourceName: source.name,
404
+ baseUrl: source.baseUrl,
405
+ apiKey: source.apiKey,
406
+ platform: source.platform === "openai-compatible" ? "openai" : source.platform,
407
+ // 使用默认值
408
+ type: "llm",
409
+ maxTokens: 128e3,
410
+ visionCapable: false,
411
+ toolsCapable: false,
412
+ structuredOutput: false,
413
+ thinkingMode: "both",
414
+ available: true,
415
+ lastChecked: /* @__PURE__ */ new Date()
416
+ }));
417
+ }, "mapManualSourceModels");
309
418
  async function loadModels(trigger = "config") {
419
+ verboseLog("loadModels entered", {
420
+ trigger,
421
+ isLoading,
422
+ pendingReload,
423
+ sourceCount: config.sources.length,
424
+ sources: summarizeSources(config.sources)
425
+ });
310
426
  if (isLoading) {
311
427
  pendingReload = true;
312
428
  if (config.debugMode) {
313
429
  ctx.logger.info(`loadModels skipped (already running), queued pending reload (trigger=${trigger})`);
314
430
  }
431
+ verboseLog("loadModels skipped because already loading", {
432
+ trigger,
433
+ pendingReload
434
+ });
315
435
  return;
316
436
  }
317
437
  isLoading = true;
318
438
  const loadStartedAt = Date.now();
319
439
  try {
320
440
  ctx.logger.info("Loading models...");
321
- const fetchedModels = [];
322
- for (const source of config.autoFetchSources) {
323
- if (!source.enabled) continue;
441
+ verboseLog("loadModels start state", {
442
+ trigger,
443
+ sourceCount: config.sources.length,
444
+ sources: summarizeSources(config.sources)
445
+ });
446
+ const allModels = [];
447
+ for (const source of config.sources) {
448
+ if (!source.enabled) {
449
+ verboseLog("skip disabled source", {
450
+ name: source.name,
451
+ autoFetchModels: source.autoFetchModels
452
+ });
453
+ continue;
454
+ }
324
455
  const sourceStartedAt = Date.now();
325
- const sourceModels = await fetcher.fetchModels(source);
326
- fetchedModels.push(...sourceModels);
456
+ let sourceModels = [];
457
+ verboseLog("processing source", {
458
+ name: source.name,
459
+ baseUrl: source.baseUrl,
460
+ platform: source.platform,
461
+ enabled: source.enabled,
462
+ autoFetchModels: source.autoFetchModels,
463
+ manualModelsCount: source.autoFetchModels ? 0 : source.manualModels.length,
464
+ hasApiKey: Boolean(source.apiKey)
465
+ });
466
+ if (source.autoFetchModels) {
467
+ sourceModels = await fetcher.fetchModels(source);
468
+ } else {
469
+ sourceModels = mapManualSourceModels(source);
470
+ }
471
+ allModels.push(...sourceModels);
327
472
  const sourceCostMs = Date.now() - sourceStartedAt;
328
- ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
473
+ ctx.logger.info(
474
+ `[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms${source.autoFetchModels ? ", auto" : ", manual"})`
475
+ );
476
+ verboseLog("source processed", {
477
+ name: source.name,
478
+ trigger,
479
+ sourceModelsCount: sourceModels.length,
480
+ sourceCostMs
481
+ });
329
482
  }
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
483
  service.updateModels(allModels);
352
484
  const totalCostMs = Date.now() - loadStartedAt;
353
- ctx.logger.info(`[source] manual: ${manualModels.length} models`);
354
485
  const modelsHash = buildModelsHash(allModels);
486
+ verboseLog("buildModelsHash value", {
487
+ modelsHash,
488
+ lastModelsHash,
489
+ allModelsCount: allModels.length
490
+ });
355
491
  if (modelsHash === lastModelsHash) {
356
492
  ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms, unchanged)`);
493
+ verboseLog("models unchanged, skip emit", {
494
+ trigger,
495
+ allModelsCount: allModels.length,
496
+ totalCostMs
497
+ });
357
498
  return;
358
499
  }
359
500
  lastModelsHash = modelsHash;
360
501
  ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms)`);
502
+ verboseLog("models updated", {
503
+ trigger,
504
+ allModelsCount: allModels.length,
505
+ totalCostMs,
506
+ lastModelsHash
507
+ });
361
508
  ctx.emit("elysia-api/models-updated", [...allModels]);
362
509
  } finally {
363
510
  isLoading = false;
511
+ verboseLog("loadModels finally", {
512
+ trigger,
513
+ pendingReload,
514
+ isLoading
515
+ });
364
516
  if (pendingReload) {
365
517
  pendingReload = false;
518
+ verboseLog("pending reload consumed", {
519
+ nextTrigger: "pending"
520
+ });
366
521
  void loadModels("pending");
367
522
  }
368
523
  }
369
524
  }
370
525
  __name(loadModels, "loadModels");
371
526
  lastConfigHash = buildConfigHash();
527
+ verboseLog("initial lastConfigHash", lastConfigHash);
372
528
  ctx.on("ready", () => {
529
+ verboseLog("ready event fired", {
530
+ sourceCount: config.sources.length,
531
+ sources: summarizeSources(config.sources)
532
+ });
373
533
  void loadModels("ready");
374
534
  });
375
535
  ctx.on("config", () => {
536
+ verboseLog("config event fired", {
537
+ sourceCount: config.sources.length,
538
+ sources: summarizeSources(config.sources),
539
+ lastConfigHash
540
+ });
376
541
  const configHash = buildConfigHash();
542
+ verboseLog("config event hash compare", {
543
+ configHash,
544
+ lastConfigHash,
545
+ equal: configHash === lastConfigHash
546
+ });
377
547
  if (configHash === lastConfigHash) {
378
548
  if (config.debugMode) {
379
549
  ctx.logger.info("aggregator: config event ignored (aggregator config unchanged)");
380
550
  }
551
+ verboseLog("config event ignored", {
552
+ reason: "unchanged"
553
+ });
381
554
  return;
382
555
  }
383
556
  lastConfigHash = configHash;
557
+ verboseLog("config event accepted", {
558
+ newLastConfigHash: lastConfigHash
559
+ });
384
560
  void loadModels("config");
385
561
  });
386
562
  ctx.command("elysia-api.models.reload", "重新加载模型列表").action(async () => {
package/lib/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Context, Service } from 'koishi';
2
2
  import { Model, ModelType } from '@elysia-api/shared';
3
3
  import { Config, name } from './config';
4
4
  export { Config, name };
5
- export declare const usage = "---\n\n## \u4F7F\u7528\u8BF4\u660E\n\n\u672C\u63D2\u4EF6\u7528\u4E8E\u81EA\u52A8\u83B7\u53D6\u548C\u7BA1\u7406\u53EF\u7528\u7684 AI \u6A21\u578B\uFF0C\u652F\u6301 OpenAI\u3001Claude\u3001Gemini \u7B49\u5E73\u53F0\u3002\n\n### \u914D\u7F6E\u6B65\u9AA4\n\n1. **\u81EA\u52A8\u83B7\u53D6**: \u5728\u300C\u81EA\u52A8\u62C9\u53D6\u6E90\u300D\u4E2D\u6DFB\u52A0 API \u7AEF\u70B9\u548C API Key\n2. **\u624B\u52A8\u6DFB\u52A0**: \u5728\u300C\u624B\u52A8\u6DFB\u52A0\u7684\u6A21\u578B\u300D\u4E2D\u76F4\u63A5\u914D\u7F6E\u6A21\u578B\n3. **\u91CD\u65B0\u52A0\u8F7D**: \u914D\u7F6E\u5B8C\u6210\u540E\u4F7F\u7528 `elysia-api.models.reload` \u547D\u4EE4\u751F\u6548\n\n---\n\n";
5
+ export declare const usage = "---\n\n## \u4F7F\u7528\u8BF4\u660E\n\n\u672C\u63D2\u4EF6\u7528\u4E8E\u81EA\u52A8\u83B7\u53D6\u548C\u7BA1\u7406\u53EF\u7528\u7684 AI \u6A21\u578B\uFF0C\u652F\u6301 OpenAI\u3001Claude\u3001Gemini \u7B49\u5E73\u53F0\u3002\n\n### \u914D\u7F6E\u6B65\u9AA4\n\n1. **\u6DFB\u52A0\u6E90**: \u5728\u300C\u6DFB\u52A0\u6E90\u300D\u4E2D\u914D\u7F6E API \u7AEF\u70B9\u3001API Key \u4E0E\u5E73\u53F0\n2. **\u81EA\u52A8/\u624B\u52A8\u6A21\u5F0F**: \u4FDD\u6301\u300C\u662F\u5426\u81EA\u52A8\u62C9\u53D6\u6A21\u578B\u300D\u4E3A\u662F\uFF0C\u6216\u5207\u6362\u4E3A\u5426\u540E\u624B\u52A8\u586B\u5199\u6A21\u578B\n3. **\u91CD\u65B0\u52A0\u8F7D**: \u914D\u7F6E\u5B8C\u6210\u540E\u4F7F\u7528 `elysia-api.models.reload` \u547D\u4EE4\u751F\u6548\n\n### \u81EA\u52A8\u62C9\u53D6\u8BF4\u660E\n\n- OpenAI / OpenAI-compatible\uFF1A\u901A\u8FC7\u6A21\u578B\u5217\u8868\u63A5\u53E3\u81EA\u52A8\u62C9\u53D6\n- Gemini\uFF1A\u901A\u8FC7 Google Gemini \u6A21\u578B\u5217\u8868\u63A5\u53E3\u81EA\u52A8\u62C9\u53D6\n- Claude\uFF1A\u4F18\u5148\u5C1D\u8BD5\u8FDC\u7A0B\u62C9\u53D6\uFF0C\u5931\u8D25\u65F6\u56DE\u9000\u5230\u5185\u7F6E\u6A21\u578B\u5217\u8868\uFF0C\u5E76\u6253\u5370\u8B66\u544A\u65E5\u5FD7\n\n---\n\n";
6
6
  export declare class AggregatorService extends Service {
7
7
  ctx: Context;
8
8
  config: Config;
package/lib/index.mjs CHANGED
@@ -30,23 +30,35 @@ var ModelFetcher = class {
30
30
  return [];
31
31
  }
32
32
  }
33
+ normalizeBaseUrl(baseUrl) {
34
+ return baseUrl.replace(/\/+$/, "");
35
+ }
36
+ buildUrl(baseUrl, path) {
37
+ const normalizedBase = this.normalizeBaseUrl(baseUrl);
38
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
39
+ return `${normalizedBase}${normalizedPath}`;
40
+ }
41
+ getModelPlatform(source) {
42
+ return source.platform === "openai-compatible" ? "openai" : source.platform;
43
+ }
33
44
  async fetchOpenAIModels(source) {
34
- const response = await fetch(`${source.baseUrl}/models`, {
35
- headers: { "Authorization": `Bearer ${source.apiKey}` }
45
+ const response = await fetch(this.buildUrl(source.baseUrl, "/models"), {
46
+ headers: { Authorization: `Bearer ${source.apiKey}` }
36
47
  });
37
48
  if (!response.ok) {
38
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
49
+ const raw = await response.text().catch(() => "");
50
+ throw new Error(`HTTP ${response.status}: ${response.statusText}${raw ? ` | ${raw}` : ""}`);
39
51
  }
40
52
  const data = await response.json();
41
- const models = data.data || [];
42
- return models.map((model) => ({
53
+ const models = Array.isArray(data?.data) ? data.data : [];
54
+ return models.filter((model) => typeof model?.id === "string" && model.id.length > 0).map((model) => ({
43
55
  id: `${source.name}:${model.id}`,
44
56
  name: model.id,
45
57
  source: "auto",
46
58
  sourceName: source.name,
47
59
  baseUrl: source.baseUrl,
48
60
  apiKey: source.apiKey,
49
- platform: "openai",
61
+ platform: this.getModelPlatform(source),
50
62
  type: this.inferModelType(model.id),
51
63
  maxTokens: this.inferMaxTokens(model.id),
52
64
  visionCapable: this.hasVisionCapability(model.id),
@@ -58,6 +70,48 @@ var ModelFetcher = class {
58
70
  }));
59
71
  }
60
72
  async fetchClaudeModels(source) {
73
+ try {
74
+ const response = await fetch(this.buildUrl(source.baseUrl, "/models"), {
75
+ headers: {
76
+ "x-api-key": source.apiKey,
77
+ "anthropic-version": "2023-06-01",
78
+ "Content-Type": "application/json"
79
+ }
80
+ });
81
+ if (!response.ok) {
82
+ const raw = await response.text().catch(() => "");
83
+ throw new Error(`HTTP ${response.status}: ${response.statusText}${raw ? ` | ${raw}` : ""}`);
84
+ }
85
+ const data = await response.json();
86
+ const models = Array.isArray(data?.data) ? data.data : [];
87
+ if (!models.length) {
88
+ throw new Error("Claude models API returned empty list");
89
+ }
90
+ return models.filter((model) => typeof model?.id === "string" && model.id.length > 0).map((model) => ({
91
+ id: `${source.name}:${model.id}`,
92
+ name: model.display_name?.trim() || model.id,
93
+ source: "auto",
94
+ sourceName: source.name,
95
+ baseUrl: source.baseUrl,
96
+ apiKey: source.apiKey,
97
+ platform: "claude",
98
+ type: "llm",
99
+ maxTokens: this.inferClaudeMaxTokens(model.id),
100
+ visionCapable: true,
101
+ toolsCapable: true,
102
+ structuredOutput: true,
103
+ thinkingMode: "both",
104
+ available: true,
105
+ lastChecked: /* @__PURE__ */ new Date()
106
+ }));
107
+ } catch (error) {
108
+ this.ctx.logger.warn(
109
+ `[Claude Fetch Fallback] Failed to fetch models from source "${source.name}" (${source.baseUrl}), fallback to built-in Claude model list: ${error}`
110
+ );
111
+ return this.getBuiltInClaudeModels(source);
112
+ }
113
+ }
114
+ getBuiltInClaudeModels(source) {
61
115
  const knownModels = [
62
116
  { id: "claude-3-7-sonnet-20250219", maxTokens: 2e5 },
63
117
  { id: "claude-3-5-sonnet-20241022", maxTokens: 2e5 },
@@ -84,30 +138,35 @@ var ModelFetcher = class {
84
138
  }
85
139
  async fetchGeminiModels(source) {
86
140
  const response = await fetch(
87
- `${source.baseUrl}/v1beta/models?key=${source.apiKey}`
141
+ this.buildUrl(source.baseUrl, `/v1beta/models?key=${encodeURIComponent(source.apiKey)}`)
88
142
  );
89
143
  if (!response.ok) {
90
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
144
+ const raw = await response.text().catch(() => "");
145
+ throw new Error(`HTTP ${response.status}: ${response.statusText}${raw ? ` | ${raw}` : ""}`);
91
146
  }
92
147
  const data = await response.json();
93
- const models = data.models || [];
94
- return models.filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((model) => ({
95
- id: `${source.name}:${model.name}`,
96
- name: model.name,
97
- source: "auto",
98
- sourceName: source.name,
99
- baseUrl: source.baseUrl,
100
- apiKey: source.apiKey,
101
- platform: "gemini",
102
- type: "llm",
103
- maxTokens: this.parseGeminiMaxTokens(model),
104
- visionCapable: true,
105
- toolsCapable: true,
106
- structuredOutput: false,
107
- thinkingMode: "both",
108
- available: true,
109
- lastChecked: /* @__PURE__ */ new Date()
110
- }));
148
+ const models = Array.isArray(data?.models) ? data.models : [];
149
+ return models.filter((m) => m?.supportedGenerationMethods?.includes("generateContent")).map((model) => {
150
+ const rawName = typeof model.name === "string" ? model.name : "";
151
+ const displayName = rawName.replace(/^models\//, "") || rawName;
152
+ return {
153
+ id: `${source.name}:${rawName}`,
154
+ name: displayName,
155
+ source: "auto",
156
+ sourceName: source.name,
157
+ baseUrl: source.baseUrl,
158
+ apiKey: source.apiKey,
159
+ platform: "gemini",
160
+ type: "llm",
161
+ maxTokens: this.parseGeminiMaxTokens(model),
162
+ visionCapable: true,
163
+ toolsCapable: true,
164
+ structuredOutput: false,
165
+ thinkingMode: "both",
166
+ available: true,
167
+ lastChecked: /* @__PURE__ */ new Date()
168
+ };
169
+ });
111
170
  }
112
171
  inferModelType(modelId) {
113
172
  const id = modelId.toLowerCase();
@@ -137,6 +196,13 @@ var ModelFetcher = class {
137
196
  }
138
197
  return 128e3;
139
198
  }
199
+ inferClaudeMaxTokens(modelId) {
200
+ const id = modelId.toLowerCase();
201
+ if (id.includes("claude-3") || id.includes("claude-sonnet") || id.includes("claude-opus") || id.includes("claude-haiku")) {
202
+ return 2e5;
203
+ }
204
+ return 2e5;
205
+ }
140
206
  hasVisionCapability(modelId) {
141
207
  const id = modelId.toLowerCase();
142
208
  return id.includes("vision") || id.includes("gpt-4o") || id.includes("gpt-4-turbo");
@@ -150,52 +216,47 @@ var ModelFetcher = class {
150
216
  return id.includes("gpt-4o") || id.includes("gpt-4-turbo");
151
217
  }
152
218
  parseGeminiMaxTokens(model) {
153
- return model.topK?.outputTokenLimit || 128e3;
219
+ return Number(model?.outputTokenLimit) || Number(model?.inputTokenLimit) || 128e3;
154
220
  }
155
221
  };
156
222
 
157
223
  // src/config.ts
158
224
  import { Schema } from "koishi";
225
+ var manualSourceModelSchema = Schema.object({
226
+ id: Schema.string().required().description("模型 ID"),
227
+ name: Schema.string().required().description("模型名称")
228
+ });
229
+ var sourceBaseSchema = Schema.object({
230
+ name: Schema.string().required().description("源名称"),
231
+ baseUrl: Schema.string().required().description("API 端点"),
232
+ apiKey: Schema.string().required().role("secret").description("API Key"),
233
+ platform: Schema.union([
234
+ Schema.const("openai").description("OpenAI"),
235
+ Schema.const("claude").description("Claude"),
236
+ Schema.const("gemini").description("Gemini"),
237
+ Schema.const("openai-compatible").description("OpenAI 兼容")
238
+ ]).description("平台类型"),
239
+ enabled: Schema.boolean().default(true).description("启用"),
240
+ autoFetchModels: Schema.boolean().default(true).description("是否自动拉取模型")
241
+ });
242
+ var sourceSchema = Schema.intersect([
243
+ sourceBaseSchema,
244
+ Schema.union([
245
+ Schema.object({}),
246
+ Schema.object({
247
+ autoFetchModels: Schema.const(false).required(),
248
+ manualModels: Schema.array(manualSourceModelSchema).default([]).role("table").description("手动添加模型")
249
+ })
250
+ ])
251
+ ]);
159
252
  var Config = Schema.intersect([
160
- // Auto-fetch sources configuration
161
- Schema.object({
162
- autoFetchSources: Schema.array(
163
- Schema.intersect([
164
- Schema.object({
165
- name: Schema.string().required().description("源名称"),
166
- baseUrl: Schema.string().required().description("API 端点"),
167
- apiKey: Schema.string().required().role("secret").description("API Key"),
168
- platform: Schema.union([
169
- Schema.const("openai").description("OpenAI"),
170
- Schema.const("claude").description("Claude"),
171
- Schema.const("gemini").description("Gemini"),
172
- Schema.const("openai-compatible").description("OpenAI 兼容")
173
- ]).description("平台类型"),
174
- enabled: Schema.boolean().default(true).description("启用")
175
- })
176
- ])
177
- ).role("table").description("自动拉取源")
178
- }),
179
- // Manual models
180
253
  Schema.object({
181
- manualModels: Schema.array(
182
- Schema.object({
183
- id: Schema.string().required().description("模型 ID"),
184
- name: Schema.string().required().description("模型名称"),
185
- sourceName: Schema.string().required().description("源名称"),
186
- baseUrl: Schema.string().required().description("API 端点"),
187
- apiKey: Schema.string().required().role("secret").description("API Key"),
188
- platform: Schema.union([
189
- Schema.const("openai").description("OpenAI"),
190
- Schema.const("claude").description("Claude"),
191
- Schema.const("gemini").description("Gemini")
192
- ]).description("平台类型")
193
- })
194
- ).role("table").description("手动添加的模型")
195
- }),
254
+ sources: Schema.array(sourceSchema).role("list").default([]).description("添加源")
255
+ }).description("模型源配置"),
196
256
  // Debug options
197
257
  Schema.object({
198
- debugMode: Schema.boolean().default(false).description("启用调试日志")
258
+ debugMode: Schema.boolean().default(false).description("启用调试日志"),
259
+ verboseLog: Schema.boolean().default(false).description("启用详细日志(输出配置摘要、hash 与重载判断过程)")
199
260
  }).description("调试选项")
200
261
  ]);
201
262
  var name = "elysia-api-aggregator";
@@ -209,10 +270,16 @@ var usage = `---
209
270
 
210
271
  ### 配置步骤
211
272
 
212
- 1. **自动获取**: 在「自动拉取源」中添加 API 端点和 API Key
213
- 2. **手动添加**: 在「手动添加的模型」中直接配置模型
273
+ 1. **添加源**: 在「添加源」中配置 API 端点、API Key 与平台
274
+ 2. **自动/手动模式**: 保持「是否自动拉取模型」为是,或切换为否后手动填写模型
214
275
  3. **重新加载**: 配置完成后使用 \`elysia-api.models.reload\` 命令生效
215
276
 
277
+ ### 自动拉取说明
278
+
279
+ - OpenAI / OpenAI-compatible:通过模型列表接口自动拉取
280
+ - Gemini:通过 Google Gemini 模型列表接口自动拉取
281
+ - Claude:优先尝试远程拉取,失败时回退到内置模型列表,并打印警告日志
282
+
216
283
  ---
217
284
 
218
285
  `;
@@ -256,11 +323,33 @@ function apply(ctx, config) {
256
323
  let pendingReload = false;
257
324
  let lastModelsHash = "";
258
325
  let lastConfigHash = "";
326
+ const summarizeSources = /* @__PURE__ */ __name((sources) => {
327
+ return sources.map((source) => ({
328
+ name: source.name,
329
+ baseUrl: source.baseUrl,
330
+ platform: source.platform,
331
+ enabled: source.enabled,
332
+ autoFetchModels: source.autoFetchModels,
333
+ manualModelsCount: source.autoFetchModels ? 0 : source.manualModels.length,
334
+ hasApiKey: Boolean(source.apiKey)
335
+ }));
336
+ }, "summarizeSources");
337
+ const verboseLog = /* @__PURE__ */ __name((message, payload) => {
338
+ if (!config.verboseLog) return;
339
+ if (payload === void 0) {
340
+ ctx.logger.info(`[aggregator verbose] ${message}`);
341
+ return;
342
+ }
343
+ ctx.logger.info(`[aggregator verbose] ${message}: ${JSON.stringify(payload)}`);
344
+ }, "verboseLog");
259
345
  const buildConfigHash = /* @__PURE__ */ __name(() => {
260
- return JSON.stringify({
261
- autoFetchSources: config.autoFetchSources,
262
- manualModels: config.manualModels
263
- });
346
+ const normalized = {
347
+ sources: summarizeSources(config.sources)
348
+ };
349
+ const hash = JSON.stringify(normalized);
350
+ verboseLog("buildConfigHash summary", normalized);
351
+ verboseLog("buildConfigHash value", hash);
352
+ return hash;
264
353
  }, "buildConfigHash");
265
354
  const buildModelsHash = /* @__PURE__ */ __name((models) => {
266
355
  const normalized = [...models].map((m) => ({
@@ -280,81 +369,168 @@ function apply(ctx, config) {
280
369
  })).sort((a, b) => a.id.localeCompare(b.id));
281
370
  return JSON.stringify(normalized);
282
371
  }, "buildModelsHash");
372
+ const mapManualSourceModels = /* @__PURE__ */ __name((source) => {
373
+ return source.manualModels.map((m) => ({
374
+ id: `${source.name}:${m.id}`,
375
+ name: m.name,
376
+ source: "manual",
377
+ sourceName: source.name,
378
+ baseUrl: source.baseUrl,
379
+ apiKey: source.apiKey,
380
+ platform: source.platform === "openai-compatible" ? "openai" : source.platform,
381
+ // 使用默认值
382
+ type: "llm",
383
+ maxTokens: 128e3,
384
+ visionCapable: false,
385
+ toolsCapable: false,
386
+ structuredOutput: false,
387
+ thinkingMode: "both",
388
+ available: true,
389
+ lastChecked: /* @__PURE__ */ new Date()
390
+ }));
391
+ }, "mapManualSourceModels");
283
392
  async function loadModels(trigger = "config") {
393
+ verboseLog("loadModels entered", {
394
+ trigger,
395
+ isLoading,
396
+ pendingReload,
397
+ sourceCount: config.sources.length,
398
+ sources: summarizeSources(config.sources)
399
+ });
284
400
  if (isLoading) {
285
401
  pendingReload = true;
286
402
  if (config.debugMode) {
287
403
  ctx.logger.info(`loadModels skipped (already running), queued pending reload (trigger=${trigger})`);
288
404
  }
405
+ verboseLog("loadModels skipped because already loading", {
406
+ trigger,
407
+ pendingReload
408
+ });
289
409
  return;
290
410
  }
291
411
  isLoading = true;
292
412
  const loadStartedAt = Date.now();
293
413
  try {
294
414
  ctx.logger.info("Loading models...");
295
- const fetchedModels = [];
296
- for (const source of config.autoFetchSources) {
297
- if (!source.enabled) continue;
415
+ verboseLog("loadModels start state", {
416
+ trigger,
417
+ sourceCount: config.sources.length,
418
+ sources: summarizeSources(config.sources)
419
+ });
420
+ const allModels = [];
421
+ for (const source of config.sources) {
422
+ if (!source.enabled) {
423
+ verboseLog("skip disabled source", {
424
+ name: source.name,
425
+ autoFetchModels: source.autoFetchModels
426
+ });
427
+ continue;
428
+ }
298
429
  const sourceStartedAt = Date.now();
299
- const sourceModels = await fetcher.fetchModels(source);
300
- fetchedModels.push(...sourceModels);
430
+ let sourceModels = [];
431
+ verboseLog("processing source", {
432
+ name: source.name,
433
+ baseUrl: source.baseUrl,
434
+ platform: source.platform,
435
+ enabled: source.enabled,
436
+ autoFetchModels: source.autoFetchModels,
437
+ manualModelsCount: source.autoFetchModels ? 0 : source.manualModels.length,
438
+ hasApiKey: Boolean(source.apiKey)
439
+ });
440
+ if (source.autoFetchModels) {
441
+ sourceModels = await fetcher.fetchModels(source);
442
+ } else {
443
+ sourceModels = mapManualSourceModels(source);
444
+ }
445
+ allModels.push(...sourceModels);
301
446
  const sourceCostMs = Date.now() - sourceStartedAt;
302
- ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
447
+ ctx.logger.info(
448
+ `[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms${source.autoFetchModels ? ", auto" : ", manual"})`
449
+ );
450
+ verboseLog("source processed", {
451
+ name: source.name,
452
+ trigger,
453
+ sourceModelsCount: sourceModels.length,
454
+ sourceCostMs
455
+ });
303
456
  }
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
457
  service.updateModels(allModels);
326
458
  const totalCostMs = Date.now() - loadStartedAt;
327
- ctx.logger.info(`[source] manual: ${manualModels.length} models`);
328
459
  const modelsHash = buildModelsHash(allModels);
460
+ verboseLog("buildModelsHash value", {
461
+ modelsHash,
462
+ lastModelsHash,
463
+ allModelsCount: allModels.length
464
+ });
329
465
  if (modelsHash === lastModelsHash) {
330
466
  ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms, unchanged)`);
467
+ verboseLog("models unchanged, skip emit", {
468
+ trigger,
469
+ allModelsCount: allModels.length,
470
+ totalCostMs
471
+ });
331
472
  return;
332
473
  }
333
474
  lastModelsHash = modelsHash;
334
475
  ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms)`);
476
+ verboseLog("models updated", {
477
+ trigger,
478
+ allModelsCount: allModels.length,
479
+ totalCostMs,
480
+ lastModelsHash
481
+ });
335
482
  ctx.emit("elysia-api/models-updated", [...allModels]);
336
483
  } finally {
337
484
  isLoading = false;
485
+ verboseLog("loadModels finally", {
486
+ trigger,
487
+ pendingReload,
488
+ isLoading
489
+ });
338
490
  if (pendingReload) {
339
491
  pendingReload = false;
492
+ verboseLog("pending reload consumed", {
493
+ nextTrigger: "pending"
494
+ });
340
495
  void loadModels("pending");
341
496
  }
342
497
  }
343
498
  }
344
499
  __name(loadModels, "loadModels");
345
500
  lastConfigHash = buildConfigHash();
501
+ verboseLog("initial lastConfigHash", lastConfigHash);
346
502
  ctx.on("ready", () => {
503
+ verboseLog("ready event fired", {
504
+ sourceCount: config.sources.length,
505
+ sources: summarizeSources(config.sources)
506
+ });
347
507
  void loadModels("ready");
348
508
  });
349
509
  ctx.on("config", () => {
510
+ verboseLog("config event fired", {
511
+ sourceCount: config.sources.length,
512
+ sources: summarizeSources(config.sources),
513
+ lastConfigHash
514
+ });
350
515
  const configHash = buildConfigHash();
516
+ verboseLog("config event hash compare", {
517
+ configHash,
518
+ lastConfigHash,
519
+ equal: configHash === lastConfigHash
520
+ });
351
521
  if (configHash === lastConfigHash) {
352
522
  if (config.debugMode) {
353
523
  ctx.logger.info("aggregator: config event ignored (aggregator config unchanged)");
354
524
  }
525
+ verboseLog("config event ignored", {
526
+ reason: "unchanged"
527
+ });
355
528
  return;
356
529
  }
357
530
  lastConfigHash = configHash;
531
+ verboseLog("config event accepted", {
532
+ newLastConfigHash: lastConfigHash
533
+ });
358
534
  void loadModels("config");
359
535
  });
360
536
  ctx.command("elysia-api.models.reload", "重新加载模型列表").action(async () => {
@@ -1,13 +1,19 @@
1
- import { Model, AutoFetchSource } from '@elysia-api/shared';
1
+ import { Model } from '@elysia-api/shared';
2
+ import { AutoFetchAggregatorSource } from './config';
2
3
  export declare class ModelFetcher {
3
4
  private ctx;
4
5
  constructor(ctx: import('koishi').Context);
5
- fetchModels(source: AutoFetchSource): Promise<Model[]>;
6
+ fetchModels(source: AutoFetchAggregatorSource): Promise<Model[]>;
7
+ private normalizeBaseUrl;
8
+ private buildUrl;
9
+ private getModelPlatform;
6
10
  private fetchOpenAIModels;
7
11
  private fetchClaudeModels;
12
+ private getBuiltInClaudeModels;
8
13
  private fetchGeminiModels;
9
14
  private inferModelType;
10
15
  private inferMaxTokens;
16
+ private inferClaudeMaxTokens;
11
17
  private hasVisionCapability;
12
18
  private hasToolsCapability;
13
19
  private hasStructuredOutput;
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.3",
4
+ "version": "0.2.5",
5
5
  "main": "lib/index.cjs",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",