@sligqoer/dsh-usage-stats 0.2.0 → 0.2.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 +89 -1
- package/NOTICE +39 -0
- package/README.en.md +187 -0
- package/README.md +93 -263
- package/lib/bridge.js +33 -0
- package/lib/client.js +2321 -1579
- package/lib/index.js +173 -197
- package/lib/lm-fingerprint-engine.js +166 -0
- package/lib/lm-fingerprint-source.js +134 -0
- package/lib/lm-fingerprint-worker.js +31 -0
- package/lib/{model-test-history.js → model-detection-history.js} +23 -30
- package/lib/model-detection.js +255 -0
- package/lib/quota-controls.js +64 -23
- package/lib/supplemental-usage.js +77 -0
- package/lib/usage-preferences.js +51 -0
- package/locale/en.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +28 -25
- package/screenshots/controls.png +0 -0
- package/screenshots/detection.png +0 -0
- package/screenshots/quotas.png +0 -0
- package/screenshots/usage.png +0 -0
- package/third_party/brand-icons/CHANNEL-ICONS-LICENSE +21 -0
- package/third_party/brand-icons/LICENSE +59 -0
- package/third_party/brand-icons/NOTICE.txt +5 -0
- package/third_party/brand-icons/REACT-ICONS-LICENSE +104 -0
- package/third_party/brand-icons/channel-icons/LICENSE +21 -0
- package/third_party/brand-icons/channel-icons/UPSTREAM.json +6 -0
- package/third_party/brand-icons/lobe-icons/LICENSE +21 -0
- package/third_party/brand-icons/lobe-icons/UPSTREAM.json +16 -0
- package/third_party/brand-icons/react-icons/LICENSE +13 -0
- package/third_party/brand-icons/react-icons/UPSTREAM.json +6 -0
- package/third_party/lm-fingerprint/LICENSE +21 -0
- package/third_party/lm-fingerprint/UPSTREAM.json +32 -0
- package/third_party/lm-fingerprint/core/challenge-browser.mjs +210 -0
- package/third_party/lm-fingerprint/core/detector-codec.mjs +20 -0
- package/third_party/lm-fingerprint/core/fingerprint-core.mjs +121 -0
- package/third_party/lm-fingerprint/core/positional.mjs +64 -0
- package/third_party/lm-fingerprint/core/shared-detector.mjs +132 -0
- package/third_party/lm-fingerprint/data/shared_detector.bin +0 -0
- package/third_party/lm-fingerprint/data/unified_bank.json +826820 -0
- package/README.zh-CN.md +0 -357
- package/lib/fingerprints/KBF-LICENSE.txt +0 -201
- package/lib/fingerprints/NOTICE.txt +0 -6
- package/lib/fingerprints/kbf-202609.json +0 -1
- package/lib/model-capabilities.js +0 -41
- package/lib/model-fingerprints.js +0 -146
- package/lib/model-match.js +0 -407
- package/screenshots/match.png +0 -0
- package/screenshots/token-speed.png +0 -0
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/** Seeded, locally graded tasks. Scores describe these tasks only, never model identity. */
|
|
2
|
-
import { binomialTail } from './model-fingerprints.js'
|
|
3
|
-
export function capabilityTasks(seed) {
|
|
4
|
-
let state = seed >>> 0
|
|
5
|
-
const rand = max => { state = (Math.imul(state, 1664525) + 1013904223) >>> 0; return state % max }
|
|
6
|
-
const tasks = []
|
|
7
|
-
const add = (domain, prompt, answer) => tasks.push({ id: `${domain}-${tasks.length}`, domain,
|
|
8
|
-
prompt: prompt + '\nReturn only JSON with exactly one key "answer". Do not use tools.',
|
|
9
|
-
check(text) { const value = JSON.parse(text.trim()); return Object.keys(value).length === 1 && JSON.stringify(value.answer) === JSON.stringify(answer) } })
|
|
10
|
-
for (let i = 0; i < 4; i++) {
|
|
11
|
-
const a = rand(17) + 2, b = rand(23) + 1, turns = rand(16) + 30
|
|
12
|
-
let x = rand(300), initial = x, sum = 0
|
|
13
|
-
for (let j = 0; j < turns; j++) { x = (x * a + b + j) % 997; sum += j % 3 === 0 ? -x : x }
|
|
14
|
-
add('code', `Evaluate this JavaScript without executing it. What is sum?\nlet x=${initial}, sum=0; for(let j=0;j<${turns};j++){x=(x*${a}+${b}+j)%997;sum+=j%3===0?-x:x;}`, sum)
|
|
15
|
-
const edges = [], costs = Array(11).fill(Infinity); costs[0] = 0
|
|
16
|
-
for (let u = 0; u < 10; u++) for (let v = u + 1; v <= Math.min(10, u + 4); v++) edges.push([u, v, rand(31) + 1])
|
|
17
|
-
for (let u = 0; u < 10; u++) for (const [from, to, weight] of edges) if (from === u) costs[to] = Math.min(costs[to], costs[from] + weight)
|
|
18
|
-
add('reasoning', `Find the minimum total weight of a directed path from node 0 to node 10. Edges [from,to,weight]: ${JSON.stringify(edges)}`, costs[10])
|
|
19
|
-
const entries = Array.from({ length: 160 }, (_, n) => ({ key: `record_${n}`, priority: rand(1000), value: rand(100_000) }))
|
|
20
|
-
const chosen = rand(150) + 5, key = entries[chosen].key
|
|
21
|
-
add('context', `Read the following records. For key ${key}, return the value in the LAST occurrence. Ignore priorities.\n${JSON.stringify(entries.concat({ key, priority: 9999, value: entries[chosen].value + 17 }))}`, entries[chosen].value + 17)
|
|
22
|
-
const intervals = Array.from({ length: 16 }, () => { const start = rand(30); return [start, start + rand(9) + 1, rand(40) + 1] }).sort((a, b) => a[1] - b[1])
|
|
23
|
-
const dp = [0]
|
|
24
|
-
intervals.forEach(([start, , reward], j) => { let previous = j - 1; while (previous >= 0 && intervals[previous][1] > start) previous--; dp.push(Math.max(dp[j], reward + dp[previous + 1])) })
|
|
25
|
-
add('constraints', `Choose non-overlapping jobs to maximize reward. A job ending at t is compatible with a job starting at t. Return the maximum reward. Jobs [start,end,reward]: ${JSON.stringify(intervals)}`, dp.at(-1))
|
|
26
|
-
}
|
|
27
|
-
return tasks
|
|
28
|
-
}
|
|
29
|
-
export function scoreCapabilities(outcomes, reference, complete = true) {
|
|
30
|
-
const valid = outcomes.filter(v => v === 0 || v === 1), passed = valid.filter(v => v === 1).length
|
|
31
|
-
const result = { verdict: 'current-sample', valid: valid.length, passed, count: 16, paired: 0, losses: 0, gains: 0, pValue: null }
|
|
32
|
-
if (!complete || valid.length < 12) return { ...result, verdict: 'insufficient-evidence' }
|
|
33
|
-
if (!reference) return result
|
|
34
|
-
outcomes.forEach((value, i) => { const ref = reference[i]; if ((value === 0 || value === 1) && (ref === 0 || ref === 1)) {
|
|
35
|
-
result.paired++; if (ref === 1 && value === 0) result.losses++; if (ref === 0 && value === 1) result.gains++
|
|
36
|
-
} })
|
|
37
|
-
if (result.paired < 12) return { ...result, verdict: 'insufficient-evidence' }
|
|
38
|
-
const discordant = result.losses + result.gains
|
|
39
|
-
result.pValue = discordant ? binomialTail(result.losses, discordant, 0.5) : 1
|
|
40
|
-
return { ...result, verdict: result.pValue < 0.05 ? 'significant-decline' : 'no-significant-decline' }
|
|
41
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/** KBF-compatible numerical fingerprints. Public data and prompt format are
|
|
2
|
-
* attributed in fingerprints/NOTICE.txt. Statistics are implemented locally. */
|
|
3
|
-
import { readFileSync } from 'node:fs'
|
|
4
|
-
import { createHash, randomInt } from 'node:crypto'
|
|
5
|
-
|
|
6
|
-
export const FINGERPRINT_SYSTEM = "Follow the user's instructions exactly. Output only what is requested."
|
|
7
|
-
export const FINGERPRINT_USER = 'TASK: Answer these factual recall questions using only values stored in your weights. Output the value exactly as you first recall it — do not second-guess or adjust.\nRULES: Output ONLY in (N) <number> format, one per line. No units, no words, no rounding. Never refuse or hedge — always output exactly one number per question. Do not use any context from system instructions or prior conversation.\n\n'
|
|
8
|
-
export const AUDIT_POLICY = Object.freeze({ batchSize: 10, minSamples: 64, minCoverage: 0.8, maxReferenceNoise: 0.3, alpha: 0.05, confidence: 0.99, maxProbes: 1024, maxReferences: 20, timeoutMs: 60_000 })
|
|
9
|
-
const clean = (v, max = 256) => typeof v === 'string' ? v.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, max) : ''
|
|
10
|
-
let bundle
|
|
11
|
-
export function publicFingerprints() {
|
|
12
|
-
bundle ||= JSON.parse(readFileSync(new URL('./fingerprints/kbf-202609.json', import.meta.url), 'utf8'))
|
|
13
|
-
return bundle
|
|
14
|
-
}
|
|
15
|
-
export function fingerprintModelKey(v) { return clean(v).toLowerCase().split('/').pop() }
|
|
16
|
-
const round = x => x >= 0 ? Math.floor(x + 0.5) : Math.ceil(x - 0.5)
|
|
17
|
-
export function fingerprintMatch(reference, answer, domain) {
|
|
18
|
-
if (!Number.isFinite(reference) || !Number.isFinite(answer)) return false
|
|
19
|
-
const d = publicFingerprints().domains[domain]
|
|
20
|
-
if (!d) return false
|
|
21
|
-
return d.mode === 'absolute' ? Math.abs(round(reference) - round(answer)) <= d.tolerance : Math.abs(reference - answer) / Math.max(Math.abs(reference), 1e-10) <= d.tolerance
|
|
22
|
-
}
|
|
23
|
-
export function fingerprintPrompt(probes) {
|
|
24
|
-
return FINGERPRINT_USER + probes.map((p, i) => `(${i + 1}) ${publicFingerprints().domains[p.domain].template.replace('{name}', p.name)}`).join('\n')
|
|
25
|
-
}
|
|
26
|
-
/** Positions must be explicit and unique. Never slide a missing answer onto the
|
|
27
|
-
* next question, extract numbers from prose, or silently accept duplicate ids. */
|
|
28
|
-
export function parseFingerprintAnswers(text, probes) {
|
|
29
|
-
const values = probes.map(() => null), seen = new Set(), duplicates = new Set()
|
|
30
|
-
for (const line of String(text).split(/\r?\n/)) {
|
|
31
|
-
const match = line.trim().match(/^\((\d+)\)\s*([+\-−]?(?:\d[\d,]*(?:\.\d*)?|\.\d+)(?:[eE][+\-]?\d+)?)\s*$/)
|
|
32
|
-
if (!match) continue
|
|
33
|
-
const i = Number(match[1]) - 1
|
|
34
|
-
if (i < 0 || i >= probes.length) continue
|
|
35
|
-
if (seen.has(i)) { duplicates.add(i); values[i] = null; continue }
|
|
36
|
-
seen.add(i)
|
|
37
|
-
const value = Number(match[2].replaceAll(',', '').replace('−', '-')), range = publicFingerprints().domains[probes[i].domain]?.range
|
|
38
|
-
if (range && Number.isFinite(value) && value >= range[0] && value <= range[1] && !duplicates.has(i)) values[i] = value
|
|
39
|
-
}
|
|
40
|
-
return values
|
|
41
|
-
}
|
|
42
|
-
const logs = [0]
|
|
43
|
-
function logFactorial(n) { while (logs.length <= n) logs.push(logs.at(-1) + Math.log(logs.length)); return logs[n] }
|
|
44
|
-
function binomialSum(lo, hi, n, p) {
|
|
45
|
-
if (lo > hi) return 0
|
|
46
|
-
if (p === 0) return lo === 0 ? 1 : 0
|
|
47
|
-
if (p === 1) return hi === n ? 1 : 0
|
|
48
|
-
const terms = [], base = logFactorial(n)
|
|
49
|
-
for (let k = lo; k <= hi; k++) terms.push(base - logFactorial(k) - logFactorial(n - k) + k * Math.log(p) + (n - k) * Math.log1p(-p))
|
|
50
|
-
const largest = Math.max(...terms)
|
|
51
|
-
return Math.min(1, Math.exp(largest) * terms.reduce((sum, x) => sum + Math.exp(x - largest), 0))
|
|
52
|
-
}
|
|
53
|
-
export function binomialTail(k, n, p) {
|
|
54
|
-
if (!Number.isInteger(k) || !Number.isInteger(n) || k < 0 || k > n || n < 1 || !Number.isFinite(p) || p < 0 || p > 1) throw new Error('Invalid binomial counts')
|
|
55
|
-
return binomialSum(k, n, n, p)
|
|
56
|
-
}
|
|
57
|
-
export function clopperPearsonUpper(k, n) {
|
|
58
|
-
if (!Number.isInteger(k) || !Number.isInteger(n) || n < 1 || k < 0 || k > n) throw new Error('Invalid calibration counts')
|
|
59
|
-
if (k === n) return 1
|
|
60
|
-
let lo = 0, hi = 1
|
|
61
|
-
for (let i = 0; i < 60; i++) { const p = (lo + hi) / 2; if (binomialSum(0, k, n, p) > 0.01) lo = p; else hi = p }
|
|
62
|
-
return (lo + hi) / 2
|
|
63
|
-
}
|
|
64
|
-
export function scoreFingerprint(reference, outcomes, complete = true) {
|
|
65
|
-
const referenceValues = reference.probes.map(p => p.calibration), calibration = referenceValues.filter(v => v === 0 || v === 1)
|
|
66
|
-
const valid = outcomes.filter(v => v === 0 || v === 1), mismatches = valid.filter(v => v === 0).length
|
|
67
|
-
const self = reference.calibration || { total: calibration.length, mismatches: calibration.filter(v => v === 0).length }
|
|
68
|
-
const count = reference.probes.length, coverage = count ? valid.length / count : 0, selfCoverage = count ? self.total / count : 0
|
|
69
|
-
const result = { verdict: 'insufficient-evidence', reason: 'coverage', count, valid: valid.length, mismatches, coverage,
|
|
70
|
-
selfTotal: self.total, selfMismatches: self.mismatches, selfCoverage, alpha: AUDIT_POLICY.alpha, confidence: AUDIT_POLICY.confidence, p0: null, pValue: null }
|
|
71
|
-
if (!complete) return { ...result, reason: 'unfinished' }
|
|
72
|
-
if (count < AUDIT_POLICY.minSamples || valid.length < AUDIT_POLICY.minSamples || self.total < AUDIT_POLICY.minSamples) return { ...result, reason: 'sample-size' }
|
|
73
|
-
if (coverage < AUDIT_POLICY.minCoverage || selfCoverage < AUDIT_POLICY.minCoverage) return result
|
|
74
|
-
const p0 = clopperPearsonUpper(self.mismatches, self.total), pValue = binomialTail(mismatches, valid.length, p0)
|
|
75
|
-
return { ...result, verdict: pValue < AUDIT_POLICY.alpha ? 'significant-difference' : 'no-significant-difference', reason: 'statistical-test', p0, pValue }
|
|
76
|
-
}
|
|
77
|
-
export function normalizeFingerprint(value, origin = 'imported') {
|
|
78
|
-
if (!value || typeof value !== 'object') throw new Error('基线内容不是有效对象。')
|
|
79
|
-
const model = clean(value.model || value.reference_model), input = value.probes
|
|
80
|
-
if (!model || !Array.isArray(input) || input.length < AUDIT_POLICY.minSamples || input.length > AUDIT_POLICY.maxProbes) throw new Error('基线需要模型名称和 64–1,024 个探针。')
|
|
81
|
-
const self = value.target_results?.[model.split('/').pop()], vector = self?.match_vector
|
|
82
|
-
const domains = publicFingerprints().domains, seen = new Set()
|
|
83
|
-
const probes = input.map((p, i) => {
|
|
84
|
-
const domain = clean(p?.domain, 64), name = clean(p?.name, 512), answer = p?.cloze_consensus ?? p?.value, key = domain + '\0' + name
|
|
85
|
-
if (!domains[domain] || !name || !Number.isFinite(answer) || seen.has(key)) throw new Error('基线含未知领域、重复探针或无效参考答案。')
|
|
86
|
-
seen.add(key)
|
|
87
|
-
const range = domains[domain].range
|
|
88
|
-
if (answer < range[0] || answer > range[1]) throw new Error('参考答案超出探针允许范围。')
|
|
89
|
-
const calibration = p.calibration ?? vector?.[i]
|
|
90
|
-
return { domain, name, value: answer, calibration: calibration === 0 || calibration === 1 ? calibration : null }
|
|
91
|
-
})
|
|
92
|
-
const total = value.calibration?.total ?? self?.total, mismatches = value.calibration?.mismatches ?? self?.hamming
|
|
93
|
-
let calibration
|
|
94
|
-
if (!probes.some(p => p.calibration !== null)) {
|
|
95
|
-
if (!Number.isInteger(total) || total < AUDIT_POLICY.minSamples || total > probes.length || !Number.isInteger(mismatches) || mismatches < 0 || mismatches > total) throw new Error('基线缺少独立自测校准计数。')
|
|
96
|
-
calibration = { total, mismatches }
|
|
97
|
-
}
|
|
98
|
-
const config = value.config || self?.test_config || {}
|
|
99
|
-
const temperature = config.temperature ?? config.temp ?? 0
|
|
100
|
-
if (temperature !== 0) throw new Error('基线自测温度必须为 0。')
|
|
101
|
-
const system = clean(config.system || config.sys || FINGERPRINT_SYSTEM, 512)
|
|
102
|
-
if (system !== FINGERPRINT_SYSTEM) throw new Error('基线使用了不同的系统提示词,请重新校准。')
|
|
103
|
-
const reasoningEffort = clean(config.reasoningEffort || 'off', 64)
|
|
104
|
-
const selection = value.selection && { provider: clean(value.selection.provider), model: clean(value.selection.model), ...(reasoningEffort !== 'off' ? { reasoningEffort } : {}) }
|
|
105
|
-
const payload = { version: 2, model, provider: clean(value.provider || value.provider_map?.[model] || selection?.provider || '未标明'),
|
|
106
|
-
origin, date: clean(value.date || value.timestamp || new Date().toISOString(), 64), config: { system, temperature: 0, reasoningEffort }, probes,
|
|
107
|
-
...(calibration ? { calibration } : {}), ...(selection?.provider && selection.model ? { selection } : {}) }
|
|
108
|
-
// Retain public provenance, never imported endpoint URLs, headers or secrets.
|
|
109
|
-
if (origin === 'public') { payload.source = value.source; payload.sha256 = value.sha256 }
|
|
110
|
-
if (value.capabilities?.version === 1 && Number.isInteger(value.capabilities.seed) && Array.isArray(value.capabilities.outcomes) && value.capabilities.outcomes.length === 16) {
|
|
111
|
-
payload.capabilities = { version: 1, seed: value.capabilities.seed >>> 0, effort: clean(value.capabilities.effort, 64),
|
|
112
|
-
outcomes: value.capabilities.outcomes.map(v => v === 0 || v === 1 ? v : null) }
|
|
113
|
-
}
|
|
114
|
-
payload.id = origin === 'public' ? value.id : 'local/' + createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 24)
|
|
115
|
-
return payload
|
|
116
|
-
}
|
|
117
|
-
export function fingerprintSummary(ref) {
|
|
118
|
-
const scored = scoreFingerprint(ref, [])
|
|
119
|
-
return { id: ref.id, model: ref.model, provider: ref.provider, origin: ref.origin, date: ref.date, source: ref.source,
|
|
120
|
-
count: ref.probes.length, selfTotal: scored.selfTotal, selfMismatches: scored.selfMismatches,
|
|
121
|
-
effort: ref.config.reasoningEffort, hasCapabilities: !!ref.capabilities, subsetAvailable: !ref.calibration }
|
|
122
|
-
}
|
|
123
|
-
export function subsetFingerprint(ref, count) {
|
|
124
|
-
if (count >= ref.probes.length || ref.calibration) return ref
|
|
125
|
-
const indices = ref.probes.map((_, i) => i)
|
|
126
|
-
for (let i = indices.length - 1; i > 0; i--) { const j = randomInt(i + 1); [indices[i], indices[j]] = [indices[j], indices[i]] }
|
|
127
|
-
const selected = indices.slice(0, count).sort((a, b) => a - b)
|
|
128
|
-
return { ...ref, probes: selected.map(i => ref.probes[i]) }
|
|
129
|
-
}
|
|
130
|
-
export function calibrationPool(count) {
|
|
131
|
-
const groups = new Map(), seen = new Set()
|
|
132
|
-
for (const ref of publicFingerprints().references) for (const p of ref.probes) {
|
|
133
|
-
const key = p.domain + '\0' + p.name
|
|
134
|
-
if (seen.has(key)) continue
|
|
135
|
-
seen.add(key)
|
|
136
|
-
if (!groups.has(p.domain)) groups.set(p.domain, [])
|
|
137
|
-
groups.get(p.domain).push({ domain: p.domain, name: p.name })
|
|
138
|
-
}
|
|
139
|
-
const pool = []
|
|
140
|
-
while (pool.length < count && groups.size) for (const [domain, entries] of groups) {
|
|
141
|
-
if (pool.length >= count) break
|
|
142
|
-
if (!entries.length) { groups.delete(domain); continue }
|
|
143
|
-
pool.push(entries.splice(randomInt(entries.length), 1)[0])
|
|
144
|
-
}
|
|
145
|
-
return pool
|
|
146
|
-
}
|
package/lib/model-match.js
DELETED
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
/** Local, bounded evidence analysis. Response-declared names are observations,
|
|
2
|
-
* never proof of a provider's real model identity. No conversation text is kept. */
|
|
3
|
-
import { randomUUID, randomInt } from 'node:crypto'
|
|
4
|
-
import { readFileSync } from 'node:fs'
|
|
5
|
-
import { mkdir, writeFile, rename } from 'node:fs/promises'
|
|
6
|
-
import { dirname } from 'node:path'
|
|
7
|
-
import { publicFingerprints, normalizeFingerprint, fingerprintSummary, fingerprintModelKey, subsetFingerprint,
|
|
8
|
-
calibrationPool, fingerprintPrompt, parseFingerprintAnswers, fingerprintMatch, scoreFingerprint, FINGERPRINT_SYSTEM, AUDIT_POLICY } from './model-fingerprints.js'
|
|
9
|
-
import { capabilityTasks, scoreCapabilities } from './model-capabilities.js'
|
|
10
|
-
|
|
11
|
-
const LIMIT = 30
|
|
12
|
-
const usageKeys = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens']
|
|
13
|
-
const safeText = (value, max = 256) => typeof value === 'string' ? value.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, max) : ''
|
|
14
|
-
const nonnegative = value => Number.isFinite(value) && value >= 0 ? value : 0
|
|
15
|
-
export function safeUsage(value) {
|
|
16
|
-
return Object.fromEntries(usageKeys.map(key => [key, nonnegative(value?.[key])]))
|
|
17
|
-
}
|
|
18
|
-
export function modelSelection(value) {
|
|
19
|
-
const provider = safeText(value?.provider), model = safeText(value?.model)
|
|
20
|
-
if (!provider || !model) return null
|
|
21
|
-
const reasoningEffort = safeText(value.reasoningEffort, 64)
|
|
22
|
-
return { provider, model, ...(reasoningEffort ? { reasoningEffort } : {}) }
|
|
23
|
-
}
|
|
24
|
-
function record(value) {
|
|
25
|
-
const selection = modelSelection(value)
|
|
26
|
-
if (!selection) return null
|
|
27
|
-
const responseModel = safeText(value.responseModel)
|
|
28
|
-
const responseEffort = safeText(value.responseEffort, 64)
|
|
29
|
-
const stop = ['stop', 'max-tokens', 'tool-calls', 'error', 'aborted', 'unknown'].includes(value.stop) ? value.stop : 'unknown'
|
|
30
|
-
return { ...selection, time: nonnegative(value.time), requestHeader: value.requestHeader === true,
|
|
31
|
-
...(responseModel ? { responseModel } : {}), ...(responseEffort ? { responseEffort } : {}),
|
|
32
|
-
stop, usage: safeUsage(value.usage) }
|
|
33
|
-
}
|
|
34
|
-
export function normalizeEvidence(value) {
|
|
35
|
-
if (!value || value.version !== 1) return null
|
|
36
|
-
return { version: 1, latestRequest: modelSelection(value.latestRequest),
|
|
37
|
-
records: (Array.isArray(value.records) ? value.records : []).slice(-LIMIT).map(record).filter(Boolean) }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function captureModelEvidence(events, inheritedEventCount = 0) {
|
|
41
|
-
let latestRequest = null
|
|
42
|
-
const records = []
|
|
43
|
-
const inherited = Math.max(0, Math.floor(Number(inheritedEventCount) || 0))
|
|
44
|
-
for (const [index, event] of (events || []).entries()) {
|
|
45
|
-
if (event?.type === 'request/header') latestRequest = modelSelection(event.data?.header?.config) || latestRequest
|
|
46
|
-
if (event?.type !== 'assistant/message' || index < inherited) continue
|
|
47
|
-
const source = event.data?.message?.source
|
|
48
|
-
const selection = modelSelection(source)
|
|
49
|
-
if (!selection) continue
|
|
50
|
-
const paired = latestRequest?.provider === selection.provider && latestRequest?.model === selection.model
|
|
51
|
-
const response = source.replayState?.response
|
|
52
|
-
records.push(record({ ...(paired ? latestRequest : selection), time: event.time,
|
|
53
|
-
requestHeader: paired, responseModel: response?.responseModel,
|
|
54
|
-
responseEffort: response?.providerThinkingLevel,
|
|
55
|
-
stop: response?.stopReason === 'length' ? 'max-tokens' : response?.stopReason === 'toolUse' ? 'tool-calls' : response?.stopReason,
|
|
56
|
-
usage: event.data.usage }))
|
|
57
|
-
}
|
|
58
|
-
return { version: 1, latestRequest, records: records.slice(-LIMIT) }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Only obvious provider-prefix/date-snapshot variations are comparable. No
|
|
62
|
-
* aliases used by usage charts are used to decide identity or compatibility. */
|
|
63
|
-
function comparableModel(value) {
|
|
64
|
-
return safeText(value).toLowerCase().split('/').pop().replace(/[-_]\d{4}[-_]\d{2}[-_]\d{2}$/, '')
|
|
65
|
-
}
|
|
66
|
-
export function analyzeModelEvidence(value, requested) {
|
|
67
|
-
const evidence = normalizeEvidence(value)
|
|
68
|
-
const selection = modelSelection(requested) || evidence?.latestRequest || evidence?.records.at(-1) || null
|
|
69
|
-
const samples = (evidence?.records || []).filter(item => selection && item.provider === selection.provider && item.model === selection.model &&
|
|
70
|
-
(!selection.reasoningEffort || item.reasoningEffort === selection.reasoningEffort))
|
|
71
|
-
let exact = 0, compatible = 0, different = 0, missing = 0
|
|
72
|
-
for (const item of samples) {
|
|
73
|
-
if (!item.responseModel) missing++
|
|
74
|
-
else if (item.responseModel === item.model) exact++
|
|
75
|
-
else if (comparableModel(item.responseModel) === comparableModel(item.model)) compatible++
|
|
76
|
-
else different++
|
|
77
|
-
}
|
|
78
|
-
// Metadata describes declarations only. Identity remains untested even when
|
|
79
|
-
// every response repeats the requested name or every name differs.
|
|
80
|
-
return { selection: modelSelection(selection), verdict: 'insufficient-evidence', strength: 'weak',
|
|
81
|
-
declarationStatus: different ? 'name-difference' : exact + compatible ? 'names-consistent' : 'not-reported',
|
|
82
|
-
counts: { samples: samples.length, exact, compatible, different, missing },
|
|
83
|
-
// Newest first, deliberately excludes replay blocks, ids, headers and text.
|
|
84
|
-
records: samples.slice(-10).reverse(),
|
|
85
|
-
missingReasoningUsage: samples.filter(item => item.reasoningEffort && item.usage.reasoningTokens === 0).length }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export class ModelMatchError extends Error {}
|
|
89
|
-
export const PROBE_PLAN = Object.freeze({ maxTokens: 800, capabilityMaxTokens: 1024, timeoutMs: AUDIT_POLICY.timeoutMs })
|
|
90
|
-
function abortable(promise, signal) {
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
const abort = () => reject(new Error('aborted'))
|
|
93
|
-
if (signal.aborted) { abort(); return }
|
|
94
|
-
signal.addEventListener('abort', abort, { once: true })
|
|
95
|
-
Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener('abort', abort))
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const batches = values => Array.from({ length: Math.ceil(values.length / 10) }, (_, i) => values.slice(i * 10, i * 10 + 10))
|
|
100
|
-
const sumTokens = usage => usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens
|
|
101
|
-
/** Answer agreement, not the statistical test's p-value or identity probability. */
|
|
102
|
-
export function modelTestScore(job) {
|
|
103
|
-
const identity = job?.identity
|
|
104
|
-
if (job?.status !== 'complete' || job.mode !== 'audit' || !['significant-difference', 'no-significant-difference'].includes(identity?.verdict)) return null
|
|
105
|
-
const { count, valid, mismatches } = identity
|
|
106
|
-
if (![count, valid, mismatches].every(Number.isInteger) || valid < AUDIT_POLICY.minSamples || valid > count || mismatches < 0 || mismatches > valid || valid / count < AUDIT_POLICY.minCoverage) return null
|
|
107
|
-
return { value: Math.round((valid - mismatches) * 1000 / valid) / 10, matched: valid - mismatches, valid, count, method: 'answer-agreement' }
|
|
108
|
-
}
|
|
109
|
-
export function summarizeModelTest(job) {
|
|
110
|
-
if (job.status === 'running') return { verdict: 'running', reason: 'running' }
|
|
111
|
-
if (job.mode === 'calibrate' && job.status === 'complete' && job.baselineId) return { verdict: 'reference-created', reason: 'reference-created' }
|
|
112
|
-
if (job.status === 'complete') {
|
|
113
|
-
if (job.identity?.verdict === 'significant-difference') return { verdict: 'suspected-mismatch', reason: 'behavior-difference' }
|
|
114
|
-
if (job.capability?.verdict === 'significant-decline') return { verdict: 'suspected-decline', reason: 'capability-decline' }
|
|
115
|
-
if (job.identity?.verdict === 'no-significant-difference') return { verdict: 'no-anomaly', reason: 'behavior-consistent' }
|
|
116
|
-
if (job.mode === 'capability' && job.capability?.verdict === 'no-significant-decline') return { verdict: 'no-decline', reason: 'capability-consistent' }
|
|
117
|
-
if (job.mode === 'capability' && job.capability?.verdict === 'current-sample') return { verdict: 'capability-complete', reason: 'no-capability-reference' }
|
|
118
|
-
}
|
|
119
|
-
const results = job.results || []
|
|
120
|
-
const invalid = Object.fromEntries(['incomplete', 'unparseable', 'unavailable', 'timeout', 'parameters-ignored'].map(code => [code, results.filter(r => r.outcome === code).length]))
|
|
121
|
-
const reason = job.status === 'cancelled' ? 'cancelled' : job.status === 'interrupted' ? 'interrupted' : job.status === 'budget-exhausted' ? 'budget' :
|
|
122
|
-
invalid['parameters-ignored'] ? 'parameters' : invalid.timeout ? 'timeout' : invalid.unavailable ? 'request-failed' : invalid.incomplete ? 'truncated' :
|
|
123
|
-
invalid.unparseable ? 'format' : job.identity?.reason === 'coverage' || job.identity?.reason === 'sample-size' ? 'coverage' : job.mode === 'calibrate' ? 'reference-failed' : 'unfinished'
|
|
124
|
-
return { verdict: 'inconclusive', reason, invalid }
|
|
125
|
-
}
|
|
126
|
-
const jobView = job => ({ id: job.id, sessionId: job.sessionId, selection: job.selection, testSelection: job.testSelection, capabilitySelection: job.capabilitySelection,
|
|
127
|
-
mode: job.mode, status: job.status, phase: job.phase, startedAt: job.startedAt, finishedAt: job.finishedAt, progress: job.results.length,
|
|
128
|
-
plan: job.plan, reference: job.reference && fingerprintSummary(job.reference), baselineId: job.baselineId || null,
|
|
129
|
-
results: job.results.map(({ time, selection, phase, outcome, stop, usage, usageReported, valid, mismatches, ttftMs, decodeMs }) => ({ time, selection, phase, outcome, stop, usage, usageReported, valid, mismatches, ttftMs, decodeMs })),
|
|
130
|
-
usage: safeUsage(job.usage), usageReported: job.results.some(r => r.usageReported),
|
|
131
|
-
usageIncomplete: job.status === 'running' || job.results.some(r => !r.usageReported),
|
|
132
|
-
identity: job.identity || null, capability: job.capability || null, message: job.message || '', conclusion: summarizeModelTest(job), score: modelTestScore(job), usageSaved: job.usageSaved === true,
|
|
133
|
-
channel: { ttftMs: job.results.some(r => r.ttftMs !== null) ? job.results.filter(r => r.ttftMs !== null).reduce((n, r) => n + r.ttftMs, 0) / job.results.filter(r => r.ttftMs !== null).length : null,
|
|
134
|
-
tps: job.results.some(r => r.decodeMs > 0 && r.usage.outputTokens > 0) ? job.results.filter(r => r.decodeMs > 0 && r.usage.outputTokens > 0).reduce((n, r) => n + r.usage.outputTokens, 0) * 1000 / job.results.filter(r => r.decodeMs > 0 && r.usage.outputTokens > 0).reduce((n, r) => n + r.decodeMs, 0) : null } })
|
|
135
|
-
|
|
136
|
-
/** No inference happens in catalog, import, export or plan. A one-use quote
|
|
137
|
-
* freezes reference, parameters and maximum workload before the manual start. */
|
|
138
|
-
export function createModelMatchRunner(ctx, options = {}) {
|
|
139
|
-
const jobs = new Map(), quotes = new Map(), references = new Map()
|
|
140
|
-
let disposed = false, loaded = false, storageWarning = '', saveQueue = Promise.resolve()
|
|
141
|
-
async function checkpoint(job, patch = {}) {
|
|
142
|
-
if (!options.onUpdate) return
|
|
143
|
-
try { await options.onUpdate({ ...jobView({ ...job, ...patch }), usageSaved: true }); job.usageSaved = true }
|
|
144
|
-
catch { job.usageSaved = false; job.halt = true; job.message = '检测用量保存失败,已停止后续请求;请检查本地存储。' }
|
|
145
|
-
}
|
|
146
|
-
function load() {
|
|
147
|
-
if (loaded) return
|
|
148
|
-
loaded = true
|
|
149
|
-
for (const raw of publicFingerprints().references) { const ref = normalizeFingerprint(raw, 'public'); references.set(ref.id, ref) }
|
|
150
|
-
if (!options.path) return
|
|
151
|
-
try {
|
|
152
|
-
const data = JSON.parse(readFileSync(options.path, 'utf8'))
|
|
153
|
-
for (const raw of (data.references || []).slice(0, AUDIT_POLICY.maxReferences)) {
|
|
154
|
-
try { const ref = normalizeFingerprint(raw, raw.origin === 'calibrated' ? 'calibrated' : 'imported'); references.set(ref.id, ref) } catch { storageWarning = '部分本地基线无效,请重新导入或校准。' }
|
|
155
|
-
}
|
|
156
|
-
} catch (error) { if (error.code !== 'ENOENT') storageWarning = '本地基线文件无法读取,请检查文件后重新加载插件。' }
|
|
157
|
-
}
|
|
158
|
-
function save() {
|
|
159
|
-
if (!options.path) return Promise.resolve()
|
|
160
|
-
const document = JSON.stringify({ version: 2, references: [...references.values()].filter(r => r.origin !== 'public') })
|
|
161
|
-
const next = saveQueue.catch(() => {}).then(async () => {
|
|
162
|
-
await mkdir(dirname(options.path), { recursive: true })
|
|
163
|
-
const temporary = options.path + '.tmp'
|
|
164
|
-
await writeFile(temporary, document, { mode: 0o600 })
|
|
165
|
-
await rename(temporary, options.path)
|
|
166
|
-
})
|
|
167
|
-
saveQueue = next
|
|
168
|
-
return next
|
|
169
|
-
}
|
|
170
|
-
function getReference(id) { load(); const ref = references.get(id); if (!ref) throw new ModelMatchError('参考基线已不可用,请重新选择。'); return ref }
|
|
171
|
-
function ensureLocalCapacity() { if ([...references.values()].filter(r => r.origin !== 'public').length >= AUDIT_POLICY.maxReferences) throw new ModelMatchError('本地基线已达 20 份,请先移除旧基线。') }
|
|
172
|
-
async function resolveTestSelection(selection, effort) {
|
|
173
|
-
const llm = ctx.get('llm')
|
|
174
|
-
if (typeof llm?.stream !== 'function' || typeof llm?.resolveModelInfo !== 'function') throw new ModelMatchError('当前宿主不支持模型参数校验,请更新 DSH。')
|
|
175
|
-
let info
|
|
176
|
-
try { info = await llm.resolveModelInfo(selection.provider, selection.model, AbortSignal.timeout(10_000)) }
|
|
177
|
-
catch { throw new ModelMatchError('模型信息无法读取,请检查供应商配置后重试。') }
|
|
178
|
-
const efforts = info.reasoning?.efforts?.map(e => e.id)
|
|
179
|
-
if (effort === 'off') {
|
|
180
|
-
if (efforts && !efforts.includes('off')) throw new ModelMatchError('该模型无法关闭推理,不能使用关闭推理的公开基线。请建立相同推理强度的本地参考。')
|
|
181
|
-
return { provider: selection.provider, model: selection.model, ...(efforts ? { reasoningEffort: 'off' } : {}) }
|
|
182
|
-
}
|
|
183
|
-
const actual = effort || info.reasoning?.defaultEffort
|
|
184
|
-
if (actual && !efforts?.includes(actual)) throw new ModelMatchError('参考与目标的推理强度不兼容,请重新校准。')
|
|
185
|
-
return { provider: selection.provider, model: selection.model, ...(actual ? { reasoningEffort: actual } : {}) }
|
|
186
|
-
}
|
|
187
|
-
async function request(job, task, phase, config) {
|
|
188
|
-
const timeout = new AbortController(), timer = setTimeout(() => timeout.abort(), options.timeoutMs || PROBE_PLAN.timeoutMs)
|
|
189
|
-
timer.unref?.()
|
|
190
|
-
const signal = AbortSignal.any([job.controller.signal, timeout.signal])
|
|
191
|
-
const item = { time: 0, selection: modelSelection(config.selection), phase, outcome: 'unavailable', stop: 'unknown', usage: safeUsage(null), usageReported: false, valid: 0, mismatches: 0, ttftMs: null, decodeMs: null }
|
|
192
|
-
let iterator, text = '', firstToken = 0, finish, values = task.probes?.map(() => null)
|
|
193
|
-
const started = Date.now()
|
|
194
|
-
try {
|
|
195
|
-
iterator = ctx.get('llm').stream({ ...config.selection, temperature: config.temperature, maxTokens: task.probes ? task.probes.length * 80 : PROBE_PLAN.capabilityMaxTokens,
|
|
196
|
-
messages: [{ role: 'system', content: [{ type: 'text', text: FINGERPRINT_SYSTEM }] }, { role: 'user', content: [{ type: 'text', text: task.prompt }] }], signal })[Symbol.asyncIterator]()
|
|
197
|
-
for (;;) {
|
|
198
|
-
const next = await abortable(iterator.next(), signal)
|
|
199
|
-
if (signal.aborted) throw new Error('aborted')
|
|
200
|
-
if (next.done) break
|
|
201
|
-
const chunk = next.value
|
|
202
|
-
if (chunk?.type === 'text-delta') {
|
|
203
|
-
if (chunk.text && !firstToken) firstToken = Date.now()
|
|
204
|
-
text += String(chunk.text || '')
|
|
205
|
-
if (text.length > 32_768) { item.stop = 'max-tokens'; break }
|
|
206
|
-
}
|
|
207
|
-
if (chunk?.type === 'usage') { item.usage = safeUsage(chunk.usage); item.usageReported = usageKeys.some(k => Number.isFinite(chunk.usage?.[k])) }
|
|
208
|
-
if (chunk?.type === 'finish') { finish = chunk; break }
|
|
209
|
-
}
|
|
210
|
-
if (finish) item.stop = record({ ...job.selection, stop: finish.reason?.kind })?.stop || 'unknown'
|
|
211
|
-
if (item.stop === 'stop') {
|
|
212
|
-
// Public KBF references suppress reasoning. A relay reporting reasoning
|
|
213
|
-
// despite off invalidates comparability, rather than receiving a pass.
|
|
214
|
-
if ((config.noReasoning || config.selection.reasoningEffort === 'off') && item.usage.reasoningTokens > 0) item.outcome = 'parameters-ignored'
|
|
215
|
-
else if (task.probes) {
|
|
216
|
-
values = parseFingerprintAnswers(text, task.probes)
|
|
217
|
-
item.valid = values.filter(Number.isFinite).length
|
|
218
|
-
item.mismatches = task.probes.filter((p, i) => Number.isFinite(p.value) && Number.isFinite(values[i]) && !fingerprintMatch(p.value, values[i], p.domain)).length
|
|
219
|
-
item.outcome = item.valid ? 'scored' : 'unparseable'
|
|
220
|
-
} else {
|
|
221
|
-
try { item.outcome = task.check(text) ? 'pass' : 'fail' } catch { item.outcome = 'unparseable' }
|
|
222
|
-
item.valid = ['pass', 'fail'].includes(item.outcome) ? 1 : 0
|
|
223
|
-
}
|
|
224
|
-
if (firstToken) { item.ttftMs = firstToken - started; item.decodeMs = Date.now() - firstToken }
|
|
225
|
-
} else item.outcome = item.stop === 'max-tokens' ? 'incomplete' : 'unavailable'
|
|
226
|
-
} catch {
|
|
227
|
-
item.outcome = job.controller.signal.aborted ? 'cancelled' : timeout.signal.aborted ? 'timeout' : 'unavailable'
|
|
228
|
-
item.stop = signal.aborted ? 'aborted' : 'error'
|
|
229
|
-
} finally {
|
|
230
|
-
clearTimeout(timer); timeout.abort()
|
|
231
|
-
try { Promise.resolve(iterator?.return?.()).catch(() => {}) } catch { /* broken adapters cannot block cancellation */ }
|
|
232
|
-
text = ''
|
|
233
|
-
}
|
|
234
|
-
job.results.push(item)
|
|
235
|
-
item.time = Date.now()
|
|
236
|
-
for (const key of usageKeys) job.usage[key] += item.usage[key]
|
|
237
|
-
if (['unavailable', 'timeout', 'parameters-ignored'].includes(item.outcome)) { job.halt = true; job.message = item.outcome === 'parameters-ignored' ? '通道未遵守关闭推理的测试参数,请重新校准。' : '请求未完成,已停止后续请求;检查通道后可重新测试。' }
|
|
238
|
-
if (sumTokens(job.usage) >= job.plan.budgetTokens) { job.halt = true; job.budgetExhausted = true; job.message = '已达到已上报 Token 预算,后续请求已停止。' }
|
|
239
|
-
await checkpoint(job)
|
|
240
|
-
return { values: task.probes ? values : undefined, outcome: item.outcome, score: item.outcome === 'pass' ? 1 : item.outcome === 'fail' ? 0 : null }
|
|
241
|
-
}
|
|
242
|
-
const stopped = job => disposed || job.halt || job.controller.signal.aborted
|
|
243
|
-
async function runFingerprints(job, probes, phase, temperature) {
|
|
244
|
-
const values = []
|
|
245
|
-
job.phase = phase
|
|
246
|
-
for (const batch of batches(probes)) {
|
|
247
|
-
if (stopped(job)) break
|
|
248
|
-
const response = await request(job, { probes: batch, prompt: fingerprintPrompt(batch) }, phase, { selection: job.testSelection, temperature, noReasoning: job.reference?.config.reasoningEffort === 'off' || (job.mode === 'calibrate' && !job.testSelection.reasoningEffort) })
|
|
249
|
-
values.push(...(response.values || batch.map(() => null)))
|
|
250
|
-
}
|
|
251
|
-
while (values.length < probes.length) values.push(null)
|
|
252
|
-
return values
|
|
253
|
-
}
|
|
254
|
-
async function runCapabilities(job, seed, selection) {
|
|
255
|
-
job.phase = 'capabilities'
|
|
256
|
-
const outcomes = []
|
|
257
|
-
for (const task of capabilityTasks(seed)) { if (stopped(job)) break; outcomes.push((await request(job, task, 'capabilities', { selection, temperature: 0 })).score) }
|
|
258
|
-
while (outcomes.length < 16) outcomes.push(null)
|
|
259
|
-
return outcomes
|
|
260
|
-
}
|
|
261
|
-
async function run(job, quote) {
|
|
262
|
-
await checkpoint(job)
|
|
263
|
-
let referenceToSave
|
|
264
|
-
if (job.mode === 'calibrate') {
|
|
265
|
-
const passes = []
|
|
266
|
-
for (const [i, temp] of [0, 0.7, 0.7].entries()) { if (stopped(job)) break; passes.push(await runFingerprints(job, quote.pool, 'consensus-' + (i + 1), temp)) }
|
|
267
|
-
if (!stopped(job) && passes.length === 3) {
|
|
268
|
-
const stable = quote.pool.flatMap((p, i) => passes.every(v => Number.isFinite(v[i]) && fingerprintMatch(passes[0][i], v[i], p.domain)) ? [{ ...p, value: passes[0][i] }] : [])
|
|
269
|
-
if (stable.length < AUDIT_POLICY.minSamples) { job.halt = true; job.message = '稳定探针少于 64 个,未保存基线;可提高校准题量后重试。' }
|
|
270
|
-
else {
|
|
271
|
-
const holdout = await runFingerprints(job, stable, 'calibration', 0)
|
|
272
|
-
const probes = stable.map((p, i) => ({ ...p, calibration: Number.isFinite(holdout[i]) ? (fingerprintMatch(p.value, holdout[i], p.domain) ? 1 : 0) : null }))
|
|
273
|
-
referenceToSave = normalizeFingerprint({ model: quote.claimedModel, provider: job.selection.provider, date: new Date().toISOString(), selection: job.testSelection,
|
|
274
|
-
config: { system: FINGERPRINT_SYSTEM, temperature: 0, reasoningEffort: job.testSelection.reasoningEffort || 'off' }, probes }, 'calibrated')
|
|
275
|
-
const calibrationCheck = scoreFingerprint(referenceToSave, probes.map(p => p.calibration))
|
|
276
|
-
if (calibrationCheck.verdict === 'insufficient-evidence' || calibrationCheck.p0 > AUDIT_POLICY.maxReferenceNoise) { job.halt = true; job.message = '参考自测覆盖不足或波动过大,未保存基线。' }
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
} else if (job.mode === 'audit') {
|
|
280
|
-
const values = await runFingerprints(job, job.reference.probes, 'fingerprint', 0)
|
|
281
|
-
const outcomes = job.reference.probes.map((p, i) => Number.isFinite(values[i]) ? (fingerprintMatch(p.value, values[i], p.domain) ? 1 : 0) : null)
|
|
282
|
-
job.identity = scoreFingerprint(job.reference, outcomes, !stopped(job))
|
|
283
|
-
}
|
|
284
|
-
if (!stopped(job) && (job.mode === 'capability' || quote.includeCapabilities)) {
|
|
285
|
-
const seed = quote.capabilitySeed, outcomes = await runCapabilities(job, seed, quote.capabilitySelection)
|
|
286
|
-
const reference = job.reference?.capabilities
|
|
287
|
-
job.capability = scoreCapabilities(outcomes, reference?.outcomes, !stopped(job))
|
|
288
|
-
if (referenceToSave && outcomes.filter(v => v === 0 || v === 1).length >= 12) referenceToSave.capabilities = { version: 1, seed, effort: quote.capabilitySelection.reasoningEffort || '', outcomes }
|
|
289
|
-
}
|
|
290
|
-
let status = job.controller.signal.aborted || disposed ? 'cancelled' : job.budgetExhausted ? 'budget-exhausted' : job.halt ? 'failed' : 'complete'
|
|
291
|
-
if (status === 'complete' && referenceToSave) {
|
|
292
|
-
ensureLocalCapacity()
|
|
293
|
-
const normalized = normalizeFingerprint(referenceToSave, 'calibrated')
|
|
294
|
-
references.set(normalized.id, normalized)
|
|
295
|
-
try {
|
|
296
|
-
await save()
|
|
297
|
-
if (disposed || job.controller.signal.aborted) { references.delete(normalized.id); await save(); status = 'cancelled' }
|
|
298
|
-
else { job.reference = normalized; job.baselineId = normalized.id; job.identity = scoreFingerprint(normalized, normalized.probes.map(p => p.calibration)); job.message = '参考基线已保存,可用于同模型的后续检测。' }
|
|
299
|
-
}
|
|
300
|
-
catch { references.delete(normalized.id); status = 'failed'; job.message = '基线保存失败,请检查本地存储后重新校准。' }
|
|
301
|
-
}
|
|
302
|
-
if (status !== 'complete') {
|
|
303
|
-
if (job.identity) job.identity = { ...job.identity, verdict: 'insufficient-evidence', reason: 'unfinished', pValue: null }
|
|
304
|
-
if (job.capability) job.capability = { ...job.capability, verdict: 'insufficient-evidence', pValue: null }
|
|
305
|
-
}
|
|
306
|
-
job.finishedAt = Date.now()
|
|
307
|
-
await checkpoint(job, { status })
|
|
308
|
-
if ((disposed || job.controller.signal.aborted) && status !== 'cancelled') {
|
|
309
|
-
status = 'cancelled'
|
|
310
|
-
if (job.baselineId) { references.delete(job.baselineId); job.baselineId = null; await save() }
|
|
311
|
-
if (job.identity) job.identity = { ...job.identity, verdict: 'insufficient-evidence', reason: 'unfinished', pValue: null }
|
|
312
|
-
if (job.capability) job.capability = { ...job.capability, verdict: 'insufficient-evidence', pValue: null }
|
|
313
|
-
await checkpoint(job, { status })
|
|
314
|
-
}
|
|
315
|
-
if (options.onUpdate && job.usageSaved === false && status === 'complete') {
|
|
316
|
-
status = 'failed'
|
|
317
|
-
if (job.identity) job.identity = { ...job.identity, verdict: 'insufficient-evidence', reason: 'unfinished', pValue: null }
|
|
318
|
-
if (job.capability) job.capability = { ...job.capability, verdict: 'insufficient-evidence', pValue: null }
|
|
319
|
-
}
|
|
320
|
-
job.status = status
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
catalog() { load(); return { references: [...references.values()].map(fingerprintSummary), policy: AUDIT_POLICY, storageWarning } },
|
|
324
|
-
exportReference(id) { return getReference(id) },
|
|
325
|
-
async importReference(raw) {
|
|
326
|
-
load()
|
|
327
|
-
let ref
|
|
328
|
-
try { ref = normalizeFingerprint(raw) } catch (error) { throw new ModelMatchError(error.message) }
|
|
329
|
-
if (!references.has(ref.id)) ensureLocalCapacity()
|
|
330
|
-
const previous = references.get(ref.id); references.set(ref.id, ref)
|
|
331
|
-
try { await save() } catch { if (previous) references.set(ref.id, previous); else references.delete(ref.id); throw new ModelMatchError('基线保存失败,请检查本地存储。') }
|
|
332
|
-
return fingerprintSummary(ref)
|
|
333
|
-
},
|
|
334
|
-
async removeReference(id) {
|
|
335
|
-
const ref = getReference(id)
|
|
336
|
-
if (ref.origin === 'public') throw new ModelMatchError('公开参考随插件提供,无法移除。')
|
|
337
|
-
if ([...jobs.values()].some(j => j.status === 'running' && j.reference?.id === id)) throw new ModelMatchError('该基线正在使用,请先取消测试。')
|
|
338
|
-
references.delete(id)
|
|
339
|
-
try { await save() } catch { references.set(id, ref); throw new ModelMatchError('基线移除失败,请重试。') }
|
|
340
|
-
},
|
|
341
|
-
async plan(body) {
|
|
342
|
-
if (disposed) throw new ModelMatchError('匹配服务已停止。')
|
|
343
|
-
load()
|
|
344
|
-
const mode = ['audit', 'calibrate', 'capability'].includes(body?.mode) ? body.mode : 'audit'
|
|
345
|
-
const selection = modelSelection(body?.selection), sessionId = safeText(body?.sessionId)
|
|
346
|
-
if (!selection || !sessionId) throw new ModelMatchError('请先选择当前会话与模型。')
|
|
347
|
-
let reference, pool, count = 0, includeCapabilities = mode === 'calibrate' || body.includeCapabilities === true
|
|
348
|
-
let testSelection, capabilitySelection, capabilitySeed = randomInt(0x100000000), claimedModel = selection.model
|
|
349
|
-
if (mode === 'calibrate') {
|
|
350
|
-
ensureLocalCapacity()
|
|
351
|
-
if (body.ackReference !== true) throw new ModelMatchError('请确认所选通道可作为可信参考。')
|
|
352
|
-
claimedModel = safeText(body.claimedModel)
|
|
353
|
-
if (!claimedModel) throw new ModelMatchError('请选择该参考对应的目标模型。')
|
|
354
|
-
count = body.probeCount === 256 ? 256 : 128; pool = calibrationPool(count)
|
|
355
|
-
testSelection = await resolveTestSelection(selection, selection.reasoningEffort || 'off')
|
|
356
|
-
} else if (mode === 'audit' || body.referenceId) {
|
|
357
|
-
reference = getReference(body.referenceId)
|
|
358
|
-
if (fingerprintModelKey(reference.model) !== fingerprintModelKey(selection.model)) throw new ModelMatchError('参考基线与当前模型不同,请选择对应基线或建立本地参考。')
|
|
359
|
-
if (mode === 'audit') {
|
|
360
|
-
const selfCheck = scoreFingerprint(reference, reference.probes.map(() => 1))
|
|
361
|
-
if (selfCheck.verdict === 'insufficient-evidence' || selfCheck.p0 > AUDIT_POLICY.maxReferenceNoise) throw new ModelMatchError('参考自测覆盖不足或波动过大,请重新校准后检测。')
|
|
362
|
-
}
|
|
363
|
-
count = body.probeCount === 128 && !reference.calibration ? Math.min(128, reference.probes.length) : reference.probes.length
|
|
364
|
-
testSelection = await resolveTestSelection(selection, mode === 'capability' ? selection.reasoningEffort || '' : reference.config.reasoningEffort)
|
|
365
|
-
if (reference.capabilities) capabilitySeed = reference.capabilities.seed
|
|
366
|
-
}
|
|
367
|
-
testSelection ||= await resolveTestSelection(selection, selection.reasoningEffort || (mode === 'capability' ? '' : 'off'))
|
|
368
|
-
if (mode === 'capability' || includeCapabilities) {
|
|
369
|
-
capabilitySelection = await resolveTestSelection(selection, selection.reasoningEffort || '')
|
|
370
|
-
if (reference?.capabilities && reference.capabilities.effort !== (capabilitySelection.reasoningEffort || '')) throw new ModelMatchError('能力基线的推理强度与当前模型不同,请选择相同档位或重新校准。')
|
|
371
|
-
}
|
|
372
|
-
const requests = (mode === 'calibrate' ? 4 : mode === 'audit' ? 1 : 0) * Math.ceil(count / 10) + (mode === 'capability' || includeCapabilities ? 16 : 0)
|
|
373
|
-
const maxOutputTokens = (mode === 'calibrate' ? 4 : mode === 'audit' ? 1 : 0) * count * 80 + (mode === 'capability' || includeCapabilities ? 16 * 1024 : 0)
|
|
374
|
-
const fingerprintChars = mode === 'capability' ? 0 : batches(pool || reference.probes.slice(0, count)).reduce((n, batch) => n + fingerprintPrompt(batch).length + FINGERPRINT_SYSTEM.length, 0) * (mode === 'calibrate' ? 4 : 1)
|
|
375
|
-
const capabilityChars = mode === 'capability' || includeCapabilities ? capabilityTasks(capabilitySeed).reduce((n, task) => n + task.prompt.length + FINGERPRINT_SYSTEM.length, 0) : 0
|
|
376
|
-
if (body.budgetTokens !== undefined && (!Number.isInteger(body.budgetTokens) || body.budgetTokens < 1024 || body.budgetTokens > 500_000)) throw new ModelMatchError('Token 预算应为 1,024–500,000 的整数。')
|
|
377
|
-
const budgetTokens = body.budgetTokens ?? 120_000
|
|
378
|
-
const plan = { requests, probes: count, maxOutputTokens, estimatedInputTokens: Math.ceil((fingerprintChars + capabilityChars) / 3), budgetTokens,
|
|
379
|
-
batchSize: 10, temperature: 0, timeoutMs: PROBE_PLAN.timeoutMs, capabilities: mode === 'capability' || includeCapabilities, capabilityTasks: mode === 'capability' || includeCapabilities ? 16 : 0 }
|
|
380
|
-
while (quotes.size >= 10) quotes.delete(quotes.keys().next().value)
|
|
381
|
-
const id = randomUUID()
|
|
382
|
-
quotes.set(id, { id, mode, selection, sessionId, reference, pool, count, includeCapabilities, testSelection, capabilitySelection, capabilitySeed, claimedModel, plan, createdAt: Date.now() })
|
|
383
|
-
return { id, mode, selection, testSelection, capabilitySelection, reference: reference && fingerprintSummary(reference), claimedModel, plan }
|
|
384
|
-
},
|
|
385
|
-
start(body) {
|
|
386
|
-
if (disposed) throw new ModelMatchError('匹配服务已停止。')
|
|
387
|
-
if (body?.ackCost !== true) throw new ModelMatchError('请确认测试会消耗 Token 后再开始。')
|
|
388
|
-
const quote = quotes.get(body.planId)
|
|
389
|
-
if (!quote || Date.now() - quote.createdAt > 5 * 60_000) throw new ModelMatchError('测试预算已过期,请刷新预算后开始。')
|
|
390
|
-
if (body.sessionId !== quote.sessionId) throw new ModelMatchError('当前会话已变化,请重新生成预算。')
|
|
391
|
-
if ([...jobs.values()].some(j => j.status === 'running')) throw new ModelMatchError('已有测试正在运行,请先等待或取消。')
|
|
392
|
-
if (quote.reference && references.get(quote.reference.id) !== quote.reference) throw new ModelMatchError('参考基线已变化,请刷新预算。')
|
|
393
|
-
quotes.delete(quote.id)
|
|
394
|
-
while (jobs.size >= 10) jobs.delete(jobs.keys().next().value)
|
|
395
|
-
const job = { id: randomUUID(), sessionId: quote.sessionId, selection: quote.selection, testSelection: quote.testSelection, mode: quote.mode,
|
|
396
|
-
capabilitySelection: quote.capabilitySelection,
|
|
397
|
-
reference: quote.reference && subsetFingerprint(quote.reference, quote.count), plan: quote.plan, status: 'running', phase: 'queued',
|
|
398
|
-
startedAt: Date.now(), finishedAt: 0, controller: new AbortController(), results: [], usage: safeUsage(null) }
|
|
399
|
-
jobs.set(job.id, job)
|
|
400
|
-
queueMicrotask(() => { void run(job, quote).catch(async () => { job.status = 'failed'; job.message = '测试未完成,请检查通道或重新生成预算。'; job.finishedAt = Date.now(); await checkpoint(job) }) })
|
|
401
|
-
return jobView(job)
|
|
402
|
-
},
|
|
403
|
-
status(id, sessionId) { const job = id ? jobs.get(id) : [...jobs.values()].reverse().find(j => !sessionId || j.sessionId === sessionId); if (id && !job) throw new ModelMatchError('测试记录已过期。'); return job ? jobView(job) : null },
|
|
404
|
-
cancel(id) { const job = jobs.get(id); if (!job) throw new ModelMatchError('测试记录已过期。'); if (job.status === 'running') job.controller.abort(); return jobView(job) },
|
|
405
|
-
dispose() { disposed = true; quotes.clear(); for (const job of jobs.values()) job.controller.abort() },
|
|
406
|
-
}
|
|
407
|
-
}
|
package/screenshots/match.png
DELETED
|
Binary file
|
|
Binary file
|