dsh-vision-router 2.3.0 → 3.0.1

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 (105) hide show
  1. package/README.md +18 -3
  2. package/README.zh.md +16 -3
  3. package/docs/architecture/3.0-architecture-plan.md +252 -0
  4. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  5. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  6. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  7. package/docs/architecture/3.0-r11-adversarial-review.md +447 -0
  8. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  9. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  10. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  11. package/docs/architecture/3.0-r7-public-api.md +151 -0
  12. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  13. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  14. package/docs/architecture/dsh-support-window.md +13 -3
  15. package/docs/releases/v3.0.0.md +85 -0
  16. package/docs/releases/v3.0.1.md +77 -0
  17. package/entry.js +1 -1
  18. package/index.js +110 -71
  19. package/lib/agent-request-route-authority.js +1 -20
  20. package/lib/core-apply-lifecycle.js +37 -43
  21. package/lib/core-cache.js +87 -0
  22. package/lib/core-primitives.js +11 -144
  23. package/lib/core-vision-surface-domain.js +63 -0
  24. package/lib/core-vision-surface.js +11 -77
  25. package/lib/delegated-call-config.js +15 -24
  26. package/lib/doctor-cli.js +0 -0
  27. package/lib/doctor-runtime.js +4 -1
  28. package/lib/dsh-contract-compat.js +26 -230
  29. package/lib/dsh-host-capabilities.js +3 -9
  30. package/lib/dsh-settings-017-compat.js +17 -10
  31. package/lib/file-logger.js +8 -3
  32. package/lib/grounding-coordinate-runtime.js +1 -2
  33. package/lib/http-body-limit.js +97 -79
  34. package/lib/image-offload-compat.js +30 -21
  35. package/lib/image-resource-governor.js +210 -228
  36. package/lib/legacy-global-proxy-boundary.js +46 -3
  37. package/lib/live-model-client-prelude.js +12 -4
  38. package/lib/live-model-discovery.js +134 -43
  39. package/lib/local-vision-stabilizer.js +33 -18
  40. package/lib/maintenance-routes.js +1 -3
  41. package/lib/native-image-coexistence.js +15 -3
  42. package/lib/ollama-cold-start.js +87 -23
  43. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  44. package/lib/proxy-dispatcher-pool.js +151 -92
  45. package/lib/proxy-routing.js +75 -51
  46. package/lib/proxy-url-compat.js +3 -10
  47. package/lib/public-config.js +23 -44
  48. package/lib/public-entry.d.ts +148 -0
  49. package/lib/public-entry.js +17 -4
  50. package/lib/replay-delegation.js +12 -7
  51. package/lib/replay-envelope-v2-compat.js +12 -7
  52. package/lib/runtime-composition.js +30 -10
  53. package/lib/screenshot-source-boundary.js +2 -2
  54. package/lib/session-affinity-runtime.js +113 -97
  55. package/lib/session-affinity.js +89 -76
  56. package/lib/session-event-history-compat.js +30 -0
  57. package/lib/session-query-readers.js +263 -0
  58. package/lib/session-surface-compat.js +30 -31
  59. package/lib/session-surface-policy-domain.js +85 -0
  60. package/lib/session-surface-policy.js +2 -111
  61. package/lib/session-turn-resolver.js +2 -6
  62. package/lib/session-vision-attachment-index.js +143 -0
  63. package/lib/session-vision-delegation-boundary.js +34 -0
  64. package/lib/session-vision-delegation.js +22 -0
  65. package/lib/session-vision-index.js +25 -107
  66. package/lib/session-vision-mode-authority.js +229 -192
  67. package/lib/session-vision-mode-boundary.js +65 -22
  68. package/lib/session-vision-policy-domain.js +304 -0
  69. package/lib/session-vision-policy.js +104 -0
  70. package/lib/session-vision-state.js +368 -378
  71. package/lib/settings-client-017-compat.js +16 -10
  72. package/lib/structured-flow-hardening.js +22 -4
  73. package/lib/tesseract-exec-compat.js +9 -1
  74. package/lib/turn-budget-context.js +47 -41
  75. package/lib/update-check.js +11 -2
  76. package/lib/vision-attachment-handle-runtime.js +176 -156
  77. package/lib/vision-backend-runtime-policy.js +16 -0
  78. package/lib/vision-background-benchmark.js +55 -6
  79. package/lib/vision-background-failure-policy.js +41 -41
  80. package/lib/vision-background-stop-store-core.js +217 -0
  81. package/lib/vision-background-stop-store.js +8 -179
  82. package/lib/vision-capability-benchmark.js +11 -61
  83. package/lib/vision-capability-evidence.js +35 -0
  84. package/lib/vision-capability-fingerprint.js +72 -0
  85. package/lib/vision-capability-identity.js +116 -61
  86. package/lib/vision-capability-router.js +13 -33
  87. package/lib/vision-chunk-assembler.js +95 -0
  88. package/lib/vision-diagnostics-routes.js +42 -4
  89. package/lib/vision-execution-order-apply.js +45 -55
  90. package/lib/vision-execution-order.js +19 -19
  91. package/lib/vision-execution-policy.js +12 -7
  92. package/lib/vision-model-registry.js +30 -17
  93. package/lib/vision-model-visibility-boundary-main.js +15 -16
  94. package/lib/vision-provider-transport.js +137 -147
  95. package/lib/vision-routing-authority.js +47 -48
  96. package/lib/vision-routing-product.js +26 -29
  97. package/lib/vision-routing-settings-prelude.js +8 -3
  98. package/lib/vision-runtime-performance-store.js +166 -0
  99. package/lib/vision-runtime-performance.js +13 -142
  100. package/lib/vision-tool-runtime-boundary.js +99 -30
  101. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  102. package/lib/web/remote-settings-client.js +4 -7
  103. package/lib/web-capability-boundary.js +17 -7
  104. package/lib/wrapper-directory.js +14 -23
  105. package/package.json +37 -11
