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.
Files changed (132) hide show
  1. package/README.md +16 -7
  2. package/README.zh.md +16 -7
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/3.0-architecture-plan.md +252 -0
  8. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  9. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  10. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  11. package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
  12. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  13. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  14. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  15. package/docs/architecture/3.0-r7-public-api.md +151 -0
  16. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  17. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  18. package/docs/architecture/compat-inventory.md +26 -34
  19. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  20. package/docs/architecture/dsh-support-window.md +30 -22
  21. package/docs/architecture/final-closure-audit.md +2 -0
  22. package/docs/releases/v2.3.0.md +53 -0
  23. package/docs/releases/v3.0.0.md +85 -0
  24. package/entry.js +9 -57
  25. package/index.js +268 -761
  26. package/lib/agent-request-route-authority.js +1 -20
  27. package/lib/catalog-corrections.js +1 -2
  28. package/lib/client-maintenance-actions.js +145 -0
  29. package/lib/client-presentation-boundary-main.js +75 -17
  30. package/lib/client-presentation-boundary.js +53 -28
  31. package/lib/client.js +246 -144
  32. package/lib/core-apply-lifecycle.js +41 -0
  33. package/lib/core-cache.js +87 -0
  34. package/lib/core-primitives.js +91 -278
  35. package/lib/core-vision-surface-domain.js +63 -0
  36. package/lib/core-vision-surface.js +11 -77
  37. package/lib/delegated-call-config.js +15 -24
  38. package/lib/desktop-screenshot-tool.js +262 -0
  39. package/lib/doctor-cli.js +0 -0
  40. package/lib/doctor-runtime.js +4 -1
  41. package/lib/dsh-contract-compat.js +37 -256
  42. package/lib/dsh-host-capabilities.js +3 -9
  43. package/lib/dsh-settings-017-compat.js +17 -10
  44. package/lib/dsh-support-window.js +3 -3
  45. package/lib/file-logger.js +8 -3
  46. package/lib/grounding-coordinate-runtime.js +1 -2
  47. package/lib/http-body-limit.js +97 -79
  48. package/lib/http-compat.js +1 -2
  49. package/lib/image-offload-compat.js +30 -21
  50. package/lib/image-resource-governor.js +210 -228
  51. package/lib/legacy-global-proxy-boundary.js +46 -3
  52. package/lib/live-model-client-prelude.js +12 -4
  53. package/lib/live-model-discovery.js +134 -43
  54. package/lib/local-remote-settings-permission.js +33 -7
  55. package/lib/local-vision-stabilizer.js +31 -17
  56. package/lib/maintenance-routes.js +134 -0
  57. package/lib/native-image-coexistence.js +15 -3
  58. package/lib/ollama-cold-start.js +87 -23
  59. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  60. package/lib/proxy-dispatcher-pool.js +151 -92
  61. package/lib/proxy-routing.js +75 -51
  62. package/lib/proxy-url-compat.js +3 -10
  63. package/lib/public-config.js +36 -0
  64. package/lib/public-entry.d.ts +148 -0
  65. package/lib/public-entry.js +19 -11
  66. package/lib/remote-settings-bridge.js +11 -0
  67. package/lib/remote-settings-risk-client-boundary.js +155 -0
  68. package/lib/remote-settings-risk-confirmation.js +17 -105
  69. package/lib/replay-delegation.js +12 -7
  70. package/lib/replay-envelope-v2-compat.js +12 -7
  71. package/lib/runtime-composition.js +127 -34
  72. package/lib/runtime-i18n-boundary.js +10 -11
  73. package/lib/screenshot-source-boundary.js +2 -2
  74. package/lib/session-affinity-runtime.js +113 -97
  75. package/lib/session-affinity.js +89 -76
  76. package/lib/session-event-history-compat.js +30 -0
  77. package/lib/session-message-source-compat.js +7 -3
  78. package/lib/session-query-readers.js +263 -0
  79. package/lib/session-surface-compat.js +30 -31
  80. package/lib/session-surface-policy-domain.js +85 -0
  81. package/lib/session-surface-policy.js +2 -111
  82. package/lib/session-turn-resolver.js +2 -6
  83. package/lib/session-vision-attachment-index.js +143 -0
  84. package/lib/session-vision-delegation-boundary.js +34 -0
  85. package/lib/session-vision-delegation.js +22 -0
  86. package/lib/session-vision-index.js +179 -116
  87. package/lib/session-vision-mode-authority.js +229 -192
  88. package/lib/session-vision-mode-boundary.js +65 -22
  89. package/lib/session-vision-policy-domain.js +304 -0
  90. package/lib/session-vision-policy.js +104 -0
  91. package/lib/session-vision-state.js +368 -378
  92. package/lib/settings-client-017-compat.js +9 -7
  93. package/lib/settings-client-loader-lifecycle.js +129 -0
  94. package/lib/sharp-runtime.js +0 -7
  95. package/lib/structured-flow-hardening.js +25 -6
  96. package/lib/tesseract-exec-compat.js +9 -1
  97. package/lib/turn-budget-context.js +47 -41
  98. package/lib/update-check.js +11 -2
  99. package/lib/vision-attachment-handle-runtime.js +176 -156
  100. package/lib/vision-backend-runtime-policy.js +19 -2
  101. package/lib/vision-background-benchmark.js +41 -4
  102. package/lib/vision-background-failure-policy.js +41 -41
  103. package/lib/vision-background-stop-store-core.js +217 -0
  104. package/lib/vision-background-stop-store.js +8 -179
  105. package/lib/vision-capability-benchmark-service.js +1 -0
  106. package/lib/vision-capability-benchmark.js +11 -61
  107. package/lib/vision-capability-evidence.js +35 -0
  108. package/lib/vision-capability-fingerprint.js +72 -0
  109. package/lib/vision-capability-identity.js +116 -61
  110. package/lib/vision-capability-router.js +13 -33
  111. package/lib/vision-chunk-assembler.js +95 -0
  112. package/lib/vision-diagnostics-routes.js +246 -0
  113. package/lib/vision-evidence-guidance.js +3 -1
  114. package/lib/vision-execution-order-apply.js +45 -55
  115. package/lib/vision-execution-order.js +19 -19
  116. package/lib/vision-execution-policy.js +12 -7
  117. package/lib/vision-model-registry.js +30 -17
  118. package/lib/vision-provider-transport.js +137 -176
  119. package/lib/vision-routing-authority.js +47 -48
  120. package/lib/vision-routing-product.js +26 -29
  121. package/lib/vision-routing-settings-prelude.js +8 -3
  122. package/lib/vision-runtime-performance-store.js +166 -0
  123. package/lib/vision-runtime-performance.js +13 -142
  124. package/lib/vision-toggle-root-hardening.js +22 -5
  125. package/lib/vision-tool-runtime-boundary.js +104 -31
  126. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  127. package/lib/web/remote-settings-client.js +6 -9
  128. package/lib/web-capability-boundary.js +17 -7
  129. package/lib/wrapper-directory.js +14 -23
  130. package/package.json +56 -20
  131. package/lib/android-attachment-compat.js +0 -244
  132. 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 { CAPABILITY_BENCHMARK_SUITE_REVISION } from './vision-capability-benchmark.js'
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
- async function load(file, fsOps, at) {
78
- try {
79
- const raw = await fsOps.readFile(file, 'utf8')
80
- const body = JSON.parse(raw)
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
- const fsOps = {
102
- readFile: options.fsOps?.readFile ?? readFile,
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
- async list() {
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[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
+ }