dsh-model-reasoning-defaults 0.0.1

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/lib/index.mjs ADDED
@@ -0,0 +1,566 @@
1
+ import { ReasoningEffortId, isAgentLoopRequest } from "@deepseek-ai/dsh-llm";
2
+ import z from "@deepseek-ai/schemastery";
3
+ //#region src/index.ts
4
+ /** 插件名(同时作为 Cordis 插件 id)。 */
5
+ const name = "dsh-model-reasoning-defaults";
6
+ /** 声明依赖的服务:LLM 服务就绪后装配;注册/读取设置命名空间需要设置服务。 */
7
+ const inject = ["llm", "settings"];
8
+ const nonBlankPattern = /^\S+$/;
9
+ const KNOWN_REASONING_EFFORTS = /* @__PURE__ */ new Set([
10
+ "off",
11
+ "minimal",
12
+ "low",
13
+ "medium",
14
+ "high",
15
+ "xhigh",
16
+ "max"
17
+ ]);
18
+ const DEEPSEEK_OFFICIAL_PROVIDER_ID = "deepseek-official";
19
+ const modelConfigSchema = z.object({
20
+ id: z.string().pattern(nonBlankPattern),
21
+ reasoningEffort: z.string().pattern(nonBlankPattern)
22
+ });
23
+ const modelEntrySchema = z.union([z.string().pattern(nonBlankPattern), modelConfigSchema]);
24
+ const modelsFieldSchema = z.union([z.dict(modelEntrySchema, z.string().pattern(nonBlankPattern)), z.array(modelEntrySchema)]);
25
+ const providerConfigSchema = z.object({
26
+ reasoningEffort: z.string().pattern(nonBlankPattern),
27
+ models: modelsFieldSchema
28
+ });
29
+ const providerEntrySchema = z.union([z.string().pattern(nonBlankPattern), providerConfigSchema]);
30
+ /** Cordis 配置模式(显式注解接口类型,满足声明产物的可命名要求)。 */
31
+ const Config = z.object({
32
+ defaults: z.dict(z.string().pattern(nonBlankPattern), z.string().pattern(nonBlankPattern)).default({}),
33
+ providers: z.dict(providerEntrySchema, z.string().pattern(nonBlankPattern)).default({}),
34
+ models: modelsFieldSchema,
35
+ reasoningEffort: z.string().pattern(nonBlankPattern)
36
+ });
37
+ /**
38
+ * 从字符串简写或配置对象中提取非空推理等级。
39
+ *
40
+ * 只认单一规范字段 reasoningEffort,不做多字段兜底链:同义多源读取会在字段
41
+ * 冲突时按书写顺序静默裁决(例如 reasoningEffort 与 effort 同时配置时无声地
42
+ * 二选一),掩盖配置错误;确需别名应由用户层显式映射后再传入。
43
+ *
44
+ * @param val - 等级字符串,或 ModelConfig/ProviderConfig 形状的对象。
45
+ * @returns trim 后非空的等级字符串;无法提取时返回 undefined。
46
+ */
47
+ function extractEffort(val) {
48
+ const candidate = typeof val === "string" ? val : typeof val === "object" && val !== null ? val.reasoningEffort : void 0;
49
+ if (typeof candidate !== "string") return void 0;
50
+ const trimmed = candidate.trim();
51
+ return trimmed.length > 0 ? trimmed : void 0;
52
+ }
53
+ /**
54
+ * 将结构化 Config 归一化为统一的扁平路由字典。
55
+ *
56
+ * @param config - 插件配置对象。
57
+ * @returns 归一化后的键值映射;键与值均已去除首尾空白。
58
+ */
59
+ function normalizeConfig(config) {
60
+ const result = {};
61
+ const globalEffort = extractEffort(config.reasoningEffort);
62
+ if (globalEffort) result["*"] = globalEffort;
63
+ if (config.models) {
64
+ if (Array.isArray(config.models)) for (const item of config.models) {
65
+ const id = typeof item === "object" && item !== null && typeof item.id === "string" ? item.id.trim() : "";
66
+ if (id.length === 0) continue;
67
+ const effort = extractEffort(item);
68
+ if (effort) result["*:" + id] = effort;
69
+ }
70
+ else if (typeof config.models === "object") for (const [rawModelId, modelVal] of Object.entries(config.models)) {
71
+ const modelId = rawModelId.trim();
72
+ if (modelId.length === 0) continue;
73
+ const effort = extractEffort(modelVal);
74
+ if (effort) result["*:" + modelId] = effort;
75
+ }
76
+ }
77
+ if (config.providers && typeof config.providers === "object") for (const [rawProviderId, providerVal] of Object.entries(config.providers)) {
78
+ const providerId = rawProviderId.trim();
79
+ if (providerId.length === 0) continue;
80
+ const providerEffort = extractEffort(providerVal);
81
+ if (providerEffort) result[providerId + ":*"] = providerEffort;
82
+ if (typeof providerVal === "object" && providerVal !== null && providerVal.models) {
83
+ const models = providerVal.models;
84
+ if (Array.isArray(models)) for (const item of models) {
85
+ const id = typeof item === "object" && item !== null && typeof item.id === "string" ? item.id.trim() : "";
86
+ if (id.length === 0) continue;
87
+ const effort = extractEffort(item);
88
+ if (effort) result[providerId + ":" + id] = effort;
89
+ }
90
+ else if (typeof models === "object") for (const [rawModelId, modelVal] of Object.entries(models)) {
91
+ const modelId = rawModelId.trim();
92
+ if (modelId.length === 0) continue;
93
+ const effort = extractEffort(modelVal);
94
+ if (effort) result[providerId + ":" + modelId] = effort;
95
+ }
96
+ }
97
+ }
98
+ if (config.defaults && typeof config.defaults === "object") for (const [rawKey, value] of Object.entries(config.defaults)) {
99
+ const key = rawKey.trim();
100
+ const trimmedValue = value.trim();
101
+ if (key.length === 0 || trimmedValue.length === 0) continue;
102
+ result[key] = trimmedValue;
103
+ }
104
+ return result;
105
+ }
106
+ /**
107
+ * 静态体检插件配置,返回人类可读的问题清单(纯函数,不修改任何数据)。
108
+ *
109
+ * schemastery 的对象属性全部可选且对未知键透传,"拼错字段""模型项缺 id"
110
+ * 这类错误能通过校验却在 normalizeConfig 中被静默忽略;此清单在插件激活时
111
+ * 一次性告警,把这类静默降级变成可见信息。等级值不在已知集合也在此提示,
112
+ * 但仍会注入——最终是否可用交给适配器逐字匹配裁决。
113
+ *
114
+ * @param config - 用户原始配置(未经归一化)。
115
+ * @returns 问题描述列表;无问题时为空数组。
116
+ */
117
+ function collectConfigIssues(config) {
118
+ const issues = [];
119
+ const knownLevels = [...KNOWN_REASONING_EFFORTS].join("/");
120
+ const checkLevel = (route, level) => {
121
+ if (level !== void 0 && !KNOWN_REASONING_EFFORTS.has(level)) issues.push("路由 " + route + " 的推理等级 \"" + level + "\" 不在已知集合 (" + knownLevels + ") 内,请确认目标模型支持");
122
+ };
123
+ /**
124
+ * 检查单个模型条目:字符串简写直接校验等级;
125
+ * 对象形式要求携带有效等级,数组元素还要求有效 id。
126
+ */
127
+ const checkModelEntry = (route, entry, requireId) => {
128
+ if (typeof entry === "string") {
129
+ if (requireId) {
130
+ issues.push("数组形式模型条目缺少有效 id(字符串简写无键名可用),已被忽略:" + JSON.stringify(entry));
131
+ return;
132
+ }
133
+ checkLevel(route, extractEffort(entry));
134
+ return;
135
+ }
136
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
137
+ if (requireId && id.length === 0) {
138
+ issues.push("模型条目缺少有效 id,已被忽略:" + JSON.stringify(entry));
139
+ return;
140
+ }
141
+ const effort = extractEffort(entry);
142
+ if (effort === void 0) {
143
+ issues.push("模型条目未包含 reasoningEffort,已被忽略:" + JSON.stringify(entry));
144
+ return;
145
+ }
146
+ checkLevel(route, effort);
147
+ };
148
+ checkLevel("*", extractEffort(config.reasoningEffort));
149
+ if (config.models) {
150
+ if (Array.isArray(config.models)) config.models.forEach((entry, index) => {
151
+ checkModelEntry("*:<models[" + index + "]>", entry, true);
152
+ });
153
+ else if (typeof config.models === "object") for (const [modelId, entry] of Object.entries(config.models)) checkModelEntry("*:" + modelId, entry, false);
154
+ }
155
+ if (config.providers && typeof config.providers === "object") for (const [providerId, providerVal] of Object.entries(config.providers)) {
156
+ if (typeof providerVal === "string") {
157
+ checkLevel(providerId + ":*", extractEffort(providerVal));
158
+ continue;
159
+ }
160
+ const hasModels = providerVal.models !== void 0;
161
+ const providerEffort = extractEffort(providerVal);
162
+ if (!hasModels && providerEffort === void 0) {
163
+ issues.push("provider " + providerId + " 未包含 reasoningEffort 或 models,整条配置已被忽略");
164
+ continue;
165
+ }
166
+ checkLevel(providerId + ":*", providerEffort);
167
+ if (hasModels) {
168
+ const models = providerVal.models;
169
+ if (Array.isArray(models)) models.forEach((entry, index) => {
170
+ checkModelEntry(providerId + ":<models[" + index + "]>", entry, true);
171
+ });
172
+ else if (typeof models === "object") for (const [modelId, entry] of Object.entries(models)) checkModelEntry(providerId + ":" + modelId, entry, false);
173
+ }
174
+ }
175
+ if (config.defaults && typeof config.defaults === "object") for (const [rawKey, value] of Object.entries(config.defaults)) {
176
+ const key = rawKey.trim();
177
+ if (key.length === 0) {
178
+ issues.push("扁平路由键为空白,已被忽略:" + JSON.stringify(rawKey));
179
+ continue;
180
+ }
181
+ checkLevel(key, extractEffort(value));
182
+ }
183
+ return issues;
184
+ }
185
+ /**
186
+ * 注册全局 agent/request waterfall。
187
+ *
188
+ * cordis 的 ctx.on 内部会把监听器注册为当前 fiber 上的 effect(见 events
189
+ * 服务的 register()),无论 global 标志如何都会在 fiber 卸载时自动清理;
190
+ * global 只影响事件派发时的上下文过滤。因此无需再包一层 ctx.effect。
191
+ *
192
+ * 该事件的运行时接收者是 Scoped<Agent>,Cordis 根 Context 的事件泛型表不会
193
+ * 暴露它,因此在最小边界内窄化签名(仅约束本插件用到的事件名与参数)。
194
+ *
195
+ * @param ctx - 当前插件上下文。
196
+ * @param listener - agent/request waterfall 监听器。
197
+ */
198
+ function registerAgentRequestListener(ctx, listener) {
199
+ ctx.on.bind(ctx)("agent/request", listener, { global: true });
200
+ }
201
+ /**
202
+ * 生成一个路由的候选配置键。
203
+ *
204
+ * 兼容键保留在明确的优先级表中,避免不同调用入口各自实现一套匹配规则。
205
+ *
206
+ * @param provider - 请求使用的 provider。
207
+ * @param model - 请求使用的 model。
208
+ * @returns 按匹配优先级排列的候选键。
209
+ */
210
+ function candidateKeys(provider, model) {
211
+ return [
212
+ provider + ":" + model,
213
+ provider + ":*",
214
+ provider + "/*",
215
+ provider,
216
+ "*:" + model,
217
+ "*"
218
+ ];
219
+ }
220
+ /**
221
+ * 查找一个路由对应的默认推理等级。
222
+ *
223
+ * @param defaults - 归一化后的扁平路由映射。
224
+ * @param provider - 请求使用的 provider。
225
+ * @param model - 请求使用的 model。
226
+ * @returns 找到的非空推理等级,未找到时返回 undefined。
227
+ */
228
+ function lookupEffort(defaults, provider, model) {
229
+ for (const key of candidateKeys(provider, model)) {
230
+ const effort = defaults[key];
231
+ if (typeof effort === "string" && effort.length > 0) return effort;
232
+ }
233
+ }
234
+ /**
235
+ * 在不修改原对象的前提下应用默认推理等级。
236
+ *
237
+ * 该函数同时用于 GenerateOptions 和 LlmCallConfig,因此调用方可以保留
238
+ * 原请求对象中的 messages、signal 等其它字段,并安全处理深冻结输入。
239
+ *
240
+ * 第二参数只接受归一化后的扁平路由表;结构化 Config 必须先经 normalizeConfig。
241
+ * 曾尝试用对象形状嗅探让本函数同时接纳两种入参,但路由表允许任意非空白键,
242
+ * 含 "models"/"providers" 等键的路由表会被误判成结构化配置(整表归一化为空、
243
+ * 注入全部失效),反之仅含顶层 reasoningEffort 的最小结构化配置又会漏判——
244
+ * 两类误判都是静默失效,因此彻底拆掉形状嗅探。
245
+ *
246
+ * @param config - 待补齐的 LLM 请求或调用配置。
247
+ * @param defaults - normalizeConfig 产出的扁平路由表。
248
+ * @returns 原配置或带有 reasoningEffort 的浅复制对象。
249
+ */
250
+ function applyDefaultReasoningEffort(config, defaults) {
251
+ if (config.reasoningEffort !== void 0 || !config.provider || !config.model) return config;
252
+ const effort = lookupEffort(defaults, config.provider, config.model);
253
+ if (effort === void 0) return config;
254
+ return {
255
+ ...config,
256
+ reasoningEffort: ReasoningEffortId(effort)
257
+ };
258
+ }
259
+ /**
260
+ * 设置页 UI 专用命名空间。该命名空间由本插件注册为可写设置项,浏览器设置
261
+ * 面板通过宿主统一的 settings API 编辑它;生效优先级位于自动提取之上、
262
+ * cordis 插件配置之下(见 resolveEffectiveDefaults)。
263
+ */
264
+ const UI_DEFAULTS_NAMESPACE = "model-reasoning-defaults";
265
+ const uiDefaultsSectionSchema = z.object({ defaults: z.dict(z.string().pattern(nonBlankPattern), z.string().pattern(nonBlankPattern)).default({}) });
266
+ /**
267
+ * 取当前上下文上的设置服务;不存在或 get 不可调用时返回 undefined。
268
+ *
269
+ * apply() 的注入门槛判定与本提取路径共用这一个函数,避免两处标准不一致
270
+ * 造成"装了包装层却永远提取不到数据"的错位。
271
+ */
272
+ function settingsService(ctx) {
273
+ const settings = ctx.settings;
274
+ if (!settings || typeof settings.get !== "function") return void 0;
275
+ return settings;
276
+ }
277
+ const warnedMessages = /* @__PURE__ */ new Set();
278
+ /** 经当前上下文的 logger 发出一次性告警。 */
279
+ function warnOnce(ctx, message) {
280
+ if (warnedMessages.has(message)) return;
281
+ warnedMessages.add(message);
282
+ ctx.logger?.warn?.("[" + name + "] " + message);
283
+ }
284
+ /**
285
+ * 创建一条提取结果的记录器:等级 trim 后写入映射;不在已知集合时一次性告警
286
+ * 但仍注入,让适配器的逐字匹配给出最终裁决(避免本地白名单过期后挡住上游
287
+ * 新增的等级)。设置提取位于每次请求的热路径上,告警按消息去重防止刷屏。
288
+ *
289
+ * @param ctx - Cordis 上下文。
290
+ * @param extracted - 提取结果的目标映射。
291
+ * @returns 记录函数。
292
+ */
293
+ function createEffortRecorder(ctx, extracted) {
294
+ return (routeKey, level) => {
295
+ if (typeof level !== "string") return;
296
+ const trimmedLevel = level.trim();
297
+ if (trimmedLevel.length === 0) return;
298
+ if (!KNOWN_REASONING_EFFORTS.has(trimmedLevel)) warnOnce(ctx, "设置中路由 " + routeKey + " 的推理等级 \"" + trimmedLevel + "\" 不在已知集合内,请确认目标模型支持");
299
+ extracted[routeKey] = trimmedLevel;
300
+ };
301
+ }
302
+ /**
303
+ * 从当前 Cordis 上下文的设置服务中读取各 Provider 与模型的默认推理等级。
304
+ *
305
+ * 字段约定(每个来源只读单一规范字段,不做别名兜底):
306
+ * - llm-pi-ai:provider 级 `reasoning` 是宿主原生声明字段;模型级
307
+ * `models[].reasoning` 与 `modelOverrides.<model>.reasoning` 是本插件约定
308
+ * 读取的扩展字段,依赖其设置解析对未声明键的透传行为。宿主在模型层另有
309
+ * 原生字段 `reasoningEfforts`,但那是"可选等级集合 + 线格式映射"的能力
310
+ * 声明而非默认值选择,语义不同,本插件不读取;若上游收紧对未声明键的
311
+ * 解析策略,`reasoning` 这部分提取会静默失效。
312
+ * - llm-deepseek:顶层 `reasoningEffort` 为宿主原生字段,映射到其注册的
313
+ * provider 路由 deepseek-official(见 DEEPSEEK_OFFICIAL_PROVIDER_ID)。
314
+ *
315
+ * 整个函数不包 try/catch:宿主的 settings.get 对未注册命名空间返回 undefined
316
+ * 且不会抛错,函数体已对全部外部数据做了形状防御,剩余可能抛错的只有代码
317
+ * 缺陷,应当让它暴露,而不是吞掉后以空映射继续主流程。
318
+ *
319
+ * @param ctx - Cordis 上下文。
320
+ * @returns 提取出的路由默认映射字典。
321
+ */
322
+ function extractSettingsDefaults(ctx) {
323
+ const extracted = {};
324
+ const settings = settingsService(ctx);
325
+ if (!settings) return extracted;
326
+ const record = createEffortRecorder(ctx, extracted);
327
+ const piAi = settings.get("llm-pi-ai");
328
+ if (piAi?.providers && typeof piAi.providers === "object") for (const [providerId, providerProfile] of Object.entries(piAi.providers)) {
329
+ if (!providerId || !providerProfile || typeof providerProfile !== "object") continue;
330
+ if (providerId === DEEPSEEK_OFFICIAL_PROVIDER_ID) {
331
+ warnOnce(ctx, "llm-pi-ai 中名为 deepseek-official 的 provider 无法注册该路由,已跳过其推理等级提取");
332
+ continue;
333
+ }
334
+ record(providerId + ":*", providerProfile.reasoning);
335
+ if (Array.isArray(providerProfile.models)) for (const m of providerProfile.models) {
336
+ const id = m && typeof m === "object" && typeof m.id === "string" ? m.id.trim() : "";
337
+ if (id.length === 0) continue;
338
+ record(providerId + ":" + id, m.reasoning);
339
+ }
340
+ if (providerProfile.modelOverrides && typeof providerProfile.modelOverrides === "object") for (const [modelId, override] of Object.entries(providerProfile.modelOverrides)) {
341
+ const id = modelId.trim();
342
+ if (id.length === 0 || !override || typeof override !== "object") continue;
343
+ record(providerId + ":" + id, override.reasoning);
344
+ }
345
+ }
346
+ const deepseek = settings.get("llm-deepseek");
347
+ if (deepseek && typeof deepseek === "object") record("deepseek-official:*", deepseek.reasoningEffort);
348
+ return extracted;
349
+ }
350
+ /**
351
+ * 从本插件专用的设置命名空间读取设置页 UI 写入的路由表。
352
+ *
353
+ * 该命名空间由 apply() 注册、浏览器设置面板写入,是"设置里改默认推理强度"
354
+ * 的存储层;每次请求实时读取,改动即时生效。形状非法(非对象/缺字段)时
355
+ * 返回空映射——schema 校验已保证写入合法,这里的防御只覆盖外部手工编辑
356
+ * settings 文档的场景。
357
+ *
358
+ * @param ctx - Cordis 上下文。
359
+ * @returns 路由映射字典。
360
+ */
361
+ function extractUiNamespaceDefaults(ctx) {
362
+ const extracted = {};
363
+ const settings = settingsService(ctx);
364
+ if (!settings) return extracted;
365
+ const section = settings.get(UI_DEFAULTS_NAMESPACE);
366
+ if (!section || typeof section !== "object") return extracted;
367
+ const record = createEffortRecorder(ctx, extracted);
368
+ const defaults = section.defaults;
369
+ if (defaults && typeof defaults === "object") for (const [rawKey, level] of Object.entries(defaults)) {
370
+ const key = rawKey.trim();
371
+ if (key.length === 0) continue;
372
+ record(key, level);
373
+ }
374
+ return extracted;
375
+ }
376
+ const UI_NS_RETRY_INTERVAL_MS = 250;
377
+ const UI_NS_RETRY_MAX_ATTEMPTS = 240;
378
+ /**
379
+ * 在设置服务上注册本插件的可编辑命名空间(跨热重载安全)。
380
+ *
381
+ * 每次激活都先直接尝试注册——不缓存"已注册"标记,因为旧代的卸载会把命名空间
382
+ * 一并注销,缓存的真值会在"停用后再启用"的场景下变成谎言。撞上
383
+ * "already registered" 说明旧代尚未释放,进入短周期轮询直到键位空出;
384
+ * 其它错误告警后放弃,不阻断主流程(patch 层配置仍然生效)。
385
+ *
386
+ * @param ctx - 当前插件上下文。
387
+ * @param settings - 当前上下文的设置服务。
388
+ */
389
+ function registerUiDefaultsNamespace(ctx, settings) {
390
+ if (settings.register === void 0) {
391
+ warnOnce(ctx, "当前设置服务不支持命名空间注册,设置页将无法编辑推理等级默认值");
392
+ return;
393
+ }
394
+ const attempt = () => {
395
+ try {
396
+ settings.register(UI_DEFAULTS_NAMESPACE, uiDefaultsSectionSchema, { applies: "live" });
397
+ return "ok";
398
+ } catch (error) {
399
+ if (!String(error).includes("already registered")) {
400
+ ctx.logger?.warn?.("[" + name + "] 注册设置命名空间失败,设置页将无法编辑推理等级默认值:" + String(error));
401
+ return "ok";
402
+ }
403
+ return "busy";
404
+ }
405
+ };
406
+ if (attempt() !== "busy") return;
407
+ let attempts = 0;
408
+ const timer = setInterval(() => {
409
+ attempts += 1;
410
+ if (attempts > UI_NS_RETRY_MAX_ATTEMPTS) {
411
+ clearInterval(timer);
412
+ warnOnce(ctx, "等待旧代释放设置命名空间超时,本次运行设置页不可用;请重启宿主或再次重载本插件");
413
+ return;
414
+ }
415
+ if (attempt() === "ok") clearInterval(timer);
416
+ }, UI_NS_RETRY_INTERVAL_MS);
417
+ ctx.effect(() => () => {
418
+ clearInterval(timer);
419
+ }, name + ": stop UI namespace registration retry");
420
+ }
421
+ const runtimePatchStateKey = Symbol.for("dsh-model-reasoning-defaults.runtime-patch-state");
422
+ /**
423
+ * 取最后激活的配置层。热重载期间新 fiber 覆盖旧 fiber,旧 fiber 卸载后
424
+ * 新配置仍保持生效;最后一个 fiber 卸载时才还原原始服务方法。
425
+ *
426
+ * @param state - 同一 LLM 服务上的包装状态。
427
+ * @returns 当前生效的 defaults;没有活跃层时返回 undefined。
428
+ */
429
+ function currentDefaults(state) {
430
+ let current;
431
+ for (const defaults of state.defaults.values()) current = defaults;
432
+ return current;
433
+ }
434
+ /**
435
+ * 解析当前生效的完整 defaults 映射。三个配置层按优先级从低到高合并:
436
+ * 设置服务自动提取项(llm-pi-ai / llm-deepseek 原生字段)→ 本插件设置
437
+ * 命名空间(设置页 UI 写入)→ 插件自身 cordis 配置(patch 层)。
438
+ * 同一路由键上高优先级层覆盖低优先级层。
439
+ *
440
+ * @param ctx - 当前共享包装层绑定的上下文(热重载后为新 fiber 的 ctx)。
441
+ * @param state - 同一 LLM 服务上的包装状态。
442
+ * @returns 合并后的路由映射;三层均为空时为空对象。
443
+ */
444
+ function resolveEffectiveDefaults(ctx, state) {
445
+ const pluginDefaults = currentDefaults(state) ?? {};
446
+ const uiStoreDefaults = extractUiNamespaceDefaults(ctx);
447
+ return {
448
+ ...extractSettingsDefaults(ctx),
449
+ ...uiStoreDefaults,
450
+ ...pluginDefaults
451
+ };
452
+ }
453
+ /**
454
+ * 将当前活跃层应用到配置。保留原对象是卸载后仍被外部持有的旧包装的必要语义,
455
+ * 也避免把一个空对象伪装成真实配置来源。
456
+ *
457
+ * @param state - 同一 LLM 服务上的包装状态(ctx 从 state 读取,保证热重载后
458
+ * 提取路径跟随最新 fiber)。
459
+ * @param config - 待处理的调用配置。
460
+ * @returns 原配置或带默认推理等级的浅复制对象。
461
+ */
462
+ function applyCurrentDefault(state, config) {
463
+ const defaults = resolveEffectiveDefaults(state.ctx, state);
464
+ return Object.keys(defaults).length === 0 ? config : applyDefaultReasoningEffort(config, defaults);
465
+ }
466
+ /**
467
+ * 从当前 stream 包装上恢复跨热重载共享的状态。
468
+ *
469
+ * @param llm - LLM 服务实例。
470
+ * @returns 现有包装状态;服务尚未被本插件包装时返回 undefined。
471
+ */
472
+ function existingRuntimePatchState(llm) {
473
+ return llm.stream[runtimePatchStateKey];
474
+ }
475
+ /**
476
+ * 创建覆盖三个公开入口的 LLM 服务包装。
477
+ *
478
+ * 手写请求在 stream 入口浅复制补齐后再进 waterfall,保证每个监听器只处理
479
+ * 一次;loop 构建的请求由宿主打 markAgentLoopRequest 进程标识并深冻结,
480
+ * 经 isAgentLoopRequest 判定后按原引用透传——浅复制会丢失该进程标识。
481
+ *
482
+ * @param ctx - 当前插件上下文。
483
+ * @param llm - LLM 服务实例。
484
+ * @returns 可复用的包装状态。
485
+ */
486
+ function createRuntimePatchState(ctx, llm) {
487
+ const originalStream = llm.stream;
488
+ const originalPrepareCall = llm.prepareCall;
489
+ const originalResolveCallConfig = llm.resolveCallConfig;
490
+ let state;
491
+ const stream = (options) => {
492
+ const patched = isAgentLoopRequest(options) ? options : applyCurrentDefault(state, options);
493
+ return originalStream.call(llm, patched);
494
+ };
495
+ const prepareCall = async (config, signal) => {
496
+ return originalPrepareCall.call(llm, applyCurrentDefault(state, config), signal);
497
+ };
498
+ const resolveCallConfig = async (config, signal) => {
499
+ return originalResolveCallConfig.call(llm, applyCurrentDefault(state, config), signal);
500
+ };
501
+ state = {
502
+ ctx,
503
+ defaults: /* @__PURE__ */ new Map(),
504
+ originalStream,
505
+ originalPrepareCall,
506
+ originalResolveCallConfig,
507
+ stream,
508
+ prepareCall,
509
+ resolveCallConfig
510
+ };
511
+ Object.defineProperty(stream, runtimePatchStateKey, { value: state });
512
+ return state;
513
+ }
514
+ /**
515
+ * 安装一层可卸载的 LLM 服务包装。
516
+ *
517
+ * 每个插件 fiber 只持有自己的配置层。这样新 fiber 先装配、旧 fiber 后卸载时,
518
+ * 不会把服务方法恢复为已经失效的旧包装。
519
+ *
520
+ * @param ctx - 当前插件上下文。
521
+ * @param llm - LLM 服务实例。
522
+ * @param defaults - 默认推理等级映射。
523
+ */
524
+ function installRuntimePatches(ctx, llm, defaults) {
525
+ const state = existingRuntimePatchState(llm) ?? createRuntimePatchState(ctx, llm);
526
+ state.ctx = ctx;
527
+ if (llm.stream !== state.stream) llm.stream = state.stream;
528
+ if (llm.prepareCall !== state.prepareCall) llm.prepareCall = state.prepareCall;
529
+ if (llm.resolveCallConfig !== state.resolveCallConfig) llm.resolveCallConfig = state.resolveCallConfig;
530
+ const token = Symbol(name);
531
+ state.defaults.set(token, defaults);
532
+ ctx.effect(() => () => {
533
+ state.defaults.delete(token);
534
+ if (state.defaults.size !== 0) return;
535
+ if (llm.stream === state.stream) llm.stream = state.originalStream;
536
+ if (llm.prepareCall === state.prepareCall) llm.prepareCall = state.originalPrepareCall;
537
+ if (llm.resolveCallConfig === state.resolveCallConfig) llm.resolveCallConfig = state.originalResolveCallConfig;
538
+ }, name + ": restore LLM method wrappers");
539
+ return state;
540
+ }
541
+ /**
542
+ * 安装默认推理等级注入逻辑。
543
+ *
544
+ * @param ctx - 当前插件上下文。
545
+ * @param config - 插件配置。
546
+ */
547
+ function apply(ctx, config = {}) {
548
+ const defaults = normalizeConfig(config);
549
+ for (const issue of collectConfigIssues(config)) ctx.logger?.warn?.("[" + name + "] " + issue);
550
+ const settings = settingsService(ctx);
551
+ if (settings) registerUiDefaultsNamespace(ctx, settings);
552
+ if (Object.keys(defaults).length === 0 && !settings) {
553
+ ctx.logger?.info?.("[" + name + "] 未配置推理等级默认值且无设置服务,跳过注入");
554
+ return;
555
+ }
556
+ const llm = ctx.llm;
557
+ const state = installRuntimePatches(ctx, llm, defaults);
558
+ registerAgentRequestListener(ctx, async function(_payload, next) {
559
+ return applyCurrentDefault(state, await next());
560
+ });
561
+ ctx.logger?.info?.("[" + name + "] 已激活:routes=" + Object.keys(defaults).join(", "));
562
+ }
563
+ //#endregion
564
+ export { Config, UI_DEFAULTS_NAMESPACE, apply, applyDefaultReasoningEffort, extractSettingsDefaults, extractUiNamespaceDefaults, inject, name, normalizeConfig };
565
+
566
+ //# sourceMappingURL=index.mjs.map