@@ -1,46 +1,42 @@
1
- import { normalizeVisionRoutingMode } from './vision-routing-product.js'
2
-
3
- export const BACKGROUND_MEASUREMENT_MODES = Object.freeze(['off', 'local-free', 'all'])
4
-
5
- const BACKGROUND_MEASUREMENT_SET = new Set(BACKGROUND_MEASUREMENT_MODES)
6
- const MANUAL_MEASUREMENT_GRANT = Symbol('vision-router-manual-measurement-grant')
7
-
1
+ import { normalizeVisionRoutingMode, } from './vision-routing-product.js';
2
+ export const BACKGROUND_MEASUREMENT_MODES = Object.freeze(['off', 'local-free', 'all']);
3
+ const BACKGROUND_MEASUREMENT_SET = new Set(BACKGROUND_MEASUREMENT_MODES);
4
+ const MANUAL_MEASUREMENT_GRANT = Symbol('vision-router-manual-measurement-grant');
8
5
  function plainObject(value) {
9
- return value !== null && typeof value === 'object' && !Array.isArray(value)
6
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
10
7
  }
11
-
12
8
  export function normalizeBackgroundMeasurementAuthority(value, fallback = 'off') {
13
- const normalizedFallback = BACKGROUND_MEASUREMENT_SET.has(fallback) ? fallback : 'off'
14
- return typeof value === 'string' && BACKGROUND_MEASUREMENT_SET.has(value.trim())
15
- ? value.trim()
16
- : normalizedFallback
9
+ const normalizedFallback = typeof fallback === 'string' && BACKGROUND_MEASUREMENT_SET.has(fallback)
10
+ ? fallback
11
+ : 'off';
12
+ const candidate = typeof value === 'string' ? value.trim() : '';
13
+ return BACKGROUND_MEASUREMENT_SET.has(candidate)
14
+ ? candidate
15
+ : normalizedFallback;
17
16
  }
