dsh-vision-router 2.2.8 → 3.0.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.md +16 -7
- package/README.zh.md +16 -7
- package/docs/architecture/2.3.0-compat-retirement.md +147 -0
- package/docs/architecture/2.3.0-execution-plan.md +410 -0
- package/docs/architecture/2.3.0-final-audit.md +279 -0
- package/docs/architecture/2.3.0-reopened-plan.md +720 -0
- package/docs/architecture/3.0-architecture-plan.md +252 -0
- package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
- package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
- package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
- package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
- package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
- package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
- package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
- package/docs/architecture/3.0-r7-public-api.md +151 -0
- package/docs/architecture/3.0-r8-ci-release.md +248 -0
- package/docs/architecture/3.0-r9-final-audit.md +370 -0
- package/docs/architecture/compat-inventory.md +26 -34
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +30 -22
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +9 -57
- package/index.js +268 -761
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/catalog-corrections.js +1 -2
- package/lib/client-maintenance-actions.js +145 -0
- package/lib/client-presentation-boundary-main.js +75 -17
- package/lib/client-presentation-boundary.js +53 -28
- package/lib/client.js +246 -144
- package/lib/core-apply-lifecycle.js +41 -0
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +91 -278
- package/lib/core-vision-surface-domain.js +63 -0
- package/lib/core-vision-surface.js +11 -77
- package/lib/delegated-call-config.js +15 -24
- package/lib/desktop-screenshot-tool.js +262 -0
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +37 -256
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/dsh-support-window.js +3 -3
- package/lib/file-logger.js +8 -3
- package/lib/grounding-coordinate-runtime.js +1 -2
- package/lib/http-body-limit.js +97 -79
- package/lib/http-compat.js +1 -2
- package/lib/image-offload-compat.js +30 -21
- package/lib/image-resource-governor.js +210 -228
- package/lib/legacy-global-proxy-boundary.js +46 -3
- package/lib/live-model-client-prelude.js +12 -4
- package/lib/live-model-discovery.js +134 -43
- package/lib/local-remote-settings-permission.js +33 -7
- package/lib/local-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +134 -0
- package/lib/native-image-coexistence.js +15 -3
- package/lib/ollama-cold-start.js +87 -23
- package/lib/pi-ai-bridge-wire-compat.js +3 -2
- package/lib/proxy-dispatcher-pool.js +151 -92
- package/lib/proxy-routing.js +75 -51
- package/lib/proxy-url-compat.js +3 -10
- package/lib/public-config.js +36 -0
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +19 -11
- package/lib/remote-settings-bridge.js +11 -0
- package/lib/remote-settings-risk-client-boundary.js +155 -0
- package/lib/remote-settings-risk-confirmation.js +17 -105
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +127 -34
- package/lib/runtime-i18n-boundary.js +10 -11
- package/lib/screenshot-source-boundary.js +2 -2
- package/lib/session-affinity-runtime.js +113 -97
- package/lib/session-affinity.js +89 -76
- package/lib/session-event-history-compat.js +30 -0
- package/lib/session-message-source-compat.js +7 -3
- package/lib/session-query-readers.js +263 -0
- package/lib/session-surface-compat.js +30 -31
- package/lib/session-surface-policy-domain.js +85 -0
- package/lib/session-surface-policy.js +2 -111
- package/lib/session-turn-resolver.js +2 -6
- package/lib/session-vision-attachment-index.js +143 -0
- package/lib/session-vision-delegation-boundary.js +34 -0
- package/lib/session-vision-delegation.js +22 -0
- package/lib/session-vision-index.js +179 -116
- package/lib/session-vision-mode-authority.js +229 -192
- package/lib/session-vision-mode-boundary.js +65 -22
- package/lib/session-vision-policy-domain.js +304 -0
- package/lib/session-vision-policy.js +104 -0
- package/lib/session-vision-state.js +368 -378
- package/lib/settings-client-017-compat.js +9 -7
- package/lib/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +25 -6
- package/lib/tesseract-exec-compat.js +9 -1
- package/lib/turn-budget-context.js +47 -41
- package/lib/update-check.js +11 -2
- package/lib/vision-attachment-handle-runtime.js +176 -156
- package/lib/vision-backend-runtime-policy.js +19 -2
- package/lib/vision-background-benchmark.js +41 -4
- package/lib/vision-background-failure-policy.js +41 -41
- package/lib/vision-background-stop-store-core.js +217 -0
- package/lib/vision-background-stop-store.js +8 -179
- package/lib/vision-capability-benchmark-service.js +1 -0
- package/lib/vision-capability-benchmark.js +11 -61
- package/lib/vision-capability-evidence.js +35 -0
- package/lib/vision-capability-fingerprint.js +72 -0
- package/lib/vision-capability-identity.js +116 -61
- package/lib/vision-capability-router.js +13 -33
- package/lib/vision-chunk-assembler.js +95 -0
- package/lib/vision-diagnostics-routes.js +246 -0
- package/lib/vision-evidence-guidance.js +3 -1
- package/lib/vision-execution-order-apply.js +45 -55
- package/lib/vision-execution-order.js +19 -19
- package/lib/vision-execution-policy.js +12 -7
- package/lib/vision-model-registry.js +30 -17
- package/lib/vision-provider-transport.js +137 -176
- package/lib/vision-routing-authority.js +47 -48
- package/lib/vision-routing-product.js +26 -29
- package/lib/vision-routing-settings-prelude.js +8 -3
- package/lib/vision-runtime-performance-store.js +166 -0
- package/lib/vision-runtime-performance.js +13 -142
- package/lib/vision-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +104 -31
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +6 -9
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +56 -20
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -397
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { BENCHMARK_AXES, } from './vision-capability-evidence.js';
|
|
6
|
+
import { CAPABILITY_BENCHMARK_SUITE_REVISION } from './vision-capability-fingerprint.js';
|
|
7
|
+
const CACHE_VERSION = 3;
|
|
8
|
+
const MAX_ENTRIES = 256;
|
|
9
|
+
function propertyBag(value) {
|
|
10
|
+
return value !== null && typeof value === 'object'
|
|
11
|
+
? value
|
|
12
|
+
: undefined;
|
|
13
|
+
}
|
|
14
|
+
function cleanText(value, max = 256) {
|
|
15
|
+
return typeof value === 'string' && value.trim() !== ''
|
|
16
|
+
? value.trim().slice(0, max)
|
|
17
|
+
: '';
|
|
18
|
+
}
|
|
19
|
+
function benchmarkAxis(value) {
|
|
20
|
+
return BENCHMARK_AXES.includes(value)
|
|
21
|
+
? value
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
24
|
+
function persistentFailureClass(value) {
|
|
25
|
+
return value === 'unavailable' || value === 'protocol'
|
|
26
|
+
? value
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
function cleanStop(value) {
|
|
30
|
+
const record = propertyBag(value);
|
|
31
|
+
if (record === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (Number(record.suiteRevision) !== CAPABILITY_BENCHMARK_SUITE_REVISION) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const fingerprint = cleanText(record.fingerprint, 64);
|
|
37
|
+
if (!/^ep2_[0-9a-f]{32}$/.test(fingerprint))
|
|
38
|
+
return undefined;
|
|
39
|
+
const key = cleanText(record.key);
|
|
40
|
+
const provider = cleanText(record.provider);
|
|
41
|
+
const model = cleanText(record.model);
|
|
42
|
+
const axis = benchmarkAxis(cleanText(record.axis, 32));
|
|
43
|
+
const errorClass = persistentFailureClass(cleanText(record.errorClass, 48));
|
|
44
|
+
const errorCode = cleanText(record.errorCode, 96);
|
|
45
|
+
const recordedAt = Number(record.recordedAt);
|
|
46
|
+
const expiresAt = Number(record.expiresAt);
|
|
47
|
+
if (!key || !provider || !model || axis === undefined || errorClass === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(recordedAt) || recordedAt <= 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= recordedAt)
|
|
53
|
+
return undefined;
|
|
54
|
+
return {
|
|
55
|
+
fingerprint,
|
|
56
|
+
key,
|
|
57
|
+
provider,
|
|
58
|
+
model,
|
|
59
|
+
axis,
|
|
60
|
+
errorClass,
|
|
61
|
+
...(errorCode ? { errorCode } : {}),
|
|
62
|
+
recordedAt,
|
|
63
|
+
expiresAt,
|
|
64
|
+
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function recordKey(value) {
|
|
68
|
+
return `${value.fingerprint}\u0000${value.axis}`;
|
|
69
|
+
}
|
|
70
|
+
function selectRetainedStops(values, at, maxEntries = MAX_ENTRIES) {
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
return [...values]
|
|
73
|
+
.map(cleanStop)
|
|
74
|
+
.filter((item) => item !== undefined && item.expiresAt > at)
|
|
75
|
+
.sort((a, b) => b.recordedAt - a.recordedAt
|
|
76
|
+
|| recordKey(a).localeCompare(recordKey(b)))
|
|
77
|
+
.filter((item) => {
|
|
78
|
+
const key = recordKey(item);
|
|
79
|
+
if (seen.has(key))
|
|
80
|
+
return false;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
return true;
|
|
83
|
+
})
|
|
84
|
+
.slice(0, maxEntries);
|
|
85
|
+
}
|
|
86
|
+
function retainedStopMap(values, at, maxEntries = MAX_ENTRIES) {
|
|
87
|
+
return new Map(selectRetainedStops(values, at, maxEntries)
|
|
88
|
+
.map((item) => [recordKey(item), item]));
|
|
89
|
+
}
|
|
90
|
+
async function save(file, records, fsOps, at) {
|
|
91
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
92
|
+
const stops = selectRetainedStops(records.values(), at);
|
|
93
|
+
await fsOps.mkdir(path.dirname(file), { recursive: true });
|
|
94
|
+
await fsOps.writeFile(temporary, JSON.stringify({ version: CACHE_VERSION, stops }), { encoding: 'utf8', mode: 0o600 });
|
|
95
|
+
await fsOps.rename(temporary, file);
|
|
96
|
+
}
|
|
97
|
+
async function load(file, fsOps, at) {
|
|
98
|
+
try {
|
|
99
|
+
const raw = await fsOps.readFile(file, 'utf8');
|
|
100
|
+
const body = propertyBag(JSON.parse(raw));
|
|
101
|
+
const stops = body?.stops;
|
|
102
|
+
const version = Number(body?.version);
|
|
103
|
+
if (!Array.isArray(stops)
|
|
104
|
+
|| (version !== 2 && version !== CACHE_VERSION)) {
|
|
105
|
+
return new Map();
|
|
106
|
+
}
|
|
107
|
+
const records = retainedStopMap(stops, at);
|
|
108
|
+
if (version === 2)
|
|
109
|
+
await save(file, records, fsOps, at);
|
|
110
|
+
return records;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (propertyBag(error)?.code === 'ENOENT')
|
|
114
|
+
return new Map();
|
|
115
|
+
return new Map();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Durable stop owner for unattended capability measurement.
|
|
120
|
+
*
|
|
121
|
+
* Only endpoint/model evidence that is benchmark-axis-scoped and safe to
|
|
122
|
+
* persist (protocol or unavailable) can enter this store. AUTH remains a
|
|
123
|
+
* process-local access condition and is rejected by runtime validation even
|
|
124
|
+
* when legacy v2 cache data contains it.
|
|
125
|
+
*/
|
|
126
|
+
export function createBackgroundBenchmarkStopStoreCore(options) {
|
|
127
|
+
const file = options.file;
|
|
128
|
+
const fsOps = {
|
|
129
|
+
readFile: options.fsOps?.readFile ?? readFile,
|
|
130
|
+
mkdir: options.fsOps?.mkdir ?? mkdir,
|
|
131
|
+
writeFile: options.fsOps?.writeFile ?? writeFile,
|
|
132
|
+
rename: options.fsOps?.rename ?? rename,
|
|
133
|
+
};
|
|
134
|
+
const logger = options.logger;
|
|
135
|
+
const now = typeof options.now === 'function' ? options.now : Date.now;
|
|
136
|
+
let records = new Map();
|
|
137
|
+
let saveTail = Promise.resolve();
|
|
138
|
+
const ready = load(file, fsOps, Number(now()))
|
|
139
|
+
.then((loaded) => {
|
|
140
|
+
records = loaded;
|
|
141
|
+
});
|
|
142
|
+
const persist = () => {
|
|
143
|
+
const at = Number(now());
|
|
144
|
+
saveTail = saveTail
|
|
145
|
+
.then(() => save(file, records, fsOps, at))
|
|
146
|
+
.catch((error) => {
|
|
147
|
+
logger?.warn?.('vision-router: background stop cache write failed: %s', cleanText(propertyBag(error)?.message ?? error, 240));
|
|
148
|
+
});
|
|
149
|
+
return saveTail;
|
|
150
|
+
};
|
|
151
|
+
const reconcile = () => {
|
|
152
|
+
const at = Number(now());
|
|
153
|
+
const retained = retainedStopMap(records.values(), at);
|
|
154
|
+
const changed = retained.size !== records.size
|
|
155
|
+
|| [...retained.keys()].some((key) => !records.has(key));
|
|
156
|
+
records = retained;
|
|
157
|
+
return changed;
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
file,
|
|
161
|
+
async list() {
|
|
162
|
+
await ready;
|
|
163
|
+
if (reconcile())
|
|
164
|
+
await persist();
|
|
165
|
+
return [...records.values()]
|
|
166
|
+
.sort((a, b) => b.recordedAt - a.recordedAt);
|
|
167
|
+
},
|
|
168
|
+
async mark({ fingerprint, key, provider, model, axis, errorClass, errorCode, recordedAt = Number(now()), expiresAt, } = {}) {
|
|
169
|
+
await ready;
|
|
170
|
+
reconcile();
|
|
171
|
+
const clean = cleanStop({
|
|
172
|
+
fingerprint,
|
|
173
|
+
key,
|
|
174
|
+
provider,
|
|
175
|
+
model,
|
|
176
|
+
axis,
|
|
177
|
+
errorClass,
|
|
178
|
+
errorCode,
|
|
179
|
+
recordedAt,
|
|
180
|
+
expiresAt,
|
|
181
|
+
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
182
|
+
});
|
|
183
|
+
if (clean === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
const keyOfStop = recordKey(clean);
|
|
186
|
+
records.set(keyOfStop, clean);
|
|
187
|
+
reconcile();
|
|
188
|
+
await persist();
|
|
189
|
+
return records.get(keyOfStop);
|
|
190
|
+
},
|
|
191
|
+
async clearStop(fingerprint, axis) {
|
|
192
|
+
await ready;
|
|
193
|
+
const removed = records.delete(`${String(fingerprint ?? '')}\u0000${String(axis ?? '')}`);
|
|
194
|
+
if (removed)
|
|
195
|
+
await persist();
|
|
196
|
+
return removed;
|
|
197
|
+
},
|
|
198
|
+
async clearFingerprint(fingerprint) {
|
|
199
|
+
await ready;
|
|
200
|
+
const wanted = String(fingerprint ?? '');
|
|
201
|
+
let removed = false;
|
|
202
|
+
for (const [key, record] of records.entries()) {
|
|
203
|
+
if (record.fingerprint !== wanted)
|
|
204
|
+
continue;
|
|
205
|
+
records.delete(key);
|
|
206
|
+
removed = true;
|
|
207
|
+
}
|
|
208
|
+
if (removed)
|
|
209
|
+
await persist();
|
|
210
|
+
return removed;
|
|
211
|
+
},
|
|
212
|
+
async flush() {
|
|
213
|
+
await ready;
|
|
214
|
+
await saveTail;
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -1,190 +1,19 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
3
1
|
import path from 'node:path'
|
|
4
|
-
import process from 'node:process'
|
|
5
2
|
import { resolveDshHome } from './doctor.js'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
// v1 stops had no expiry; v2 AUTH stops also persisted a secret-derived
|
|
9
|
-
// credential fingerprint. Do not migrate either format: v3 persists only
|
|
10
|
-
// non-secret endpoint/model stop evidence.
|
|
11
|
-
const CACHE_VERSION = 3
|
|
12
|
-
const MAX_ENTRIES = 256
|
|
13
|
-
const AXES = new Set(['ocr', 'general', 'document', 'structured', 'grounding'])
|
|
14
|
-
const CLASSES = new Set(['unavailable', 'protocol'])
|
|
15
|
-
|
|
16
|
-
function cleanText(value, max = 256) {
|
|
17
|
-
return typeof value === 'string' && value.trim() !== '' ? value.trim().slice(0, max) : ''
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function cleanStop(value) {
|
|
21
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
22
|
-
if (Number(value.suiteRevision) !== CAPABILITY_BENCHMARK_SUITE_REVISION) return undefined
|
|
23
|
-
const fingerprint = cleanText(value.fingerprint, 64)
|
|
24
|
-
if (!/^ep2_[0-9a-f]{32}$/.test(fingerprint)) return undefined
|
|
25
|
-
const key = cleanText(value.key)
|
|
26
|
-
const provider = cleanText(value.provider)
|
|
27
|
-
const model = cleanText(value.model)
|
|
28
|
-
const axis = cleanText(value.axis, 32)
|
|
29
|
-
const errorClass = cleanText(value.errorClass, 48)
|
|
30
|
-
const errorCode = cleanText(value.errorCode, 96)
|
|
31
|
-
const recordedAt = Number(value.recordedAt)
|
|
32
|
-
const expiresAt = Number(value.expiresAt)
|
|
33
|
-
if (!key || !provider || !model || !AXES.has(axis) || !CLASSES.has(errorClass)) return undefined
|
|
34
|
-
if (!Number.isFinite(recordedAt) || recordedAt <= 0) return undefined
|
|
35
|
-
if (!Number.isFinite(expiresAt) || expiresAt <= recordedAt) return undefined
|
|
36
|
-
return {
|
|
37
|
-
fingerprint,
|
|
38
|
-
key,
|
|
39
|
-
provider,
|
|
40
|
-
model,
|
|
41
|
-
axis,
|
|
42
|
-
errorClass,
|
|
43
|
-
...(errorCode ? { errorCode } : {}),
|
|
44
|
-
recordedAt,
|
|
45
|
-
expiresAt,
|
|
46
|
-
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function recordKey(value) {
|
|
51
|
-
return `${value.fingerprint}\u0000${value.axis}`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function selectRetainedStops(values, at, maxEntries = MAX_ENTRIES) {
|
|
55
|
-
const seen = new Set()
|
|
56
|
-
return [...values]
|
|
57
|
-
.map(cleanStop)
|
|
58
|
-
.filter((item) => item && item.expiresAt > at)
|
|
59
|
-
.sort((a, b) => b.recordedAt - a.recordedAt || recordKey(a).localeCompare(recordKey(b)))
|
|
60
|
-
.filter((item) => {
|
|
61
|
-
const key = recordKey(item)
|
|
62
|
-
if (seen.has(key)) return false
|
|
63
|
-
seen.add(key)
|
|
64
|
-
return true
|
|
65
|
-
})
|
|
66
|
-
.slice(0, maxEntries)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function retainedStopMap(values, at, maxEntries = MAX_ENTRIES) {
|
|
70
|
-
return new Map(selectRetainedStops(values, at, maxEntries).map((item) => [recordKey(item), item]))
|
|
71
|
-
}
|
|
3
|
+
import { createBackgroundBenchmarkStopStoreCore } from './vision-background-stop-store-core.js'
|
|
72
4
|
|
|
73
5
|
export function backgroundBenchmarkStopCachePath(dshHome = resolveDshHome()) {
|
|
74
6
|
return path.join(dshHome, 'cache', 'vision-router', 'background-benchmark-stops.json')
|
|
75
7
|
}
|
|
76
8
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!body || !Array.isArray(body.stops) || ![2, CACHE_VERSION].includes(body.version)) return new Map()
|
|
82
|
-
const records = retainedStopMap(body.stops, at)
|
|
83
|
-
if (body.version === 2) await save(file, records, fsOps, at)
|
|
84
|
-
return records
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (error?.code === 'ENOENT') return new Map()
|
|
87
|
-
return new Map()
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function save(file, records, fsOps, at) {
|
|
92
|
-
const temporary = `${file}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`
|
|
93
|
-
const stops = selectRetainedStops(records.values(), at)
|
|
94
|
-
await fsOps.mkdir(path.dirname(file), { recursive: true })
|
|
95
|
-
await fsOps.writeFile(temporary, JSON.stringify({ version: CACHE_VERSION, stops }), { encoding: 'utf8', mode: 0o600 })
|
|
96
|
-
await fsOps.rename(temporary, file)
|
|
97
|
-
}
|
|
98
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Keep DSH-home discovery at the dynamic Host/path boundary. The typed core
|
|
11
|
+
* owns durable stop validation, migration, retention and persistence.
|
|
12
|
+
*/
|
|
99
13
|
export function createBackgroundBenchmarkStopStore(options = {}) {
|
|
100
14
|
const file = options.cacheFile ?? backgroundBenchmarkStopCachePath(options.dshHome)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
mkdir: options.fsOps?.mkdir ?? mkdir,
|
|
104
|
-
writeFile: options.fsOps?.writeFile ?? writeFile,
|
|
105
|
-
rename: options.fsOps?.rename ?? rename,
|
|
106
|
-
}
|
|
107
|
-
const logger = options.logger
|
|
108
|
-
const now = typeof options.now === 'function' ? options.now : Date.now
|
|
109
|
-
let records = new Map()
|
|
110
|
-
let saveTail = Promise.resolve()
|
|
111
|
-
const ready = load(file, fsOps, Number(now())).then((loaded) => { records = loaded })
|
|
112
|
-
|
|
113
|
-
const persist = () => {
|
|
114
|
-
const at = Number(now())
|
|
115
|
-
saveTail = saveTail
|
|
116
|
-
.then(() => save(file, records, fsOps, at))
|
|
117
|
-
.catch((error) => logger?.warn?.('vision-router: background stop cache write failed: %s', cleanText(error?.message ?? error, 240)))
|
|
118
|
-
return saveTail
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const reconcile = () => {
|
|
122
|
-
const at = Number(now())
|
|
123
|
-
const retained = retainedStopMap(records.values(), at)
|
|
124
|
-
const changed = retained.size !== records.size || [...retained.keys()].some((key) => !records.has(key))
|
|
125
|
-
records = retained
|
|
126
|
-
return changed
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
15
|
+
return createBackgroundBenchmarkStopStoreCore({
|
|
16
|
+
...options,
|
|
130
17
|
file,
|
|
131
|
-
|
|
132
|
-
await ready
|
|
133
|
-
if (reconcile()) await persist()
|
|
134
|
-
return [...records.values()].sort((a, b) => b.recordedAt - a.recordedAt)
|
|
135
|
-
},
|
|
136
|
-
async mark({
|
|
137
|
-
fingerprint,
|
|
138
|
-
key,
|
|
139
|
-
provider,
|
|
140
|
-
model,
|
|
141
|
-
axis,
|
|
142
|
-
errorClass,
|
|
143
|
-
errorCode,
|
|
144
|
-
recordedAt = Number(now()),
|
|
145
|
-
expiresAt,
|
|
146
|
-
} = {}) {
|
|
147
|
-
await ready
|
|
148
|
-
reconcile()
|
|
149
|
-
const clean = cleanStop({
|
|
150
|
-
fingerprint,
|
|
151
|
-
key,
|
|
152
|
-
provider,
|
|
153
|
-
model,
|
|
154
|
-
axis,
|
|
155
|
-
errorClass,
|
|
156
|
-
errorCode,
|
|
157
|
-
recordedAt,
|
|
158
|
-
expiresAt,
|
|
159
|
-
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
160
|
-
})
|
|
161
|
-
if (!clean) return undefined
|
|
162
|
-
records.set(recordKey(clean), clean)
|
|
163
|
-
reconcile()
|
|
164
|
-
await persist()
|
|
165
|
-
return records.get(recordKey(clean))
|
|
166
|
-
},
|
|
167
|
-
async clearStop(fingerprint, axis) {
|
|
168
|
-
await ready
|
|
169
|
-
const removed = records.delete(`${String(fingerprint ?? '')}\u0000${String(axis ?? '')}`)
|
|
170
|
-
if (removed) await persist()
|
|
171
|
-
return removed
|
|
172
|
-
},
|
|
173
|
-
async clearFingerprint(fingerprint) {
|
|
174
|
-
await ready
|
|
175
|
-
const wanted = String(fingerprint ?? '')
|
|
176
|
-
let removed = false
|
|
177
|
-
for (const [key, record] of records.entries()) {
|
|
178
|
-
if (record.fingerprint !== wanted) continue
|
|
179
|
-
records.delete(key)
|
|
180
|
-
removed = true
|
|
181
|
-
}
|
|
182
|
-
if (removed) await persist()
|
|
183
|
-
return removed
|
|
184
|
-
},
|
|
185
|
-
async flush() {
|
|
186
|
-
await ready
|
|
187
|
-
await saveTail
|
|
188
|
-
},
|
|
189
|
-
}
|
|
18
|
+
})
|
|
190
19
|
}
|
|
@@ -307,6 +307,7 @@ export function createExactCapabilityInvoker(ctx, core, candidate, config, optio
|
|
|
307
307
|
maxTokens: benchmarkMaxTokensForProvider(exactHttpProvider),
|
|
308
308
|
signal: callSignal,
|
|
309
309
|
affinityId: benchmarkAffinityId,
|
|
310
|
+
providerTransport: options.providerTransport,
|
|
310
311
|
resolveCredential: (ref) => resolvedCredentialValue(ctx, ref),
|
|
311
312
|
}),
|
|
312
313
|
fixtureTimeoutMs,
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
import process from 'node:process'
|
|
3
1
|
import { VISION_INTENTS } from './vision-capability-router.js'
|
|
2
|
+
import {
|
|
3
|
+
CAPABILITY_BENCHMARK_RENDERER_SCOPE,
|
|
4
|
+
CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
5
|
+
capabilityBenchmarkFingerprint,
|
|
6
|
+
} from './vision-capability-fingerprint.js'
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
CAPABILITY_BENCHMARK_RENDERER_SCOPE,
|
|
10
|
+
CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
11
|
+
capabilityBenchmarkFingerprint,
|
|
12
|
+
} from './vision-capability-fingerprint.js'
|
|
4
13
|
|
|
5
14
|
const WIDTH = 768
|
|
6
15
|
const HEIGHT = 512
|
|
7
16
|
|
|
8
|
-
export const CAPABILITY_BENCHMARK_SUITE_REVISION = 5
|
|
9
|
-
// Renderer-only fixes must invalidate prior evidence even when the scoring and
|
|
10
|
-
// prompt contract (the suite revision) is unchanged.
|
|
11
|
-
export const CAPABILITY_BENCHMARK_RENDERER_SCOPE = `${process.platform}/${process.arch}-proof-badge-v2`
|
|
12
|
-
|
|
13
17
|
function esc(value) {
|
|
14
18
|
return String(value).replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch])
|
|
15
19
|
}
|
|
@@ -160,60 +164,6 @@ export function listCapabilityBenchmarkFixtures(intents = CORE_BENCHMARK_INTENTS
|
|
|
160
164
|
return fixtures
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
function isSensitiveKey(key) {
|
|
164
|
-
return /(^|[_-])(api[_-]?key|key|token|secret|password|authorization|auth|signature|sig)([_-]|$)/i.test(String(key ?? ''))
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function normalizeEndpoint(value) {
|
|
168
|
-
const raw = String(value ?? '').trim()
|
|
169
|
-
if (raw === '') return ''
|
|
170
|
-
try {
|
|
171
|
-
const url = new URL(raw)
|
|
172
|
-
url.username = ''
|
|
173
|
-
url.password = ''
|
|
174
|
-
url.hash = ''
|
|
175
|
-
for (const key of [...url.searchParams.keys()]) {
|
|
176
|
-
if (isSensitiveKey(key)) url.searchParams.delete(key)
|
|
177
|
-
}
|
|
178
|
-
url.searchParams.sort()
|
|
179
|
-
const pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '')
|
|
180
|
-
return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${pathname}${url.search}`
|
|
181
|
-
} catch {
|
|
182
|
-
return raw.replace(/\/+$/, '')
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function sanitizeFingerprintValue(value, keyHint = '') {
|
|
187
|
-
if (value === null || value === undefined) return value ?? null
|
|
188
|
-
if (Array.isArray(value)) return value.map((item) => sanitizeFingerprintValue(item))
|
|
189
|
-
if (typeof value === 'object') {
|
|
190
|
-
return Object.fromEntries(
|
|
191
|
-
Object.entries(value)
|
|
192
|
-
.filter(([key]) => !isSensitiveKey(key))
|
|
193
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
194
|
-
.map(([key, child]) => [key, sanitizeFingerprintValue(child, key)]),
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
if (typeof value === 'string' && /(endpoint|base[_-]?url|url)$/i.test(keyHint)) return normalizeEndpoint(value)
|
|
198
|
-
if (typeof value === 'bigint') return value.toString()
|
|
199
|
-
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value
|
|
200
|
-
return String(value)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export function capabilityBenchmarkFingerprint({ provider, model, endpoint, config, credentialFingerprint } = {}) {
|
|
204
|
-
const payload = JSON.stringify({
|
|
205
|
-
schema: 'vision-capability-endpoint-v2',
|
|
206
|
-
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
207
|
-
rendererScope: CAPABILITY_BENCHMARK_RENDERER_SCOPE,
|
|
208
|
-
provider: String(provider ?? '').trim(),
|
|
209
|
-
model: String(model ?? '').trim(),
|
|
210
|
-
endpoint: normalizeEndpoint(endpoint),
|
|
211
|
-
config: sanitizeFingerprintValue(config && typeof config === 'object' ? config : null),
|
|
212
|
-
credentialFingerprint: String(credentialFingerprint ?? 'none'),
|
|
213
|
-
})
|
|
214
|
-
return `ep2_${createHash('sha256').update(payload).digest('hex').slice(0, 32)}`
|
|
215
|
-
}
|
|
216
|
-
|
|
217
167
|
function clamp01(value) {
|
|
218
168
|
return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0))
|
|
219
169
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const VISION_INTENTS = [
|
|
2
|
+
'structured',
|
|
3
|
+
'ocr',
|
|
4
|
+
'document',
|
|
5
|
+
'ui',
|
|
6
|
+
'grounding',
|
|
7
|
+
'detection',
|
|
8
|
+
'general',
|
|
9
|
+
'chart_diagram',
|
|
10
|
+
'code_screenshot',
|
|
11
|
+
'visual_compare',
|
|
12
|
+
];
|
|
13
|
+
export const BENCHMARK_AXES = [
|
|
14
|
+
'structured',
|
|
15
|
+
'ocr',
|
|
16
|
+
'document',
|
|
17
|
+
'grounding',
|
|
18
|
+
'general',
|
|
19
|
+
];
|
|
20
|
+
const DIRECT_TASK_AXIS = Object.freeze({
|
|
21
|
+
structured: 'structured',
|
|
22
|
+
ocr: 'ocr',
|
|
23
|
+
document: 'document',
|
|
24
|
+
grounding: 'grounding',
|
|
25
|
+
general: 'general',
|
|
26
|
+
});
|
|
27
|
+
export function normalizeVisionIntent(intent) {
|
|
28
|
+
return typeof intent === 'string'
|
|
29
|
+
&& VISION_INTENTS.includes(intent)
|
|
30
|
+
? intent
|
|
31
|
+
: 'general';
|
|
32
|
+
}
|
|
33
|
+
export function benchmarkAxisForVisionIntent(intent) {
|
|
34
|
+
return DIRECT_TASK_AXIS[normalizeVisionIntent(intent)];
|
|
35
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
export const CAPABILITY_BENCHMARK_SUITE_REVISION = 5;
|
|
4
|
+
// Renderer-only fixes must invalidate prior evidence even when the scoring and
|
|
5
|
+
// prompt contract (the suite revision) is unchanged.
|
|
6
|
+
export const CAPABILITY_BENCHMARK_RENDERER_SCOPE = `${process.platform}/${process.arch}-proof-badge-v2`;
|
|
7
|
+
function isSensitiveKey(key) {
|
|
8
|
+
return /(^|[_-])(api[_-]?key|key|token|secret|password|authorization|auth|signature|sig)([_-]|$)/i
|
|
9
|
+
.test(String(key ?? ''));
|
|
10
|
+
}
|
|
11
|
+
function normalizeEndpoint(value) {
|
|
12
|
+
const raw = String(value ?? '').trim();
|
|
13
|
+
if (raw === '')
|
|
14
|
+
return '';
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(raw);
|
|
17
|
+
url.username = '';
|
|
18
|
+
url.password = '';
|
|
19
|
+
url.hash = '';
|
|
20
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
21
|
+
if (isSensitiveKey(key))
|
|
22
|
+
url.searchParams.delete(key);
|
|
23
|
+
}
|
|
24
|
+
url.searchParams.sort();
|
|
25
|
+
const pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
|
|
26
|
+
return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${pathname}${url.search}`;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return raw.replace(/\/+$/, '');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function sanitizeFingerprintValue(value, keyHint = '') {
|
|
33
|
+
if (value === null || value === undefined)
|
|
34
|
+
return value ?? null;
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
return value.map((item) => sanitizeFingerprintValue(item));
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'object') {
|
|
39
|
+
return Object.fromEntries(Object.entries(value)
|
|
40
|
+
.filter(([key]) => !isSensitiveKey(key))
|
|
41
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
42
|
+
.map(([key, child]) => [
|
|
43
|
+
key,
|
|
44
|
+
sanitizeFingerprintValue(child, key),
|
|
45
|
+
]));
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === 'string'
|
|
48
|
+
&& /(endpoint|base[_-]?url|url)$/i.test(keyHint)) {
|
|
49
|
+
return normalizeEndpoint(value);
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'bigint')
|
|
52
|
+
return value.toString();
|
|
53
|
+
if (typeof value === 'number'
|
|
54
|
+
|| typeof value === 'boolean'
|
|
55
|
+
|| typeof value === 'string') {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
export function capabilityBenchmarkFingerprint({ provider, model, endpoint, config, credentialFingerprint, } = {}) {
|
|
61
|
+
const payload = JSON.stringify({
|
|
62
|
+
schema: 'vision-capability-endpoint-v2',
|
|
63
|
+
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
64
|
+
rendererScope: CAPABILITY_BENCHMARK_RENDERER_SCOPE,
|
|
65
|
+
provider: String(provider ?? '').trim(),
|
|
66
|
+
model: String(model ?? '').trim(),
|
|
67
|
+
endpoint: normalizeEndpoint(endpoint),
|
|
68
|
+
config: sanitizeFingerprintValue(config && typeof config === 'object' ? config : null),
|
|
69
|
+
credentialFingerprint: String(credentialFingerprint ?? 'none'),
|
|
70
|
+
});
|
|
71
|
+
return `ep2_${createHash('sha256').update(payload).digest('hex').slice(0, 32)}`;
|
|
72
|
+
}
|