mslxdff 0.1.170 → 0.1.172
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/bin/mslxdff.js +3 -3
- package/docs/cli_help_mini.md +143 -0
- package/package.json +1 -1
- package/src/anthropic/count-tokens.js +39 -0
- package/src/anthropic/translate.js +20 -41
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +131 -131
- package/src/chat-pipeline/policy.js +76 -76
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +133 -133
- package/src/cli/commands/provider/models.js +145 -145
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +279 -279
- package/src/daemon.js +112 -112
- package/src/model-capabilities/context-lookup.js +65 -0
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/merge.js +50 -35
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +225 -225
- package/src/providers/AGENTS.md +49 -0
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/codearts/concurrency.js +34 -0
- package/src/providers/codearts/index.js +11 -3
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/payload.js +4 -2
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +203 -200
- package/src/providers/qoder/index.js +249 -249
- package/src/providers/qoder/sse.js +145 -145
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +327 -327
- package/src/providers/qwenwork/payload.js +144 -142
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/chat.js +10 -5
- package/src/providers/raccoon/const.js +4 -4
- package/src/providers/registry.js +79 -79
- package/src/providers/traework/models.js +4 -3
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/payload.js +16 -7
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/chat.js +7 -0
- package/src/reasoning.js +32 -32
- package/src/responses/translate.js +4 -0
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/exhausted-handler.js +5 -0
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +294 -291
- package/src/routes/chat/via-route-handler.js +146 -146
- package/src/routes/hedge.js +255 -255
- package/src/routes/messages-route.js +1 -1
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream-scan.js +47 -21
- package/src/routes/stream.js +385 -385
- package/src/runtime/bootstrap.js +50 -50
- package/src/runtime/provider-gate.js +36 -36
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +69 -69
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/talk-full.js +1 -0
- package/src/talk-log.js +3 -2
- package/src/thinking-effort.js +67 -0
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-engine/sdk/convert.js +6 -0
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream-responses.js +5 -0
- package/src/upstream.js +289 -289
package/src/models.js
CHANGED
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
const KNOWN_FREE_OPENCODE_MODELS = ["big-pickle"];
|
|
2
|
-
const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
3
|
-
const DEFAULT_REFRESH_MS = 2 * 60 * 60 * 1000;
|
|
4
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname } from "node:path";
|
|
1
|
+
const KNOWN_FREE_OPENCODE_MODELS = ["big-pickle"];
|
|
2
|
+
const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
3
|
+
const DEFAULT_REFRESH_MS = 2 * 60 * 60 * 1000;
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
6
|
import { compatFetch } from "./compat.js";
|
|
7
|
-
|
|
8
|
-
export function isFreeModel(id) {
|
|
9
|
-
return (typeof id === "string" && id.endsWith("-free")) ||
|
|
10
|
-
KNOWN_FREE_OPENCODE_MODELS.includes(id);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function filterFreeModels(list) {
|
|
14
|
-
const seen = new Set();
|
|
15
|
-
const out = [];
|
|
16
|
-
for (const m of list || []) {
|
|
17
|
-
if (!(m && m.id)) continue;
|
|
18
|
-
if (!isFreeModel(m.id)) continue;
|
|
19
|
-
if (seen.has(m.id)) continue;
|
|
20
|
-
seen.add(m.id);
|
|
21
|
-
out.push(m);
|
|
22
|
-
}
|
|
23
|
-
return out;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function createModelsService({ baseUrl, headers, ttlMs = CACHE_TTL_MS, refreshMs = DEFAULT_REFRESH_MS, cacheFile, providers } = {}) {
|
|
27
|
-
// 多供应商聚合模式:给 `providers` 数组,则缓存并返回所有供应商的模型列表
|
|
28
|
-
if (providers?.length) {
|
|
29
|
-
let aggregate = null;
|
|
30
|
-
let aggFetchedAt = 0;
|
|
31
|
-
let lastAllowlistKey = "";
|
|
32
|
-
async function currentAllowlistKey() {
|
|
33
|
-
try {
|
|
34
|
-
const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
|
|
35
|
-
return providers.map((p) => {
|
|
36
|
-
const allowAny = loadProviderAllowAnyModels(p.id) ? "any" : "block";
|
|
37
|
-
return `${p.id}:${allowAny}:${loadProviderAllowedModels(p.id).join(",")}`;
|
|
38
|
-
}).join("|");
|
|
39
|
-
} catch {
|
|
40
|
-
return "";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function aggLoad() {
|
|
44
|
-
const all = [];
|
|
45
|
-
for (const p of providers) {
|
|
46
|
-
try {
|
|
47
|
-
let list = (await p.listModels?.()) ?? [];
|
|
48
|
-
// 白名单过滤:空名单且 allowAny=false → 该供应商不暴露任何模型(安全默认全拦,auto 直接跳过该供应商)
|
|
49
|
-
try {
|
|
50
|
-
const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
|
|
51
|
-
const allowed = loadProviderAllowedModels(p.id);
|
|
52
|
-
const allowAny = loadProviderAllowAnyModels(p.id);
|
|
53
|
-
if (!allowed.length && !allowAny) {
|
|
54
|
-
// 该供应商被全拦,auto 直接跳过整个供应商
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (allowed.length) {
|
|
58
|
-
const allowedSet = new Set(allowed);
|
|
59
|
-
const { splitModelId } = await import("./providers/model-id.js");
|
|
60
|
-
list = list.filter((m) => {
|
|
61
|
-
if (!m || !m.id) return false;
|
|
62
|
-
const { raw } = splitModelId(m.id, providers.map((x) => x.id));
|
|
63
|
-
return allowedSet.has(String(raw || "").trim());
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
} catch {}
|
|
67
|
-
all.push(...list);
|
|
68
|
-
} catch {
|
|
69
|
-
// 单供应商取数失败不拖垮整体
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
aggregate = { object: "list", data: all };
|
|
73
|
-
try {
|
|
74
|
-
lastAllowlistKey = await currentAllowlistKey();
|
|
75
|
-
} catch {}
|
|
76
|
-
if (cacheFile) persistModels(aggregate, cacheFile);
|
|
77
|
-
return aggregate;
|
|
78
|
-
}
|
|
79
|
-
async function get() {
|
|
80
|
-
const now = Date.now();
|
|
81
|
-
if (aggregate && now - aggFetchedAt < ttlMs) {
|
|
82
|
-
// 热更新白名单:allowlist 变化时,即使命中 TTL 也要重载(否则 clear 后仍返回旧过滤结果)
|
|
83
|
-
try {
|
|
84
|
-
const curKey = await currentAllowlistKey();
|
|
85
|
-
if (curKey !== lastAllowlistKey) {
|
|
86
|
-
const out = await aggLoad();
|
|
87
|
-
aggFetchedAt = Date.now();
|
|
88
|
-
return out;
|
|
89
|
-
}
|
|
90
|
-
} catch {}
|
|
91
|
-
// 否则尝试在缓存上二次过滤(处理 allowlist 从空变非空等未触发重载的场景)
|
|
92
|
-
try {
|
|
93
|
-
const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
|
|
94
|
-
const { splitModelId } = await import("./providers/model-id.js");
|
|
95
|
-
const filteredData = aggregate.data.filter((m) => {
|
|
96
|
-
if (!m || !m.id) return false;
|
|
97
|
-
const { provider, raw } = splitModelId(m.id, providers.map((x) => x.id));
|
|
98
|
-
const allowed = loadProviderAllowedModels(provider);
|
|
99
|
-
const allowAny = loadProviderAllowAnyModels(provider);
|
|
100
|
-
if (!allowed.length && !allowAny) return false;
|
|
101
|
-
if (!allowed.length) return true;
|
|
102
|
-
return allowed.includes(String(raw || "").trim());
|
|
103
|
-
});
|
|
104
|
-
if (filteredData.length !== aggregate.data.length) return { ...aggregate, data: filteredData };
|
|
105
|
-
return aggregate;
|
|
106
|
-
} catch {
|
|
107
|
-
return aggregate;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const out = await aggLoad();
|
|
111
|
-
aggFetchedAt = Date.now();
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
return { get, startAutoRefresh: () => {}, stopAutoRefresh: () => {} };
|
|
115
|
-
}
|
|
116
|
-
let cache = null;
|
|
117
|
-
let fetchedAt = 0;
|
|
118
|
-
let inflight = null;
|
|
119
|
-
let timer = null;
|
|
120
|
-
|
|
121
|
-
async function load() {
|
|
122
|
-
let data = await fetchUpstreamModels({ baseUrl, headers });
|
|
123
|
-
// 白名单过滤:opencode 亦支持 allowlist
|
|
124
|
-
try {
|
|
125
|
-
const { loadProviderAllowedModels } = await import("./state.js");
|
|
126
|
-
const allowed = loadProviderAllowedModels("opencode");
|
|
127
|
-
if (allowed.length) {
|
|
128
|
-
const allowedSet = new Set(allowed);
|
|
129
|
-
data = { ...data, data: (data.data || []).filter((m) => m && m.id && allowedSet.has(String(m.id).trim())) };
|
|
130
|
-
}
|
|
131
|
-
} catch {}
|
|
132
|
-
cache = data;
|
|
133
|
-
fetchedAt = Date.now();
|
|
134
|
-
if (cacheFile) persistModels(data, cacheFile);
|
|
135
|
-
return data;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function get() {
|
|
139
|
-
const now = Date.now();
|
|
140
|
-
if (cache && now - fetchedAt < ttlMs) return cache;
|
|
141
|
-
if (inflight) return inflight;
|
|
142
|
-
|
|
143
|
-
inflight = (async () => {
|
|
144
|
-
try {
|
|
145
|
-
return await load();
|
|
146
|
-
} catch (err) {
|
|
147
|
-
// serve stale on failure if we have it, else rethrow
|
|
148
|
-
if (cache) return cache;
|
|
149
|
-
throw err;
|
|
150
|
-
} finally {
|
|
151
|
-
inflight = null;
|
|
152
|
-
}
|
|
153
|
-
})();
|
|
154
|
-
return inflight;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function startAutoRefresh(intervalMs = refreshMs) {
|
|
158
|
-
if (timer) return stopAutoRefresh;
|
|
159
|
-
timer = setInterval(() => {
|
|
160
|
-
void load().catch(() => {
|
|
161
|
-
// keep serving stale cache on background refresh failure
|
|
162
|
-
});
|
|
163
|
-
}, intervalMs);
|
|
164
|
-
timer.unref?.();
|
|
165
|
-
return stopAutoRefresh;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function stopAutoRefresh() {
|
|
169
|
-
if (timer) {
|
|
170
|
-
clearInterval(timer);
|
|
171
|
-
timer = null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return { get, startAutoRefresh, stopAutoRefresh };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function fetchUpstreamModels({ baseUrl, headers, connectTimeoutMs = 30_000 }) {
|
|
179
|
-
const url = `${baseUrl}/zen/v1/models`;
|
|
180
|
-
for (let attempt = 0; ; attempt++) {
|
|
181
|
-
const res = await attemptFetch(url, headers, connectTimeoutMs);
|
|
182
|
-
if (res instanceof Error) {
|
|
183
|
-
if (attempt < NETWORK_RETRIES) continue;
|
|
184
|
-
throw res;
|
|
185
|
-
}
|
|
186
|
-
if (isRetryable(res.status) && attempt < STATUS_RETRIES) {
|
|
187
|
-
await sleep(2000);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (!res.ok) throw new Error(`models fetch failed: HTTP ${res.status}`);
|
|
191
|
-
const json = await res.json().catch(() => ({}));
|
|
192
|
-
const raw = Array.isArray(json) ? json : json.data ?? json.models ?? [];
|
|
193
|
-
return { object: "list", data: filterFreeModels(raw) };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function attemptFetch(url, headers, connectTimeoutMs) {
|
|
198
|
-
const controller = new AbortController();
|
|
199
|
-
const timer = setTimeout(() => controller.abort(), connectTimeoutMs);
|
|
200
|
-
try {
|
|
201
|
-
return await compatFetch(url, { headers, signal: controller.signal });
|
|
202
|
-
} catch (err) {
|
|
203
|
-
return err;
|
|
204
|
-
} finally {
|
|
205
|
-
clearTimeout(timer);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function isRetryable(status) {
|
|
210
|
-
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function persistModels(data, cacheFile) {
|
|
214
|
-
try {
|
|
215
|
-
mkdirSync(dirname(cacheFile), { recursive: true });
|
|
216
|
-
writeFileSync(cacheFile, JSON.stringify({ cachedAt: Date.now(), ...data }));
|
|
217
|
-
} catch {
|
|
218
|
-
// persistence is best-effort
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function sleep(ms) {
|
|
223
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const NETWORK_RETRIES = 2;
|
|
7
|
+
|
|
8
|
+
export function isFreeModel(id) {
|
|
9
|
+
return (typeof id === "string" && id.endsWith("-free")) ||
|
|
10
|
+
KNOWN_FREE_OPENCODE_MODELS.includes(id);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function filterFreeModels(list) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const m of list || []) {
|
|
17
|
+
if (!(m && m.id)) continue;
|
|
18
|
+
if (!isFreeModel(m.id)) continue;
|
|
19
|
+
if (seen.has(m.id)) continue;
|
|
20
|
+
seen.add(m.id);
|
|
21
|
+
out.push(m);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createModelsService({ baseUrl, headers, ttlMs = CACHE_TTL_MS, refreshMs = DEFAULT_REFRESH_MS, cacheFile, providers } = {}) {
|
|
27
|
+
// 多供应商聚合模式:给 `providers` 数组,则缓存并返回所有供应商的模型列表
|
|
28
|
+
if (providers?.length) {
|
|
29
|
+
let aggregate = null;
|
|
30
|
+
let aggFetchedAt = 0;
|
|
31
|
+
let lastAllowlistKey = "";
|
|
32
|
+
async function currentAllowlistKey() {
|
|
33
|
+
try {
|
|
34
|
+
const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
|
|
35
|
+
return providers.map((p) => {
|
|
36
|
+
const allowAny = loadProviderAllowAnyModels(p.id) ? "any" : "block";
|
|
37
|
+
return `${p.id}:${allowAny}:${loadProviderAllowedModels(p.id).join(",")}`;
|
|
38
|
+
}).join("|");
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function aggLoad() {
|
|
44
|
+
const all = [];
|
|
45
|
+
for (const p of providers) {
|
|
46
|
+
try {
|
|
47
|
+
let list = (await p.listModels?.()) ?? [];
|
|
48
|
+
// 白名单过滤:空名单且 allowAny=false → 该供应商不暴露任何模型(安全默认全拦,auto 直接跳过该供应商)
|
|
49
|
+
try {
|
|
50
|
+
const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
|
|
51
|
+
const allowed = loadProviderAllowedModels(p.id);
|
|
52
|
+
const allowAny = loadProviderAllowAnyModels(p.id);
|
|
53
|
+
if (!allowed.length && !allowAny) {
|
|
54
|
+
// 该供应商被全拦,auto 直接跳过整个供应商
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (allowed.length) {
|
|
58
|
+
const allowedSet = new Set(allowed);
|
|
59
|
+
const { splitModelId } = await import("./providers/model-id.js");
|
|
60
|
+
list = list.filter((m) => {
|
|
61
|
+
if (!m || !m.id) return false;
|
|
62
|
+
const { raw } = splitModelId(m.id, providers.map((x) => x.id));
|
|
63
|
+
return allowedSet.has(String(raw || "").trim());
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
all.push(...list);
|
|
68
|
+
} catch {
|
|
69
|
+
// 单供应商取数失败不拖垮整体
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
aggregate = { object: "list", data: all };
|
|
73
|
+
try {
|
|
74
|
+
lastAllowlistKey = await currentAllowlistKey();
|
|
75
|
+
} catch {}
|
|
76
|
+
if (cacheFile) persistModels(aggregate, cacheFile);
|
|
77
|
+
return aggregate;
|
|
78
|
+
}
|
|
79
|
+
async function get() {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (aggregate && now - aggFetchedAt < ttlMs) {
|
|
82
|
+
// 热更新白名单:allowlist 变化时,即使命中 TTL 也要重载(否则 clear 后仍返回旧过滤结果)
|
|
83
|
+
try {
|
|
84
|
+
const curKey = await currentAllowlistKey();
|
|
85
|
+
if (curKey !== lastAllowlistKey) {
|
|
86
|
+
const out = await aggLoad();
|
|
87
|
+
aggFetchedAt = Date.now();
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
// 否则尝试在缓存上二次过滤(处理 allowlist 从空变非空等未触发重载的场景)
|
|
92
|
+
try {
|
|
93
|
+
const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
|
|
94
|
+
const { splitModelId } = await import("./providers/model-id.js");
|
|
95
|
+
const filteredData = aggregate.data.filter((m) => {
|
|
96
|
+
if (!m || !m.id) return false;
|
|
97
|
+
const { provider, raw } = splitModelId(m.id, providers.map((x) => x.id));
|
|
98
|
+
const allowed = loadProviderAllowedModels(provider);
|
|
99
|
+
const allowAny = loadProviderAllowAnyModels(provider);
|
|
100
|
+
if (!allowed.length && !allowAny) return false;
|
|
101
|
+
if (!allowed.length) return true;
|
|
102
|
+
return allowed.includes(String(raw || "").trim());
|
|
103
|
+
});
|
|
104
|
+
if (filteredData.length !== aggregate.data.length) return { ...aggregate, data: filteredData };
|
|
105
|
+
return aggregate;
|
|
106
|
+
} catch {
|
|
107
|
+
return aggregate;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const out = await aggLoad();
|
|
111
|
+
aggFetchedAt = Date.now();
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
return { get, startAutoRefresh: () => {}, stopAutoRefresh: () => {} };
|
|
115
|
+
}
|
|
116
|
+
let cache = null;
|
|
117
|
+
let fetchedAt = 0;
|
|
118
|
+
let inflight = null;
|
|
119
|
+
let timer = null;
|
|
120
|
+
|
|
121
|
+
async function load() {
|
|
122
|
+
let data = await fetchUpstreamModels({ baseUrl, headers });
|
|
123
|
+
// 白名单过滤:opencode 亦支持 allowlist
|
|
124
|
+
try {
|
|
125
|
+
const { loadProviderAllowedModels } = await import("./state.js");
|
|
126
|
+
const allowed = loadProviderAllowedModels("opencode");
|
|
127
|
+
if (allowed.length) {
|
|
128
|
+
const allowedSet = new Set(allowed);
|
|
129
|
+
data = { ...data, data: (data.data || []).filter((m) => m && m.id && allowedSet.has(String(m.id).trim())) };
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
cache = data;
|
|
133
|
+
fetchedAt = Date.now();
|
|
134
|
+
if (cacheFile) persistModels(data, cacheFile);
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function get() {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
if (cache && now - fetchedAt < ttlMs) return cache;
|
|
141
|
+
if (inflight) return inflight;
|
|
142
|
+
|
|
143
|
+
inflight = (async () => {
|
|
144
|
+
try {
|
|
145
|
+
return await load();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// serve stale on failure if we have it, else rethrow
|
|
148
|
+
if (cache) return cache;
|
|
149
|
+
throw err;
|
|
150
|
+
} finally {
|
|
151
|
+
inflight = null;
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
return inflight;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function startAutoRefresh(intervalMs = refreshMs) {
|
|
158
|
+
if (timer) return stopAutoRefresh;
|
|
159
|
+
timer = setInterval(() => {
|
|
160
|
+
void load().catch(() => {
|
|
161
|
+
// keep serving stale cache on background refresh failure
|
|
162
|
+
});
|
|
163
|
+
}, intervalMs);
|
|
164
|
+
timer.unref?.();
|
|
165
|
+
return stopAutoRefresh;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function stopAutoRefresh() {
|
|
169
|
+
if (timer) {
|
|
170
|
+
clearInterval(timer);
|
|
171
|
+
timer = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { get, startAutoRefresh, stopAutoRefresh };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function fetchUpstreamModels({ baseUrl, headers, connectTimeoutMs = 30_000 }) {
|
|
179
|
+
const url = `${baseUrl}/zen/v1/models`;
|
|
180
|
+
for (let attempt = 0; ; attempt++) {
|
|
181
|
+
const res = await attemptFetch(url, headers, connectTimeoutMs);
|
|
182
|
+
if (res instanceof Error) {
|
|
183
|
+
if (attempt < NETWORK_RETRIES) continue;
|
|
184
|
+
throw res;
|
|
185
|
+
}
|
|
186
|
+
if (isRetryable(res.status) && attempt < STATUS_RETRIES) {
|
|
187
|
+
await sleep(2000);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!res.ok) throw new Error(`models fetch failed: HTTP ${res.status}`);
|
|
191
|
+
const json = await res.json().catch(() => ({}));
|
|
192
|
+
const raw = Array.isArray(json) ? json : json.data ?? json.models ?? [];
|
|
193
|
+
return { object: "list", data: filterFreeModels(raw) };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function attemptFetch(url, headers, connectTimeoutMs) {
|
|
198
|
+
const controller = new AbortController();
|
|
199
|
+
const timer = setTimeout(() => controller.abort(), connectTimeoutMs);
|
|
200
|
+
try {
|
|
201
|
+
return await compatFetch(url, { headers, signal: controller.signal });
|
|
202
|
+
} catch (err) {
|
|
203
|
+
return err;
|
|
204
|
+
} finally {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isRetryable(status) {
|
|
210
|
+
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function persistModels(data, cacheFile) {
|
|
214
|
+
try {
|
|
215
|
+
mkdirSync(dirname(cacheFile), { recursive: true });
|
|
216
|
+
writeFileSync(cacheFile, JSON.stringify({ cachedAt: Date.now(), ...data }));
|
|
217
|
+
} catch {
|
|
218
|
+
// persistence is best-effort
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sleep(ms) {
|
|
223
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const NETWORK_RETRIES = 2;
|
|
227
227
|
const STATUS_RETRIES = 2;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# src/providers — 供应商契约(改任何 provider 前必读)
|
|
2
|
+
|
|
3
|
+
> 从 `AGENTS.md` 下沉(2026-09-29 分层,就近生效)。改完跑:`npm run docs:check` + 相关 provider 测试 + 裸调验证 `-provider <id> models`。
|
|
4
|
+
> 相关 ADR:0007(前缀路由)/0010(allowlist)/0019(share-keys)/0023(key 直连)/0026(cline id)/0029(qoder)/0033(cline 自动同步)/0037(qwenwork)/0038(zcode)/0041(globalqwenwork)。
|
|
5
|
+
|
|
6
|
+
## 通用机制(dispatcher / keyring / 门禁)
|
|
7
|
+
|
|
8
|
+
- `src/providers/` 每供应商一文件,前缀路由 `<provider>/raw`(裸 id 归 opencode),dispatcher 剥前缀转发;`providerKeys: {id:[k1,k2]}` round-robin 30s 冷却;`shareKeys` via `x-mslxdff-share-keys` 瞬时借用且 opencode 恒排除。
|
|
9
|
+
- `providerConfigs.allowAnyModels` 默认 `false`(`opencode:true` 例外)+ `allowlist` 空即 `403`;`isModelAllowed` 对未配置 provider 放行(兼容测试),`listModels` 同步过滤;`providerConfigs.{modelsPath,chatPath}` 可配异形路径(`workbuddy=/v3/config` & `/v2/chat/completions`,其余默认 `/models` & `/chat/completions`,`b.ai=/v1/models`、`cline=/api/v1/models` 已验证)。
|
|
10
|
+
- `src/models.js` 的 `aggLoad`/`get` 二次过滤:`!allowed.length && !allowAny → 跳过整供应商`;`src/routes/chat/index.js` 的 `isAllowlistBlock` 在 `useAuto` 时软跳 `continue`(显式请求才硬 `403`)。
|
|
11
|
+
- 定制 provider 启用门禁下沉为纯函数 `src/runtime/provider-gate.js` —— auth 号型(workbuddy/traework/qoder)**不要求 baseUrl**,旧门禁 `baseUrl && keys` 曾把 qoder 整家静默跳过(表现:`/v1/models` 零条目 + `Model qoder/x is not supported`,无日志)。
|
|
12
|
+
- CLI:`provider <id> models` 与 `model list --provider` 为直查一级答案(`--json` 供脚本),HTTP `GET /v1/providers/:id/models`;价格列 `x0.00/浮动/—` 对齐展示(workbuddy 按 credits 升序)。
|
|
13
|
+
- 新增 provider 三步走:registry 注册 + classify(local-only 判据见 ADR-0023)+ allowlist 只增不减补齐;必须裸调验证 `free` 是否免 Key;`free` 4 个仅目录免 Key ≠ 对话免 Key(cline 教训)。
|
|
14
|
+
|
|
15
|
+
## qoder(原生 COSY 栈,ADR-0029/0036)
|
|
16
|
+
|
|
17
|
+
- 凭证单一源 `auths/qoder-<uid>.json`(0600,含每号 `region` —— `state.normalizeAuths` 会剥该字段,故 region 只能存 auths 文件);签到按每号 region 选域名(cn=`openapi.qoder.com.cn` 走 `daily-check-in/status→claim`,409=今日已领;global=`openapi.qoder.sh` 该端点 404 → 回落 `/sash/api/v1/me/campaigns→/{id}/claim`);认证只需 `Bearer <device_token>` + `cosy-clienttype: 10`(**无 COSY 签名、claim 空 body**);默认只领 `CLAIM_BENEFIT`,促销条目 `VIEW_DETAILS` 需 `-provider qoder checkin --any`(实测能 claim,但奖励是订阅折扣、不进 quota);余额 `GET /api/v2/quota/usage`;daemon 每日 09:00 自动(`MSLXDFF_QODER_CHECKIN=0` 关,`_CHECKIN_HOUR` 改点)。
|
|
18
|
+
- **流式坏号判决面有两条**:①首包 HTTP 非 200;②**HTTP 200 但 SSE 信封内 `statusCodeValue` 非 200**(限流裹成 `200` + `{"code":"403","message":{"isQueued":true,"retryAfterSeconds":30}}`,只盖①会漏掉②——实测后果:94 次 empty turn 502、0 次恢复)。②的取法:返回 Response **前预读上游首帧**取判决(`PEEK_MAX_FRAMES=8`,遇判决或首个有效载荷即停),预读字节原样回灌给 `reshapeQoderStream`(`prefetched` 参数),对外仍恒 200、状态码只走内部头 `x-mslxdff-qoder-upstream-status`(fetch 异常折 502)。`test/qoder-envelope-status.test.js` 4 例。
|
|
19
|
+
- **同请求粘号**(`scope=reqId`,`MSLXDFF_QODER_STICKY_MS` 默认 600s,`0`=关):一次客户端请求内复用同一号,仅 401/403/429/5xx 冷却才换;`pick=new|sticky|switch|forced` 经 `x-mslxdff-qoder-account` 回显、冷却经 `x-mslxdff-qoder-cooldown` 回显。`test/qoder-cooldown-stream.test.js` 3 例。
|
|
20
|
+
- **wire 四条硬规矩**(openspec qoder-p0-fixes,`test/qoder-p0-fixes.test.js` 19 例):① 推理模型 `model_config.is_reasoning` 与 `chat_context.extra.modelConfig` 必须**双写**(只写前者上游不吐 reasoning_content);数据源=`models.js peekModels` 快照(不吃 TTL、chat 路径恒 0 额外上游调用、冷目录降级 false 不阻断,preheat 灌缓存);② 客户端无 tools 发 `tools:[]`——baseprompt 模板内嵌 14 工具永不出门;③ 上游 `finish_reason` 如实透传(length 截断从此可见),仅全程未给才兜底 stop/tool_calls,stream 与 aggregate 收尾判定同构;④ SSE 未完结行超 2MiB 即断(完整行消费后判、不连坐合法帧;流式流内 error 收尾、聚合 502 走短冷却)。
|
|
21
|
+
- **判决三分类与冷却三档(ADR-0044)**:同一个 `10605` 容器既裹额度也裹排队,`sse.js` 判序固定「先 `findQuotaSignal`(内层 code 110 / Billing 措辞)→ 后 `findQueueSignal`(code 10605 / `isQueued:true` / `serviceAvailable:false`)」。排队**不新增 kind**(仍 `upstream`,只挂 `err.queued`),经内部头 `x-mslxdff-qoder-queued` 交出门面,对外仍恒 200。门面冷却分三档:额度 `quotaCooldownMs`(1h) > **排队 `queueCooldownMs`(300s)** > 其它 `cooldownMs`(30s),且额度与排队**都在同一请求内当场换号**(`isQuota || isQueued` 且 `ring.available()>0` → `continue`);空转(模型零输出)仍不换号不冷却(ADR-0036 否决方案 D 的口径不变,别把两者混为一谈)。全号都在冷却时 `pickKey` 的 `ring.next() || keys[0]` 仍会 `forced` 补一发(冷却是轮转偏好不是硬拒)。`test/qoder-queue-switch.test.js` 8 例;`test/qoder-envelope-status.test.js` 现为 5 例(前者首已按新行为改写)。
|
|
22
|
+
- `GET /v1/models` 在 `modelPicks` 非空时按勾选集裁剪(`filterByPicks`,空勾选=不裁剪,`?all=1` 绕过),`-models` 交互候选取数恒走 `?all=1`;交互选择器按终端行高开窗分页(`src/chooser.js`,多选行形状 `❯ [✓] <id>`)。
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## qwenwork(cn)与 globalqwenwork(国际站)— 同协议两租户(ADR-0037/0041)
|
|
26
|
+
|
|
27
|
+
- 千问办公两站 `gateway.qwenwork.cn` / `gateway.qwenwork.ai`:COSY 签名(**同一枚 RSA 模数**)、SSE 端点 `/algo/api/v2/service/pro/sse/agent_chat_generation`、chat body、refresh `{refresh_token}` **逐字同构**(现网双站实测 200),故 globalqwenwork 的协议模块从 qwenwork 逐字 vendor(仓库约定:provider 目录自包含、不跨目录 import)。**差异只在三处租户参数**:auth 域名、OAuth `client_id`(cn `e883ade2…` / intl `cc65e5fc…`)、深链 `redirect_uri`(`qwenwork-cn://` / `qwenwork://`);**凭证与积分池两站不互通**(cn token 打 .ai → 401 INVALID_TOKEN)。
|
|
28
|
+
- ⚠ **registry.js 子串陷阱**:qwenwork 的 matcher 是 `includes("qwenwork")`,而 `globalqwenwork` / `globalqwenwork://native` 都含该子串且 `getCustomProviderFactory` first-match-wins —— **globalqwenwork 条目必须排在 qwenwork 之前**,否则国际站 token 被 cn 工厂接管(乱 401 + refresh 污染 cn 号)。`test/globalqwenwork-provider.test.js` 第 2 例锁死该顺序;动 registry 顺序前先跑它。
|
|
29
|
+
- 模型池零交集:cn `flash`/`pro`/`qwen3.8-max-preview`(pf 0.1/1/1.8,reasoning)↔ intl 仅 `qwork-auto`(→qwen3.8-flash)/`qwork-advanced`(→gpt-5.6-sol)(pf 0,非 reasoning,is_vl,max_input 180000)。登录各自种 allowlist、`allowAnyModels=false`(额度池防 auto 烧分)。国际站官方客户端自身走 gRPC `model.chat.ChatService/ChatCompletionStream`(其 `region/endpoints` 在 .ai 404),本仓不走 —— 见 ADR-0041 决策 1。
|
|
30
|
+
## workbuddy
|
|
31
|
+
|
|
32
|
+
- 专用 `POST /v2/chat/completions` **强制 `stream:true`** + `GET /v3/config` 按 credits 升序前缀 `workbuddy/`(**2026-10-10 换端点**:旧 `GET /console/enterprises/personal/models` 对多数 token 恒 401/500 → 目录 0 条 → `aggLoad` 判 `!allowed.length` 把整家跳过;`/v3/config` 返回**随 UA 变**,本仓 UA 与 chat 同身份故目录即"自己能调的那套";`listModels` 已改逐 key 轮换,单个坏 token 不再清空目录);401/403 自动 `POST /v2/plugin/auth/token/refresh` 回写 `state.json`+`auths/workbuddy-<uid>.json`(0600 tmp+rename)。
|
|
33
|
+
- 凭据目录 `authDirFor()` 跟账本走 + `authDirCandidates()` 只读兜底旧位置 + `listAccountDocs()` 单一读取出口(provider/CLI/token-auto 四处复制品合一);`-workbuddy remove` 扫全部候选目录防旧副本「复活」(ADR-0025,`test/workbuddy-authdir.test.js` 6 例)。
|
|
34
|
+
- 接入禁手填 `<key>`,必须 `node workbuddy-token-auto.js` 自动化(whistle MITM :8899;whistle/MITM 取 codex token 路径已证伪);桌面登录态跨平台发现 `src/providers/workbuddy/desktop-info.js`(显式 `--file`/env > 后缀快查 > 文件名 BFS depth≤6;Win `%LOCALAPPDATA%`/`%APPDATA%`、mac `~/Library/Application Support`、Linux `~/.config`);设备授权加号(0.1.93):`POST /v2/plugin/auth/state?platform=CLI` → 浏览器登录 → 轮询 `token?state=`,UA `CLI/2.63.2 CodeBuddy/2.63.2` 可 `MSLXDFF_WORKBUDDY_UA` 覆盖;每日 `workbuddy-checkin.js` 双域幂等 `code 10001` 视为成功。
|
|
35
|
+
- `share` 默认 `off` 需 `MSLXDFF_WORKBUDDY_SHARE_KEYS=1`;`-setto workbuddy [modelId]` 原子写 `~/.workbuddy/models.json`(仅 127.0.0.1/v1)。
|
|
36
|
+
- 出网 11128 指纹剥离(`payload.js` CC_SENTENCES 清单式):上游扫 system/assistant,命中行首 `x-anthropic-billing-header:` 整行或官方固定整句("You are Claude Code…" / "To give feedback…anthropics/claude-code/issues")即 400 "unapproved channel"(实测整句纯子串匹配、user/tool 不扫、换说法放行)——只剥固定形态,新拦句加清单。
|
|
37
|
+
|
|
38
|
+
## cline
|
|
39
|
+
|
|
40
|
+
- 指纹头:UA `Cline/3.0.47` + `X-CLIENT-TYPE: cline-sdk` + `X-CLIENT-VERSION` 同 UA;对话链 = `POST /api/v1/auth/refresh`(**每次 refresh 轮换 refreshToken 必须回写**)→ `Bearer workos:<accessToken>` + 固定 `<base>/api/v1/chat/completions`(**baseUrl 已含 /api/v1 时只拼 `/chat/completions`,否则双 /api/v1 会 401/empty response**);deepseek/z-ai 免费通道必须 `stream:true` + SSE 聚合否则 `500 empty response content`;`X-Task-ID`/`session_id` 每次唯一。
|
|
41
|
+
- 发上游剥 `provider/` 前缀(`cline/deepseek/X→deepseek/X`)恢复 forceStream 聚合 + 非 JSON 响应体 TypeError 加固(`test/cline-chat-id.test.js` 4 用例,0.1.91)。
|
|
42
|
+
- 白名单自动同步(ADR-0033):`allowlist-sync.js` 纯函数 `planMerge`(只增不减)+ `createAllowlistSync`(无新增不写盘、异常吞掉留痕),由 `cline/index.js` 注入;`cline/models.js` 仅在**上游成功取数**路径调 `notifyRefresh`(内置兜底常量不写盘,防污染);`MSLXDFF_CLINE_AUTOSYNC=0` 关;**教训:`saveProviderAllowedModels`/`saveProviderConfig` 重建 cfg 时起手 `{baseUrl,keys}` 未带 `allowAnyModels` 会静默抹成 false**(已修,勿回退)。
|
|
43
|
+
- 登录走 WorkOS 设备授权流(client_id `client_01K3A541FN8TA3EPPHTD2325AR`),refreshToken 落 `providerConfigs.{cline}.keys`;bench 检测 keys 含 refreshToken 即走 refresh→指纹头→stream 测速专用分支。
|
|
44
|
+
|
|
45
|
+
## bench 与额度保护(`src/bench/`)
|
|
46
|
+
|
|
47
|
+
- `bench/bench --via` 仅测(allowlist ∩ 全局 picks)交集(`filterBenchModels`,picks 空回退 allowlist);计费模型(无 `-free` 后缀)需显式点名才测(`max_tokens=5` 也计费);`--via` 额度保护默认跳过 `opencode` 需 `--include-opencode` 且 TTY 二次确认 `y/N`;0.1.84 起完全串行 `直连→逐 peer`(`MSLXDFF_BENCH_DELAY_MS=120`)逐模型实时打印(`--json` 进度走 `stderr`)。
|
|
48
|
+
- `aihubmix gemini 400 no_available_channel` 为上游无通道非 bench bug;peer name 统一短化 `host:port`。
|
|
49
|
+
- **烧额度的实测必须先经用户明确同意**:付费/订阅/按量计费上游的裸调、A/B 长文生成一律先问(2026-09-23 教训);免费池探针(public 免费模型、短 prompt 微量)可直接跑。
|