18
-
19
17
  /**
20
18
  * Resolve only user-delegated v2 authority. Evidence, preference, credentials,
21
19
  * capability gaps, or internal shadow switches must never widen this result.
22
20
  */
23
21
  export function resolveVisionRoutingAuthority(config = {}) {
24
- const source = plainObject(config) ? config : {}
25
- const execution = normalizeVisionRoutingMode(source.routingMode)
26
- const backgroundMeasurement = normalizeBackgroundMeasurementAuthority(source.backgroundBenchmarking)
27
- const autoSelectionAuthorized = execution === 'auto'
28
- const backgroundMeasurementAuthorized = backgroundMeasurement !== 'off'
29
-
30
- return Object.freeze({
31
- execution,
32
- autoSelectionAuthorized,
33
- backgroundMeasurement,
34
- backgroundMeasurementAuthorized,
35
- // Current background profiler is useful only for Auto preparation, but the
36
- // standing measurement grant remains independent from execution authority.
37
- backgroundMeasurementActive: autoSelectionAuthorized && backgroundMeasurementAuthorized,
38
- ephemeralRuntimeObservation: autoSelectionAuthorized,
39
- // No v2 setting currently grants durable user-specific behavioral learning.
40
- persistentLearning: false,
41
- })
22
+ const source = plainObject(config) ? config : {};
23
+ const execution = normalizeVisionRoutingMode(source.routingMode);
24
+ const backgroundMeasurement = normalizeBackgroundMeasurementAuthority(source.backgroundBenchmarking);
25
+ const autoSelectionAuthorized = execution === 'auto';
26
+ const backgroundMeasurementAuthorized = backgroundMeasurement !== 'off';
27
+ return Object.freeze({
28
+ execution,
29
+ autoSelectionAuthorized,
30
+ backgroundMeasurement,
31
+ backgroundMeasurementAuthorized,
32
+ // Current background profiler is useful only for Auto preparation, but the
33
+ // standing measurement grant remains independent from execution authority.
34
+ backgroundMeasurementActive: autoSelectionAuthorized && backgroundMeasurementAuthorized,
35
+ ephemeralRuntimeObservation: autoSelectionAuthorized,
36
+ // No v2 setting currently grants durable user-specific behavioral learning.
37
+ persistentLearning: false,
38
+ });
42
39
  }
43
-
44
40
  /**
45
41
  * A manual measurement grant is intentionally opaque and process-local. The
46
42
  * local HTTP action creates one only after an explicit user POST. Requiring the
@@ -48,23 +44,26 @@ export function resolveVisionRoutingAuthority(config = {}) {
48
44
  * inheriting "manual" semantics by convention.
49
45
  */
50
46
  export function grantManualMeasurementFromUserAction(source = 'local-ui') {
51
- if (source !== 'local-ui') {
52
- throw new TypeError('manual measurement authority source must be local-ui')
53
- }
54
- return Object.freeze({
55
- kind: 'manual-measurement',
56
- source,
57
- [MANUAL_MEASUREMENT_GRANT]: true,
58
- })
47
+ if (source !== 'local-ui') {
48
+ throw new TypeError('manual measurement authority source must be local-ui');
49
+ }
50
+ const grant = {
51
+ kind: 'manual-measurement',
52
+ source,
53
+ [MANUAL_MEASUREMENT_GRANT]: true,
54
+ };
55
+ return Object.freeze(grant);
59
56
  }
60
-
61
57
  export function hasManualMeasurementAuthority(value) {
62
- return !!value && value[MANUAL_MEASUREMENT_GRANT] === true && value.kind === 'manual-measurement'
58
+ if (!value || typeof value !== 'object')
59
+ return false;
60
+ const candidate = value;
61
+ return candidate[MANUAL_MEASUREMENT_GRANT] === true && candidate.kind === 'manual-measurement';
63
62
  }
