mslxdff 0.1.128 → 0.1.130
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/chat/tools.js +7 -1
- package/src/chat-pipeline/engine.js +1 -1
- package/src/chat-pipeline/index.js +17 -21
- package/src/chat-pipeline/policy.js +2 -2
- package/src/cli/commands/provider/bench-via.js +11 -0
- package/src/model-capabilities/index.js +1 -1
- package/src/opencode-identity.js +51 -0
- package/src/upstream.js +5 -7
- package/src/chat-pipeline/dedup.js +0 -83
- package/src/chat-pipeline/planner.js +0 -31
package/package.json
CHANGED
package/src/chat/tools.js
CHANGED
|
@@ -7,6 +7,7 @@ import { FORBIDDEN } from "./config.js";
|
|
|
7
7
|
import { logDir } from "../logs.js";
|
|
8
8
|
import { defaultStateFile, loadProviderKeys, loadProviderConfigs } from "../state.js";
|
|
9
9
|
import { compatFetch } from "../compat.js";
|
|
10
|
+
import { opencodeClientIdentity } from "../opencode-identity.js";
|
|
10
11
|
|
|
11
12
|
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
12
13
|
const allowedRoots = [
|
|
@@ -220,12 +221,17 @@ export async function curlTool({ url, method, headers, body, timeoutMs }) {
|
|
|
220
221
|
if (headers && typeof headers === "object") {
|
|
221
222
|
for (const [k, v] of Object.entries(headers)) if (typeof v === "string") h[String(k)] = v;
|
|
222
223
|
}
|
|
223
|
-
//
|
|
224
|
+
// 上游自动补最小可用头(zen 免费层门禁:UA 带版本 + opencode 形状 session/request)
|
|
224
225
|
if (/opencode\.ai/i.test(target)) {
|
|
226
|
+
const ident = opencodeClientIdentity();
|
|
225
227
|
if (!h["x-opencode-client"]) h["x-opencode-client"] = "desktop";
|
|
226
228
|
if (!h["Authorization"] && !h["authorization"]) h["Authorization"] = "Bearer public";
|
|
227
229
|
if (!h["Accept"]) h["Accept"] = m === "GET" ? "*/*" : "application/json";
|
|
228
230
|
if (!h["Content-Type"] && m !== "GET" && m !== "HEAD") h["Content-Type"] = "application/json";
|
|
231
|
+
if (!h["User-Agent"] && !h["user-agent"]) h["User-Agent"] = ident["User-Agent"];
|
|
232
|
+
if (!h["x-opencode-session"]) h["x-opencode-session"] = ident["x-opencode-session"];
|
|
233
|
+
if (!h["x-opencode-request"]) h["x-opencode-request"] = ident["x-opencode-request"];
|
|
234
|
+
if (!h["x-opencode-project"]) h["x-opencode-project"] = "global";
|
|
229
235
|
}
|
|
230
236
|
// 本机自动带 token(/v1/* 需要鉴权);/v1/models 必须是 GET,模型常误用 POST 直接纠正
|
|
231
237
|
if (/127\.0\.0\.1|localhost/i.test(target) && /\/v1\//i.test(target)) {
|
|
@@ -7,7 +7,7 @@ import { runSerialTrial } from "./serial-trial.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export function createEngine(deps = {}) {
|
|
9
9
|
const { raceDeps, serialDeps } = deps;
|
|
10
|
-
async function run(
|
|
10
|
+
async function run(state) {
|
|
11
11
|
const race = await runAutoRace(state, raceDeps);
|
|
12
12
|
if (race.done) return;
|
|
13
13
|
await runSerialTrial({ ...state, order: race.order }, serialDeps);
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { analyzePolicy } from "./policy.js";
|
|
3
|
-
import { planRoute } from "./planner.js";
|
|
4
3
|
import { createEngine } from "./engine.js";
|
|
5
4
|
import { runHook } from "../plugins.js";
|
|
6
5
|
import { isFreeModel } from "../models.js";
|
|
7
|
-
import { shouldUseGroupForModel } from "../state/schemas/use-group.js";
|
|
8
6
|
import { clientIp, summarizePrompt } from "../routes/helpers.js";
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* ChatPipeline 深模块门面 — 对外 execute(req) 单一 inlet
|
|
12
|
-
* 内部组合 Policy→
|
|
10
|
+
* 内部组合 Policy→Engine:解析 header/model → 产 order → 委托 engine 执行
|
|
13
11
|
* gateway 仅薄适配:readBody + request:received hook + 调 execute
|
|
14
12
|
*/
|
|
15
13
|
export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, token, plugins, maxHops } = {}) {
|
|
@@ -31,6 +29,20 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
|
|
|
31
29
|
req.body = { ...req.body, model: requested };
|
|
32
30
|
}
|
|
33
31
|
|
|
32
|
+
// 事件/日志帮手先于 order 推导声明:evt 在 auto-scope 分支即被使用,
|
|
33
|
+
// 声明置后会让 useAuto && autoProvider 的请求在 TDZ 上崩(线上 -chat 回退路径)
|
|
34
|
+
const logCall = (model, status) => logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(req?.body?.stream), stages });
|
|
35
|
+
const logError = (model, status, message) => logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
|
|
36
|
+
const evt = (type, data) => {
|
|
37
|
+
const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
|
|
38
|
+
if (bus) bus.emit(entry);
|
|
39
|
+
logs?.appendEvent?.(entry);
|
|
40
|
+
};
|
|
41
|
+
const done = (info) => {
|
|
42
|
+
if (!plugins?.length) return;
|
|
43
|
+
runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(req?.body?.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
|
|
44
|
+
};
|
|
45
|
+
|
|
34
46
|
// order 推导 + plugin model:select 可改
|
|
35
47
|
// 语义:指定模型 = 死锁单模型(本机→组员同款,挂了就报挂,不兜其他 picks);只有 auto 才轮 picks
|
|
36
48
|
// x-mslxdff-auto-provider 头可把 auto 候选限定到单供应商(opencode=裸 id 免费池),-chat 默认带
|
|
@@ -55,18 +67,6 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
|
|
|
55
67
|
const canForwardPeers = Boolean(peers) && hops < (maxHops ?? 3);
|
|
56
68
|
mark("ordered");
|
|
57
69
|
|
|
58
|
-
const logCall = (model, status) => logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(req?.body?.stream), stages });
|
|
59
|
-
const logError = (model, status, message) => logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
|
|
60
|
-
const evt = (type, data) => {
|
|
61
|
-
const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
|
|
62
|
-
if (bus) bus.emit(entry);
|
|
63
|
-
logs?.appendEvent?.(entry);
|
|
64
|
-
};
|
|
65
|
-
const done = (info) => {
|
|
66
|
-
if (!plugins?.length) return;
|
|
67
|
-
runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(req?.body?.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
|
|
68
|
-
};
|
|
69
|
-
|
|
70
70
|
evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(req?.body?.stream), prompt: summarizePrompt(req?.body), rawModel: policy.rawModel, requested, lockModel: lockModel || null });
|
|
71
71
|
if (aliasInfo) evt("alias", { reqId, alias: aliasInfo, rawModel: policy.rawModel, requested });
|
|
72
72
|
if (Object.keys(shareKeys).length) evt("share-keys", { reqId, providers: Object.keys(shareKeys) });
|
|
@@ -88,11 +88,7 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
|
|
|
88
88
|
const handlerCtx = { reqId, model: null, body: req?.body, hops, peers, plugins, evt, logError, logCall, logs, workbuddyUid, sessionId: clientSession };
|
|
89
89
|
if (clientSession) evt("client-session", { reqId, sessionId: clientSession.slice(0, 24) });
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
candidates: order,
|
|
93
|
-
viaRoute: Boolean(!useAuto && requested.includes("/") && canForwardPeers && shouldUseGroupForModel(requested)) ? { via: true } : null,
|
|
94
|
-
});
|
|
95
|
-
await engine.run(plan, {
|
|
91
|
+
await engine.run({
|
|
96
92
|
reqId, startedAt, req, res, body: req?.body, policy,
|
|
97
93
|
useAuto, lockModel, requested, hops,
|
|
98
94
|
canFallback, canForwardPeers,
|
|
@@ -102,5 +98,5 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
|
|
|
102
98
|
});
|
|
103
99
|
}
|
|
104
100
|
|
|
105
|
-
return { execute, _policy: analyzePolicy,
|
|
101
|
+
return { execute, _policy: analyzePolicy, _engine: engine };
|
|
106
102
|
}
|
|
@@ -40,7 +40,7 @@ export function analyzePolicy({ headers = {}, body = {} } = {}) {
|
|
|
40
40
|
if (!aliasInfo) aliasInfo = null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// workbuddy <uid>:model 形式的 uid 钉死在 normalizeFullId 侧处理,这里透传原始 requested
|
|
43
|
+
// workbuddy <uid>:model 形式的 uid 钉死在 normalizeFullId 侧处理,这里透传原始 requested
|
|
44
44
|
// 若 requested 含 workbuddy/ 前缀且含 :,则尝试提取 uid
|
|
45
45
|
let extractedUid = workbuddyUid;
|
|
46
46
|
if (!extractedUid && requested.startsWith("workbuddy/") && requested.includes(":")) {
|
|
@@ -52,7 +52,7 @@ export function analyzePolicy({ headers = {}, body = {} } = {}) {
|
|
|
52
52
|
const useAuto = isAutoModel(requested);
|
|
53
53
|
|
|
54
54
|
// 对 workbuddy 前缀的 model,做 normalizeFullId 归一(剥 uid 供上游)
|
|
55
|
-
// 但保留 requested 为完整带前缀形态,供
|
|
55
|
+
// 但保留 requested 为完整带前缀形态,供 serial-trial 做 ViaRoute 判定与组员路由
|
|
56
56
|
let normalizedForUpstream = requested;
|
|
57
57
|
try {
|
|
58
58
|
const norm = normalizeFullId(requested);
|
|
@@ -5,6 +5,7 @@ import { runOne } from "../../../bench/runner.js";
|
|
|
5
5
|
import { formatViaReport } from "../../../bench/report.js";
|
|
6
6
|
import { clineBenchOne } from "../../../bench/cline-bench.js";
|
|
7
7
|
import { workbuddyBenchOne } from "../../../bench/workbuddy-bench.js";
|
|
8
|
+
import { opencodeClientIdentity } from "../../../opencode-identity.js";
|
|
8
9
|
|
|
9
10
|
export function buildHeadersForProvider(providerId, apiKey, auth) {
|
|
10
11
|
const h = {};
|
|
@@ -21,6 +22,16 @@ export function buildHeadersForProvider(providerId, apiKey, auth) {
|
|
|
21
22
|
if (auth?.enterpriseId) { h["X-Enterprise-Id"] = auth.enterpriseId; h["X-Tenant-Id"] = auth.enterpriseId; }
|
|
22
23
|
return h;
|
|
23
24
|
}
|
|
25
|
+
// opencode 免费层门禁:UA 带版本 + opencode 形状 session/request,否则 403
|
|
26
|
+
if (String(providerId).toLowerCase() === "opencode") {
|
|
27
|
+
const ident = opencodeClientIdentity();
|
|
28
|
+
h["Content-Type"] = "application/json";
|
|
29
|
+
if (!h["User-Agent"]) h["User-Agent"] = ident["User-Agent"];
|
|
30
|
+
h["x-opencode-client"] = "desktop";
|
|
31
|
+
h["x-opencode-session"] = ident["x-opencode-session"];
|
|
32
|
+
h["x-opencode-request"] = ident["x-opencode-request"];
|
|
33
|
+
h["x-opencode-project"] = "global";
|
|
34
|
+
}
|
|
24
35
|
if (apiKey) h["Authorization"] = `Bearer ${apiKey}`;
|
|
25
36
|
return h;
|
|
26
37
|
}
|
|
@@ -121,7 +121,7 @@ export function createCapabilitiesService({
|
|
|
121
121
|
return { ready, get, list, providers, npmIndex: () => new Map(npmIndex) };
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
//
|
|
124
|
+
// 模块级单例:HTTP handler 懒加载,测试 _reset 后注入
|
|
125
125
|
let _global = null;
|
|
126
126
|
export function globalCapabilities() {
|
|
127
127
|
if (!_global) _global = createCapabilitiesService({ cacheFile: defaultCacheFile() });
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
// opencode 客户端身份规格单一来源(源码 packages/schema/src/identifier.ts +
|
|
4
|
+
// packages/opencode/src/session/llm/request.ts):id = <prefix>_ + 12 位 hex
|
|
5
|
+
// (timestamp*4096+同毫秒计数,截 48bit) + 14 位 base62,共 26 字符;UA 必须 "opencode/<semver>"。
|
|
6
|
+
// zen 免费层 2026-09-17 起按该规格放行(缺版本 UA 或 id 形状不符 → 403 FreeTierError)。
|
|
7
|
+
const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
8
|
+
const DEFAULT_OPENCODE_UA = "opencode/1.17.20";
|
|
9
|
+
let idLastTs = 0;
|
|
10
|
+
let idCounter = 0;
|
|
11
|
+
|
|
12
|
+
export function opencodeIdTail(timestamp = Date.now()) {
|
|
13
|
+
if (timestamp !== idLastTs) {
|
|
14
|
+
idLastTs = timestamp;
|
|
15
|
+
idCounter = 0;
|
|
16
|
+
}
|
|
17
|
+
idCounter += 1;
|
|
18
|
+
const value = BigInt(timestamp) * 0x1000n + BigInt(idCounter);
|
|
19
|
+
let tail = "";
|
|
20
|
+
for (let i = 0; i < 6; i++) {
|
|
21
|
+
tail += Number((value >> BigInt(40 - 8 * i)) & 0xffn)
|
|
22
|
+
.toString(16)
|
|
23
|
+
.padStart(2, "0");
|
|
24
|
+
}
|
|
25
|
+
for (const b of crypto.randomBytes(14)) tail += ID_CHARS[b % 62];
|
|
26
|
+
return tail;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 摘要派生:同种子恒同 id(会话亲和),形态仍合规
|
|
30
|
+
export function digestIdTail(digest) {
|
|
31
|
+
let tail = digest.subarray(0, 6).toString("hex");
|
|
32
|
+
for (const b of digest.subarray(6, 20)) tail += ID_CHARS[b % 62];
|
|
33
|
+
return tail;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function genId(prefix) {
|
|
37
|
+
return `${prefix}${opencodeIdTail()}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function opencodeUa() {
|
|
41
|
+
return process.env.MSLXDFF_OPENCODE_UA || DEFAULT_OPENCODE_UA;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 旁路(-chat curl 工具 / bench 直连)共用同一身份,避免各自伪造头再被 403
|
|
45
|
+
export function opencodeClientIdentity() {
|
|
46
|
+
return {
|
|
47
|
+
"User-Agent": opencodeUa(),
|
|
48
|
+
"x-opencode-session": genId("ses_"),
|
|
49
|
+
"x-opencode-request": genId("msg_"),
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/upstream.js
CHANGED
|
@@ -7,11 +7,9 @@ import { isFreeModel } from "./models.js";
|
|
|
7
7
|
import { fmtShanghaiYMDHMS } from "./time.js";
|
|
8
8
|
import { createTransport } from "./transport/index.js";
|
|
9
9
|
import { isResponsesModel, chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "./upstream-responses.js";
|
|
10
|
-
import {
|
|
10
|
+
import { digestIdTail, genId, opencodeClientIdentity, opencodeUa } from "./opencode-identity.js";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
return `${prefix}${uuid().replace(/-/g, "")}`;
|
|
14
|
-
}
|
|
12
|
+
export { opencodeClientIdentity, opencodeUa };
|
|
15
13
|
// 上游按 session 做粘性路由(实测:固定 session 两次请求均 ~1.2s;每次随机时可能撞冷机器 26s+)。
|
|
16
14
|
// 客户端(opencode AI SDK 路径)不带会话标识 → 用对话首两条消息(system + 首条 user)哈希做稳定会话:
|
|
17
15
|
// 同一会话多轮里这两条不变 ⇒ 路由亲和稳定;不同会话天然分散。
|
|
@@ -25,7 +23,7 @@ function sessionFromMessages(messages) {
|
|
|
25
23
|
};
|
|
26
24
|
const seed = `${pick("system")}|${pick("user")}`.slice(0, 4000);
|
|
27
25
|
if (seed === "|") return null;
|
|
28
|
-
return `ses_${crypto.createHash("sha1").update(seed).digest(
|
|
26
|
+
return `ses_${digestIdTail(crypto.createHash("sha1").update(seed).digest())}`;
|
|
29
27
|
} catch {
|
|
30
28
|
return null;
|
|
31
29
|
}
|
|
@@ -56,7 +54,7 @@ export function createOpencodeHeaderBuilder({ authToken = "public", env = proces
|
|
|
56
54
|
"Content-Type": "application/json",
|
|
57
55
|
Authorization: "",
|
|
58
56
|
"x-opencode-client": "desktop",
|
|
59
|
-
"User-Agent":
|
|
57
|
+
"User-Agent": opencodeUa(),
|
|
60
58
|
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
|
|
61
59
|
"X-Title": "Hermes Agent",
|
|
62
60
|
}
|
|
@@ -73,7 +71,7 @@ export function createOpencodeHeaderBuilder({ authToken = "public", env = proces
|
|
|
73
71
|
const base = {
|
|
74
72
|
...baseHeaders,
|
|
75
73
|
Accept: isStream ? "text/event-stream" : "*/*",
|
|
76
|
-
"User-Agent":
|
|
74
|
+
"User-Agent": opencodeUa(),
|
|
77
75
|
"x-opencode-session": session,
|
|
78
76
|
"x-opencode-request": genId("msg_"),
|
|
79
77
|
"x-opencode-project": "global",
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 请求去重(防前端双击/重试风暴)
|
|
5
|
-
* key = ip | requested | stream | bodyHash(messages+model变体)
|
|
6
|
-
* 窗口内重复到达的相同请求直接 429 返回,提示前端去重
|
|
7
|
-
* 默认窗口 1000ms,可用 MSLXDFF_DEDUP_WINDOW_MS 覆盖,0 为关闭
|
|
8
|
-
*/
|
|
9
|
-
export function dedupWindowMs() {
|
|
10
|
-
const raw = process.env.MSLXDFF_DEDUP_WINDOW_MS;
|
|
11
|
-
if (raw != null && String(raw).trim() !== "") {
|
|
12
|
-
const n = Number(raw);
|
|
13
|
-
if (Number.isFinite(n) && n >= 0) return Math.floor(n);
|
|
14
|
-
}
|
|
15
|
-
return 1000;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function hashBody(body) {
|
|
19
|
-
try {
|
|
20
|
-
const m = body?.messages;
|
|
21
|
-
const s = JSON.stringify({
|
|
22
|
-
model: body?.model || "",
|
|
23
|
-
stream: Boolean(body?.stream),
|
|
24
|
-
max_tokens: body?.max_tokens ?? body?.maxTokens ?? null,
|
|
25
|
-
messages: Array.isArray(m) ? m.map((x) => ({ role: x.role, content: typeof x.content === "string" ? x.content.slice(0, 4000) : JSON.stringify(x.content).slice(0, 4000) })) : [],
|
|
26
|
-
// 工具调用等也纳入,避免误判
|
|
27
|
-
tools: body?.tools ? JSON.stringify(body.tools).slice(0, 1000) : "",
|
|
28
|
-
});
|
|
29
|
-
return createHash("sha1").update(s).digest("hex").slice(0, 16);
|
|
30
|
-
} catch {
|
|
31
|
-
return String(body?.model || "").slice(0, 32);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let _global = null;
|
|
36
|
-
export function globalDedup() {
|
|
37
|
-
if (!_global) _global = createDedup({ windowMs: dedupWindowMs() });
|
|
38
|
-
// 若环境变量在运行时被改,同步窗口
|
|
39
|
-
const want = dedupWindowMs();
|
|
40
|
-
if (_global.windowMs !== want) {
|
|
41
|
-
_global.windowMs = want;
|
|
42
|
-
}
|
|
43
|
-
return _global;
|
|
44
|
-
}
|
|
45
|
-
export function _resetGlobalDedup() { _global = null; }
|
|
46
|
-
|
|
47
|
-
export function createDedup({ windowMs = dedupWindowMs(), now = Date.now } = {}) {
|
|
48
|
-
const map = new Map(); // key -> at
|
|
49
|
-
let sweepAt = 0;
|
|
50
|
-
|
|
51
|
-
function sweep() {
|
|
52
|
-
const t = now();
|
|
53
|
-
if (t - sweepAt < windowMs) return;
|
|
54
|
-
sweepAt = t;
|
|
55
|
-
for (const [k, at] of map) {
|
|
56
|
-
if (t - at > windowMs) map.delete(k);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function keyFor({ ip, requested, body }) {
|
|
61
|
-
const h = hashBody(body);
|
|
62
|
-
const stream = body?.stream ? "1" : "0";
|
|
63
|
-
return `${ip || "-"}|${requested || "-"}|${stream}|${h}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function check({ ip, requested, body }) {
|
|
67
|
-
if (!windowMs) return { dup: false, key: null };
|
|
68
|
-
sweep();
|
|
69
|
-
const key = keyFor({ ip, requested, body });
|
|
70
|
-
const at = map.get(key);
|
|
71
|
-
const t = now();
|
|
72
|
-
if (at != null && t - at < windowMs) {
|
|
73
|
-
return { dup: true, key, ageMs: t - at };
|
|
74
|
-
}
|
|
75
|
-
map.set(key, t);
|
|
76
|
-
return { dup: false, key };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function _size() { return map.size; }
|
|
80
|
-
function _clear() { map.clear(); }
|
|
81
|
-
|
|
82
|
-
return { check, keyFor, _size, _clear, windowMs };
|
|
83
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_AUTO_MODELS, rankModels } from "../auto.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* RoutePlanner 纯决策 — 根据 Policy + auto 状态产 RoutePlan
|
|
5
|
-
* 无网络副作用,供单测注入 FakeAuto
|
|
6
|
-
*/
|
|
7
|
-
export function planRoute(policy, autoState = {}) {
|
|
8
|
-
const { requested, useAuto, lockModel } = policy;
|
|
9
|
-
const { candidates = [], errors = {}, latencies = {}, viaRoute = null } = autoState;
|
|
10
|
-
|
|
11
|
-
// 锁模型时直接单点
|
|
12
|
-
if (lockModel) {
|
|
13
|
-
return { strategy: "direct", order: [requested], concLimit: 1, hedgeDelayMs: 0 };
|
|
14
|
-
}
|
|
15
|
-
// ViaRoute 单路径(显式锁模型且 via 表命中)
|
|
16
|
-
if (viaRoute && !useAuto && requested.includes("/")) {
|
|
17
|
-
return { strategy: "via", order: [requested], via: viaRoute, concLimit: 1, hedgeDelayMs: 0 };
|
|
18
|
-
}
|
|
19
|
-
// Auto 并发择优
|
|
20
|
-
if (useAuto && candidates.length > 1) {
|
|
21
|
-
const concLimit = Math.min(candidates.length, 5);
|
|
22
|
-
return { strategy: "autoRace", order: candidates, concLimit, hedgeDelayMs: 1000 };
|
|
23
|
-
}
|
|
24
|
-
// 显式模型回退链
|
|
25
|
-
if (!useAuto && candidates.length) {
|
|
26
|
-
const others = candidates.filter((m) => m !== requested);
|
|
27
|
-
const order = requested ? [requested, ...others] : candidates;
|
|
28
|
-
return { strategy: "direct", order, concLimit: 1, hedgeDelayMs: 0 };
|
|
29
|
-
}
|
|
30
|
-
return { strategy: "direct", order: requested ? [requested] : [""], concLimit: 1, hedgeDelayMs: 0 };
|
|
31
|
-
}
|