mslxdff 0.1.212 → 0.1.214
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/package.json +1 -1
- package/src/cli/format.js +8 -0
- package/src/cli/group-test.js +7 -7
- package/src/model-trace.js +20 -0
- package/src/routes/groups-relay.js +48 -10
- package/src/routes/index.js +5 -1
- package/src/routes/relay-queue.js +4 -1
- package/src/runtime/broadband.js +5 -0
package/package.json
CHANGED
package/src/cli/format.js
CHANGED
|
@@ -76,6 +76,14 @@ export function fmtEvent(e) {
|
|
|
76
76
|
return `${head} fallback! 客户端请求 ${m(e.requested)} 实际返回 ${m(e.actual)} 原因=${m(e.reason)} via=${m(e.via)} notice=${m(e.notice)}`;
|
|
77
77
|
case "relay-try":
|
|
78
78
|
return `${head} relay try 给宽带中继请求 target=${e.target} model=${m(e.model)} via=${e.via}`;
|
|
79
|
+
case "relay-miss":
|
|
80
|
+
return `${head} relay miss 宽带中继无候选(${e.reason || "-"}${e.candidates != null ? ` candidates=${e.candidates}` : ""})`;
|
|
81
|
+
case "relay-received":
|
|
82
|
+
return `${head} relay recv ${e.via === "broadband" ? "经中继" : "直连"}转给 target=${e.target} group=${e.group} model=${m(e.model)} msgs=${e.messages ?? "-"} tools=${e.tools ?? "-"} maxTok=${e.maxTokens ?? "-"}`;
|
|
83
|
+
case "relay-queued":
|
|
84
|
+
return `${head} relay queued 已入队交成员 target=${e.target} reqId=${e.reqId || "-"}`;
|
|
85
|
+
case "relay-member-result":
|
|
86
|
+
return `${head} relay result 成员回 target=${e.target} ${e.ok ? "ok" : "FAIL"} ${e.status ? `HTTP ${e.status}` : ""} ${e.ms ? `${e.ms}ms` : ""}${e.respChars ? ` resp=${e.respChars}B` : ""} ${m(e.error || e.respBody || "")}`;
|
|
79
87
|
case "relay-fail":
|
|
80
88
|
return `${head} relay fail ${e.target} ${e.status ? `HTTP ${e.status}` : ""} ${m(e.message)}`;
|
|
81
89
|
case "relay-ip-change":
|
package/src/cli/group-test.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 为什么需要它:这条链路跨 3 台机器 + 1 道鉴权,坏了只会看到一句 `502 fetch failed`,
|
|
4
4
|
// 完全无法定位是哪一跳断的。本模块**按阶段**记录,把断点直接指出来:
|
|
5
|
-
// ① 定位本机组/组长 → ② 取组长 token(调组长 forward
|
|
5
|
+
// ① 定位本机组/组长 → ② 取组长 token(调组长 forward 用组长自己的 token 恒可)→
|
|
6
6
|
// ③ POST 组长 /v1/groups/relay/forward → ④ 判读回包(200 拿内容 / 401 鉴权 / 404 成员不存在 / 502·504 转发失败)
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// 鉴权(2026-10-19 修正):forward 路由由 `requiresAuth:true` 改为鉴权下沉到 handler——
|
|
9
|
+
// 接受**组长 token 或本组成员 token**。旧实现只认组长 token,导致生产里成员经组长转发一律 401、
|
|
10
|
+
// 组员接力从未通过(deepseek-v4.1-flash-free.log 所有 relay-fail 均 401)。本命令仍用组长 token 调
|
|
11
|
+
// (组长 token 恒可),若拿不到则明确报出这一步。
|
|
12
12
|
import { loadPeers, loadGroupsJoined, loadToken } from "../state.js";
|
|
13
13
|
import { compatFetch, timeoutSignal } from "../compat.js";
|
|
14
14
|
|
|
@@ -87,8 +87,8 @@ export async function runGroupLinkTest({
|
|
|
87
87
|
// 按阶段归类失败 —— 这是本命令存在的意义:一眼看出断在哪一跳
|
|
88
88
|
if (res.status === 401) {
|
|
89
89
|
return { status: "error", stage: "auth", httpStatus: 401, target, leaderUrl: lead.leaderUrl, group: groupName, ms,
|
|
90
|
-
error: "组长拒了鉴权(401
|
|
91
|
-
hint: "
|
|
90
|
+
error: "组长拒了鉴权(401):本机持有的 token 既不是组长 token、也不在该组成员表里",
|
|
91
|
+
hint: "检查本机是否真的已加入该组(state.peers[leader].token / groupsJoined 是否过期);用 -addtogroup 重新入组。注:0.1.213+ 组长已接受成员 token,正常入组的节点不该再 401。" };
|
|
92
92
|
}
|
|
93
93
|
if (res.status === 404) {
|
|
94
94
|
return { status: "error", stage: "target-missing", httpStatus: 404, target, leaderUrl: lead.leaderUrl, group: groupName, ms, error: txt.slice(0, 200), hint: "该目标不在组内(名称/url 对不上);用 -group list 核对" };
|
package/src/model-trace.js
CHANGED
|
@@ -87,6 +87,10 @@ const DECISION_FIELDS = [
|
|
|
87
87
|
"heldOpen", "terminalForm", "requestWaitedMs", "budgetMs", "budgetOut",
|
|
88
88
|
// 终局判决(ADR-0058):取证时第一眼要看的三件——判的哪种、命中哪条判据、是否因此没问组员
|
|
89
89
|
"kind", "marker", "peersSkipped",
|
|
90
|
+
// 中继/组员目标(2026-10-19):跨机链路排障第一眼要的是**这一跳打给了谁**——target= 目标成员
|
|
91
|
+
// (url 或 relay://id)、group= 组、leaderUrl= 经谁中继;缺了这几项,relay-try/fail 只剩状态码,
|
|
92
|
+
// 看日志根本认不出是哪个组员出的问题(用户实证要求「日志要全面」)。
|
|
93
|
+
"target", "group", "leaderUrl", "candidates", "tried",
|
|
90
94
|
];
|
|
91
95
|
|
|
92
96
|
function decisionKv(data = {}) {
|
|
@@ -141,6 +145,22 @@ export function formatModelTrace({ type, reqId, model, data = {}, request = null
|
|
|
141
145
|
detail = `upstream_response status=${data.status ?? "-"} finish=${d.sawFinishReason || "-"} chunks=${d.receivedChunks ?? "-"} bytes=${d.receivedBytes ?? "-"} tools=${d.toolCalls ?? 0} chars=${d.chars ?? 0} ${usageTxt} elapsed=${data.totalMs ?? "-"}ms${data.upstream ? ` upstream=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
|
|
142
146
|
} else if (type === "result" || type === "client-response") {
|
|
143
147
|
detail = `client status=${data.status ?? "-"} via=${data.via || "-"} actual=${data.actual || model || "-"} fallback=${data.fallback?.fallback ? 1 : 0} total=${totalMs ?? data.durationMs ?? "-"}ms${data.upstream ? ` upstream=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
|
|
148
|
+
} else if (type === "relay-try") {
|
|
149
|
+
detail = `指定宽带中继 target=${data.target || "?"} group=${data.group || "-"} via=${data.via || "-"}`;
|
|
150
|
+
} else if (type === "relay-received") {
|
|
151
|
+
detail = `中继收到 转给 target=${data.target || "?"} group=${data.group || "-"} via=${data.via || "-"} msgs=${data.messages ?? "-"} tools=${data.tools ?? "-"} maxTok=${data.maxTokens ?? "-"}`;
|
|
152
|
+
} else if (type === "relay-queued") {
|
|
153
|
+
detail = `已入队交成员 target=${data.target || "?"} reqId=${data.reqId || "-"}`;
|
|
154
|
+
} else if (type === "relay-member-result") {
|
|
155
|
+
detail = `成员回 target=${data.target || "?"} ${data.ok ? "ok" : "FAIL"}${data.status ? ` status=${data.status}` : ""}${data.ms ? ` ${data.ms}ms` : ""}${data.respChars ? ` resp=${data.respChars}B` : ""} ${safeText(data.error || data.respBody || "", 200)}`;
|
|
156
|
+
} else if (type === "relay-fail") {
|
|
157
|
+
detail = `中继失败 target=${data.target || "?"} group=${data.group || "-"} via=${data.via || "-"}${data.status ? ` status=${data.status}` : ""} ${safeText(data.message || "")}`;
|
|
158
|
+
} else if (type === "relay-miss") {
|
|
159
|
+
detail = `宽带中继无候选可试(全部候选未通/无存活成员,交回本地兜底)`;
|
|
160
|
+
} else if (type === "peer-race-start") {
|
|
161
|
+
detail = `组员竞速开始 peers=${data.peers ?? "-"}`;
|
|
162
|
+
} else if (type === "peer-budget-out") {
|
|
163
|
+
detail = `组员竞速预算用尽 reason=${safeText(data.reason || "", 80)} budgetMs=${data.budgetMs ?? "-"} remaining=${data.remaining ?? "-"}`;
|
|
144
164
|
} else detail = safeText(`${decisionKv(data)} ${data.message || data.error || ""}`, 200);
|
|
145
165
|
return `[${base.time}] [req=${base.req}] [model=${base.model}] [stage=${base.stage}] ${detail}`;
|
|
146
166
|
}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import { clientIp, json, readBody, parseHops, errMsg } from "./helpers.js";
|
|
1
|
+
import { clientIp, json, readBody, parseHops, errMsg, authorized } from "./helpers.js";
|
|
2
2
|
import { DEFAULT_MAX_HOPS } from "../peers.js";
|
|
3
3
|
import { enqueueRelay, dequeueRelayForPoll, resolveRelay, subscribeStream, unsubscribeStream } from "./relay-queue.js";
|
|
4
4
|
import { compatFetch } from "../compat.js";
|
|
5
5
|
|
|
6
|
+
/** 请求规模摘要(给中继日志用):消息数/角色分布/工具数/maxTokens/model —— 排障第一眼要看清"转了什么"。 */
|
|
7
|
+
function relayReqSummary(b = {}) {
|
|
8
|
+
const msgs = Array.isArray(b?.messages) ? b.messages : [];
|
|
9
|
+
const roles = {};
|
|
10
|
+
for (const m of msgs) roles[String(m?.role || "?")] = (roles[String(m?.role || "?")] || 0) + 1;
|
|
11
|
+
return { model: b?.model || "-", stream: b?.stream === true, messages: msgs.length, roles, tools: Array.isArray(b?.tools) ? b.tools.length : 0, maxTokens: Number(b?.max_tokens ?? b?.max_completion_tokens) || null };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 响应正文摘要(供中继日志):单行化 + 截断,只为让人看出上游回的是什么。 */
|
|
15
|
+
function snippet(v, n = 300) {
|
|
16
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
17
|
+
return String(s || "").replace(/\s+/g, " ").slice(0, n);
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
export async function heartbeatHandler({ req, res, groups, bus, logs }) {
|
|
7
21
|
const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
|
|
8
22
|
if (!auth) return json(res, 401, { error: "bearer token required" });
|
|
@@ -148,11 +162,20 @@ export async function streamHandler({ req, res, groups, bus, logs }) {
|
|
|
148
162
|
await new Promise(() => {});
|
|
149
163
|
}
|
|
150
164
|
|
|
151
|
-
export async function forwardHandler({ req, res, groups, bus, logs }) {
|
|
165
|
+
export async function forwardHandler({ req, res, groups, bus, logs, token }) {
|
|
152
166
|
let body;
|
|
153
167
|
try { body = await readBody(req); } catch { return json(res, 400, { error: "Invalid JSON body" }); }
|
|
154
168
|
const groupName = body?.group || body?.name;
|
|
155
169
|
const target = body?.target || body?.url;
|
|
170
|
+
// 鉴权(对齐 heartbeat/poll/result):接受**组长自己的 token**(本机 forward 自用)**或本组的任一成员 token**
|
|
171
|
+
// (成员经组长转发用的就是成员 token)。路由层 authorized() 只认组长那一个 token,故本路由 requiresAuth:false,
|
|
172
|
+
// 校验下沉到这里——否则成员中继一律 401(现网 2026-10-19:所有 relay-fail 均 status=401)。
|
|
173
|
+
const bearer = (/^Bearer (.+)$/.exec(req.headers["authorization"] || "") || [])[1] || "";
|
|
174
|
+
const authed = Boolean(bearer) && (authorized(req, token) || Boolean(groupName && groups?.membersForToken?.(groupName, bearer)));
|
|
175
|
+
if (!authed) {
|
|
176
|
+
res.setHeader("WWW-Authenticate", "Bearer");
|
|
177
|
+
return json(res, 401, { error: "Unauthorized" });
|
|
178
|
+
}
|
|
156
179
|
const hops = parseHops(req.headers["x-mslxdff-hops"] || body?.hops);
|
|
157
180
|
if (!groupName || !target) return json(res, 400, { error: "group and target required" });
|
|
158
181
|
if (hops >= DEFAULT_MAX_HOPS) return json(res, 429, { error: "max hops exceeded" });
|
|
@@ -160,10 +183,15 @@ export async function forwardHandler({ req, res, groups, bus, logs }) {
|
|
|
160
183
|
const targetMember = Object.values(members).find((m) => m.url === target) || Object.entries(members).find(([id]) => id === target)?.[1];
|
|
161
184
|
if (!targetMember) return json(res, 404, { error: `target ${target} not found in group ${groupName}` });
|
|
162
185
|
const isBb = targetMember.kind === "broadband" || String(targetMember.url).startsWith("relay://");
|
|
186
|
+
const reqSummary = relayReqSummary(body.body || body);
|
|
187
|
+
const fwdEvt = (type, extra) => { const e = { ts: Date.now(), type, group: groupName, target, hops, ...extra }; try { logs?.appendEvent?.(e); } catch {} try { bus?.emit?.(e); } catch {} };
|
|
188
|
+
// 组长「收到」:谁让我转给谁、转的是什么规模 —— 中继全流程的第一条日志
|
|
189
|
+
fwdEvt("relay-received", { via: isBb ? "broadband" : "direct", reqId: body.reqId || null, ...reqSummary });
|
|
163
190
|
if (!isBb) {
|
|
164
191
|
// 标记走直连:**在 fetch 之前**就设,这样直连失败(catch 里的 502)也带着它——
|
|
165
192
|
// 那正是最需要区分的一跳(组长直连该成员不通 vs 宽带成员回报失败,两者的 body 可能一模一样)。
|
|
166
193
|
res.setHeader("x-mslxdff-relay-via", "direct");
|
|
194
|
+
const t0 = Date.now();
|
|
167
195
|
try {
|
|
168
196
|
const fwdBody = body.body || body;
|
|
169
197
|
const r = await compatFetch(`${targetMember.url}/v1/chat/completions`, {
|
|
@@ -171,41 +199,49 @@ export async function forwardHandler({ req, res, groups, bus, logs }) {
|
|
|
171
199
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${targetMember.token || ""}`, "x-mslxdff-hops": String(hops + 1), "x-mslxdff-model-lock": fwdBody.model || "", "Accept": "text/event-stream" },
|
|
172
200
|
body: JSON.stringify(fwdBody),
|
|
173
201
|
});
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
202
|
+
const isSSE = r.headers.get("content-type")?.includes("text/event-stream");
|
|
203
|
+
if (isSSE) {
|
|
204
|
+
// 流式直连:没法先读全 body 再转发,就记「已建立」+ 状态码
|
|
205
|
+
fwdEvt("relay-forward", { via: "direct", ok: r.status < 400, status: r.status, ms: Date.now() - t0, detail: "stream 直连建立" });
|
|
206
|
+
res.statusCode = r.status;
|
|
179
207
|
res.setHeader("Content-Type", "text/event-stream");
|
|
180
208
|
if (r.body) for await (const c of r.body) res.write(c);
|
|
181
209
|
res.end();
|
|
182
210
|
} else {
|
|
183
211
|
const txt = await r.text();
|
|
212
|
+
// 直连非流式:把上游回的正文摘要也记下(这就是「组员回了什么」)
|
|
213
|
+
fwdEvt("relay-forward", { via: "direct", ok: r.status < 400, status: r.status, ms: Date.now() - t0, respChars: txt.length, respBody: snippet(txt) });
|
|
214
|
+
res.statusCode = r.status;
|
|
184
215
|
res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
|
|
185
216
|
res.end(txt);
|
|
186
217
|
}
|
|
187
218
|
return;
|
|
188
219
|
} catch (err) {
|
|
220
|
+
fwdEvt("relay-forward", { via: "direct", ok: false, status: 502, ms: Date.now() - t0, error: errMsg(err) });
|
|
189
221
|
return json(res, 502, { error: errMsg(err) });
|
|
190
222
|
}
|
|
191
223
|
}
|
|
192
224
|
const reqId = body.reqId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
193
225
|
const fwdBody = body.body || { model: body.model, messages: body.messages, stream: body.stream };
|
|
194
|
-
const evtData = { ts: Date.now(), type: "relay-forward", target, via: "leader", group: groupName, hops, reqId, model: fwdBody.model };
|
|
195
|
-
if (bus) bus.emit(evtData);
|
|
196
|
-
logs?.appendEvent?.(evtData);
|
|
197
226
|
const staleMs = Number(process.env.MSLXDFF_BROADBAND_STALE_MS) > 0 ? Number(process.env.MSLXDFF_BROADBAND_STALE_MS) : 90_000;
|
|
198
227
|
if (typeof targetMember.lastSeen === "number" && Date.now() - targetMember.lastSeen > staleMs) {
|
|
199
228
|
res.setHeader("x-mslxdff-relay-via", "broadband");
|
|
229
|
+
fwdEvt("relay-member-result", { reqId, ok: false, status: 502, error: "broadband member stale (no heartbeat)" });
|
|
200
230
|
return json(res, 502, { error: "broadband member stale (no heartbeat)" });
|
|
201
231
|
}
|
|
202
232
|
// 标记走的是宽带中继:下游据此一眼分清「组长直连失败(502 fetch failed)」vs「宽带成员回报的失败」,
|
|
203
233
|
// 两者响应体可能都是 {"error":"fetch failed"},只有这个头能区分(成员侧已把真实原因写进 errors.log)。
|
|
204
234
|
res.setHeader("x-mslxdff-relay-via", "broadband");
|
|
235
|
+
const tFwd = Date.now();
|
|
236
|
+
// 组长「已交给成员」:入队即记 —— 成员有没有真正收到,由成员侧那条 relay-task-recv 佐证
|
|
237
|
+
fwdEvt("relay-queued", { reqId, via: "broadband" });
|
|
205
238
|
try {
|
|
206
239
|
const resultPromise = enqueueRelay({ group: groupName, target: targetMember.url, reqId, body: fwdBody, hops });
|
|
207
240
|
const result = await resultPromise;
|
|
208
241
|
if (result && typeof result.status === "number") {
|
|
242
|
+
const rb = result.body == null ? "" : (typeof result.body === "string" ? result.body : JSON.stringify(result.body));
|
|
243
|
+
// 组长「收到成员回什么」:状态码 + 正文摘要 + 耗时 —— 中继全流程的收尾日志
|
|
244
|
+
fwdEvt("relay-member-result", { reqId, ok: result.status < 400, status: result.status, ms: Date.now() - tFwd, respChars: rb.length, respBody: snippet(rb) });
|
|
209
245
|
res.statusCode = result.status;
|
|
210
246
|
if (result.status >= 500) res.setHeader("x-mslxdff-relay-member-status", String(result.status));
|
|
211
247
|
if (result.headers) for (const [k, v] of Object.entries(result.headers)) res.setHeader(k, v);
|
|
@@ -215,8 +251,10 @@ export async function forwardHandler({ req, res, groups, bus, logs }) {
|
|
|
215
251
|
} else res.end();
|
|
216
252
|
return;
|
|
217
253
|
}
|
|
254
|
+
fwdEvt("relay-member-result", { reqId, ok: true, status: 200, ms: Date.now() - tFwd, respBody: snippet(result) });
|
|
218
255
|
return json(res, 200, result);
|
|
219
256
|
} catch (err) {
|
|
257
|
+
fwdEvt("relay-member-result", { reqId, ok: false, status: 504, ms: Date.now() - tFwd, error: errMsg(err) || "relay timeout" });
|
|
220
258
|
return json(res, 504, { error: errMsg(err) || "relay timeout" });
|
|
221
259
|
}
|
|
222
260
|
}
|
package/src/routes/index.js
CHANGED
|
@@ -110,7 +110,11 @@ const ROUTES = [
|
|
|
110
110
|
{
|
|
111
111
|
method: "POST",
|
|
112
112
|
path: "/v1/groups/relay/forward",
|
|
113
|
-
|
|
113
|
+
// 鉴权下沉到 forwardHandler:接受**组长 token 或本组成员 token**(成员经组长转发的正是成员 token)。
|
|
114
|
+
// 路由层 authorized(req, token) 只认组长自己那一个 token ⇒ 用 requiresAuth:true 会把成员中继一律 401
|
|
115
|
+
// (现网 2026-10-19 实证:deepseek-v4.1-flash-free.log 里所有 relay-fail 都是 status=401,组员接力从未通过)。
|
|
116
|
+
// 同族 heartbeat/poll/result 同理走 requiresAuth:false + membersForToken 自校验。
|
|
117
|
+
requiresAuth: false,
|
|
114
118
|
handler: forwardHandler,
|
|
115
119
|
},
|
|
116
120
|
{
|
|
@@ -140,7 +140,10 @@ export async function tryBroadbandRelay({ groups, token: myToken, model, body, h
|
|
|
140
140
|
}
|
|
141
141
|
} catch {}
|
|
142
142
|
}
|
|
143
|
-
if (!allCandidates.length)
|
|
143
|
+
if (!allCandidates.length) {
|
|
144
|
+
evt?.("relay-miss", { reqId, model, reason: "no-candidates", candidates: 0 });
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
144
147
|
for (const cand of allCandidates) {
|
|
145
148
|
try {
|
|
146
149
|
evt?.("relay-try", { reqId, model, via: cand.via, target: cand.target, group: cand.group });
|
package/src/runtime/broadband.js
CHANGED
|
@@ -24,6 +24,11 @@ export function createBroadbandExec({ token, upstream, logs, log = console.log,
|
|
|
24
24
|
const t0 = Date.now();
|
|
25
25
|
const model = String(body?.model || "");
|
|
26
26
|
const fl = withOutputFloor(body); // 首发输出额度托底:leader 转来的 body 不经 pipeline,成员侧自托(评审 P1)
|
|
27
|
+
// 成员「收到任务」:谁转来的、是什么规模、转给谁 —— 中继全流程在**成员这一跳**的入口日志。
|
|
28
|
+
// 没有它,组长侧只知道"入队了",无从确认成员是否真收到、收到的是什么(用户实证要求「日志要全面」)。
|
|
29
|
+
const nMsgs = Array.isArray(body?.messages) ? body.messages.length : 0;
|
|
30
|
+
const nTools = Array.isArray(body?.tools) ? body.tools.length : 0;
|
|
31
|
+
log(`broadband task recv: group=${g.name} req=${reqId} from leader=${g.leaderUrl} model=${model} messages=${nMsgs} tools=${nTools}`);
|
|
27
32
|
if (fl !== body) log(`output-floor(broadband): req=${reqId} ${(body?.max_tokens ?? body?.max_completion_tokens)}→${(fl.max_tokens ?? fl.max_completion_tokens)}`);
|
|
28
33
|
try {
|
|
29
34
|
const upRes = await upstream.chat(fl);
|