dsh-multi-router 0.5.6 → 0.5.8
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/CHANGELOG.md +13 -0
- package/README.md +10 -4
- package/cordis.patch.yml +2 -2
- package/lib/{chunk-XKDXPB5T.js → chunk-ILRW3ELD.js} +28 -11
- package/lib/chunk-ILRW3ELD.js.map +7 -0
- package/lib/{chunk-CT6ZYIAF.js → chunk-P43AWJ4Q.js} +9 -5
- package/lib/chunk-P43AWJ4Q.js.map +7 -0
- package/lib/client.js +93 -37
- package/lib/client.js.map +3 -3
- package/lib/host-v0.5.8.js +514 -0
- package/lib/host.js +154 -24
- package/lib/index.js +154 -24
- package/lib/index.js.map +4 -4
- package/lib/manager.js +1 -1
- package/lib/shim.js +1 -1
- package/package.json +1 -1
- package/lib/chunk-CT6ZYIAF.js.map +0 -7
- package/lib/chunk-XKDXPB5T.js.map +0 -7
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createShim
|
|
3
|
+
} from "./chunk-P43AWJ4Q.js";
|
|
4
|
+
import {
|
|
5
|
+
MANAGEMENT_PATH,
|
|
6
|
+
Manager,
|
|
7
|
+
managementHandler,
|
|
8
|
+
version
|
|
9
|
+
} from "./chunk-ILRW3ELD.js";
|
|
10
|
+
import {
|
|
11
|
+
createAccountClient
|
|
12
|
+
} from "./chunk-U2MU2M5C.js";
|
|
13
|
+
import "./chunk-OFFVP6YN.js";
|
|
14
|
+
import "./chunk-FTM7RHQV.js";
|
|
15
|
+
import "./chunk-S5KMFAQM.js";
|
|
16
|
+
import "./chunk-DNQPZMPF.js";
|
|
17
|
+
import {
|
|
18
|
+
Router,
|
|
19
|
+
SessionMetrics,
|
|
20
|
+
abortable
|
|
21
|
+
} from "./chunk-J3XO7RTF.js";
|
|
22
|
+
import {
|
|
23
|
+
PROVIDERS,
|
|
24
|
+
PROVIDER_NAMES,
|
|
25
|
+
ensureConfig
|
|
26
|
+
} from "./chunk-MCHV2R25.js";
|
|
27
|
+
import {
|
|
28
|
+
normalizeCredits
|
|
29
|
+
} from "./chunk-TEZ2FMEO.js";
|
|
30
|
+
|
|
31
|
+
// src/index.mjs
|
|
32
|
+
import z from "@deepseek-ai/schemastery";
|
|
33
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
34
|
+
import { resolveRetryPolicy as resolveRetryPolicy2, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
|
|
35
|
+
import { join as join2, resolve } from "node:path";
|
|
36
|
+
|
|
37
|
+
// upstream/workbuddy/src/adapter.ts
|
|
38
|
+
import { createProvider } from "@earendil-works/pi-ai";
|
|
39
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
40
|
+
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
41
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
42
|
+
var WORKBUDDY_PROVIDER = "workbuddy";
|
|
43
|
+
var WORKBUDDY_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
44
|
+
var REQUEST_IMAGE_BUDGETS = {
|
|
45
|
+
maxRequestImageBytes: 20971520,
|
|
46
|
+
requestImagePixelBudget: 4194304,
|
|
47
|
+
requestImageMaxBytes: 1048576
|
|
48
|
+
};
|
|
49
|
+
var INERT_AUTH = {
|
|
50
|
+
credentials: {
|
|
51
|
+
async read() {
|
|
52
|
+
return void 0;
|
|
53
|
+
},
|
|
54
|
+
async list() {
|
|
55
|
+
return [];
|
|
56
|
+
},
|
|
57
|
+
async modify() {
|
|
58
|
+
throw new Error("dsh-workbuddy-connect: the workbuddy route has no pi-ai credential lifecycle");
|
|
59
|
+
},
|
|
60
|
+
async delete() {
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
authContext: {
|
|
64
|
+
async env() {
|
|
65
|
+
return void 0;
|
|
66
|
+
},
|
|
67
|
+
async fileExists() {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
73
|
+
function withLegacyImageBudget(store) {
|
|
74
|
+
return new Proxy(store, {
|
|
75
|
+
get(target, property, receiver) {
|
|
76
|
+
if (property === "imageHostPath") {
|
|
77
|
+
return target.imageHostPath.bind(target);
|
|
78
|
+
}
|
|
79
|
+
if (property !== "readImageRequest") return Reflect.get(target, property, receiver);
|
|
80
|
+
return (...args) => {
|
|
81
|
+
const [ref, policy, signal] = args;
|
|
82
|
+
const present = policy?.maxPixels;
|
|
83
|
+
const withPixels = Number.isSafeInteger(present) && present > 0 ? policy : { ...policy, maxPixels: REQUEST_IMAGE_BUDGETS.requestImagePixelBudget };
|
|
84
|
+
return target.readImageRequest(ref, withPixels, signal);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
var RATE_SEPARATOR = " \xB7 ";
|
|
90
|
+
function displaySuffix(info) {
|
|
91
|
+
const parts = [
|
|
92
|
+
normalizeCredits(info.billing?.credits),
|
|
93
|
+
...info.billing?.badges ?? []
|
|
94
|
+
].filter((part) => part !== void 0 && part !== "");
|
|
95
|
+
return parts.length === 0 ? void 0 : parts.join(" \xB7 ");
|
|
96
|
+
}
|
|
97
|
+
function withCatalogDisplay(name2, info) {
|
|
98
|
+
const suffix = displaySuffix(info);
|
|
99
|
+
return suffix === void 0 ? name2 : `${name2}${RATE_SEPARATOR}${suffix}`;
|
|
100
|
+
}
|
|
101
|
+
function reasoningFields(info, observed, region = "cn") {
|
|
102
|
+
const reasoning = info.reasoning;
|
|
103
|
+
if (reasoning === void 0 || reasoning.supports !== true) {
|
|
104
|
+
return { reasoning: false };
|
|
105
|
+
}
|
|
106
|
+
const declared = reasoning.supportedEfforts;
|
|
107
|
+
const efforts = declared !== void 0 && declared.length > 0 ? declared : observed?.validation === "validating" && observed.efforts.length > 0 ? observed.efforts : void 0;
|
|
108
|
+
if (efforts === void 0) {
|
|
109
|
+
return { reasoning: false };
|
|
110
|
+
}
|
|
111
|
+
const map = {
|
|
112
|
+
// Probing never grants `off`; only an explicit declaration does.
|
|
113
|
+
//
|
|
114
|
+
// On CN, `onlyReasoning` vetoes it even when declared (#87). That catalog
|
|
115
|
+
// ships both flags true on `deepseek-v4.1-flash` and `kimi-k2.8-preview`,
|
|
116
|
+
// which cannot both hold — a model that only ever reasons has no
|
|
117
|
+
// non-thinking mode to select — and the endpoint sides with
|
|
118
|
+
// `onlyReasoning`, answering HTTP 400 (「模型不支持该思考强度」) for every
|
|
119
|
+
// `off` request. Offering a level that is certain to fail is worse than not
|
|
120
|
+
// offering it, so the contradiction is resolved toward the flag the wire
|
|
121
|
+
// agrees with.
|
|
122
|
+
//
|
|
123
|
+
// Scoped to CN on purpose: the international catalog does not carry this
|
|
124
|
+
// contradiction, and hiding Off there would remove a level those models
|
|
125
|
+
// can still use. The request layer strips the spelling in both regions
|
|
126
|
+
// (`dropUnsupportedEffort`), so a model whose flags flip between catalog
|
|
127
|
+
// reads cannot put `off` on the wire either way.
|
|
128
|
+
off: reasoning.canDisableThinking === true && (region !== "cn" || reasoning.onlyReasoning !== true) && declared !== void 0 && declared.length > 0 ? "off" : null,
|
|
129
|
+
// `minimal` is not in the upstream effort vocabulary (EFFORT_VALUES), so
|
|
130
|
+
// no declared set — and no probe candidate — can ever contain it.
|
|
131
|
+
minimal: null,
|
|
132
|
+
low: efforts.includes("low") ? "low" : null,
|
|
133
|
+
medium: efforts.includes("medium") ? "medium" : null,
|
|
134
|
+
high: efforts.includes("high") ? "high" : null,
|
|
135
|
+
xhigh: efforts.includes("xhigh") ? "xhigh" : null,
|
|
136
|
+
max: efforts.includes("max") ? "max" : null
|
|
137
|
+
};
|
|
138
|
+
return { reasoning: true, thinkingLevelMap: map };
|
|
139
|
+
}
|
|
140
|
+
function toPiModel(info, baseUrl, observed, providerId, region) {
|
|
141
|
+
return {
|
|
142
|
+
id: info.id,
|
|
143
|
+
name: info.name,
|
|
144
|
+
api: "openai-completions",
|
|
145
|
+
provider: providerId,
|
|
146
|
+
baseUrl,
|
|
147
|
+
input: info.supportsImages === true ? ["text", "image"] : ["text"],
|
|
148
|
+
...reasoningFields(info, observed, region),
|
|
149
|
+
cost: NO_COST,
|
|
150
|
+
contextWindow: info.contextWindow,
|
|
151
|
+
maxTokens: info.maxTokens,
|
|
152
|
+
// pi-ai cannot infer WorkBuddy's field spelling from the shim's random
|
|
153
|
+
// loopback URL, so name the upstream-required field explicitly.
|
|
154
|
+
compat: { maxTokensField: "max_tokens" }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function createWorkBuddyAdapter(options) {
|
|
158
|
+
const { shim, store, catalog, resolveAttachments, resolveImageAccess, observe, hidden } = options;
|
|
159
|
+
const providerId = options.providerId ?? WORKBUDDY_PROVIDER;
|
|
160
|
+
const displayName = options.displayName ?? "WorkBuddy";
|
|
161
|
+
const region = options.region ?? "cn";
|
|
162
|
+
const buildModels = () => {
|
|
163
|
+
const baseUrl = `${shim.baseUrl()}/v1`;
|
|
164
|
+
return catalog.current().map((info) => toPiModel(info, baseUrl, observe?.(info.id), providerId, region));
|
|
165
|
+
};
|
|
166
|
+
const base = createProvider({
|
|
167
|
+
id: providerId,
|
|
168
|
+
name: displayName,
|
|
169
|
+
auth: {
|
|
170
|
+
apiKey: {
|
|
171
|
+
name: "WorkBuddy OAuth bearer token",
|
|
172
|
+
async resolve({ credential }) {
|
|
173
|
+
const apiKey = credential?.key;
|
|
174
|
+
return apiKey === void 0 || apiKey.length === 0 ? void 0 : { auth: { apiKey }, source: "WorkBuddy" };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
models: buildModels(),
|
|
179
|
+
api: openAICompletionsApi()
|
|
180
|
+
});
|
|
181
|
+
const provider = { ...base, getModels: () => buildModels() };
|
|
182
|
+
const profile = {
|
|
183
|
+
provider: providerId,
|
|
184
|
+
displayName,
|
|
185
|
+
streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
|
|
186
|
+
retryPolicy: resolveRetryPolicy(void 0, "dsh-workbuddy-connect retryPolicy"),
|
|
187
|
+
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
188
|
+
// Required since 0.1.5-alpha.2; the live catalog only exposes models that
|
|
189
|
+
// probed successfully, so there is never a per-model failure to report.
|
|
190
|
+
modelErrors: /* @__PURE__ */ new Map(),
|
|
191
|
+
...REQUEST_IMAGE_BUDGETS,
|
|
192
|
+
piProvider: provider
|
|
193
|
+
};
|
|
194
|
+
let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
195
|
+
const adapter = new WorkBuddyPiAiAdapter(catalog, hidden ?? (() => []), {
|
|
196
|
+
profiles: () => profiles,
|
|
197
|
+
auth: INERT_AUTH,
|
|
198
|
+
// Resolve the shim's per-process shared secret as the OpenAI apiKey so
|
|
199
|
+
// pi-ai sends it as `Authorization: Bearer <shared-secret>`. The shim
|
|
200
|
+
// validates this before forwarding and resolves the real WorkBuddy token
|
|
201
|
+
// itself via the store, so the secret never reaches upstream.
|
|
202
|
+
resolveApiKey: async () => shim.token(),
|
|
203
|
+
// Every store the adapter hands to pi-ai passes the legacy-budget wrapper:
|
|
204
|
+
// see withLegacyImageBudget — the mismatch it heals depends on which host
|
|
205
|
+
// generation owns the attachment service, not on anything observable here.
|
|
206
|
+
...resolveAttachments === void 0 ? {} : {
|
|
207
|
+
resolveAttachments: () => {
|
|
208
|
+
const store2 = resolveAttachments();
|
|
209
|
+
return store2 === void 0 ? void 0 : withLegacyImageBudget(store2);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
...resolveImageAccess === void 0 ? {} : { resolveImageAccess }
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
adapter,
|
|
216
|
+
invalidate: () => {
|
|
217
|
+
profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
var WorkBuddyPiAiAdapter = class extends PiAiAdapter {
|
|
222
|
+
constructor(catalog, hidden, options) {
|
|
223
|
+
super(options);
|
|
224
|
+
this.catalog = catalog;
|
|
225
|
+
this.hidden = hidden;
|
|
226
|
+
}
|
|
227
|
+
/** Catalog entry for one model id, or undefined when the catalog omits it. */
|
|
228
|
+
infoFor(model) {
|
|
229
|
+
return this.catalog.current().find((entry) => entry.id === model);
|
|
230
|
+
}
|
|
231
|
+
async listModels(provider) {
|
|
232
|
+
const models = await super.listModels(provider);
|
|
233
|
+
const hidden = new Set(this.hidden());
|
|
234
|
+
return models.flatMap((model) => {
|
|
235
|
+
if (hidden.has(model.id)) return [];
|
|
236
|
+
const info = this.infoFor(model.id);
|
|
237
|
+
if (info === void 0) return [model];
|
|
238
|
+
return [{ ...model, name: withCatalogDisplay(model.name, info) }];
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async resolveModel(provider, model, signal) {
|
|
242
|
+
const resolved = await super.resolveModel(provider, model, signal);
|
|
243
|
+
const info = this.infoFor(model);
|
|
244
|
+
if (info === void 0) return resolved;
|
|
245
|
+
return { ...resolved, name: withCatalogDisplay(resolved.name, info) };
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/plugin-reload.mjs
|
|
250
|
+
import { readFile } from "node:fs/promises";
|
|
251
|
+
import { watch } from "node:fs";
|
|
252
|
+
import { join } from "node:path";
|
|
253
|
+
function outsideActivation(hmr, operation) {
|
|
254
|
+
const scope = hmr?.executing;
|
|
255
|
+
return typeof scope?.exit === "function" ? scope.exit(operation) : operation();
|
|
256
|
+
}
|
|
257
|
+
var PluginReload = class {
|
|
258
|
+
constructor({ service, profile, canReload, hmr = () => void 0, busy = () => false, now = Date.now, runningVersion }) {
|
|
259
|
+
Object.assign(this, { service, profile, canReload, hmr, busy, now, runningVersion });
|
|
260
|
+
this.phase = "current";
|
|
261
|
+
this.installedVersion = runningVersion;
|
|
262
|
+
this.retryAt = 0;
|
|
263
|
+
}
|
|
264
|
+
state() {
|
|
265
|
+
return { runningVersion: this.runningVersion, installedVersion: this.installedVersion, phase: this.phase, canReload: !!this.canReload(), message: this.message };
|
|
266
|
+
}
|
|
267
|
+
start() {
|
|
268
|
+
const dir = this.profile()?.dir;
|
|
269
|
+
if (dir) try {
|
|
270
|
+
this.watcher = watch(dir, () => this.schedule());
|
|
271
|
+
this.watcher.unref();
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
this.schedule();
|
|
275
|
+
}
|
|
276
|
+
schedule(delay = 500) {
|
|
277
|
+
if (this.closed) return;
|
|
278
|
+
clearTimeout(this.timer);
|
|
279
|
+
this.timer = setTimeout(() => {
|
|
280
|
+
void this.check().catch(() => {
|
|
281
|
+
});
|
|
282
|
+
}, delay);
|
|
283
|
+
this.timer.unref();
|
|
284
|
+
}
|
|
285
|
+
stop() {
|
|
286
|
+
this.closed = true;
|
|
287
|
+
clearTimeout(this.timer);
|
|
288
|
+
this.watcher?.close();
|
|
289
|
+
}
|
|
290
|
+
check() {
|
|
291
|
+
if (this.closed) return Promise.resolve();
|
|
292
|
+
if (this.flight) return this.flight;
|
|
293
|
+
this.flight = this.inspect().finally(() => {
|
|
294
|
+
this.flight = void 0;
|
|
295
|
+
});
|
|
296
|
+
return this.flight;
|
|
297
|
+
}
|
|
298
|
+
async inspect() {
|
|
299
|
+
const dir = this.profile()?.dir;
|
|
300
|
+
if (!dir) return;
|
|
301
|
+
let manifest, installed;
|
|
302
|
+
try {
|
|
303
|
+
manifest = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
|
|
304
|
+
installed = JSON.parse(await readFile(join(dir, "node_modules/dsh-multi-router/package.json"), "utf8"));
|
|
305
|
+
} catch {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (this.closed || !manifest.dsh?.profile?.bundles?.includes("dsh-multi-router")) return;
|
|
309
|
+
if (installed.name !== "dsh-multi-router" || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(installed.version)) return;
|
|
310
|
+
this.installedVersion = installed.version;
|
|
311
|
+
if (installed.version === this.runningVersion) {
|
|
312
|
+
this.phase = "current";
|
|
313
|
+
this.message = void 0;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!this.canReload() || !this.service()?.setBundleEnabled) {
|
|
317
|
+
this.phase = "restart-required";
|
|
318
|
+
this.message = "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u8FD0\u884C\u4E2D\u91CD\u8F7D\uFF0C\u8BF7\u5347\u7EA7 DSH \u6216\u91CD\u542F\u5E94\u7528\u3002";
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (this.busy()) {
|
|
322
|
+
this.phase = "waiting";
|
|
323
|
+
this.message = "\u65B0\u7248\u672C\u5DF2\u5B89\u88C5\uFF0C\u7B49\u5F85\u5F53\u524D\u8BF7\u6C42\u548C\u8D26\u53F7\u64CD\u4F5C\u7ED3\u675F\u540E\u81EA\u52A8\u751F\u6548\u3002";
|
|
324
|
+
this.schedule(1e3);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const root = join(dir, "node_modules/dsh-multi-router");
|
|
328
|
+
try {
|
|
329
|
+
const patch = await readFile(join(root, "cordis.patch.yml"), "utf8");
|
|
330
|
+
if (!patch.includes(`id: llm-multi-router-v2-${installed.version}
|
|
331
|
+
`) || !patch.includes(`name: ./lib/host-v${installed.version}.js`)) throw Error("legacy entry");
|
|
332
|
+
await readFile(join(root, `lib/host-v${installed.version}.js`));
|
|
333
|
+
} catch {
|
|
334
|
+
this.phase = "restart-required";
|
|
335
|
+
this.message = "\u5B89\u88C5\u7248\u672C\u7F3A\u5C11\u7248\u672C\u5316\u5165\u53E3\uFF0C\u91CD\u65B0\u5B89\u88C5\u6700\u65B0\u7248\u540E\u518D\u8BD5\u3002";
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (this.now() < this.retryAt) return;
|
|
339
|
+
this.phase = "reloading";
|
|
340
|
+
this.message = "\u6B63\u5728\u91CD\u65B0\u52A0\u8F7D\u63D2\u4EF6\u4E0E\u914D\u7F6E\u9762\u677F\u2026";
|
|
341
|
+
this.retryAt = this.now() + 6e4;
|
|
342
|
+
try {
|
|
343
|
+
const result = await outsideActivation(this.hmr(), () => this.service().setBundleEnabled("dsh-multi-router", true));
|
|
344
|
+
if (this.closed) return;
|
|
345
|
+
this.phase = "restart-required";
|
|
346
|
+
this.message = result.application === "applied" ? "DSH \u5C1A\u672A\u5207\u6362\u5230\u65B0\u7248\u672C\uFF0C\u8BF7\u91CD\u65B0\u52A0\u8F7D\u63D2\u4EF6\u6216\u67E5\u770B\u7EC4\u4EF6\u9519\u8BEF\u3002" : "DSH \u672A\u80FD\u91CD\u8F7D\u63D2\u4EF6\uFF0C\u8BF7\u5347\u7EA7 DSH \u6216\u91CD\u542F\u5E94\u7528\u3002";
|
|
347
|
+
} catch {
|
|
348
|
+
if (!this.closed) {
|
|
349
|
+
this.phase = "failed";
|
|
350
|
+
this.message = "\u63D2\u4EF6\u91CD\u8F7D\u672A\u5B8C\u6210\uFF0C\u53EF\u7A0D\u540E\u91CD\u8BD5\uFF1B\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\u4FDD\u6301\u4E0D\u53D8\u3002";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/index.mjs
|
|
357
|
+
var name = "dsh-multi-router";
|
|
358
|
+
var inject = ["llm"];
|
|
359
|
+
var defaultConfigFile = () => join2(resolveDshHome(), "multi-router", "router.json");
|
|
360
|
+
var Config = z.object({ configFile: z.string().description("Absolute path to multi-account router.json") });
|
|
361
|
+
async function apply(ctx, options = {}) {
|
|
362
|
+
let disposed = false, manager, stopWatch, monitorTimer, startup;
|
|
363
|
+
const releases = [], shims = [], wrappers = [];
|
|
364
|
+
ctx.effect(() => () => {
|
|
365
|
+
disposed = true;
|
|
366
|
+
stopWatch?.();
|
|
367
|
+
clearInterval(monitorTimer);
|
|
368
|
+
manager?.runtime?.stop();
|
|
369
|
+
if (manager) manager.closed = true;
|
|
370
|
+
manager?.plugins.abort.abort();
|
|
371
|
+
manager?.checkins.abort.abort();
|
|
372
|
+
manager?.workbuddyLogin.abort.abort();
|
|
373
|
+
for (const release of releases) release();
|
|
374
|
+
return Promise.all([...shims.map((shim) => shim.close()), ...manager?.refreshes.values() ?? [], ...manager?.monitorFlights.values() ?? [], startup, manager?.localScan, manager?.checkins.flight, manager?.plugins.flight, manager?.plugins.saveQueue.catch(() => {
|
|
375
|
+
}), manager?.telemetry.queue.catch(() => {
|
|
376
|
+
})]);
|
|
377
|
+
});
|
|
378
|
+
const file = resolve(options.configFile ?? process.env.DSH_MULTI_ROUTER_CONFIG ?? defaultConfigFile());
|
|
379
|
+
try {
|
|
380
|
+
const config = await ensureConfig(file);
|
|
381
|
+
if (disposed) return;
|
|
382
|
+
const router = new Router(
|
|
383
|
+
config,
|
|
384
|
+
(a) => createAccountClient(a, manager?.config ?? config, ctx.get("attachments")),
|
|
385
|
+
(event) => ctx.logger.info("multi-router", JSON.stringify(event))
|
|
386
|
+
);
|
|
387
|
+
manager = new Manager(file, router, () => {
|
|
388
|
+
for (const wrapper of wrappers) wrapper.invalidate();
|
|
389
|
+
ctx.emit("llm/adapters-updated");
|
|
390
|
+
}, { service: () => ctx.get("pluginManager"), profile: () => ctx.get("profileContext") });
|
|
391
|
+
await manager.load();
|
|
392
|
+
if (disposed) return;
|
|
393
|
+
manager.startup = { phase: "discovering" };
|
|
394
|
+
manager.runtime = new PluginReload({
|
|
395
|
+
runningVersion: version,
|
|
396
|
+
service: () => ctx.get("pluginManager"),
|
|
397
|
+
profile: () => ctx.get("profileContext"),
|
|
398
|
+
canReload: () => !!ctx.get("hmr"),
|
|
399
|
+
hmr: () => ctx.get("hmr"),
|
|
400
|
+
busy: () => !!(manager.localScan || manager.requests || manager.plugins.flight || manager.checkins.flight || manager.workbuddyLogin.flight || manager.workbuddyLogin.pending || manager.refreshes.size || manager.monitorFlights.size || shims.some((s) => s.active()))
|
|
401
|
+
});
|
|
402
|
+
ctx.inject?.(["webServer"], (webCtx) => {
|
|
403
|
+
webCtx.effect(() => webCtx.webServer.register({ kind: "exact", path: MANAGEMENT_PATH, handler: managementHandler(manager) }));
|
|
404
|
+
});
|
|
405
|
+
const metrics = new SessionMetrics(manager.telemetry);
|
|
406
|
+
ctx.on("session/event", (session, event) => {
|
|
407
|
+
try {
|
|
408
|
+
return metrics.event(session, event)?.catch(() => ctx.logger.warn("multi-router: performance persistence failed"));
|
|
409
|
+
} catch {
|
|
410
|
+
ctx.logger.warn("multi-router: performance event unavailable");
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
ctx.on("session/disposed", (session) => metrics.dispose(session));
|
|
414
|
+
for (const provider of ["multi-router", ...PROVIDERS]) {
|
|
415
|
+
if (disposed) return;
|
|
416
|
+
const isRouter = provider === "multi-router";
|
|
417
|
+
const shim = createShim(router, { modelPrefix: isRouter ? "" : `${provider}::` });
|
|
418
|
+
shims.push(shim);
|
|
419
|
+
await shim.ready;
|
|
420
|
+
if (disposed) {
|
|
421
|
+
await shim.close();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const wrapper = createWorkBuddyAdapter({
|
|
425
|
+
providerId: provider,
|
|
426
|
+
displayName: isRouter ? "\u7EDF\u4E00\u8DEF\u7531" : `${PROVIDER_NAMES[provider]} \xB7 \u5185\u7F6E\u8D26\u53F7\u6C60`,
|
|
427
|
+
shim,
|
|
428
|
+
catalog: { current: () => isRouter ? manager.adapterModels() : manager.providerModels(provider) },
|
|
429
|
+
hidden: () => isRouter ? manager.adapterModels().filter((r) => r.visible === false).map((r) => r.id) : manager.hiddenModels(provider),
|
|
430
|
+
region: provider.endsWith("global") || provider === "workbuddy-ai" ? "global" : "cn",
|
|
431
|
+
resolveAttachments: () => ctx.get("attachments"),
|
|
432
|
+
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(
|
|
433
|
+
attachments,
|
|
434
|
+
(path) => ctx.get("fs")?.processPathFromHostPath(path),
|
|
435
|
+
ref
|
|
436
|
+
)
|
|
437
|
+
});
|
|
438
|
+
if (!isRouter) {
|
|
439
|
+
for (const method of ["listModels", "resolveModel", "prepareCall"]) {
|
|
440
|
+
const original = wrapper.adapter[method].bind(wrapper.adapter);
|
|
441
|
+
wrapper.adapter[method] = async (...args) => {
|
|
442
|
+
await abortable(manager.ensureCatalog(provider), args[2]);
|
|
443
|
+
return original(...args);
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const streamWithSnapshot = wrapper.adapter.streamWithSnapshot.bind(wrapper.adapter);
|
|
448
|
+
wrapper.adapter.streamWithSnapshot = (options2, snapshot) => {
|
|
449
|
+
const call = metrics.beginCall(options2.sessionId, options2.purpose);
|
|
450
|
+
const route = router.config.routes[(isRouter ? "" : `${provider}::`) + options2.model];
|
|
451
|
+
const effort = options2.reasoningEffort ?? route?.reasoningEffort;
|
|
452
|
+
const profiles = new Map(snapshot.profiles), profile = profiles.get(options2.provider);
|
|
453
|
+
if (!profile) return streamWithSnapshot(options2, snapshot);
|
|
454
|
+
profiles.set(options2.provider, { ...profile, headers: { ...profile.headers, "X-Multi-Router-Reasoning": effort ?? "default", "X-Multi-Router-Call": call.callId, ...call.session ? { "X-Multi-Router-Session": call.session } : {} } });
|
|
455
|
+
return streamWithSnapshot({ ...options2, ...effort ? { reasoningEffort: effort } : {} }, { ...snapshot, profiles });
|
|
456
|
+
};
|
|
457
|
+
const withDefault = (model) => {
|
|
458
|
+
const effort = router.config.routes[(isRouter ? "" : `${provider}::`) + model.id]?.reasoningEffort;
|
|
459
|
+
return effort && model.reasoning ? { ...model, reasoning: { ...model.reasoning, defaultEffort: effort } } : model;
|
|
460
|
+
};
|
|
461
|
+
const resolveModel = wrapper.adapter.resolveModel.bind(wrapper.adapter), prepareCall = wrapper.adapter.prepareCall.bind(wrapper.adapter);
|
|
462
|
+
wrapper.adapter.resolveModel = async (...args) => withDefault(await resolveModel(...args));
|
|
463
|
+
wrapper.adapter.prepareCall = async (...args) => {
|
|
464
|
+
const prepared = await prepareCall(...args);
|
|
465
|
+
return { ...prepared, model: withDefault(prepared.model) };
|
|
466
|
+
};
|
|
467
|
+
wrapper.adapter.providerRetryPolicy = () => resolveRetryPolicy2({ mode: "normal", maxRetries: 0 }, provider);
|
|
468
|
+
wrappers.push(wrapper);
|
|
469
|
+
releases.push(ctx.llm.registerAdapter([provider], wrapper.adapter));
|
|
470
|
+
}
|
|
471
|
+
stopWatch = manager.watch(() => ctx.logger.warn("multi-router: invalid config change; keeping last valid configuration"));
|
|
472
|
+
startup = (async () => {
|
|
473
|
+
await manager.syncLocalAccounts().catch(() => ctx.logger.warn("multi-router: local login discovery failed; manual import remains available"));
|
|
474
|
+
if (disposed) return;
|
|
475
|
+
await manager.finishInitialSetup().catch(() => ctx.logger.warn("multi-router: first-install guidance could not be saved"));
|
|
476
|
+
if (disposed) return;
|
|
477
|
+
manager.startup = { phase: "ready" };
|
|
478
|
+
manager.checkins.tick();
|
|
479
|
+
void manager.checkFreeModels().catch(() => ctx.logger.warn("multi-router: free catalog persistence failed"));
|
|
480
|
+
void manager.monitorAll().catch(() => ctx.logger.warn("multi-router: usage persistence failed"));
|
|
481
|
+
})();
|
|
482
|
+
void manager.plugins.tick().catch(() => ctx.logger.warn("multi-router: automatic update check unavailable"));
|
|
483
|
+
manager.runtime.start();
|
|
484
|
+
ctx.on("plugin-manager/changed", () => manager.runtime.schedule());
|
|
485
|
+
monitorTimer = setInterval(() => {
|
|
486
|
+
void manager.syncLocalAccounts().then(() => {
|
|
487
|
+
if (!disposed) return manager.monitorAll();
|
|
488
|
+
}).catch(() => ctx.logger.warn("multi-router: local login discovery or usage persistence failed"));
|
|
489
|
+
manager.checkins.tick();
|
|
490
|
+
void manager.plugins.tick().catch(() => ctx.logger.warn("multi-router: automatic update check unavailable"));
|
|
491
|
+
void manager.checkFreeModels().catch(() => ctx.logger.warn("multi-router: free catalog persistence failed"));
|
|
492
|
+
void manager.runtime.check().catch(() => {
|
|
493
|
+
});
|
|
494
|
+
}, 6e4);
|
|
495
|
+
monitorTimer.unref();
|
|
496
|
+
ctx.emit("llm/adapters-updated");
|
|
497
|
+
} catch (error) {
|
|
498
|
+
clearInterval(monitorTimer);
|
|
499
|
+
stopWatch?.();
|
|
500
|
+
manager?.runtime?.stop();
|
|
501
|
+
for (const release of releases) release();
|
|
502
|
+
await Promise.all(shims.map((shim) => shim.close()));
|
|
503
|
+
ctx.logger.error("multi-router: initialization failed", error.code ?? error.message);
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
export {
|
|
508
|
+
Config,
|
|
509
|
+
apply,
|
|
510
|
+
defaultConfigFile,
|
|
511
|
+
inject,
|
|
512
|
+
name
|
|
513
|
+
};
|
|
514
|
+
//# sourceMappingURL=index.js.map
|