@gehennawu/dsh-service 2.0.10 → 2.1.0

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/index.js CHANGED
@@ -85,8 +85,11 @@ import {
85
85
  normalizeStepFunStepPlanUsage as normalizeStepFunStepPlanUsageAdapter,
86
86
  normalizeXiaomiTokenPlanUsage as normalizeXiaomiTokenPlanUsageAdapter,
87
87
  normalizeZaiCodingUsage as normalizeZaiCodingUsageAdapter,
88
+ MAX_QUOTA_REALM_CAPS,
88
89
  prepareQuotaAdapterConfig,
90
+ sanitizeRealmCaps,
89
91
  quotaAdapterEndpoints,
92
+ quotaAdapterRealmCaps,
90
93
  quotaAdapterUsageUrl,
91
94
  quotaErrorCode as quotaAdapterErrorCode,
92
95
  quotaErrorDetailFromText as quotaErrorDetailFromTextAdapter,
@@ -1500,7 +1503,7 @@ async function saveUsageIndex(dshHome, index) {
1500
1503
  // ── 远端额度(v0.18):kind 映射存取、方言解析、节流状态机 ─────────────
1501
1504
 
1502
1505
  function createEmptyQuotaConfig() {
1503
- return { version: QUOTA_CONFIG_VERSION, kinds: {}, resetCards: [], allowedHosts: {} }
1506
+ return { version: QUOTA_CONFIG_VERSION, kinds: {}, resetCards: [], allowedHosts: {}, realmCaps: {} }
1504
1507
  }
1505
1508
 
1506
1509
  /** 钉住表主机名归一:小写、去尾点;拒绝 IP 字面量与非法形状。合法返回归一值,否则 undefined。 */
@@ -1540,6 +1543,26 @@ function normalizeQuotaAllowedHosts(raw) {
1540
1543
  return out
1541
1544
  }
1542
1545
 
1546
+ // realmCaps 解析:油表分母的 provider 级覆盖(`{provider: {realm: tokens}}`)。
1547
+ // 区的键只允许 `[a-z0-9_-]{1,32}`(它是上游区域码,不是文案);值必须是正整数 tokens,
1548
+ // 每分钟上限 1e12 防手滑多打个零。非法条目整键丢弃——坏配置只能让油表回落默认/文字行,
1549
+ // 绝不能让 fetch 抛错把整行打成错误态。
1550
+ function normalizeQuotaRealmCaps(raw) {
1551
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}
1552
+ const out = {}
1553
+ let providerCount = 0
1554
+ for (const [provider, caps] of Object.entries(raw)) {
1555
+ if (providerCount >= MAX_QUOTA_PROVIDERS) break
1556
+ if (typeof provider !== 'string' || provider.length === 0 || provider.length > MAX_QUOTA_PROVIDER_NAME) continue
1557
+ // 单 provider 的条目归一只维护在 quota-adapters(RPC 写路径共用同一份校验)。
1558
+ const normalized = sanitizeRealmCaps(caps, MAX_QUOTA_REALM_CAPS)
1559
+ if (Object.keys(normalized).length === 0) continue
1560
+ out[provider] = normalized
1561
+ providerCount += 1
1562
+ }
1563
+ return out
1564
+ }
1565
+
1543
1566
  // 重置卡到期串的两种「无时区」写法:纯日期与 datetime-local(面板录入格式)。
1544
1567
  // 两者都不带偏移,intl 语义是「用户墙上时间的这个点」,因此必须有客户端时区才能换算成绝对时刻。
1545
1568
  const RESET_CARD_DATE_RE = /^\d{4}-\d{2}-\d{2}$/
@@ -1708,7 +1731,7 @@ function parseQuotaConfigText(text) {
1708
1731
  kindCount += 1
1709
1732
  }
1710
1733
  }