64
-
65
63
  export function assertManualMeasurementAuthority(value) {
66
- if (hasManualMeasurementAuthority(value)) return value
67
- const error = new Error('explicit manual measurement authority is required')
68
- error.code = 'CAPABILITY_BENCHMARK_AUTHORITY_REQUIRED'
69
- throw error
64
+ if (hasManualMeasurementAuthority(value))
65
+ return value;
66
+ const error = new Error('explicit manual measurement authority is required');
67
+ error.code = 'CAPABILITY_BENCHMARK_AUTHORITY_REQUIRED';
68
+ throw error;
70
69
  }
@@ -5,40 +5,37 @@
5
5
  // A user chooses whether routing is automatic or strictly ordered, then chooses
6
6
  // a plain-language preference. The actual execution-changing auto mode is
7
7
  // enabled only when the runtime is ready to honor this contract.
8
-
9
- export const VISION_ROUTING_MODES = Object.freeze(['ordered', 'auto'])
10
- export const VISION_ROUTING_PREFERENCES = Object.freeze(['balanced', 'quality', 'speed', 'local'])
11
-
12
- const MODE_SET = new Set(VISION_ROUTING_MODES)
13
- const PREFERENCE_SET = new Set(VISION_ROUTING_PREFERENCES)
14
-
8
+ export const VISION_ROUTING_MODES = Object.freeze(['ordered', 'auto']);
9
+ export const VISION_ROUTING_PREFERENCES = Object.freeze(['balanced', 'quality', 'speed', 'local']);
10
+ const MODE_SET = new Set(VISION_ROUTING_MODES);
11
+ const PREFERENCE_SET = new Set(VISION_ROUTING_PREFERENCES);
15
12
  export function normalizeVisionRoutingMode(value, fallback = 'ordered') {
16
- const normalizedFallback = MODE_SET.has(fallback) ? fallback : 'ordered'
17
- return typeof value === 'string' && MODE_SET.has(value.trim()) ? value.trim() : normalizedFallback
13
+ const normalizedFallback = typeof fallback === 'string' && MODE_SET.has(fallback) ? fallback : 'ordered';
14
+ const candidate = typeof value === 'string' ? value.trim() : '';
15
+ return MODE_SET.has(candidate) ? candidate : normalizedFallback;
18
16
  }
19
-
20
17
  export function normalizeVisionRoutingPreference(value) {
21
- if (typeof value === 'string') {
22
- const candidate = value.trim()
23
- if (PREFERENCE_SET.has(candidate)) return candidate
24
- }
25
-
26
- return 'balanced'
18
+ if (typeof value === 'string') {
19
+ const candidate = value.trim();
20
+ if (PREFERENCE_SET.has(candidate))
21
+ return candidate;
22
+ }
23
+ return 'balanced';
27
24
  }
28
-
29
25
  export function routingPreferenceToCapabilityStrategy(preference) {
30
- const normalized = normalizeVisionRoutingPreference(preference)
31
- return normalized === 'local' ? 'privacy' : normalized
26
+ const normalized = normalizeVisionRoutingPreference(preference);
27
+ return normalized === 'local' ? 'privacy' : normalized;
32
28
  }
33
-
34
29
  export function resolveVisionRoutingProduct(config = {}) {
35
- const source = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
36
- const mode = normalizeVisionRoutingMode(source.routingMode)
37
- const preference = normalizeVisionRoutingPreference(source.routingPreference)
38
- return {
39
- mode,
40
- preference,
41
- strategy: routingPreferenceToCapabilityStrategy(preference),
42
- automatic: mode === 'auto',
43
- }
30
+ const source = config && typeof config === 'object' && !Array.isArray(config)
31
+ ? config
32
+ : {};
33
+ const mode = normalizeVisionRoutingMode(source.routingMode);
34
+ const preference = normalizeVisionRoutingPreference(source.routingPreference);
35
+ return {
36
+ mode,
37
+ preference,
38
+ strategy: routingPreferenceToCapabilityStrategy(preference),
39
+ automatic: mode === 'auto',
40
+ };
44
41
  }
