koishi-plugin-elysia-api-aggregator 0.2.2 → 0.2.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/lib/config.d.ts CHANGED
@@ -1,8 +1,27 @@
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;
7
26
  }
8
27
  export declare const Config: Schema<Config>;
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,49 +242,49 @@ 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");
185
- 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
- import_koishi.Schema.object({
207
- manualModels: import_koishi.Schema.array(
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
+ });
267
+ var sourceSchema = import_koishi.Schema.intersect([
268
+ sourceBaseSchema,
269
+ import_koishi.Schema.intersect([
270
+ import_koishi.Schema.object({
271
+ autoFetchModels: import_koishi.Schema.boolean().default(true).description("是否自动拉取模型")
272
+ }),
273
+ import_koishi.Schema.union([
274
+ import_koishi.Schema.object({
275
+ autoFetchModels: import_koishi.Schema.const(true).required()
276
+ }),
208
277
  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("平台类型")
278
+ autoFetchModels: import_koishi.Schema.const(false).required(),
279
+ manualModels: import_koishi.Schema.array(manualSourceModelSchema).role("table").description("手动添加模型")
219
280
  })
220
- ).role("table").description("手动添加的模型")
221
- }),
281
+ ])
282
+ ])
283
+ ]);
284
+ var Config = import_koishi.Schema.intersect([
285
+ import_koishi.Schema.object({
286
+ sources: import_koishi.Schema.array(sourceSchema).description("添加源")
287
+ }).description("模型源配置"),
222
288
  // Debug options
223
289
  import_koishi.Schema.object({
224
290
  debugMode: import_koishi.Schema.boolean().default(false).description("启用调试日志")
@@ -235,10 +301,16 @@ var usage = `---
235
301
 
236
302
  ### 配置步骤
237
303
 
238
- 1. **自动获取**: 在「自动拉取源」中添加 API 端点和 API Key
239
- 2. **手动添加**: 在「手动添加的模型」中直接配置模型
304
+ 1. **添加源**: 在「添加源」中配置 API 端点、API Key 与平台
305
+ 2. **自动/手动模式**: 保持「是否自动拉取模型」为是,或切换为否后手动填写模型
240
306
  3. **重新加载**: 配置完成后使用 \`elysia-api.models.reload\` 命令生效
241
307
 
308
+ ### 自动拉取说明
309
+
310
+ - OpenAI / OpenAI-compatible:通过模型列表接口自动拉取
311
+ - Gemini:通过 Google Gemini 模型列表接口自动拉取
312
+ - Claude:优先尝试远程拉取,失败时回退到内置模型列表,并打印警告日志
313
+
242
314
  ---
243
315
 
244
316
  `;
@@ -284,8 +356,7 @@ function apply(ctx, config) {
284
356
  let lastConfigHash = "";
285
357
  const buildConfigHash = /* @__PURE__ */ __name(() => {
286
358
  return JSON.stringify({
287
- autoFetchSources: config.autoFetchSources,
288
- manualModels: config.manualModels
359
+ sources: config.sources
289
360
  });
290
361
  }, "buildConfigHash");
291
362
  const buildModelsHash = /* @__PURE__ */ __name((models) => {
@@ -306,6 +377,26 @@ function apply(ctx, config) {
306
377
  })).sort((a, b) => a.id.localeCompare(b.id));
307
378
  return JSON.stringify(normalized);
308
379
  }, "buildModelsHash");
380
+ const mapManualSourceModels = /* @__PURE__ */ __name((source) => {
381
+ return source.manualModels.map((m) => ({
382
+ id: `${source.name}:${m.id}`,
383
+ name: m.name,
384
+ source: "manual",
385
+ sourceName: source.name,
386
+ baseUrl: source.baseUrl,
387
+ apiKey: source.apiKey,
388
+ platform: source.platform === "openai-compatible" ? "openai" : source.platform,
389
+ // 使用默认值
390
+ type: "llm",
391
+ maxTokens: 128e3,
392
+ visionCapable: false,
393
+ toolsCapable: false,
394
+ structuredOutput: false,
395
+ thinkingMode: "both",
396
+ available: true,
397
+ lastChecked: /* @__PURE__ */ new Date()
398
+ }));
399
+ }, "mapManualSourceModels");
309
400
  async function loadModels(trigger = "config") {
310
401
  if (isLoading) {
311
402
  pendingReload = true;
@@ -318,39 +409,24 @@ function apply(ctx, config) {
318
409
  const loadStartedAt = Date.now();
319
410
  try {
320
411
  ctx.logger.info("Loading models...");
321
- const fetchedModels = [];
322
- for (const source of config.autoFetchSources) {
412
+ const allModels = [];
413
+ for (const source of config.sources) {
323
414
  if (!source.enabled) continue;
324
415
  const sourceStartedAt = Date.now();
325
- const sourceModels = await fetcher.fetchModels(source);
326
- fetchedModels.push(...sourceModels);
416
+ let sourceModels = [];
417
+ if (source.autoFetchModels) {
418
+ sourceModels = await fetcher.fetchModels(source);
419
+ } else {
420
+ sourceModels = mapManualSourceModels(source);
421
+ }
422
+ allModels.push(...sourceModels);
327
423
  const sourceCostMs = Date.now() - sourceStartedAt;
328
- ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
424
+ ctx.logger.info(
425
+ `[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms${source.autoFetchModels ? ", auto" : ", manual"})`
426
+ );
329
427
  }
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
428
  service.updateModels(allModels);
352
429
  const totalCostMs = Date.now() - loadStartedAt;
353
- ctx.logger.info(`[source] manual: ${manualModels.length} models`);
354
430
  const modelsHash = buildModelsHash(allModels);
355
431
  if (modelsHash === lastModelsHash) {
356
432
  ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms, unchanged)`);
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,49 +216,49 @@ 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";
159
- 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
- Schema.object({
181
- manualModels: Schema.array(
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
+ });
241
+ var sourceSchema = Schema.intersect([
242
+ sourceBaseSchema,
243
+ Schema.intersect([
244
+ Schema.object({
245
+ autoFetchModels: Schema.boolean().default(true).description("是否自动拉取模型")
246
+ }),
247
+ Schema.union([
182
248
  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("平台类型")
249
+ autoFetchModels: Schema.const(true).required()
250
+ }),
251
+ Schema.object({
252
+ autoFetchModels: Schema.const(false).required(),
253
+ manualModels: Schema.array(manualSourceModelSchema).role("table").description("手动添加模型")
193
254
  })
194
- ).role("table").description("手动添加的模型")
195
- }),
255
+ ])
256
+ ])
257
+ ]);
258
+ var Config = Schema.intersect([
259
+ Schema.object({
260
+ sources: Schema.array(sourceSchema).description("添加源")
261
+ }).description("模型源配置"),
196
262
  // Debug options
197
263
  Schema.object({
198
264
  debugMode: Schema.boolean().default(false).description("启用调试日志")
@@ -209,10 +275,16 @@ var usage = `---
209
275
 
210
276
  ### 配置步骤
211
277
 
212
- 1. **自动获取**: 在「自动拉取源」中添加 API 端点和 API Key
213
- 2. **手动添加**: 在「手动添加的模型」中直接配置模型
278
+ 1. **添加源**: 在「添加源」中配置 API 端点、API Key 与平台
279
+ 2. **自动/手动模式**: 保持「是否自动拉取模型」为是,或切换为否后手动填写模型
214
280
  3. **重新加载**: 配置完成后使用 \`elysia-api.models.reload\` 命令生效
215
281
 
282
+ ### 自动拉取说明
283
+
284
+ - OpenAI / OpenAI-compatible:通过模型列表接口自动拉取
285
+ - Gemini:通过 Google Gemini 模型列表接口自动拉取
286
+ - Claude:优先尝试远程拉取,失败时回退到内置模型列表,并打印警告日志
287
+
216
288
  ---
217
289
 
218
290
  `;
@@ -258,8 +330,7 @@ function apply(ctx, config) {
258
330
  let lastConfigHash = "";
259
331
  const buildConfigHash = /* @__PURE__ */ __name(() => {
260
332
  return JSON.stringify({
261
- autoFetchSources: config.autoFetchSources,
262
- manualModels: config.manualModels
333
+ sources: config.sources
263
334
  });
264
335
  }, "buildConfigHash");
265
336
  const buildModelsHash = /* @__PURE__ */ __name((models) => {
@@ -280,6 +351,26 @@ function apply(ctx, config) {
280
351
  })).sort((a, b) => a.id.localeCompare(b.id));
281
352
  return JSON.stringify(normalized);
282
353
  }, "buildModelsHash");
354
+ const mapManualSourceModels = /* @__PURE__ */ __name((source) => {
355
+ return source.manualModels.map((m) => ({
356
+ id: `${source.name}:${m.id}`,
357
+ name: m.name,
358
+ source: "manual",
359
+ sourceName: source.name,
360
+ baseUrl: source.baseUrl,
361
+ apiKey: source.apiKey,
362
+ platform: source.platform === "openai-compatible" ? "openai" : source.platform,
363
+ // 使用默认值
364
+ type: "llm",
365
+ maxTokens: 128e3,
366
+ visionCapable: false,
367
+ toolsCapable: false,
368
+ structuredOutput: false,
369
+ thinkingMode: "both",
370
+ available: true,
371
+ lastChecked: /* @__PURE__ */ new Date()
372
+ }));
373
+ }, "mapManualSourceModels");
283
374
  async function loadModels(trigger = "config") {
284
375
  if (isLoading) {
285
376
  pendingReload = true;
@@ -292,39 +383,24 @@ function apply(ctx, config) {
292
383
  const loadStartedAt = Date.now();
293
384
  try {
294
385
  ctx.logger.info("Loading models...");
295
- const fetchedModels = [];
296
- for (const source of config.autoFetchSources) {
386
+ const allModels = [];
387
+ for (const source of config.sources) {
297
388
  if (!source.enabled) continue;
298
389
  const sourceStartedAt = Date.now();
299
- const sourceModels = await fetcher.fetchModels(source);
300
- fetchedModels.push(...sourceModels);
390
+ let sourceModels = [];
391
+ if (source.autoFetchModels) {
392
+ sourceModels = await fetcher.fetchModels(source);
393
+ } else {
394
+ sourceModels = mapManualSourceModels(source);
395
+ }
396
+ allModels.push(...sourceModels);
301
397
  const sourceCostMs = Date.now() - sourceStartedAt;
302
- ctx.logger.info(`[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms)`);
398
+ ctx.logger.info(
399
+ `[source] ${source.name}: ${sourceModels.length} models (${sourceCostMs}ms${source.autoFetchModels ? ", auto" : ", manual"})`
400
+ );
303
401
  }
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
402
  service.updateModels(allModels);
326
403
  const totalCostMs = Date.now() - loadStartedAt;
327
- ctx.logger.info(`[source] manual: ${manualModels.length} models`);
328
404
  const modelsHash = buildModelsHash(allModels);
329
405
  if (modelsHash === lastModelsHash) {
330
406
  ctx.logger.info(`Total models loaded: ${allModels.length} (${totalCostMs}ms, unchanged)`);
@@ -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.2",
4
+ "version": "0.2.4",
5
5
  "main": "lib/index.cjs",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",