1711
- return { version: QUOTA_CONFIG_VERSION, kinds, resetCards: normalizeResetCards(parsed.resetCards), allowedHosts: normalizeQuotaAllowedHosts(parsed.allowedHosts) }
1734
+ return { version: QUOTA_CONFIG_VERSION, kinds, resetCards: normalizeResetCards(parsed.resetCards), allowedHosts: normalizeQuotaAllowedHosts(parsed.allowedHosts), realmCaps: normalizeQuotaRealmCaps(parsed.realmCaps) }
1712
1735
  }
1713
1736
 
1714
1737
  async function loadQuotaConfig(dshHome) {
@@ -1739,11 +1762,16 @@ function copyQuotaConfig(config) {
1739
1762
  for (const [provider, hosts] of Object.entries(config?.allowedHosts ?? {})) {
1740
1763
  allowedHosts[provider] = Array.isArray(hosts) ? [...hosts] : []
1741
1764
  }
1765
+ const realmCaps = {}
1766
+ for (const [provider, caps] of Object.entries(config?.realmCaps ?? {})) {
1767
+ realmCaps[provider] = { ...(caps ?? {}) }
1768
+ }
1742
1769
  return {
1743
1770
  version: QUOTA_CONFIG_VERSION,
1744
1771
  kinds: { ...(config?.kinds ?? {}) },
1745
1772
  resetCards: Array.isArray(config?.resetCards) ? config.resetCards.map((card) => ({ ...card })) : [],
1746
1773
  allowedHosts,
1774
+ realmCaps,
1747
1775
  }
1748
1776
  }
1749
1777
 
@@ -5865,6 +5893,10 @@ function apply(ctx, featureConfig) {
5865
5893
  allowedHosts: Object.freeze(Object.fromEntries(
5866
5894
  Object.entries(config?.allowedHosts ?? {}).map(([provider, hosts]) => [provider, Object.freeze([...(hosts ?? [])])]),
5867
5895
  )),
5896
+ // 油表分母的 provider 级覆盖:Adapter 只读,绝不回写。
5897
+ realmCaps: Object.freeze(Object.fromEntries(
5898
+ Object.entries(config?.realmCaps ?? {}).map(([provider, caps]) => [provider, Object.freeze({ ...(caps ?? {}) })]),
5899
+ )),
5868
5900
  }),
5869
5901
  signal: controller.signal,
5870
5902
  fetchJson: fetchProviderUsage,
@@ -220,7 +220,21 @@ export function createModelConfigRoutes({ ctx, workbenchStore, loadPiCatalog = l
220
220
  const queryId = remember(queries, { kind: 'source', route: payload.route, api, modelId: payload.modelId, referenceModelId, identity: sourceIdentity, sourceCandidates })
221
221
  if (selectedSource) for (const group of groups) group.candidates = group.candidates.filter((x) => profileFingerprint(x.source) === selectedSource)
222
222
  for (const group of groups) group.candidates = group.candidates.filter((x) => !containsSensitive(x.value))
223
- return { reasoningSupported: result.groups?.find((group) => group.field === 'reasoningEfforts')?.supported === true, groups: groups.filter((x) => x.candidates.length), readonly: redactSensitive(readonly), candidates: sourceCandidates, queryId, selectedSource: selectedSource ?? '', bindingNeedsReview, sources: (result.sources ?? []).map((x) => ({ ...x, id: x.sourceId ?? x.id })), mode: 'on' }
223
+ // 身份留证(只回传有界字符串):别名归一是否发生、命中哪个候选、哪些来源认出它。
224
+ // 只影响提示文案;采纳仍走候选 ID,原始模型 ID 与写入面都不变。
225
+ const rawIdentity = result.identity
226
+ const identity = rawIdentity === null || typeof rawIdentity !== 'object'
227
+ ? undefined
228
+ : {
229
+ requestedModelId: String(rawIdentity.requestedModelId ?? referenceModelId).slice(0, 512),
230
+ modelId: String(rawIdentity.modelId ?? referenceModelId).slice(0, 512),
231
+ via: rawIdentity.via === 'alias' ? 'alias' : 'exact',
232
+ kind: typeof rawIdentity.kind === 'string' ? rawIdentity.kind.slice(0, 64) : '',
233
+ recognizedBy: Array.isArray(rawIdentity.recognizedBy)
234
+ ? rawIdentity.recognizedBy.filter((item) => typeof item === 'string' && ['pi-ai', 'modelparams', 'openrouter'].includes(item)).slice(0, 4)
235
+ : [],
236
+ }
237
+ return { reasoningSupported: result.groups?.find((group) => group.field === 'reasoningEfforts')?.supported === true, groups: groups.filter((x) => x.candidates.length), readonly: redactSensitive(readonly), candidates: sourceCandidates, queryId, selectedSource: selectedSource ?? '', bindingNeedsReview, sources: (result.sources ?? []).map((x) => ({ ...x, id: x.sourceId ?? x.id })), mode: 'on', ...(identity !== undefined ? { identity } : {}) }
224
238
  }),
