llm-simple-router 0.9.15 → 0.9.16
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/dist/core/constants.d.ts
CHANGED
package/dist/core/constants.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import fp from "fastify-plugin";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { decrypt } from "../../utils/crypto.js";
|
|
6
|
-
import { proxyGetRequest, createErrorFormatter } from "../proxy-core.js";
|
|
3
|
+
import { getAllProviders, insertRequestLog } from "../../db/index.js";
|
|
4
|
+
import { createErrorFormatter } from "../proxy-core.js";
|
|
7
5
|
import { handleProxyRequest } from "./proxy-handler.js";
|
|
8
6
|
import { createOrchestrator } from "../orchestration/orchestrator.js";
|
|
9
|
-
import {
|
|
7
|
+
import { HTTP_OK, HTTP_BAD_GATEWAY, MS_PER_SECOND } from "../../core/constants.js";
|
|
10
8
|
import { SERVICE_KEYS } from "../../core/container.js";
|
|
11
9
|
const CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
|
|
12
10
|
const MODELS_PATH = "/v1/models";
|
|
@@ -55,54 +53,75 @@ const openaiProxyRaw = (app, opts, done) => {
|
|
|
55
53
|
// 规范路径 + 兼容路径(不带 /v1 前缀)
|
|
56
54
|
app.post(CHAT_COMPLETIONS_PATH, handleChatCompletions);
|
|
57
55
|
app.post(CHAT_COMPLETIONS_COMPAT_PATH, handleChatCompletions);
|
|
56
|
+
const ANTHROPIC_DEFAULT_PAGE_SIZE = 20;
|
|
57
|
+
const ANTHROPIC_MAX_PAGE_SIZE = 1000;
|
|
58
58
|
const handleModels = async (request, reply) => {
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const provider = providers[0];
|
|
76
|
-
const apiKey = decrypt(provider.api_key, getSetting(db, "encryption_key"));
|
|
77
|
-
const cliHdrs = request.headers;
|
|
78
|
-
try {
|
|
79
|
-
const result = await proxyGetRequest(provider, apiKey, cliHdrs, MODELS_PATH);
|
|
80
|
-
insertRequestLog(db, {
|
|
81
|
-
id: randomUUID(), api_type: "openai", model: null,
|
|
82
|
-
provider_id: provider.id, status_code: result.statusCode, latency_ms: Date.now() - startTime, is_stream: 0,
|
|
83
|
-
error_message: null, created_at: new Date().toISOString(),
|
|
84
|
-
client_request: JSON.stringify({ headers: request.headers }),
|
|
85
|
-
router_key_id: request.routerKey?.id ?? null,
|
|
86
|
-
});
|
|
87
|
-
for (const [k, v] of Object.entries(result.headers))
|
|
88
|
-
reply.header(k, v);
|
|
89
|
-
return reply.code(result.statusCode).send(result.body);
|
|
59
|
+
// 聚合所有活跃 provider 的模型列表
|
|
60
|
+
const allProviders = getAllProviders(db).filter(p => p.is_active);
|
|
61
|
+
const modelMeta = new Map();
|
|
62
|
+
for (const p of allProviders) {
|
|
63
|
+
try {
|
|
64
|
+
const models = JSON.parse(p.models || '[]');
|
|
65
|
+
for (const m of models) {
|
|
66
|
+
if (!modelMeta.has(m))
|
|
67
|
+
modelMeta.set(m, { providerName: p.name, createdAt: p.created_at });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// providers.models 有 NOT NULL 约束默认 '[]',此处防御开发期误配的非法 JSON
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
90
74
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
75
|
+
const sortedIds = [...modelMeta.keys()].sort();
|
|
76
|
+
// 根据请求头判断响应格式:Anthropic 客户端发送 anthropic-version 头
|
|
77
|
+
const isAnthropicFormat = !!request.headers['anthropic-version'];
|
|
78
|
+
if (isAnthropicFormat) {
|
|
79
|
+
// Anthropic 格式: { data: [...], has_more, first_id, last_id }
|
|
80
|
+
const query = request.query;
|
|
81
|
+
const limit = Math.min(Math.max(parseInt(query.limit || String(ANTHROPIC_DEFAULT_PAGE_SIZE), 10) || ANTHROPIC_DEFAULT_PAGE_SIZE, 1), ANTHROPIC_MAX_PAGE_SIZE);
|
|
82
|
+
let sliced;
|
|
83
|
+
let hasMore;
|
|
84
|
+
if (query.after_id) {
|
|
85
|
+
const idx = sortedIds.indexOf(query.after_id);
|
|
86
|
+
const start = idx !== -1 ? idx + 1 : 0;
|
|
87
|
+
sliced = sortedIds.slice(start, start + limit);
|
|
88
|
+
hasMore = start + limit < sortedIds.length;
|
|
89
|
+
}
|
|
90
|
+
else if (query.before_id) {
|
|
91
|
+
const endIdx = sortedIds.indexOf(query.before_id);
|
|
92
|
+
const end = endIdx !== -1 ? endIdx : sortedIds.length;
|
|
93
|
+
const start = Math.max(0, end - limit);
|
|
94
|
+
sliced = sortedIds.slice(start, end);
|
|
95
|
+
hasMore = start > 0;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
sliced = sortedIds.slice(0, limit);
|
|
99
|
+
hasMore = limit < sortedIds.length;
|
|
100
|
+
}
|
|
101
|
+
const data = sliced.map(id => ({
|
|
102
|
+
type: 'model',
|
|
103
|
+
id,
|
|
104
|
+
display_name: id,
|
|
105
|
+
created_at: modelMeta.get(id).createdAt,
|
|
106
|
+
}));
|
|
107
|
+
return reply.code(HTTP_OK).send({
|
|
108
|
+
data,
|
|
109
|
+
has_more: hasMore,
|
|
110
|
+
first_id: data.length > 0 ? data[0].id : null,
|
|
111
|
+
last_id: data.length > 0 ? data[data.length - 1].id : null,
|
|
104
112
|
});
|
|
105
113
|
}
|
|
114
|
+
// OpenAI 格式: { object: "list", data: [...] }
|
|
115
|
+
const data = sortedIds.map(id => ({
|
|
116
|
+
id,
|
|
117
|
+
object: 'model',
|
|
118
|
+
created: Math.floor(new Date(modelMeta.get(id).createdAt).getTime() / MS_PER_SECOND),
|
|
119
|
+
owned_by: modelMeta.get(id).providerName,
|
|
120
|
+
}));
|
|
121
|
+
return reply.code(HTTP_OK).send({
|
|
122
|
+
object: 'list',
|
|
123
|
+
data,
|
|
124
|
+
});
|
|
106
125
|
};
|
|
107
126
|
// 规范路径 + 兼容路径
|
|
108
127
|
app.get(MODELS_PATH, handleModels);
|
package/package.json
CHANGED