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 +23 -3
- package/lib/index.cjs +273 -97
- package/lib/index.d.ts +1 -1
- package/lib/index.mjs +273 -97
- package/lib/model-fetcher.d.ts +8 -2
- package/package.json +1 -1
package/lib/config.d.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { Schema } from 'koishi';
|
|
2
|
-
import {
|
|
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
|
-
|
|
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(
|
|
61
|
-
headers: {
|
|
71
|
+
const response = await fetch(this.buildUrl(source.baseUrl, "/models"), {
|
|
72
|
+
headers: { Authorization: `Bearer ${source.apiKey}` }
|
|
62
73
|
});
|
|
63
74
|
if (!response.ok) {
|
|
64
|
-
|
|
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:
|
|
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
|
-
|
|
167
|
+
this.buildUrl(source.baseUrl, `/v1beta/models?key=${encodeURIComponent(source.apiKey)}`)
|
|
114
168
|
);
|
|
115
169
|
if (!response.ok) {
|
|
116
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
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.
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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(
|
|
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. **\
|
|
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(
|
|
35
|
-
headers: {
|
|
45
|
+
const response = await fetch(this.buildUrl(source.baseUrl, "/models"), {
|
|
46
|
+
headers: { Authorization: `Bearer ${source.apiKey}` }
|
|
36
47
|
});
|
|
37
48
|
if (!response.ok) {
|
|
38
|
-
|
|
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:
|
|
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
|
-
|
|
141
|
+
this.buildUrl(source.baseUrl, `/v1beta/models?key=${encodeURIComponent(source.apiKey)}`)
|
|
88
142
|
);
|
|
89
143
|
if (!response.ok) {
|
|
90
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
182
|
-
|
|
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.
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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(
|
|
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 () => {
|
package/lib/model-fetcher.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { Model
|
|
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:
|
|
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.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"main": "lib/index.cjs",
|
|
6
6
|
"module": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|