@@ -446,7 +446,7 @@ export const VISION_ROUTING_SETTINGS_PRELUDE = String.raw`(function(){
446
446
  try { state.backgroundTimer && state.backgroundTimer.unref && state.backgroundTimer.unref(); } catch (_) {}
447
447
  schedule();
448
448
 
449
- var dispose = function() {
449
+ var dispose = async function() {
450
450
  state.disposed = true;
451
451
  if (state.scanTimer !== undefined) clearTimeout(state.scanTimer);
452
452
  if (state.backgroundTimer !== undefined) clearInterval(state.backgroundTimer);
@@ -456,13 +456,18 @@ export const VISION_ROUTING_SETTINGS_PRELUDE = String.raw`(function(){
456
456
  try { disposeRemote(); } catch (_) {}
457
457
  });
458
458
  state.remoteDisposers.length = 0;
459
- if (remote && scope && typeof scope.dispose === 'function') void scope.dispose();
460
459
  if (state.panel && state.panel.parentNode) state.panel.parentNode.removeChild(state.panel);
461
460
  if (states) states.delete(ctx);
461
+ if (remote && scope && typeof scope.dispose === 'function') await scope.dispose();
462
462
  };
463
+ var lifecycleOwned = false;
463
464
  try {
464
- if (typeof ctx.effect === 'function') ctx.effect(function(){ return dispose; }, 'vision-router: routing settings product panel');
465
+ if (typeof ctx.effect === 'function') {
466
+ ctx.effect(function(){ return dispose; }, 'vision-router: routing settings product panel');
467
+ lifecycleOwned = true;
468
+ }
465
469
  } catch (_) {}
470
+ if (!lifecycleOwned) void dispose().catch(function(){});
466
471
  }
467
472
 