225
239
  'models3-bind-source': route(async ({ queryId, sourceId }) => {
226
240
  const query = get(queries, queryId)
@@ -103,6 +103,25 @@ export const MAX_OPENROUTER_MATCHES = 20
103
103
  /** modelparams.dev 中表达「档位式思考」的参数路径;其余 thinking.* 是 toggle/预算制,不表态。 */
104
104
  export const MODELPARAMS_EFFORT_PATHS = Object.freeze(['reasoning_effort', 'output_config.effort', 'reasoning.effort'])
105
105
 
106
+ /**
107
+ * 别名归一允许剥离的「思考档后缀」受控词表(`gemini-3.8-flash-high` 这类渠道约定)。
108
+ * 只取档位词、且排除 `off`:通用剥 `-fast`/`-pro`/`-flashx` 会把独立型号并成基座,禁止。
109
+ */
110
+ const ALIAS_THINKING_SUFFIXES = Object.freeze(
111
+ [...THINKING_LEVELS_SET].filter((level) => level !== 'off').sort((a, b) => b.length - a.length),
112
+ )
113
+
114
+ /** scope 前缀只认字母(`cn:`/`us:`/`openai:` 这类渠道/区域约定);带数字/连字符的前段视为型号本体。 */
115
+ const ALIAS_SCOPE_PREFIX_RE = /^([a-z]{2,8}):(.+)$/
116
+ /**
117
+ * 变体词黑名单:防止 `foo:free` 被反向拆成候选 `free`(OpenRouter `q=free` 会命中
118
+ * `openrouter/free`「Free Models Router」,属语义级误配)。这些词只可能是变体/档位,不是型号。
119
+ */
120
+ const ALIAS_VARIANT_WORDS = Object.freeze([
121
+ 'free', 'batch', 'beta', 'extended', 'thinking', 'preview', 'high', 'low', 'medium', 'minimal',
122
+ 'xhigh', 'max', 'off', 'on', 'auto', 'latest',
123
+ ])
124
+
106
125
  /**
107
126
  * 固定来源白名单。三重匹配:origin host + 路径(精确或前缀)+ 允许的查询键。
108
127
  * `paths` 与 `pathPrefixes` 互斥使用;前缀必须在 `length > prefix.length` 时才算命中
@@ -1157,6 +1176,160 @@ export function openRouterReference(match) {
1157
1176
  }
1158
1177
  }
1159
1178
 
1179
+ // ─────────────────────────────────────────────────────────────────────────────
1180
+ // 模型 ID 别名归一:只在原始 ID 于固定来源无记录时回退,且永不宣称精确身份
1181
+ // ─────────────────────────────────────────────────────────────────────────────
1182
+
1183
+ /**
1184
+ * 渠道配置里的模型 ID 可能带渠道自己的前缀/变体(真机实例:`workbuddy` 的
1185
+ * `cn:deepseek-v4.1-flash`、`cline` 的 `cline-pass/deepseek-v4.1-flash`、
1186
+ * OpenRouter 风格的 `...:free`)。固定来源只认规范化 ID,因此原始 ID 精确查不到时
1187
+ * 按下面的形态给出**别名候选**(顺序即优先级,去重、有界):
1188
+ * - scope-prefix :`cn:deepseek-v4.1-flash` → `deepseek-v4.1-flash`(渠道/区域前缀)
1189
+ * - variant-suffix:`inclusionai/ling-3.1-flash:free` → `inclusionai/ling-3.1-flash`(变体后缀)
1190
+ * - provider-slug :`cline-pass/deepseek-v4.1-flash` → `deepseek-v4.1-flash`(中转渠道前缀)
1191
+ * - 组合形态:对一级结果再剥离另一类(如 `provider:author/slug:variant`)。
1192
+ *
1193
+ * 保守约束(宁可漏认,不可误拆):
1194
+ * - scope 前缀段只允许 2–8 位**纯字母**,因此 Bedrock 的 `amazon.nova-2-lite-v1:0`、
1195
+ * `anthropic.claude-...-v1:0` 这类**真正带冒号**的 ID 不会被拆;`gpt-4o:free` 也不会被
1196
+ * 反向拆出 `free`(变体词黑名单再次兜底,避免撞上 `openrouter/free` 这类语义完全不同的条目);
1197
+ * - variant 后缀必须以字母开头且 ≤16 位,且基段要有分隔结构(`-`/`.`/`/` 或 ≥8 字符);
1198
+ * - provider slug 必须是最后一段、长度 ≥6 且含 `-`/`.`(拒绝 `free` 这类短词)。
1199
+ * 候选只作**来源查询键**:不自动采纳、不生成 wire/compat、不宣称与原始 ID 同一身份。
1200
+ */
1201
+ export function modelIdAliasCandidates(modelId) {
1202
+ const id = str(modelId)
1203
+ if (id === '' || id.length > MAX_MODEL_ID_LENGTH || !MODEL_ID_RE.test(id)) return []
1204
+ const MAX_ALIAS_CANDIDATES = 6
1205
+ const out = []
1206
+ const seen = new Set([id])
1207
+ const take = (value, kind) => {
1208
+ const next = str(value)
1209
+ if (next === '' || next.length > MAX_MODEL_ID_LENGTH || !MODEL_ID_RE.test(next) || seen.has(next)) return ''
1210
+ // 任何一条剥离路径都不许把「变体词」当成型号候选:`free-high` 剥档位后是 `free`、
1211
+ // `foo:free` 反过来是 `free`——它们会命中 `openrouter/free`(Free Models Router)这类
1212
+ // 语义完全不同的条目,属比漏认更糟的误配。
1213
+ if (ALIAS_VARIANT_WORDS.includes(next.toLowerCase())) return ''
1214
+ seen.add(next)
1215
+ out.push({ id: next, kind })
1216
+ return next
1217
+ }
1218
+ const scopeOf = (value) => {
1219
+ const match = ALIAS_SCOPE_PREFIX_RE.exec(value)
1220
+ if (match === null) return ''
1221
+ const rest = match[2]
1222
+ // `foo:free` 的 `free` 是变体词,不是型号:拒绝该方向,交给 variant-suffix 处理。
1223
+ return ALIAS_VARIANT_WORDS.includes(rest.toLowerCase()) ? '' : rest
1224
+ }
1225
+ const suffixOf = (value) => {
1226
+ const match = /^(.+):([a-z][a-z0-9_-]{0,15})$/.exec(value)
1227
+ if (match === null) return ''
1228
+ const base = match[1]
1229
+ return base.includes('-') || base.includes('.') || base.includes('/') || base.length >= 8 ? base : ''
1230
+ }
1231
+ const tailOf = (value) => {
1232
+ const slash = value.lastIndexOf('/')
1233
+ if (slash < 0 || slash === value.length - 1) return ''
1234
+ const tail = value.slice(slash + 1)
1235
+ return tail.length >= 6 && (tail.includes('-') || tail.includes('.')) ? tail : ''
1236
+ }
1237
+ // 思考档后缀是**受控词表**(`gemini-3.8-flash-high` 这类渠道约定),只剥档位词,
1238
+ // 绝不通用剥 `-fast`/`-pro`/`-flashx` 等兄弟型号(真机证据:它们是独立型号/定价)。
1239
+ const thinkingOf = (value) => {
1240
+ for (const level of ALIAS_THINKING_SUFFIXES) {
1241
+ if (!value.endsWith('-' + level)) continue
1242
+ const base = value.slice(0, -(level.length + 1))
1243
+ if (base.length >= 3) return base
1244
+ }
1245
+ return ''
1246
+ }
1247
+ const first = []
1248
+ for (const [kind, value] of [['scope-prefix', scopeOf(id)], ['variant-suffix', suffixOf(id)], ['provider-slug', tailOf(id)], ['thinking-suffix', thinkingOf(id)]]) {
1249
+ if (value === '') continue
1250
+ const added = take(value, kind)
1251
+ if (added !== '') first.push([kind, added])
1252
+ }
1253
+ for (const [kind, base] of first) {
1254
+ for (const [nextKind, value] of [['scope-prefix', scopeOf(base)], ['variant-suffix', suffixOf(base)], ['provider-slug', tailOf(base)], ['thinking-suffix', thinkingOf(base)]]) {
1255
+ if (out.length >= MAX_ALIAS_CANDIDATES) return out
1256
+ if (value !== '' && value !== base) take(value, kind + '+' + nextKind)
1257
+ }
1258
+ }
1259
+ return out
1260
+ }
1261
+
1262
+ /**
1263
+ * 目标模型 ID 的「来源身份」:**精确 ID 永远优先**。
1264
+ * 只有精确 ID 在本地来源(pi-ai 目录 / modelparams 索引)没有任何记录、且 OpenRouter
1265
+ * 精确查询也没有命中时,才按 `modelIdAliasCandidates` 的顺序挑候选:
1266
+ * - 优先挑**被本地来源认出**的第一个候选(`recognizedBy` 留证);
1267
+ * - 都认不出时暂取第一个候选(`recognizedBy:[]`),由调用方在 OpenRouter 回退命中后
1268
+ * 补认;仍无命中则调用方会退回精确身份,不用猜测值出建议。
1269
+ * 返回 `via:'exact'` 时 `modelId` 与 `requestedModelId` 相同。
1270
+ */
1271
+ export function resolveSmartFillIdentity(modelId, { piIndex, modelParamsIndex, openRouterMatchesCount = 0 } = {}) {
1272
+ const requested = str(modelId)
1273
+ const countFor = (index, value) => index?.byModelId?.get(value)?.length ?? 0
1274
+ const exactPi = countFor(piIndex, requested)
1275
+ const exactModelParams = countFor(modelParamsIndex, requested)
1276
+ const recognized = []
1277
+ if (exactPi > 0) recognized.push('pi-ai')
1278
+ if (exactModelParams > 0) recognized.push('modelparams')
1279
+ if (openRouterMatchesCount > 0) recognized.push('openrouter')
1280
+ if (requested === '' || recognized.length > 0) {
1281
+ return { requestedModelId: requested, modelId: requested, via: 'exact', kind: '', recognizedBy: recognized }
1282
+ }
1283
+ const candidates = modelIdAliasCandidates(requested)
1284
+ if (candidates.length === 0) {
1285
+ return { requestedModelId: requested, modelId: requested, via: 'exact', kind: '', recognizedBy: [] }
1286
+ }
1287
+ let chosen = candidates[0]
1288
+ const recognizedBy = []
1289
+ for (const candidate of candidates) {
1290
+ const pi = countFor(piIndex, candidate.id)
1291
+ const modelParams = countFor(modelParamsIndex, candidate.id)
1292
+ if (pi === 0 && modelParams === 0) continue
1293
+ chosen = candidate
1294
+ if (pi > 0) recognizedBy.push('pi-ai')
1295
+ if (modelParams > 0) recognizedBy.push('modelparams')
1296
+ break
1297
+ }
1298
+ return { requestedModelId: requested, modelId: chosen.id, via: 'alias', kind: chosen.kind, recognizedBy }
1299
+ }
1300
+
1301
+ /**
1302
+ * 两次 OpenRouter 读取(原始 ID + 别名回退)合并成一条来源状态:客户端只显示一条
1303
+ * `openrouter`,`identityFallback` 留证哪次生效、另一次的状态码是什么。
1304
+ */
1305
+ function mergeOpenRouterFallback(sources, detail) {
1306
+ const indices = []
1307
+ for (let index = 0; index < sources.length; index += 1) {
1308
+ if (sources[index].sourceId === 'openrouter') indices.push(index)
1309
+ }
1310
+ if (indices.length === 0) return
1311
+ if (indices.length === 1) {
1312
+ sources[indices[0]].identityFallback = detail
1313
+ return
1314
+ }
1315
+ const exact = sources[indices[0]]
1316
+ const fallback = sources[indices[1]]
1317
+ const usable = (entry) => entry.status === 'ok' || entry.status === 'cached' || entry.status === 'provided'
1318
+ const chosen = usable(fallback) ? fallback : exact
1319
+ const merged = {
1320
+ ...chosen,
1321
+ identityFallback: {
1322
+ ...detail,
1323
+ exactStatus: exact.status,
1324
+ exactError: exact.error ?? null,
1325
+ fallbackStatus: fallback.status,
1326
+ fallbackError: fallback.error ?? null,
1327
+ },
1328
+ }
1329
+ for (const index of [...indices].sort((a, b) => b - a)) sources.splice(index, 1)
1330
+ sources.push(merged)
1331
+ }
1332
+
1160
1333
  // ─────────────────────────────────────────────────────────────────────────────
1161
1334
  // 运行时:只解释生效投影,不从 effort ID 捏造 wire
1162
1335
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1448,7 +1621,21 @@ function capabilityGroup(field, target, pi, openRouter) {
1448
1621
  note: '外部档案只确认模型身份,跨渠道仅参考',
1449
1622
  }))
