koishi-plugin-elysia-api-aggregator 0.2.3 → 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 +22 -3
- package/lib/index.cjs +169 -93
- package/lib/index.d.ts +1 -1
- package/lib/index.mjs +169 -93
- package/lib/model-fetcher.d.ts +8 -2
- package/package.json +1 -1
package/lib/config.d.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
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;
|
|
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(
|
|
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,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
|
|
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
|
|
186
|
-
|
|
187
|
-
import_koishi.Schema.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
)
|
|
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.
|
|
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
|
-
|
|
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
|
|
322
|
-
for (const source of config.
|
|
412
|
+
const allModels = [];
|
|
413
|
+
for (const source of config.sources) {
|
|
323
414
|
if (!source.enabled) continue;
|
|
324
415
|
const sourceStartedAt = Date.now();
|
|
325
|
-
|
|
326
|
-
|
|
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(
|
|
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. **\
|
|
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,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
|
|
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
|
|
160
|
-
|
|
161
|
-
Schema.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
)
|
|
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.
|
|
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
|
-
|
|
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
|
|
296
|
-
for (const source of config.
|
|
386
|
+
const allModels = [];
|
|
387
|
+
for (const source of config.sources) {
|
|
297
388
|
if (!source.enabled) continue;
|
|
298
389
|
const sourceStartedAt = Date.now();
|
|
299
|
-
|
|
300
|
-
|
|
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(
|
|
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)`);
|
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.4",
|
|
5
5
|
"main": "lib/index.cjs",
|
|
6
6
|
"module": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|