@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/README.en.md +28 -14
- package/README.md +28 -14
- package/client.js +1 -1
- package/index.js +34 -2
- package/model-config-routes.js +15 -1
- package/model-smart-fill.js +253 -7
- package/package.json +1 -1
- package/quota-adapters.js +369 -0
- package/quota-routes.js +36 -1
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,
|
package/model-config-routes.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/model-smart-fill.js
CHANGED
|
@@ -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:
|
|
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,
|
|
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
|
}
|