1450
1623
  }
1451
- const { options, overflow } = optionOverflow(collected)
1624
+ const { options: rawOptions, overflow } = optionOverflow(collected)
1625
+ // maxTokens 语义特判:目录声明的是「模型最大输出能力」,而 route 配置里的 maxTokens 是
1626
+ // **单次请求的输出预留**——DSH compaction 用 `contextWindow − maxTokens − headroom` 计算
1627
+ // 压力阈值(阈值随 maxTokens 1:1 下降,叠到 ≤0 时自动压缩直接不可用)。二者不是同一概念,
1628
+ // 因此能力上限一律只读参考,**绝不进入自动填入**;用户按实际需要手填(适配器默认 32768)。
1629
+ const options = field === 'maxTokens'
1630
+ ? rawOptions.map((option) => ({
1631
+ ...option,
1632
+ adoptable: false,
1633
+ readOnly: true,
1634
+ confidence: 'read-only',
1635
+ kind: 'capability-ceiling',
1636
+ note: '模型目录声明的最大输出能力;route 的 maxTokens 是单次请求预留,不自动填入',
1637
+ }))
1638
+ : rawOptions
1452
1639
  const anyEvidence = pi.exact.length + pi.sameChannelOtherApi.length + pi.crossChannel.length > 0
1453
1640
  return {
1454
1641
  field,
@@ -1457,14 +1644,16 @@ function capabilityGroup(field, target, pi, openRouter) {
1457
1644
  comparison: comparisonFor(field, target.current?.[field], options),
1458
1645
  options,
1459
1646
  overflow,
1460
- readOnly: false,
1647
+ readOnly: field === 'maxTokens',
1461
1648
  adoptable: options.some((option) => option.adoptable),
1462
1649
  // 缺席理由:没有资料 vs 资料未表态。两者都不是「不支持」。
1463
1650
  absentReason: options.length === 0 ? (anyEvidence ? 'source-undeclared' : 'no-local-evidence') : undefined,
1464
1651
  unsupported: false,
1465
1652
  notes: options.length === 0
1466
1653
  ? ['资料缺席不是「不支持」,可合法手填']
1467
- : ['无自动勾选;✓ 才写入草稿'],
1654
+ : (field === 'maxTokens'
1655
+ ? ['能力上限只读;route 的 maxTokens 是单次请求输出预留,不自动填入']
1656
+ : ['无自动勾选;✓ 才写入草稿']),
1468
1657
  }
1469
1658
  }
