dsh-connect-modelscope-token-plan 0.5.0 → 0.6.2
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 +244 -208
- package/README.md +3 -1
- package/client.js +152 -29
- package/cordis.patch.yml +36 -5
- package/lib/host-config.js +194 -33
- package/lib/index.js +364 -105
- package/lib/llm-adapter.js +8 -2
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +115 -115
package/lib/host-config.js
CHANGED
|
@@ -181,37 +181,63 @@ function extractStructuredType(message) {
|
|
|
181
181
|
return match ? match[1] ?? null : null;
|
|
182
182
|
}
|
|
183
183
|
/**
|
|
184
|
-
* 一个被 peer 判为 QUOTA
|
|
184
|
+
* 一个被 peer 判为 QUOTA 的失败,按哪条判据路径处理——429 重分类的**可观测
|
|
185
|
+
* 理由**。纯函数,复用同一批判据(模块私有的 {@link RATE_SIGNAL} /
|
|
186
|
+
* {@link RATE_CAP_WORDING}、{@link hasHardQuotaWordingIn}、结构化 type),不
|
|
187
|
+
* 引入第二份词表;返回稳定短串(非自由文本),供面板/日志/测试引用“为什么
|
|
188
|
+
* 这么判”。
|
|
185
189
|
*
|
|
186
190
|
* 这是修正 v1(纯文本启发)漏判的核心:`{"message":"rpm exhausted",
|
|
187
191
|
* "type":"quota_exceeded_error","code":"8"}` 这种体——魔搭把**请求速率上限**
|
|
188
|
-
* 复用 `quota_exceeded_error` 这个名字,纯文本里没有
|
|
192
|
+
* 复用 `quota_exceeded_error` 这个名字,纯文本里没有 rate limit 字样,v1 的
|
|
189
193
|
* `looksLikeRateLimit` 既没命中限频信号也没命中硬额度,于是留在 QUOTA、不重试、
|
|
190
|
-
* 直接失败。本函数改读结构化 `type
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
194
|
+
* 直接失败。本函数改读结构化 `type` 逐层收窄,每个返回串对应一条路径:
|
|
195
|
+
*
|
|
196
|
+
* - `"not_quota_code"` 失败为空或 code≠QUOTA,纠正层不适用。
|
|
197
|
+
* - `"structured:rate_limit_type"` 结构化 type 含 rate_limit,本就是限频(防御,纠正)。
|
|
198
|
+
* - `"structured:quota_rate_cap"` 结构化 type 含 quota 且命中 {@link RATE_CAP_WORDING}
|
|
199
|
+
* (rpm/tpm/每分钟/限流…)→ 错命名,纠正。
|
|
200
|
+
* - `"structured:quota_no_cap"` 结构化 type 含 quota 且无速率信号(token/credit/
|
|
201
|
+
* balance 真耗尽)→ 保留 QUOTA。
|
|
202
|
+
* - `"structured:other_type"` 结构化 type 是其它非 quota 值 → 保守不纠正。
|
|
203
|
+
* - `"text:rate_signal"` 无结构化 type,纯文本命中 {@link RATE_SIGNAL} 且
|
|
204
|
+
* 无硬额度字样 → 纠正。
|
|
205
|
+
* - `"text:hard_quota"` 无结构化 type,命中硬额度字样 → 保留 QUOTA。
|
|
206
|
+
* - `"text:no_rate_signal"` 无结构化 type 也无限频信号 → 保留 QUOTA。
|
|
198
207
|
*
|
|
199
208
|
* 不纠正真配额耗尽:那是共享 Token Plan 池的硬耗尽,重试只会延长冷却窗口
|
|
200
209
|
* (ROADMAP §1 纪律),所以宁可快失败。
|
|
201
210
|
* @param {{code?: string, message?: string}} failure
|
|
202
|
-
* @returns {
|
|
211
|
+
* @returns {string} 标记处理路径的稳定短串。
|
|
203
212
|
*/
|
|
204
|
-
function
|
|
205
|
-
if (!failure || failure.code !== CODE.QUOTA) return
|
|
213
|
+
function reclassifyReason(failure) {
|
|
214
|
+
if (!failure || failure.code !== CODE.QUOTA) return "not_quota_code";
|
|
206
215
|
const message = typeof failure.message === "string" ? failure.message : "";
|
|
207
216
|
const type = extractStructuredType(message);
|
|
208
217
|
if (type !== null) {
|
|
209
218
|
const t = type.toLowerCase();
|
|
210
|
-
if (/rate[_\s-]?limit/.test(t)) return
|
|
211
|
-
if (/quota/.test(t)) return RATE_CAP_WORDING.test(message);
|
|
212
|
-
return
|
|
219
|
+
if (/rate[_\s-]?limit/.test(t)) return "structured:rate_limit_type";
|
|
220
|
+
if (/quota/.test(t)) return RATE_CAP_WORDING.test(message) ? "structured:quota_rate_cap" : "structured:quota_no_cap";
|
|
221
|
+
return "structured:other_type";
|
|
213
222
|
}
|
|
214
|
-
|
|
223
|
+
if (!RATE_SIGNAL.some((re) => re.test(message.toLowerCase()))) return "text:no_rate_signal";
|
|
224
|
+
return hasHardQuotaWordingIn(message) ? "text:hard_quota" : "text:rate_signal";
|
|
225
|
+
}
|
|
226
|
+
/** 判为「应纠正为 RATE_LIMIT」的理由集合——{@link shouldReclassifyQuotaToRate} 的唯一真源。 */
|
|
227
|
+
const RECLASSIFY_TRUE_REASONS = /* @__PURE__ */ new Set([
|
|
228
|
+
"structured:rate_limit_type",
|
|
229
|
+
"structured:quota_rate_cap",
|
|
230
|
+
"text:rate_signal"
|
|
231
|
+
]);
|
|
232
|
+
/**
|
|
233
|
+
* 一个被 peer 判为 QUOTA 的失败,是否其实是限频、应纠正为 RATE_LIMIT。
|
|
234
|
+
* 布尔判定单一真源:读 {@link reclassifyReason} 是否落在「应纠正」集合里,分支
|
|
235
|
+
* 逻辑不再写第二遍;可观测“为什么这么判”见 {@link reclassifyReason} 的返回串。
|
|
236
|
+
* @param {{code?: string, message?: string}} failure
|
|
237
|
+
* @returns {boolean} true 表示应纠正为 RATE_LIMIT。
|
|
238
|
+
*/
|
|
239
|
+
function shouldReclassifyQuotaToRate(failure) {
|
|
240
|
+
return RECLASSIFY_TRUE_REASONS.has(reclassifyReason(failure));
|
|
215
241
|
}
|
|
216
242
|
/**
|
|
217
243
|
* 把单个流 chunk 里的失败码在误判时纠正。
|
|
@@ -375,9 +401,12 @@ function enabledCountOf(list, rosterIds) {
|
|
|
375
401
|
* 1. `compat.supportsDeveloperRole: false`——pi-ai 缺省会**自动探测**这个标志,
|
|
376
402
|
* 对非标准 provider 一律猜 `true`,于是每条请求都用 `developer` 角色,魔搭这个
|
|
377
403
|
* 多模型代理不认识,会 400 或 403。qoder 路由证明必须显式写 false。
|
|
378
|
-
* 2. `maxTokens`
|
|
379
|
-
*
|
|
380
|
-
*
|
|
404
|
+
* 2. `maxTokens` 由 `maxTokensFor` 一处决定,优先级 **`覆盖 > 目录声明 > 不声明`**:
|
|
405
|
+
* 目录没声明时不猜数字(harness 会用它的 `defaultMaxTokens ?? 32768` 兜底,那是
|
|
406
|
+
* harness 的默认值,不是平台说的),但操作者可以在 `maxTokensOverrides` 里显式
|
|
407
|
+
* 填实测值。**本插件的目录(魔搭 `/v1/models`)从不返回这个字段**,所以「目录
|
|
408
|
+
* 说了算」在现实里等于「永远是 32768」——这条退路不是锦上添花,它是唯一出口。
|
|
409
|
+
* 完整回退顺序写在 `maxTokensFor` 上,此处不重复。
|
|
381
410
|
* 3. `reasoning: false` + **不设** `thinkingLevelMap`——魔搭 API-Inference 是
|
|
382
411
|
* 多模型代理,是否吃 `reasoning_effort` 取决于背后那个具体模型,本插件无法
|
|
383
412
|
* 离线得知每个 id 的档位表(sensenova 那份是拿真令牌逐模型探测才钉出来的)。
|
|
@@ -685,9 +714,11 @@ function contextWindowSourceOf(entry) {
|
|
|
685
714
|
/**
|
|
686
715
|
* 读目录声明的单次输出上限,0 = 未声明。
|
|
687
716
|
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
717
|
+
* 这个数**是请求参数**,不是纯展示值:`toPiDescriptor` 在它 >0 时声明为
|
|
718
|
+
* `maxTokens`(见模块头注第 2 条)。**但 `0` 有真实的代价**——魔搭目录从不返回
|
|
719
|
+
* 这个字段,所以它对本插件的每一个模型都是 0,harness 于是用 `defaultMaxTokens ??
|
|
720
|
+
* 32768` 兜底。「目录没说」在这里不等于「平台没限制」,只等于**我们不知道**;把它
|
|
721
|
+
* 当「不设上限」读会错得很远。操作者知道时用 `maxTokensFor` 的覆盖参数补上。
|
|
691
722
|
* @param {object} entry - 一条归一目录条目。
|
|
692
723
|
* @returns {number} 声明的上限,未声明为 0。
|
|
693
724
|
*/
|
|
@@ -703,6 +734,50 @@ function maxOutputLengthOf(entry) {
|
|
|
703
734
|
return 0;
|
|
704
735
|
}
|
|
705
736
|
/**
|
|
737
|
+
* 该条目**实际生效**的输出上限:`覆盖 > 目录声明 > 不声明(0)`——全插件唯一的
|
|
738
|
+
* 回退顺序,写在这里就只写一遍。
|
|
739
|
+
*
|
|
740
|
+
* 为什么覆盖必须排在目录**之前**(而不是「目录优先、覆盖只补空缺」):目录说的是
|
|
741
|
+
* 「这个平台声称能吐多少」,覆盖说的是「我实测过这个模型能吐多少」。两者冲突时后者
|
|
742
|
+
* 是更新的事实——目录是我们**抓来的**(`/v1/models`,会随上游改动、也可能常年不
|
|
743
|
+
* 更新),覆盖是操作者**写的**。魔搭这条路上目录连这个字段都不返回,所以顺序在
|
|
744
|
+
* 多数情况下没有争议;把它写死仍然重要,因为「谁赢」这件事只能有一个答案。
|
|
745
|
+
*
|
|
746
|
+
* 返回 0 的含义与 `maxOutputLengthOf` 完全一致:**不声明** `maxTokens`,让 harness
|
|
747
|
+
* 用它的兜底值——不是「上限为 0」。`0` 因此不是一个可以配置的上限(
|
|
748
|
+
* `resolveMaxTokensOverrides` 会把 0 丢掉)。
|
|
749
|
+
* @param {object} entry - 一条归一目录条目。
|
|
750
|
+
* @param {object|null} [overrides] - 归一后的 id → 上限映射(`ResolvedSettings`)。
|
|
751
|
+
* @returns {number} 生效上限,未声明为 0。
|
|
752
|
+
*/
|
|
753
|
+
function maxTokensFor(entry, overrides) {
|
|
754
|
+
const declared = maxOutputLengthOf(entry);
|
|
755
|
+
if (overrides === void 0 || overrides === null) return declared;
|
|
756
|
+
const id = str(entry?.id, "");
|
|
757
|
+
if (id === "") return declared;
|
|
758
|
+
const override = overrides[id];
|
|
759
|
+
return typeof override === "number" && Number.isInteger(override) && override > 0 ? override : declared;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* 这个条目的输出上限数字从真值链哪一级来——与 {@link maxTokensFor} 配对的来源标记,
|
|
763
|
+
* 形状与 {@link contextWindowSourceOf} 刻意保持同一套词汇。
|
|
764
|
+
*
|
|
765
|
+
* 只有两级,因为这里**没有策展表**:窗口有 `context-catalog.ts` 那份实测策展表,
|
|
766
|
+
* 输出上限没有(魔搭从不声明它,策展一张「猜每个模型能吐多少」的表没有依据)。
|
|
767
|
+
* `catalog` 的含义因此是「覆盖没命中」——包括目录没声明、值就是 0 的情况,面板据此
|
|
768
|
+
* 不画徽章。
|
|
769
|
+
* @param {object} entry - 一条归一目录条目。
|
|
770
|
+
* @param {object|null} [overrides] - 归一后的 id → 上限映射。
|
|
771
|
+
* @returns {"catalog"|"override"} 来源标记。
|
|
772
|
+
*/
|
|
773
|
+
function maxTokensSourceOf(entry, overrides) {
|
|
774
|
+
if (overrides === void 0 || overrides === null) return "catalog";
|
|
775
|
+
const id = str(entry?.id, "");
|
|
776
|
+
if (id === "") return "catalog";
|
|
777
|
+
const override = overrides[id];
|
|
778
|
+
return typeof override === "number" && Number.isInteger(override) && override > 0 ? "override" : "catalog";
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
706
781
|
* 把一条目录条目映射成适配器交给 pi-ai 的 model descriptor。
|
|
707
782
|
*
|
|
708
783
|
* vision 用的是与快照/roster 完全相同的判定(`isVisionModel`),所以模型选择器
|
|
@@ -711,14 +786,17 @@ function maxOutputLengthOf(entry) {
|
|
|
711
786
|
* @param {object} [options] - 装配参数。
|
|
712
787
|
* @param {string} [options.providerId] - 该 descriptor 所属的 provider id。
|
|
713
788
|
* @param {string} [options.baseUrl] - OpenAI 兼容源站。
|
|
789
|
+
* @param {object} [options.maxTokensOverrides] - 逐模型输出上限覆盖(id → tokens),
|
|
790
|
+
* 经 `maxTokensFor` 决定 `maxTokens`。缺省 = 目录说了算(对本插件的目录即「不声
|
|
791
|
+
* 明」)。
|
|
714
792
|
* @returns {object} pi-ai descriptor。
|
|
715
793
|
*/
|
|
716
794
|
function toPiDescriptor(entry, options = {}) {
|
|
717
|
-
const { providerId = LLM_PROVIDER_ID, baseUrl } = options;
|
|
795
|
+
const { providerId = LLM_PROVIDER_ID, baseUrl, maxTokensOverrides } = options;
|
|
718
796
|
const id = str(entry?.id, "");
|
|
719
797
|
if (id === "") throw new Error("toPiDescriptor: catalog entry has no id");
|
|
720
798
|
const vision = isVisionModel(entry);
|
|
721
|
-
const
|
|
799
|
+
const effectiveMaxTokens = maxTokensFor(entry, maxTokensOverrides);
|
|
722
800
|
return {
|
|
723
801
|
id,
|
|
724
802
|
name: str(entry.name, id),
|
|
@@ -729,7 +807,7 @@ function toPiDescriptor(entry, options = {}) {
|
|
|
729
807
|
reasoning: false,
|
|
730
808
|
cost: { ...NO_COST },
|
|
731
809
|
contextWindow: contextWindowOf(entry),
|
|
732
|
-
...
|
|
810
|
+
...effectiveMaxTokens > 0 ? { maxTokens: effectiveMaxTokens } : {},
|
|
733
811
|
compat: {
|
|
734
812
|
maxTokensField: "max_tokens",
|
|
735
813
|
supportsDeveloperRole: false
|
|
@@ -810,11 +888,11 @@ function buildRosterRows(entries, project) {
|
|
|
810
888
|
* `rosterWithAvailability` 仍显示它们,灰显加原因,但 picker 不提供注定 429 的
|
|
811
889
|
* 请求;两处口径不同是故意的,别统一。
|
|
812
890
|
* @param {object[]} entries - 归一目录条目。
|
|
813
|
-
* @param {object} options - `{ providerId, baseUrl, enabledIds, unavailableModelIds }`。
|
|
891
|
+
* @param {object} options - `{ providerId, baseUrl, enabledIds, unavailableModelIds, maxTokensOverrides }`。
|
|
814
892
|
* @returns {object[]} pi-ai descriptor,按首见顺序。
|
|
815
893
|
*/
|
|
816
894
|
function buildDescriptors(entries, options = {}) {
|
|
817
|
-
const { providerId = LLM_PROVIDER_ID, baseUrl, enabledIds = [], unavailableModelIds = [] } = options;
|
|
895
|
+
const { providerId = LLM_PROVIDER_ID, baseUrl, enabledIds = [], unavailableModelIds = [], maxTokensOverrides } = options;
|
|
818
896
|
const filtered = filterByEnabled(entries, enabledIds).filter((entry) => isChatModel(entry));
|
|
819
897
|
const blocked = new Set(Array.isArray(unavailableModelIds) ? unavailableModelIds : []);
|
|
820
898
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -834,7 +912,8 @@ function buildDescriptors(entries, options = {}) {
|
|
|
834
912
|
id
|
|
835
913
|
}, {
|
|
836
914
|
providerId,
|
|
837
|
-
...baseUrl !== void 0 ? { baseUrl } : {}
|
|
915
|
+
...baseUrl !== void 0 ? { baseUrl } : {},
|
|
916
|
+
...maxTokensOverrides !== void 0 ? { maxTokensOverrides } : {}
|
|
838
917
|
});
|
|
839
918
|
}
|
|
840
919
|
return out;
|
|
@@ -1001,9 +1080,14 @@ function resolveDrawModelChoice(entries, requested, preferred) {
|
|
|
1001
1080
|
* 面板保留它们在清单里、灰显,让用户看得见「为什么模型从选择器里不见了」。
|
|
1002
1081
|
* @param {object[]} entries - 归一目录条目。
|
|
1003
1082
|
* @param {string[]} unavailableIds - 额度耗尽的模型 id;空数组 = 全部可用。
|
|
1004
|
-
* @
|
|
1083
|
+
* @param {object} [maxTokensOverrides] - 逐模型输出上限覆盖(id → tokens)。**位置参数
|
|
1084
|
+
* 而非 options 对象**:本函数的既有调用面就是两个位置参数,第三个在三个调用点都是
|
|
1085
|
+
* 一次机械追加;改成对象会动三处签名与它们各自的测试假件,收益只是好看。窗口那边
|
|
1086
|
+
* (`contextSource`)**不**走这个参数——它在 fetchModels 归一时就被打标了,两条路
|
|
1087
|
+
* 的机制不同,这里不假装统一。
|
|
1088
|
+
* @returns {{id: string, name: string, vision: boolean, capability: ModelCapability, available: boolean, quotaExhausted: boolean, contextWindow: number, contextSource: "catalog"|"override"|"curated", maxOutputLength: number, maxOutputSource: "catalog"|"override"}[]}
|
|
1005
1089
|
*/
|
|
1006
|
-
function rosterWithAvailability(entries, unavailableIds) {
|
|
1090
|
+
function rosterWithAvailability(entries, unavailableIds, maxTokensOverrides) {
|
|
1007
1091
|
const blocked = new Set(Array.isArray(unavailableIds) ? unavailableIds : []);
|
|
1008
1092
|
return buildRosterRows(entries, (entry, id) => ({
|
|
1009
1093
|
id,
|
|
@@ -1014,7 +1098,8 @@ function rosterWithAvailability(entries, unavailableIds) {
|
|
|
1014
1098
|
quotaExhausted: blocked.has(id),
|
|
1015
1099
|
contextWindow: contextWindowOf(entry),
|
|
1016
1100
|
contextSource: contextWindowSourceOf(entry),
|
|
1017
|
-
maxOutputLength:
|
|
1101
|
+
maxOutputLength: maxTokensFor(entry, maxTokensOverrides),
|
|
1102
|
+
maxOutputSource: maxTokensSourceOf(entry, maxTokensOverrides)
|
|
1018
1103
|
}));
|
|
1019
1104
|
}
|
|
1020
1105
|
|
|
@@ -1044,7 +1129,7 @@ const inject = ["webServer"];
|
|
|
1044
1129
|
* 产物反而引入第二份真源;这里用常量、由 test/config.test.mjs 钉住与
|
|
1045
1130
|
* package.json 一致。
|
|
1046
1131
|
*/
|
|
1047
|
-
const PLUGIN_VERSION = "0.
|
|
1132
|
+
const PLUGIN_VERSION = "0.6.2";
|
|
1048
1133
|
/**
|
|
1049
1134
|
* 默认值。刻意不含额度常数——理由见本文件头。
|
|
1050
1135
|
* 面板头条是官方魔粒余额,次数口径的推算已在面板收敛时删除,这里若留着
|
|
@@ -1080,9 +1165,55 @@ const CONFIG_DEFAULTS = Object.freeze({
|
|
|
1080
1165
|
inferenceTimeoutMs: 3e4,
|
|
1081
1166
|
/** 事件流保留条数(429/错误事件的环形上限)。 */
|
|
1082
1167
|
maxEvents: 50,
|
|
1168
|
+
/**
|
|
1169
|
+
* 单模型「额度耗尽」的推断窗口(分钟)。
|
|
1170
|
+
*
|
|
1171
|
+
* 官方只给**账户级**魔粒余额,没有 per-model 额度端点,所以「哪个模型刚才在
|
|
1172
|
+
* 限频」只能从本插件的 429 事件流推断(规则是纯函数 `exhaustedModelIds`,
|
|
1173
|
+
* usage-store.ts):某模型最近在这个窗口内出现过 `kind:"quota"` 事件
|
|
1174
|
+
* (重分类后的 code,见 llm-error-fix),picker 就不再提供它、面板 roster
|
|
1175
|
+
* 灰显;事件移出窗口即恢复提供。
|
|
1176
|
+
*
|
|
1177
|
+
* 取 60 分钟:覆盖一次「耗尽 → 用户看到 → 换模型」的合理观察期,又不至于让
|
|
1178
|
+
* 一次深夜的耗尽事件把模型关一整天。**恢复不可观测是这条规则已知的代价**:
|
|
1179
|
+
* 成功的流不发事件,所以额度若在窗口内恢复(按天重置恰好在窗口内发生),
|
|
1180
|
+
* 模型最多灰显满一个窗口——方向是保守的(宁可少提供一会,不提供注定 429
|
|
1181
|
+
* 的模型),且面板灰显上写着原因,不是静默消失。
|
|
1182
|
+
*/
|
|
1183
|
+
exhaustionWindowMinutes: 60,
|
|
1083
1184
|
/** provider 注册的 patch 默认值(面板保存值优先于它)。 */
|
|
1084
1185
|
registerProvider: false,
|
|
1085
1186
|
/**
|
|
1187
|
+
* 逐模型的单次输出上限覆盖(模型 id → tokens),空对象 = 目录说了算。
|
|
1188
|
+
*
|
|
1189
|
+
* **为什么需要它**:本插件对输出上限的原始策略是「目录声明了才声明」
|
|
1190
|
+
* (llm-models.ts 模块头注第 2 条),而魔搭 `/v1/models` **从不返回**这个字段
|
|
1191
|
+
* (docs/REFERENCES.md 实测:每条只有 `id`/`object`/`owned_by`/`created`)。
|
|
1192
|
+
* 两条合起来的结果是**每一个**模型的 descriptor 都不带 `maxTokens`,harness 于是
|
|
1193
|
+
* 走它的 `defaultMaxTokens ?? 32768` 兜底——32768 不是魔搭声明的数,也不是任何
|
|
1194
|
+
* 实测过的数,它只是 harness 的默认值。
|
|
1195
|
+
*
|
|
1196
|
+
* 目录给不出真值时,操作者是唯一的信息源:他知道某个模型实测能吐 64k。没有这个
|
|
1197
|
+
* 字段,这份知识**无处可写**——写进目录缓存会被下一轮轮询覆盖。于是他只能看着
|
|
1198
|
+
* 输出在 32768 处被截断,而手上明明有答案。
|
|
1199
|
+
*
|
|
1200
|
+
* **语义与优先级**:`覆盖 > 目录声明 > 不声明`。目录声明了时覆盖仍然赢(这是
|
|
1201
|
+
* 「覆盖」二字的全部含义);目录没声明时覆盖就是唯一的真值来源。空对象(默认)
|
|
1202
|
+
* = 完全维持旧行为,不改变任何已有部署的输出。
|
|
1203
|
+
*
|
|
1204
|
+
* **键是精确 id,不做子串匹配**——与 sensenova 那种「按名字包含」的倍率旋钮
|
|
1205
|
+
* 刻意不同:上限是**请求参数**,一个宽松的键会顺手改掉一批没被点名的模型
|
|
1206
|
+
* (键写 `flash` 会命中七八条)。只认整串相等。
|
|
1207
|
+
*
|
|
1208
|
+
* **值必须是正整数**:0 有保留含义(`maxOutputLengthOf` 里 0 = 未声明),所以
|
|
1209
|
+
* 非法值一律**丢弃**(非整数 / NaN / Infinity / 字符串数字 / 0 / 负数),不是夹取
|
|
1210
|
+
* ——夹取等于替用户声明一个他没说过的上限。
|
|
1211
|
+
*
|
|
1212
|
+
* 读取点只有一处:`llm-models.ts` 的 `maxTokensFor`。descriptor 构建与面板 roster
|
|
1213
|
+
* **都**调它,所以面板显示的数字与线上真正发的 `max_tokens` 不可能不一致。
|
|
1214
|
+
*/
|
|
1215
|
+
maxTokensOverrides: {},
|
|
1216
|
+
/**
|
|
1086
1217
|
* 出图工具的 patch 默认值(面板保存值优先于它)。
|
|
1087
1218
|
*
|
|
1088
1219
|
* **与 `registerProvider` 是两个独立开关**:前者是「把魔搭当 chat provider」,
|
|
@@ -1165,6 +1296,32 @@ function assertHttpUrl(value, field) {
|
|
|
1165
1296
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${field} must be an http(s) URL, got ${parsed.protocol}`);
|
|
1166
1297
|
}
|
|
1167
1298
|
/**
|
|
1299
|
+
* 归一「逐模型输出上限覆盖」。
|
|
1300
|
+
*
|
|
1301
|
+
* 三个刻意的选择,都与姊妹插件的同类函数不同:
|
|
1302
|
+
*
|
|
1303
|
+
* 1. **键精确相等**,不做子串匹配。上限是请求参数,宽松匹配会改掉一批没被点名的
|
|
1304
|
+
* 模型;倍率那种「按名字包含」的旋钮可以宽松,因为它只影响画图。
|
|
1305
|
+
* 2. **值必须是正整数**,非法值**丢弃**而不是夹取。0 在 `maxOutputLengthOf` 里已经
|
|
1306
|
+
* 表示「未声明」,把 `-5` 夹成 1 等于替用户声明了一个他没说过的上限;丢弃则
|
|
1307
|
+
* 退回目录/兜底,方向是保守的。
|
|
1308
|
+
* 3. **非对象一律回落到默认空对象**,不抛错——坏配置不能让 apply 抛出去(本文件的
|
|
1309
|
+
* 既有纪律,见 resolveSettings 的 catch 分支)。
|
|
1310
|
+
* @param {unknown} raw - patch 行里的原始值(加载器送来的未校验输入)。
|
|
1311
|
+
* @returns {Record<string, number>} id → 上限,只含合法项。
|
|
1312
|
+
*/
|
|
1313
|
+
function resolveMaxTokensOverrides(raw) {
|
|
1314
|
+
if (raw === void 0 || raw === null) return CONFIG_DEFAULTS.maxTokensOverrides;
|
|
1315
|
+
if (typeof raw !== "object" || Array.isArray(raw)) return { ...CONFIG_DEFAULTS.maxTokensOverrides };
|
|
1316
|
+
const out = {};
|
|
1317
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1318
|
+
if (key === "") continue;
|
|
1319
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) continue;
|
|
1320
|
+
out[key] = value;
|
|
1321
|
+
}
|
|
1322
|
+
return out;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1168
1325
|
* 把 patch 行的原始配置解析成有效设置。
|
|
1169
1326
|
*
|
|
1170
1327
|
* 坏行不许从这里抛出去——apply 在挂载期跑,抛错等于整机插件消失。问题以
|
|
@@ -1189,7 +1346,9 @@ function resolveSettings(config) {
|
|
|
1189
1346
|
pollSeconds: clampInt(source.pollSeconds, CONFIG_DEFAULTS.pollSeconds, 5, 3600),
|
|
1190
1347
|
inferenceTimeoutMs: clampInt(source.inferenceTimeoutMs, CONFIG_DEFAULTS.inferenceTimeoutMs, 1e3, 6e5),
|
|
1191
1348
|
maxEvents: clampInt(source.maxEvents, CONFIG_DEFAULTS.maxEvents, 1, 1e3),
|
|
1349
|
+
exhaustionWindowMinutes: clampInt(source.exhaustionWindowMinutes, CONFIG_DEFAULTS.exhaustionWindowMinutes, 5, 1440),
|
|
1192
1350
|
registerProvider: source.registerProvider === true,
|
|
1351
|
+
maxTokensOverrides: resolveMaxTokensOverrides(source.maxTokensOverrides),
|
|
1193
1352
|
drawEnabled: source.drawEnabled === true,
|
|
1194
1353
|
drawModelId: str(source.drawModelId, CONFIG_DEFAULTS.drawModelId),
|
|
1195
1354
|
drawTimeoutMs,
|
|
@@ -1209,7 +1368,9 @@ function resolveSettings(config) {
|
|
|
1209
1368
|
pollSeconds: CONFIG_DEFAULTS.pollSeconds,
|
|
1210
1369
|
inferenceTimeoutMs: CONFIG_DEFAULTS.inferenceTimeoutMs,
|
|
1211
1370
|
maxEvents: CONFIG_DEFAULTS.maxEvents,
|
|
1371
|
+
exhaustionWindowMinutes: CONFIG_DEFAULTS.exhaustionWindowMinutes,
|
|
1212
1372
|
registerProvider: false,
|
|
1373
|
+
maxTokensOverrides: CONFIG_DEFAULTS.maxTokensOverrides,
|
|
1213
1374
|
drawEnabled: false,
|
|
1214
1375
|
drawModelId: CONFIG_DEFAULTS.drawModelId,
|
|
1215
1376
|
drawTimeoutMs: CONFIG_DEFAULTS.drawTimeoutMs,
|