468
473
  function patchLoader(loader) {
@@ -0,0 +1,166 @@
1
+ import { BENCHMARK_AXES, } from './vision-capability-evidence.js';
2
+ export const DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS = 60 * 60 * 1000;
3
+ export const DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES = 8;
4
+ export const DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES = 2;
5
+ export const DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS = 128;
6
+ function finiteLatency(value) {
7
+ const number = Number(value);
8
+ return Number.isFinite(number) && number >= 0 ? number : undefined;
9
+ }
10
+ function median(values) {
11
+ const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
12
+ if (sorted.length === 0)
13
+ return undefined;
14
+ const middle = Math.floor(sorted.length / 2);
15
+ const right = sorted[middle];
16
+ if (right === undefined)
17
+ return undefined;
18
+ if (sorted.length % 2 === 1)
19
+ return right;
20
+ const left = sorted[middle - 1];
21
+ return left === undefined ? right : (left + right) / 2;
22
+ }
23
+ function cleanBackendKey(value) {
24
+ const text = typeof value === 'string' ? value.trim() : '';
25
+ return text === '' ? undefined : text.slice(0, 512);
26
+ }
27
+ function isBenchmarkAxis(value) {
28
+ return typeof value === 'string'
29
+ && BENCHMARK_AXES.includes(value);
30
+ }
31
+ export function createVisionRuntimePerformanceSampleStore(options = {}) {
32
+ const now = typeof options.now === 'function' ? options.now : Date.now;
33
+ const maxAgeMs = Math.max(1_000, Number(options.maxAgeMs) || DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS);
34
+ const maxSamples = Math.max(1, Math.min(64, Math.floor(Number(options.maxSamples) || DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES)));
35
+ const minSamples = Math.max(1, Math.min(maxSamples, Math.floor(Number(options.minSamples) || DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES)));
36
+ const maxBackends = Math.max(1, Math.min(1024, Math.floor(Number(options.maxBackends) || DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS)));
37
+ const records = new Map();
38
+ let identityContext = options.context;
39
+ const identityResolver = options.identityResolver;
40
+ const resolvedStorageKey = (backendKey) => {
41
+ const key = cleanBackendKey(backendKey);
42
+ if (key === undefined)
43
+ return undefined;
44
+ let identity;
45
+ try {
46
+ identity = identityContext && identityResolver !== undefined
47
+ ? identityResolver(key, identityContext)
48
+ : undefined;
49
+ }
50
+ catch {
51
+ identity = undefined;
52
+ }
53
+ return identity ? `${key}\u0000${String(identity)}` : key;
54
+ };
55
+ const pruneAxis = (samples, at) => samples.filter((sample) => at - sample.at <= maxAgeMs);
56
+ const pruneBackend = (key, at) => {
57
+ const axes = records.get(key);
58
+ if (axes === undefined)
59
+ return undefined;
60
+ for (const [axis, samples] of axes) {
61
+ const current = pruneAxis(samples, at);
62
+ if (current.length === 0)
63
+ axes.delete(axis);
64
+ else if (current.length !== samples.length)
65
+ axes.set(axis, current);
66
+ }
67
+ if (axes.size === 0) {
68
+ records.delete(key);
69
+ return undefined;
70
+ }
71
+ records.delete(key);
72
+ records.set(key, axes);
73
+ return axes;
74
+ };
75
+ const bound = () => {
76
+ while (records.size > maxBackends) {
77
+ const oldest = records.keys().next().value;
78
+ if (oldest === undefined)
79
+ break;
80
+ records.delete(oldest);
81
+ }
82
+ };
83
+ return {
84
+ maxAgeMs,
85
+ maxSamples,
86
+ minSamples,
87
+ bindContext(context) {
88
+ identityContext = context;
89
+ },
90
+ record(backendKey, axisValue, latencyMsValue, atValue = now()) {
91
+ const key = resolvedStorageKey(backendKey);
92
+ const latencyMs = finiteLatency(latencyMsValue);
93
+ const at = Number(atValue);
94
+ if (key === undefined
95
+ || !isBenchmarkAxis(axisValue)
96
+ || latencyMs === undefined
97
+ || !Number.isFinite(at)) {
98
+ return false;
99
+ }
100
+ let axes = pruneBackend(key, at);
101
+ if (axes === undefined) {
102
+ axes = new Map();
103
+ records.set(key, axes);
104
+ }
105
+ const current = pruneAxis(axes.get(axisValue) ?? [], at);
106
+ current.push({ at, latencyMs });
107
+ if (current.length > maxSamples) {
108
+ current.splice(0, current.length - maxSamples);
109
+ }
110
+ axes.set(axisValue, current);
111
+ records.delete(key);
112
+ records.set(key, axes);
113
+ bound();
114
+ return true;
115
+ },
116
+ get(backendKey, atValue = now()) {
117
+ const key = resolvedStorageKey(backendKey);
118
+ const at = Number(atValue);
119
+ if (key === undefined || !Number.isFinite(at))
120
+ return undefined;
121
+ const axes = pruneBackend(key, at);
122
+ if (axes === undefined)
123
+ return undefined;
124
+ const observedLatencyMsByAxis = {};
125
+ const runtimeLatencyMsByAxis = {};
126
+ const sampleCountByAxis = {};
127
+ const observedAtByAxis = {};
128
+ for (const [axis, samples] of axes) {
129
+ const latencyMs = median(samples.map((sample) => sample.latencyMs));
130
+ if (latencyMs === undefined)
131
+ continue;
132
+ observedLatencyMsByAxis[axis] = latencyMs;
133
+ sampleCountByAxis[axis] = samples.length;
134
+ observedAtByAxis[axis] = Math.max(...samples.map((sample) => sample.at));
135
+ if (samples.length >= minSamples)
136
+ runtimeLatencyMsByAxis[axis] = latencyMs;
137
+ }
138
+ return {
139
+ runtimeLatencyMsByAxis,
140
+ observedLatencyMsByAxis,
141
+ sampleCountByAxis,
142
+ observedAtByAxis,
143
+ maxAgeMs,
144
+ minSamples,
145
+ };
146
+ },
147
+ clear(backendKey) {
148
+ if (backendKey === undefined) {
149
+ records.clear();
150
+ return;
151
+ }
152
+ const base = cleanBackendKey(backendKey);
153
+ if (base === undefined)
154
+ return;
155
+ records.delete(base);
156
+ const prefix = `${base}\u0000`;
157
+ for (const key of [...records.keys()]) {
158
+ if (key.startsWith(prefix))
159
+ records.delete(key);
160
+ }
161
+ },
162
+ size() {
163
+ return records.size;
164
+ },
165
+ };
166
+ }
@@ -1,37 +1,19 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks'
2
- import {
3
- benchmarkAxisForVisionIntent,
4
- inferToolVisionIntent,
5
- } from './vision-capability-router.js'
2
+ import { benchmarkAxisForVisionIntent } from './vision-capability-evidence.js'
3
+ import { inferToolVisionIntent } from './vision-capability-router.js'
6
4
  import { capabilityEvidenceFingerprint } from './vision-capability-identity.js'
7
5
  import { providerTransportFor } from './live-model-discovery.js'
6
+ import { createVisionRuntimePerformanceSampleStore } from './vision-runtime-performance-store.js'
8
7
 
9
- export const DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS = 60 * 60 * 1000
10
- export const DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES = 8
11
- export const DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES = 2
12
- export const DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS = 128
8
+ export {
9
+ DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS,
10
+ DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES,
11
+ DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES,
12
+ DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS,
13
+ } from './vision-runtime-performance-store.js'
13
14
 
14
15
  const runtimeScope = new AsyncLocalStorage()
15
16
 
16
- function finiteLatency(value) {
17
- const number = Number(value)
18
- return Number.isFinite(number) && number >= 0 ? number : undefined
19
- }
20
-
21
- function median(values) {
22
- const sorted = values.filter(Number.isFinite).sort((a, b) => a - b)
23
- if (sorted.length === 0) return undefined
24
- const middle = Math.floor(sorted.length / 2)
25
- return sorted.length % 2 === 1
26
- ? sorted[middle]
27
- : (sorted[middle - 1] + sorted[middle]) / 2
28
- }
29
-
30
- function cleanBackendKey(value) {
31
- const text = typeof value === 'string' ? value.trim() : ''
32
- return text === '' ? undefined : text.slice(0, 512)
33
- }
34
-
35
17
  function backendParts(backendKey) {
36
18
  const key = cleanBackendKey(backendKey)
37
19
  if (!key) return undefined
@@ -91,124 +73,13 @@ function failureFinish(chunk) {
91
73
  }
92
74
 
93
75
  export function createVisionRuntimePerformanceStore(options = {}) {
94
- const now = typeof options.now === 'function' ? options.now : Date.now
95
- const maxAgeMs = Math.max(1_000, Number(options.maxAgeMs) || DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS)
96
- const maxSamples = Math.max(1, Math.min(64, Math.floor(Number(options.maxSamples) || DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES)))
97
- const minSamples = Math.max(1, Math.min(maxSamples, Math.floor(Number(options.minSamples) || DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES)))
98
- const maxBackends = Math.max(1, Math.min(1024, Math.floor(Number(options.maxBackends) || DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS)))
99
- const records = new Map()
100
- let identityContext = options.context
101
76
  const identityResolver = typeof options.identityResolver === 'function'
102
77
  ? options.identityResolver
103
78
  : (backendKey, ctx) => runtimePerformanceIdentityFor(ctx, backendKey)
104
-
105
- const resolvedStorageKey = (backendKey) => {
106
- const key = cleanBackendKey(backendKey)
107
- if (!key) return undefined
108
- let identity
109
- try { identity = identityContext ? identityResolver(key, identityContext) : undefined } catch { identity = undefined }
110
- return identity ? `${key}\u0000${identity}` : key
111
- }
112
-
113
- const pruneAxis = (samples, at) => samples.filter((sample) => at - sample.at <= maxAgeMs)
114
-
115
- const pruneBackend = (key, at) => {
116
- const axes = records.get(key)
117
- if (!axes) return undefined
118
- for (const [axis, samples] of axes) {
119
- const current = pruneAxis(samples, at)
120
- if (current.length === 0) axes.delete(axis)
121
- else if (current.length !== samples.length) axes.set(axis, current)
122
- }
123
- if (axes.size === 0) {
124
- records.delete(key)
125
- return undefined
126
- }
127
- // LRU touch only on runtime-observation reads/writes; routing diagnostics
128
- // are allowed to read this performance store because it is not the v1
129
- // breaker and has no execution side effects.
130
- records.delete(key)
131
- records.set(key, axes)
132
- return axes
133
- }
134
-
135
- const bound = () => {
136
- while (records.size > maxBackends) {
137
- const oldest = records.keys().next().value
138
- if (oldest === undefined) break
139
- records.delete(oldest)
140
- }
141
- }
142
-
143
- return {
144
- maxAgeMs,
145
- maxSamples,
146
- minSamples,
147
- bindContext(ctx) {
148
- identityContext = ctx
149
- },
150
- record(backendKey, axis, latencyMs, at = now()) {
151
- const key = resolvedStorageKey(backendKey)
152
- const latency = finiteLatency(latencyMs)
153
- const timestamp = Number(at)
154
- if (!key || !benchmarkAxisForVisionIntent(axis) || latency === undefined || !Number.isFinite(timestamp)) return false
155
- let axes = pruneBackend(key, timestamp)
156
- if (!axes) {
157
- axes = new Map()
158
- records.set(key, axes)
159
- }
160
- const current = pruneAxis(axes.get(axis) ?? [], timestamp)
161
- current.push({ at: timestamp, latencyMs: latency })
162
- if (current.length > maxSamples) current.splice(0, current.length - maxSamples)
163
- axes.set(axis, current)
164
- records.delete(key)
165
- records.set(key, axes)
166
- bound()
167
- return true
168
- },
169
- get(backendKey, at = now()) {
170
- const key = resolvedStorageKey(backendKey)
171
- const timestamp = Number(at)
172
- if (!key || !Number.isFinite(timestamp)) return undefined
173
- const axes = pruneBackend(key, timestamp)
174
- if (!axes) return undefined
175
- const observedLatencyMsByAxis = {}
176
- const runtimeLatencyMsByAxis = {}
177
- const sampleCountByAxis = {}
178
- const observedAtByAxis = {}
179
- for (const [axis, samples] of axes) {
180
- const latencies = samples.map((sample) => sample.latencyMs)
181
- const value = median(latencies)
182
- if (value === undefined) continue
183
- observedLatencyMsByAxis[axis] = value
184
- sampleCountByAxis[axis] = samples.length
185
- observedAtByAxis[axis] = Math.max(...samples.map((sample) => sample.at))
186
- if (samples.length >= minSamples) runtimeLatencyMsByAxis[axis] = value
187
- }
188
- return {
189
- runtimeLatencyMsByAxis,
190
- observedLatencyMsByAxis,
191
- sampleCountByAxis,
192
- observedAtByAxis,
193
- maxAgeMs,
194
- minSamples,
195
- }
196
- },
197
- clear(backendKey) {
198
- if (backendKey === undefined) {
199
- records.clear()
200
- return
201
- }
202
- const base = cleanBackendKey(backendKey)
203
- if (!base) return
204
- records.delete(base)
205
- const prefix = `${base}\u0000`
206
- for (const key of [...records.keys()]) if (key.startsWith(prefix)) records.delete(key)
207
- },
208
- size() {
209
- return records.size
210
- },
211
- }
79
+ return createVisionRuntimePerformanceSampleStore({
80
+ ...options,
81
+ identityResolver,
82
+ })
212
83
  }
213
84
 
214
85
  export function withVisionRuntimePerformanceScope(toolName, args, fn) {