1470
1659
 
@@ -2157,7 +2346,8 @@ export async function querySmartFill(options = {}) {
2157
2346
  }
2158
2347
  }
2159
2348
 
2160
- // 3) OpenRouter:直接载荷优先,否则走缓存条件请求
2349
+ // 3) OpenRouter:先用**原始 ID**查询(精确身份优先);仅在原始 ID 于本地来源无记录、
2350
+ // 且精确查询也没有命中时,按别名候选回退**一次**(不级联,最多多一次请求)。
2161
2351
  let openRouterPayload = null
2162
2352
  if (options.openRouterPayload !== undefined) {
2163
2353
  openRouterPayload = options.openRouterPayload
@@ -2176,18 +2366,49 @@ export async function querySmartFill(options = {}) {
2176
2366
  } else {
2177
2367
  sources.push({ sourceId: 'openrouter', status: 'skipped', network: false, error: 'no-source' })
2178
2368
  }
2369
+ const exactMatches = openRouterPayload !== null ? openRouterMatches(target.modelId, openRouterPayload) : undefined
2370
+ let identity = resolveSmartFillIdentity(target.modelId, {
2371
+ piIndex,
2372
+ modelParamsIndex,
2373
+ openRouterMatchesCount: exactMatches?.matches?.length ?? 0,
2374
+ })
2375
+ let matches = exactMatches
2376
+ let identityFallback = null
2377
+ if (identity.via === 'alias' && options.cache !== undefined) {
2378
+ attemptedNetwork = true
2379
+ const request = openRouterRequest(identity.modelId)
2380
+ const fallbackPayload = await readViaCache(options.cache, {
2381
+ sourceId: request.sourceId,
2382
+ url: request.url,
2383
+ force: Boolean(options.forceRefresh),
2384
+ parse: parseOpenRouterPayload,
2385
+ sources,
2386
+ label: 'openrouter',
2387
+ })
2388
+ mergeOpenRouterFallback(sources, { requestedModelId: identity.requestedModelId, matchedModelId: identity.modelId, kind: identity.kind })
2389
+ matches = fallbackPayload !== null ? openRouterMatches(identity.modelId, fallbackPayload) : undefined
2390
+ identityFallback = { attempted: true, modelId: identity.modelId, kind: identity.kind, matched: matches?.matches?.length ?? 0 }
2391
+ if (matches !== undefined && matches.matches.length > 0) {
2392
+ identity.recognizedBy = [...new Set([...identity.recognizedBy, 'openrouter'])]
2393
+ }
2394
+ }
2395
+ // 别名只有在至少一个来源真的认出它时才启用;纯猜测不产生任何建议,退回精确身份。
2396
+ if (identity.via === 'alias' && identity.recognizedBy.length === 0) {
2397
+ identity = { requestedModelId: identity.requestedModelId, modelId: identity.requestedModelId, via: 'exact', kind: '', recognizedBy: [] }
2398
+ matches = exactMatches
2399
+ }
2400
+ const effectiveTarget = identity.via === 'alias' ? { ...target, modelId: identity.modelId } : target
2179
2401
 
2180
2402
  // 4) 运行时(本地投影,不触网、不发推理)
2181
2403
  const runtime = options.runtimeInfo !== undefined ? runtimeReference(options.runtimeInfo) : undefined
2182
2404
  sources.push({ sourceId: 'runtime', status: runtime === undefined ? 'unavailable' : 'ok', network: false, error: null })
2183
2405
 
2184
- const modelParams = modelParamsIndex !== null ? modelParamsReference(modelParamsIndex, target.modelId) : undefined
2185
- const matches = openRouterPayload !== null ? openRouterMatches(target.modelId, openRouterPayload) : undefined
2406
+ const modelParams = modelParamsIndex !== null ? modelParamsReference(modelParamsIndex, effectiveTarget.modelId) : undefined
2186
2407
  const references = matches !== undefined
2187
2408
  ? matches.matches.map((match) => openRouterReference(match)).filter((reference) => reference !== undefined)
2188
2409
  : []
2189
2410
  const built = buildSmartFillGroups({
2190
- target,
2411
+ target: effectiveTarget,
2191
2412
  piIndex,
2192
2413
  modelParams,
2193
2414
  openRouter: matches !== undefined ? { matches: matches.matches, ambiguous: matches.ambiguous, references } : undefined,
@@ -2195,6 +2416,22 @@ export async function querySmartFill(options = {}) {
2195
2416
  })
2196
2417
 
2197
2418
  if (matches !== undefined && matches.ambiguous) notes.push('OpenRouter 命中多个同名条目:保留全部候选,由用户选择')
2419
+ if (identity.via === 'alias') {
2420
+ // 别名只用于取资料:证据一律降级为「模型身份参考」。models3-fill 恒传 modelOnly,
2421
+ // buildSmartFillGroups 已降级过一遍;这里再兜底其它调用方,避免别名身份冒充精确渠道证据。
2422
+ for (const group of built.groups) {
2423
+ for (const option of group.options ?? []) {
2424
+ if (option.evidence === 'exact-channel-protocol') {
2425
+ option.evidence = 'model-identity'
2426
+ option.confidence = 'reference'
2427
+ }
2428
+ if (option.evidence === 'model-identity') {
2429
+ option.note = '用于查询的 ID「' + identity.requestedModelId.slice(0, 128) + '」无来源记录;按别名「' + identity.modelId.slice(0, 128) + '」匹配,仅作模型身份参考'
2430
+ }
2431
+ }
2432
+ }
2433
+ notes.push('用于查询的模型 ID 在固定来源没有记录:已按别名查询,建议仅作模型身份参考,需逐项确认')
2434
+ }
2198
2435
  return {
2199
2436
  mode: 'on',
2200
2437
  network: attemptedNetwork,
@@ -2202,5 +2439,14 @@ export async function querySmartFill(options = {}) {
2202
2439
  sources,
2203
2440
  runtime: runtime ?? null,
2204
2441
  notes: [...notes, ...built.notes],
2442
+ // 身份留证:客户端据此提示「按别名查询」,原始 ID 与匹配 ID 都返回但不作任何写入默认。
2443
+ identity: {
2444
+ requestedModelId: identity.requestedModelId,
2445
+ modelId: identity.modelId,
2446
+ via: identity.via,
2447
+ kind: identity.kind,
2448
+ recognizedBy: [...identity.recognizedBy],
2449
+ ...(identityFallback !== null ? { openRouterFallback: identityFallback } : {}),
2450
+ },
2205
2451
  }
2206
2452
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gehennawu/dsh-service",
3
- "version": "2.0.10",
3
+ "version": "2.1.0",
4
4
  "description": "DSH Web 运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
5
5
  "type": "module",
6
6
  "scripts": {