@bd7pil/reeve 0.1.4 → 0.1.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/README.md +4 -3
- package/default-fallback.json +49 -0
- package/package.json +4 -4
- package/provision.json +29 -23
- package/src/gate/classify.js +77 -0
- package/src/gate/config.js +172 -0
- package/src/gate/gate.js +446 -0
- package/src/gate/lanes.js +194 -0
- package/src/gate/state.js +129 -0
- package/src/index.js +77 -85
- package/src/models.js +11 -0
- package/src/snapshot.js +2 -24
- package/src/stats.js +142 -179
- package/default-chain.json +0 -251
- package/src/chain.js +0 -358
- package/src/proxy.js +0 -639
package/src/stats.js
CHANGED
|
@@ -1,64 +1,31 @@
|
|
|
1
|
-
// 性能统计:把
|
|
1
|
+
// 性能统计:把 **GATE 台账**(请求级归因)与编排台账(任务级)聚合成面板要看的形状。
|
|
2
2
|
//
|
|
3
3
|
// 分工(两条取数路径,别混):
|
|
4
|
-
// ·
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// · GATE 侧 = "请求级归因":哪条链(chain)、真打了谁(provider/model)、第几次尝试(n)、
|
|
5
|
+
// 成没成、多久、有没有重试。**只有 GATE 知道真模型是谁** —— 引擎侧的 session 记录里
|
|
6
|
+
// 模型名永远是 `reeve/pro` 这类别名(见 PROGRESS §6.2 归因边界)。
|
|
7
7
|
// · 编排侧 = "任务级台账":谁派给了谁、跑多久、超时/停止/对账、在飞与排队。
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// 为什么不做 logs.db 直读:引擎运行时里的 node:sqlite 版本未知(Bifrost 的库用了较新的
|
|
13
|
-
// 索引语法,EL7 的 python3.6 读它直接报 malformed schema)—— 直读是**最后**的手段,先不做。
|
|
9
|
+
// 取数:GATE 的台账是 JSONL(`~/.reeve/gate.jsonl`,插件与 GATE 同进程、同机器)——
|
|
10
|
+
// 直接按行读、只聚合窗口内的行。**不问 HTTP、不要 token**(旧代理层那套已随退役删除)。
|
|
11
|
+
// 空网/只读盘的机器上读不到就如实标 unavailable,面板给出手工核对的办法。
|
|
14
12
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// 行里的关键字段:`id, timestamp(ISO, 纳秒), provider, model, fallback_index, number_of_retries,
|
|
19
|
-
// routing_rule_id, routing_rule_name, selected_key_name, latency(ms, 浮点), upstream_latency,
|
|
20
|
-
// overhead_latency, status('success'|'error'), stream, session_id, user_agent, app`
|
|
21
|
-
// · 排序参数生效(order=asc 拿到最旧的);未知参数**被静默忽略**(试过 content=false 等一律无效)
|
|
22
|
-
// · `content_summary` 里是**完整提示词内容**,每行约 6.7 KB —— 聚合时一律丢掉(面板不该看内容)
|
|
23
|
-
// · 无 token → 401;limit 上限在 1000(5000 → 400)
|
|
24
|
-
// · `stats` 块除 total_requests 外全是 0,不可信 → 自己按行聚合
|
|
25
|
-
//
|
|
26
|
-
// 因此轮询策略:先 `limit=1` 探一下(`total_count` + 最新 id 就是"有没有变"的指纹),
|
|
27
|
-
// 变了才抓 `limit` 行回来聚合 —— 空闲时每分钟只有一次 6 KB 的请求。
|
|
13
|
+
// 行形状(gate/lanes.js 的 attemptRow,**没有密钥材料**):
|
|
14
|
+
// ts, req, n, chain, agent, provider, model, lane, key(键的"名字"), status('ok'|'error'),
|
|
15
|
+
// code, latency_ms, retries, detail
|
|
28
16
|
|
|
29
17
|
export const STATS_DEFAULTS = Object.freeze({
|
|
30
18
|
/** 轮询间隔(unref 定时器,失败只记日志) */
|
|
31
19
|
intervalMs: 60_000,
|
|
32
|
-
/**
|
|
33
|
-
limit:
|
|
20
|
+
/** 每次读多少行(读的是台账**尾部**的若干行) */
|
|
21
|
+
limit: 400,
|
|
34
22
|
/** 聚合窗口:只有落在这个窗口里的行参与统计 */
|
|
35
23
|
windowMs: 24 * 60 * 60 * 1000,
|
|
36
|
-
/** 模型目录的刷新间隔(`/v1/models`
|
|
24
|
+
/** 模型目录的刷新间隔(`/v1/models` 会去上游探测池;别跟统计同频) */
|
|
37
25
|
modelsIntervalMs: 30 * 60 * 1000,
|
|
38
26
|
timeoutMs: 6_000,
|
|
39
27
|
})
|
|
40
28
|
|
|
41
|
-
/** 面板要的形状里保留哪些字段 —— 其余(尤其 content_summary)一律丢掉。 */
|
|
42
|
-
const KEEP_FIELDS = [
|
|
43
|
-
'id',
|
|
44
|
-
'timestamp',
|
|
45
|
-
'provider',
|
|
46
|
-
'model',
|
|
47
|
-
'fallback_index',
|
|
48
|
-
'number_of_retries',
|
|
49
|
-
'routing_rule_id',
|
|
50
|
-
'routing_rule_name',
|
|
51
|
-
'selected_key_name',
|
|
52
|
-
'latency',
|
|
53
|
-
'upstream_latency',
|
|
54
|
-
'status',
|
|
55
|
-
'stream',
|
|
56
|
-
'session_id',
|
|
57
|
-
'prompt_tokens',
|
|
58
|
-
'completion_tokens',
|
|
59
|
-
'total_tokens',
|
|
60
|
-
]
|
|
61
|
-
|
|
62
29
|
// ── 纯函数(可测) ─────────────────────────────────────────────────────────
|
|
63
30
|
|
|
64
31
|
/** 最近秩百分位(不做插值 —— 面板看的是"典型有多慢",不是数学分位)。输入会被排序副本。 */
|
|
@@ -83,19 +50,6 @@ export function summarizeLatency(values) {
|
|
|
83
50
|
}
|
|
84
51
|
}
|
|
85
52
|
|
|
86
|
-
/** `reeve/pro` → `pro`;不是 REEVE 的档位规则就返回 null("不属于本层")。 */
|
|
87
|
-
export function tierOfRuleName(name) {
|
|
88
|
-
const m = /^reeve\/(flash|pro|multimodal)$/.exec(String(name ?? ''))
|
|
89
|
-
return m ? m[1] : null
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** 丢内容、留计数:面板只该看归因与延迟,不该看提示词。 */
|
|
93
|
-
export function normalizeLogRow(row) {
|
|
94
|
-
const out = {}
|
|
95
|
-
for (const k of KEEP_FIELDS) if (k in row) out[k] = row[k]
|
|
96
|
-
return out
|
|
97
|
-
}
|
|
98
|
-
|
|
99
53
|
/** `provider/model` 的引用串(链里就是这个写法)。 */
|
|
100
54
|
export function modelRef(row) {
|
|
101
55
|
return row?.provider && row?.model ? `${row.provider}/${row.model}` : null
|
|
@@ -106,73 +60,72 @@ export function parseTs(value) {
|
|
|
106
60
|
return Number.isFinite(t) ? t : null
|
|
107
61
|
}
|
|
108
62
|
|
|
63
|
+
/** 台账一排(`createLedger.readLine` 的产物解析后)→ 行对象;坏行返回 null(调用方丢)。 */
|
|
64
|
+
export function parseGateRow(line) {
|
|
65
|
+
try {
|
|
66
|
+
const row = JSON.parse(String(line))
|
|
67
|
+
return row && typeof row === 'object' ? row : null
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
109
73
|
/**
|
|
110
|
-
*
|
|
111
|
-
* `source` 只表达"这批数字从哪来",`window` 里给出实际覆盖的时间范围(别让面板假装是 24h)。
|
|
74
|
+
* GATE 台账行 → 归因统计(请求级 + 尝试级)。
|
|
112
75
|
*
|
|
113
|
-
*
|
|
114
|
-
* ·
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* ·
|
|
76
|
+
* 口径:
|
|
77
|
+
* · 一次调用 = 一个 `req`(没有 `req` 的旧行按"每行一次调用"算,如实);
|
|
78
|
+
* `n` 是这一次调用里的第几次尝试(1 起);失败换候选 → n 递增。
|
|
79
|
+
* · `fallbackRequests` = 尝试次数 >1 的调用;`fallbackHops` = Σ(n_max − 1)。
|
|
80
|
+
* · 调用级成败看**最后一次尝试**的状态(换到谁成功就是成功)。
|
|
81
|
+
* · 延迟分两层:`attemptLatency`(单次上游往返)与 `requestLatency`(一次调用的总耗时 = 同一 req 的 latency_ms 之和)。
|
|
118
82
|
*/
|
|
119
|
-
export function
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const calls = withTs.filter((x) => x.row.model)
|
|
83
|
+
export function aggregateGateRows(rows, { now = Date.now(), windowMs = STATS_DEFAULTS.windowMs } = {}) {
|
|
84
|
+
const withTs = (rows ?? [])
|
|
85
|
+
.filter((r) => r && (r.provider || r.model))
|
|
86
|
+
.map((r) => ({ row: r, at: parseTs(r.ts) }))
|
|
124
87
|
const since = now - windowMs
|
|
125
|
-
const inWindow =
|
|
126
|
-
// 窗口内一条都没有(机器闲了很久)→ 退成"
|
|
127
|
-
const relaxed = inWindow.length === 0 &&
|
|
128
|
-
const used = relaxed ?
|
|
88
|
+
const inWindow = withTs.filter((x) => x.at != null && x.at >= since)
|
|
89
|
+
// 窗口内一条都没有(机器闲了很久)→ 退成"就用读到的这些行",并如实标 relaxed
|
|
90
|
+
const relaxed = inWindow.length === 0 && withTs.length > 0
|
|
91
|
+
const used = relaxed ? withTs : inWindow
|
|
129
92
|
|
|
130
|
-
const
|
|
93
|
+
const chains = new Map()
|
|
131
94
|
const providers = new Map()
|
|
132
95
|
const models = new Map()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
96
|
+
const byReq = new Map()
|
|
97
|
+
const byAttemptIndex = {}
|
|
98
|
+
const attemptLatencies = []
|
|
136
99
|
let ok = 0
|
|
137
100
|
let error = 0
|
|
138
|
-
let cancelled = 0
|
|
139
|
-
const latencies = []
|
|
140
101
|
|
|
141
|
-
for (const { row, at } of used) {
|
|
142
|
-
const isOk = row.status === '
|
|
143
|
-
const isCancelled = row.status === 'cancelled'
|
|
102
|
+
for (const [idx, { row, at }] of used.entries()) {
|
|
103
|
+
const isOk = row.status === 'ok'
|
|
144
104
|
if (isOk) ok += 1
|
|
145
|
-
else if (isCancelled) cancelled += 1
|
|
146
105
|
else error += 1
|
|
147
|
-
const lat = Number(row.
|
|
148
|
-
if (Number.isFinite(lat))
|
|
106
|
+
const lat = Number(row.latency_ms)
|
|
107
|
+
if (Number.isFinite(lat)) attemptLatencies.push(lat)
|
|
108
|
+
const n = Number(row.n)
|
|
109
|
+
const nIdx = Number.isFinite(n) && n >= 1 ? n : 1
|
|
110
|
+
byAttemptIndex[nIdx] = (byAttemptIndex[nIdx] ?? 0) + 1
|
|
149
111
|
|
|
150
|
-
const
|
|
151
|
-
const bucketKey = tier ?? '(非 REEVE 档位)'
|
|
112
|
+
const chainKey = row.chain ?? '(未标链)'
|
|
152
113
|
const bucket =
|
|
153
|
-
|
|
154
|
-
{
|
|
114
|
+
chains.get(chainKey) ??
|
|
115
|
+
{ chain: chainKey, agent: null, n: 0, ok: 0, error: 0, hops: 0, latency: [], providers: new Map() }
|
|
116
|
+
if (!bucket.agent && row.agent) bucket.agent = row.agent
|
|
155
117
|
bucket.n += 1
|
|
156
118
|
if (isOk) bucket.ok += 1
|
|
157
|
-
else if (isCancelled) bucket.cancelled += 1
|
|
158
119
|
else bucket.error += 1
|
|
159
|
-
|
|
160
|
-
if (fb > 0) {
|
|
161
|
-
bucket.fallbackRequests += 1
|
|
162
|
-
bucket.fallbackHops += fb
|
|
163
|
-
fallbackRequests += 1
|
|
164
|
-
fallbackHops += fb
|
|
165
|
-
}
|
|
166
|
-
byIndex[fb] = (byIndex[fb] ?? 0) + 1
|
|
120
|
+
if (nIdx > 1) bucket.hops += 1
|
|
167
121
|
if (Number.isFinite(lat)) bucket.latency.push(lat)
|
|
168
122
|
const ref = modelRef(row)
|
|
169
123
|
if (ref) bucket.providers.set(ref, (bucket.providers.get(ref) ?? 0) + 1)
|
|
170
|
-
|
|
124
|
+
chains.set(chainKey, bucket)
|
|
171
125
|
|
|
172
|
-
const p = providers.get(row.provider ?? '(未知)') ?? { provider: row.provider ?? '(未知)', n: 0, ok: 0, error: 0,
|
|
126
|
+
const p = providers.get(row.provider ?? '(未知)') ?? { provider: row.provider ?? '(未知)', n: 0, ok: 0, error: 0, latency: [], lastAt: null }
|
|
173
127
|
p.n += 1
|
|
174
128
|
if (isOk) p.ok += 1
|
|
175
|
-
else if (isCancelled) p.cancelled += 1
|
|
176
129
|
else p.error += 1
|
|
177
130
|
if (Number.isFinite(lat)) p.latency.push(lat)
|
|
178
131
|
if (at != null && (p.lastAt == null || at > p.lastAt)) p.lastAt = at
|
|
@@ -182,35 +135,38 @@ export function aggregateBifrostRows(rows, { now = Date.now(), windowMs = STATS_
|
|
|
182
135
|
const m = models.get(ref) ?? { ref, provider: row.provider, model: row.model, n: 0, ok: 0, error: 0, lastAt: null }
|
|
183
136
|
m.n += 1
|
|
184
137
|
if (isOk) m.ok += 1
|
|
185
|
-
else
|
|
138
|
+
else m.error += 1
|
|
186
139
|
if (at != null && (m.lastAt == null || at > m.lastAt)) m.lastAt = at
|
|
187
140
|
models.set(ref, m)
|
|
188
141
|
}
|
|
142
|
+
|
|
143
|
+
// 没有 `req` 的旧行按"每行一次调用"算(诚实:不能让它们被错并成一次)
|
|
144
|
+
const reqKey = row.req != null ? String(row.req) : `row:${idx}`
|
|
145
|
+
const req = byReq.get(reqKey) ?? { attempts: 0, maxN: 0, lastOk: null, chain: chainKey, totalMs: 0, at: null }
|
|
146
|
+
req.attempts += 1
|
|
147
|
+
if (nIdx > req.maxN) req.maxN = nIdx
|
|
148
|
+
req.lastOk = isOk
|
|
149
|
+
if (Number.isFinite(lat)) req.totalMs += lat
|
|
150
|
+
if (at != null && (req.at == null || at > req.at)) req.at = at
|
|
151
|
+
byReq.set(reqKey, req)
|
|
189
152
|
}
|
|
190
153
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.map(([ref, n]) => ({ ref, n }))
|
|
206
|
-
.sort((a, b) => b.n - a.n),
|
|
207
|
-
}))
|
|
208
|
-
.sort((a, b) => {
|
|
209
|
-
const ia = a.tier ? TIER_ORDER.indexOf(a.tier) : 99
|
|
210
|
-
const ib = b.tier ? TIER_ORDER.indexOf(b.tier) : 99
|
|
211
|
-
return ia - ib || b.n - a.n
|
|
212
|
-
})
|
|
154
|
+
let fallbackRequests = 0
|
|
155
|
+
let fallbackHops = 0
|
|
156
|
+
let requestsOk = 0
|
|
157
|
+
let requestsError = 0
|
|
158
|
+
const requestLatencies = []
|
|
159
|
+
for (const req of byReq.values()) {
|
|
160
|
+
if (req.maxN > 1) {
|
|
161
|
+
fallbackRequests += 1
|
|
162
|
+
fallbackHops += req.maxN - 1
|
|
163
|
+
}
|
|
164
|
+
if (req.lastOk === true) requestsOk += 1
|
|
165
|
+
else requestsError += 1
|
|
166
|
+
if (req.totalMs > 0) requestLatencies.push(req.totalMs)
|
|
167
|
+
}
|
|
213
168
|
|
|
169
|
+
const tsList = used.map((x) => x.at).filter((v) => v != null)
|
|
214
170
|
return {
|
|
215
171
|
window: {
|
|
216
172
|
ms: windowMs,
|
|
@@ -218,18 +174,29 @@ export function aggregateBifrostRows(rows, { now = Date.now(), windowMs = STATS_
|
|
|
218
174
|
until: tsList.length ? Math.max(...tsList) : null,
|
|
219
175
|
relaxed,
|
|
220
176
|
},
|
|
221
|
-
fetched:
|
|
222
|
-
probes: probes.length,
|
|
177
|
+
fetched: (rows ?? []).length,
|
|
223
178
|
used: used.length,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
179
|
+
attempts: used.length,
|
|
180
|
+
requests: byReq.size,
|
|
181
|
+
attemptsOk: ok,
|
|
182
|
+
attemptsError: error,
|
|
183
|
+
requestsOk,
|
|
184
|
+
requestsError,
|
|
185
|
+
attemptLatency: summarizeLatency(attemptLatencies),
|
|
186
|
+
requestLatency: summarizeLatency(requestLatencies),
|
|
187
|
+
fallback: { requests: fallbackRequests, hops: fallbackHops, byIndex: byAttemptIndex },
|
|
188
|
+
byChain: [...chains.values()]
|
|
189
|
+
.map(({ latency, providers: ps, ...rest }) => ({
|
|
190
|
+
...rest,
|
|
191
|
+
latency: summarizeLatency(latency),
|
|
192
|
+
providers: [...ps.entries()]
|
|
193
|
+
.map(([ref, n]) => ({ ref, n }))
|
|
194
|
+
.sort((a, b) => b.n - a.n)
|
|
195
|
+
.slice(0, 8),
|
|
196
|
+
}))
|
|
197
|
+
.sort((a, b) => b.n - a.n),
|
|
231
198
|
byProvider: [...providers.values()]
|
|
232
|
-
.map((p) => ({ provider: p.provider, n: p.n, ok: p.ok, error: p.error,
|
|
199
|
+
.map((p) => ({ provider: p.provider, n: p.n, ok: p.ok, error: p.error, lastAt: p.lastAt, latency: summarizeLatency(p.latency) }))
|
|
233
200
|
.sort((a, b) => b.n - a.n),
|
|
234
201
|
byModel: [...models.values()].sort((a, b) => b.n - a.n).slice(0, 20),
|
|
235
202
|
}
|
|
@@ -294,76 +261,72 @@ export function aggregateLedger(records, { now = Date.now() } = {}) {
|
|
|
294
261
|
|
|
295
262
|
/**
|
|
296
263
|
* 快照里的 `stats` 一节。
|
|
297
|
-
* `source` 三态:`
|
|
264
|
+
* `source` 三态:`gate.ledger`(拿到了)/ `unavailable`(拿不到,附 error)/ `disabled`。
|
|
298
265
|
*/
|
|
299
|
-
export function buildStats({
|
|
300
|
-
return { generatedAt: now, checkedAt, windowMs, source, error,
|
|
266
|
+
export function buildStats({ gate = null, orchestrate = null, now = Date.now(), windowMs = STATS_DEFAULTS.windowMs, source = 'unavailable', error = null, checkedAt = null } = {}) {
|
|
267
|
+
return { generatedAt: now, checkedAt, windowMs, source, error, gate, orchestrate }
|
|
301
268
|
}
|
|
302
269
|
|
|
303
270
|
// ── IO(薄,注入 fetch 便于测试) ────────────────────────────────────────────
|
|
304
271
|
|
|
305
|
-
function headers(token) {
|
|
306
|
-
return token ? { 'X-Bifrost-Setup-Token': token } : {}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
272
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
273
|
+
* 读 GATE 台账的**尾部**若干行(JSONL)。只读文件尾 —— 台账会轮转,但就算没轮转也不该整读。
|
|
274
|
+
* 第一行可能是被截断的半行 → 丢掉(解析失败的行一律丢,不猜)。
|
|
275
|
+
* @returns {{rows: any[], truncated: boolean, error: string|null}}
|
|
312
276
|
*/
|
|
313
|
-
export async function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
offset = 0,
|
|
319
|
-
order = 'desc',
|
|
320
|
-
fetchImpl = fetch,
|
|
321
|
-
timeoutMs = STATS_DEFAULTS.timeoutMs,
|
|
322
|
-
} = {}) {
|
|
323
|
-
const url = `http://${host}:${port}/api/logs?limit=${Number(limit)}&offset=${Number(offset)}&sort_by=timestamp&order=${order}`
|
|
277
|
+
export async function readLedgerTail({ path, limit = STATS_DEFAULTS.limit, tailBytes = 512 * 1024, readFileImpl = null, statImpl = null } = {}) {
|
|
278
|
+
const { open, stat } = await import('node:fs/promises')
|
|
279
|
+
const doStat = statImpl ?? stat
|
|
280
|
+
const doOpen = readFileImpl ?? open
|
|
281
|
+
let fh = null
|
|
324
282
|
try {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
283
|
+
const st = await doStat(path)
|
|
284
|
+
const start = Math.max(0, Number(st.size) - tailBytes)
|
|
285
|
+
fh = await doOpen(path, 'r')
|
|
286
|
+
const len = Number(st.size) - start
|
|
287
|
+
const buf = Buffer.alloc(len)
|
|
288
|
+
await fh.read(buf, 0, len, start)
|
|
289
|
+
const text = buf.toString('utf8')
|
|
290
|
+
const lines = text.split('\n').filter((l) => l.trim().length > 0)
|
|
291
|
+
const truncated = start > 0
|
|
292
|
+
const body = truncated ? lines.slice(1) : lines
|
|
293
|
+
const rows = body.map(parseGateRow).filter(Boolean)
|
|
294
|
+
return { rows: rows.slice(-limit), truncated, error: null }
|
|
337
295
|
} catch (err) {
|
|
338
|
-
return { rows: [],
|
|
296
|
+
return { rows: [], truncated: false, error: String(err?.message ?? err).slice(0, 140) }
|
|
297
|
+
} finally {
|
|
298
|
+
if (fh) await fh.close().catch(() => {})
|
|
339
299
|
}
|
|
340
300
|
}
|
|
341
301
|
|
|
342
|
-
/**
|
|
343
|
-
export async function
|
|
302
|
+
/** GATE 自己的模型目录(`provider/model` ∪ 链名)—— 面板选择器的候选来源之一。 */
|
|
303
|
+
export async function fetchGateModels({ port, host = '127.0.0.1', fetchImpl = fetch, timeoutMs = STATS_DEFAULTS.timeoutMs } = {}) {
|
|
344
304
|
try {
|
|
345
305
|
const res = await fetchImpl(`http://${host}:${port}/v1/models`, { signal: AbortSignal.timeout(timeoutMs) })
|
|
346
|
-
if (res.status !== 200) return { models: [], error: `HTTP ${res.status}` }
|
|
306
|
+
if (res.status !== 200) return { models: [], chains: [], error: `HTTP ${res.status}` }
|
|
347
307
|
const doc = await res.json()
|
|
308
|
+
const chains = []
|
|
348
309
|
const models = (Array.isArray(doc?.data) ? doc.data : [])
|
|
349
310
|
.map((m) => {
|
|
350
311
|
const ref = String(m?.id ?? '')
|
|
312
|
+
if (m?.owned_by === 'reeve') {
|
|
313
|
+
chains.push(ref)
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
351
316
|
const i = ref.indexOf('/')
|
|
352
317
|
if (i <= 0) return null
|
|
353
318
|
return { ref, provider: ref.slice(0, i), model: ref.slice(i + 1), context: Number(m?.context_length) || null }
|
|
354
319
|
})
|
|
355
320
|
.filter(Boolean)
|
|
356
|
-
return { models, error: null }
|
|
321
|
+
return { models, chains, error: null }
|
|
357
322
|
} catch (err) {
|
|
358
|
-
return { models: [], error: String(err?.message ?? err).slice(0, 140) }
|
|
323
|
+
return { models: [], chains: [], error: String(err?.message ?? err).slice(0, 140) }
|
|
359
324
|
}
|
|
360
325
|
}
|
|
361
326
|
|
|
362
327
|
/**
|
|
363
|
-
* 面板选择器的候选 =
|
|
364
|
-
*
|
|
365
|
-
* (zhipuai-coding-plan 的对话路径是自定义的,`/v1/models` 探不到;opencode zen 与
|
|
366
|
-
* alibaba-cn 同理)。发现不全就如实标出来,而不是假装列表就是全集。
|
|
328
|
+
* 面板选择器的候选 = GATE 目录(声明 ∪ 池)∪ 链上已经在用的 ∪ 引擎注册的(reeve/*)。
|
|
329
|
+
* 发现不全就如实标出来(每条候选带 sources),而不是假装列表就是全集。
|
|
367
330
|
*/
|
|
368
331
|
export function mergeModelCandidates({ discovered = [], chained = [], engine = [] } = {}) {
|
|
369
332
|
const out = new Map()
|
|
@@ -377,7 +340,7 @@ export function mergeModelCandidates({ discovered = [], chained = [], engine = [
|
|
|
377
340
|
if (!cur.sources.includes(source)) cur.sources.push(source)
|
|
378
341
|
out.set(ref, cur)
|
|
379
342
|
}
|
|
380
|
-
for (const m of discovered) add(m, '
|
|
343
|
+
for (const m of discovered) add(m, 'gate')
|
|
381
344
|
for (const ref of chained) add(ref, 'chain')
|
|
382
345
|
for (const m of engine) add(m, 'engine')
|
|
383
346
|
return [...out.values()].sort((a, b) => a.ref.localeCompare(b.ref))
|