dsh-vision-router 2.3.0 → 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 (103) hide show
  1. package/README.md +12 -3
  2. package/README.zh.md +12 -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 +441 -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 +5 -3
  15. package/docs/releases/v3.0.0.md +85 -0
  16. package/entry.js +1 -1
  17. package/index.js +108 -71
  18. package/lib/agent-request-route-authority.js +1 -20
  19. package/lib/core-apply-lifecycle.js +37 -43
  20. package/lib/core-cache.js +87 -0
  21. package/lib/core-primitives.js +3 -143
  22. package/lib/core-vision-surface-domain.js +63 -0
  23. package/lib/core-vision-surface.js +11 -77
  24. package/lib/delegated-call-config.js +15 -24
  25. package/lib/doctor-cli.js +0 -0
  26. package/lib/doctor-runtime.js +4 -1
  27. package/lib/dsh-contract-compat.js +26 -230
  28. package/lib/dsh-host-capabilities.js +3 -9
  29. package/lib/dsh-settings-017-compat.js +17 -10
  30. package/lib/file-logger.js +8 -3
  31. package/lib/grounding-coordinate-runtime.js +1 -2
  32. package/lib/http-body-limit.js +97 -79
  33. package/lib/image-offload-compat.js +30 -21
  34. package/lib/image-resource-governor.js +210 -228
  35. package/lib/legacy-global-proxy-boundary.js +46 -3
  36. package/lib/live-model-client-prelude.js +12 -4
  37. package/lib/live-model-discovery.js +134 -43
  38. package/lib/local-vision-stabilizer.js +31 -17
  39. package/lib/maintenance-routes.js +1 -3
  40. package/lib/native-image-coexistence.js +15 -3
  41. package/lib/ollama-cold-start.js +87 -23
  42. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  43. package/lib/proxy-dispatcher-pool.js +151 -92
  44. package/lib/proxy-routing.js +75 -51
  45. package/lib/proxy-url-compat.js +3 -10
  46. package/lib/public-config.js +23 -44
  47. package/lib/public-entry.d.ts +148 -0
  48. package/lib/public-entry.js +17 -4
  49. package/lib/replay-delegation.js +12 -7
  50. package/lib/replay-envelope-v2-compat.js +12 -7
  51. package/lib/runtime-composition.js +22 -4
  52. package/lib/screenshot-source-boundary.js +2 -2
  53. package/lib/session-affinity-runtime.js +113 -97
  54. package/lib/session-affinity.js +89 -76
  55. package/lib/session-event-history-compat.js +30 -0
  56. package/lib/session-query-readers.js +263 -0
  57. package/lib/session-surface-compat.js +30 -31
  58. package/lib/session-surface-policy-domain.js +85 -0
  59. package/lib/session-surface-policy.js +2 -111
  60. package/lib/session-turn-resolver.js +2 -6
  61. package/lib/session-vision-attachment-index.js +143 -0
  62. package/lib/session-vision-delegation-boundary.js +34 -0
  63. package/lib/session-vision-delegation.js +22 -0
  64. package/lib/session-vision-index.js +25 -107
  65. package/lib/session-vision-mode-authority.js +229 -192
  66. package/lib/session-vision-mode-boundary.js +65 -22
  67. package/lib/session-vision-policy-domain.js +304 -0
  68. package/lib/session-vision-policy.js +104 -0
  69. package/lib/session-vision-state.js +368 -378
  70. package/lib/settings-client-017-compat.js +9 -7
  71. package/lib/structured-flow-hardening.js +22 -4
  72. package/lib/tesseract-exec-compat.js +9 -1
  73. package/lib/turn-budget-context.js +47 -41
  74. package/lib/update-check.js +11 -2
  75. package/lib/vision-attachment-handle-runtime.js +176 -156
  76. package/lib/vision-backend-runtime-policy.js +16 -0
  77. package/lib/vision-background-benchmark.js +41 -4
  78. package/lib/vision-background-failure-policy.js +41 -41
  79. package/lib/vision-background-stop-store-core.js +217 -0
  80. package/lib/vision-background-stop-store.js +8 -179
  81. package/lib/vision-capability-benchmark.js +11 -61
  82. package/lib/vision-capability-evidence.js +35 -0
  83. package/lib/vision-capability-fingerprint.js +72 -0
  84. package/lib/vision-capability-identity.js +116 -61
  85. package/lib/vision-capability-router.js +13 -33
  86. package/lib/vision-chunk-assembler.js +95 -0
  87. package/lib/vision-diagnostics-routes.js +34 -4
  88. package/lib/vision-execution-order-apply.js +45 -55
  89. package/lib/vision-execution-order.js +19 -19
  90. package/lib/vision-execution-policy.js +12 -7
  91. package/lib/vision-model-registry.js +30 -17
  92. package/lib/vision-provider-transport.js +137 -147
  93. package/lib/vision-routing-authority.js +47 -48
  94. package/lib/vision-routing-product.js +26 -29
  95. package/lib/vision-routing-settings-prelude.js +8 -3
  96. package/lib/vision-runtime-performance-store.js +166 -0
  97. package/lib/vision-runtime-performance.js +13 -142
  98. package/lib/vision-tool-runtime-boundary.js +99 -30
  99. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  100. package/lib/web/remote-settings-client.js +4 -7
  101. package/lib/web-capability-boundary.js +17 -7
  102. package/lib/wrapper-directory.js +14 -23
  103. package/package.json +37 -11
@@ -1,21 +1,21 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks'
2
-
3
- const executionOrderScope = new AsyncLocalStorage()
4
-
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const executionOrderScope = new AsyncLocalStorage();
5
3
  function normalizedPair(pair) {
6
- const provider = typeof pair?.provider === 'string' ? pair.provider.trim() : ''
7
- const model = typeof pair?.model === 'string' ? pair.model.trim() : ''
8
- if (provider === '' || model === '') {
9
- throw new TypeError('vision execution order entries require non-empty provider and model')
10
- }
11
- return Object.freeze({ provider, model })
4
+ const value = pair !== null && typeof pair === 'object'
5
+ ? pair
6
+ : {};
7
+ const provider = typeof value.provider === 'string' ? value.provider.trim() : '';
8
+ const model = typeof value.model === 'string' ? value.model.trim() : '';
9
+ if (provider === '' || model === '') {
10
+ throw new TypeError('vision execution order entries require non-empty provider and model');
11
+ }
12
+ return Object.freeze({ provider, model });
12
13
  }
13
-
14
14
  function normalizedOrder(order) {
15
- if (!Array.isArray(order)) throw new TypeError('vision execution order must be an array')
16
- return Object.freeze(order.map(normalizedPair))
15
+ if (!Array.isArray(order))
16
+ throw new TypeError('vision execution order must be an array');
17
+ return Object.freeze(order.map(normalizedPair));
17
18
  }
18
-
19
19
  /**
20
20
  * Run one Router-owned visual execution with an explicit provider/model order.
21
21
  *
@@ -24,15 +24,15 @@ function normalizedOrder(order) {
24
24
  * Host services, or mutable caller objects.
25
25
  */
26
26
  export function withVisionExecutionOrder(order, fn) {
27
- if (typeof fn !== 'function') throw new TypeError('withVisionExecutionOrder requires a function')
28
- const scopedOrder = normalizedOrder(order)
29
- return executionOrderScope.run({ order: scopedOrder }, fn)
27
+ if (typeof fn !== 'function')
28
+ throw new TypeError('withVisionExecutionOrder requires a function');
29
+ const scopedOrder = normalizedOrder(order);
30
+ return executionOrderScope.run({ order: scopedOrder }, fn);
30
31
  }
31
-
32
32
  /**
33
33
  * Current Router-owned visual order for this async call chain, or undefined
34
34
  * outside an explicit visual execution scope.
35
35
  */
36
36
  export function currentVisionExecutionOrder() {
37
- return executionOrderScope.getStore()?.order
37
+ return executionOrderScope.getStore()?.order;
38
38
  }
@@ -517,15 +517,20 @@ export function contextWithVisionExecutionPolicy(ctx, options = {}) {
517
517
  },
518
518
  })
519
519
  wrappedContexts.set(ctx, wrapped)
520
+ let lifecycleOwned = false
520
521
  try {
521
- ctx.effect?.(
522
- () => () => {
523
- if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
524
- },
525
- 'vision-router: strict vision execution policy context',
526
- )
522
+ if (typeof ctx.effect === 'function') {
523
+ ctx.effect(
524
+ () => () => {
525
+ if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
526
+ },
527
+ 'vision-router: strict vision execution policy context',
528
+ )
529
+ lifecycleOwned = true
530
+ }
527
531
  } catch {
528
- /* lifecycle hardening must never block plugin apply */
532
+ // The current caller may still use this private view.
529
533
  }
534
+ if (!lifecycleOwned && wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
530
535
  return wrapped
531
536
  }
@@ -222,16 +222,23 @@ export function installVisionModelRegistry(ctx, liveDiscovery, options = {}) {
222
222
  if (!liveDiscovery || typeof liveDiscovery.snapshot !== 'function') return liveDiscovery
223
223
  if (liveDiscovery.snapshot.__visionRouterRegistry === true) return liveDiscovery
224
224
 
225
- const originalSnapshot = liveDiscovery.snapshot.bind(liveDiscovery)
225
+ const originalSnapshot = liveDiscovery.snapshot
226
226
  const originalHasModel = typeof liveDiscovery.hasModel === 'function'
227
- ? liveDiscovery.hasModel.bind(liveDiscovery)
228
- : () => false
227
+ ? liveDiscovery.hasModel
228
+ : undefined
229
229
  const originalEvidenceSource = typeof liveDiscovery.evidenceSource === 'function'
230
- ? liveDiscovery.evidenceSource.bind(liveDiscovery)
230
+ ? liveDiscovery.evidenceSource
231
231
  : undefined
232
+ const callSnapshot = (request) => Reflect.apply(originalSnapshot, liveDiscovery, [request])
233
+ const callHasModel = (provider, model) => originalHasModel === undefined
234
+ ? false
235
+ : Reflect.apply(originalHasModel, liveDiscovery, [provider, model])
236
+ const callEvidenceSource = (provider, model) => originalEvidenceSource === undefined
237
+ ? undefined
238
+ : Reflect.apply(originalEvidenceSource, liveDiscovery, [provider, model])
232
239
 
233
240
  const wrappedSnapshot = async (request) => decorateVisionModelSnapshot(
234
- await originalSnapshot(request),
241
+ await callSnapshot(request),
235
242
  { ctx, config: options.config ?? {} },
236
243
  )
237
244
  // Diagnostics-only provenance. This must never become the admission oracle:
@@ -239,15 +246,15 @@ export function installVisionModelRegistry(ctx, liveDiscovery, options = {}) {
239
246
  // endpoint evidence OR exact endpoint-scoped trusted hint).
240
247
  const wrappedEvidenceSource = (provider, model) => {
241
248
  if (isProviderActive(ctx, provider) && hasTrustedVisionHint(ctx, provider, model)) return 'known'
242
- if (originalHasModel(provider, model)) return 'live'
249
+ if (callHasModel(provider, model)) return 'live'
243
250
  try {
244
- return originalEvidenceSource?.(provider, model)
251
+ return callEvidenceSource(provider, model)
245
252
  } catch {
246
253
  return undefined
247
254
  }
248
255
  }
249
256
  const wrappedHasModel = (provider, model) => (
250
- originalHasModel(provider, model) ||
257
+ callHasModel(provider, model) ||
251
258
  (isProviderActive(ctx, provider) && hasTrustedVisionHint(ctx, provider, model))
252
259
  )
253
260
 
@@ -258,17 +265,23 @@ export function installVisionModelRegistry(ctx, liveDiscovery, options = {}) {
258
265
  liveDiscovery.hasModel = wrappedHasModel
259
266
  liveDiscovery.evidenceSource = wrappedEvidenceSource
260
267
 
268
+ const restore = () => {
269
+ if (liveDiscovery.snapshot === wrappedSnapshot) liveDiscovery.snapshot = originalSnapshot
270
+ if (liveDiscovery.hasModel === wrappedHasModel) {
271
+ if (originalHasModel === undefined) delete liveDiscovery.hasModel
272
+ else liveDiscovery.hasModel = originalHasModel
273
+ }
274
+ if (liveDiscovery.evidenceSource === wrappedEvidenceSource) {
275
+ if (originalEvidenceSource === undefined) delete liveDiscovery.evidenceSource
276
+ else liveDiscovery.evidenceSource = originalEvidenceSource
277
+ }
278
+ }
261
279
  try {
262
- ctx?.effect?.(() => () => {
263
- if (liveDiscovery.snapshot === wrappedSnapshot) liveDiscovery.snapshot = originalSnapshot
264
- if (liveDiscovery.hasModel === wrappedHasModel) liveDiscovery.hasModel = originalHasModel
265
- if (liveDiscovery.evidenceSource === wrappedEvidenceSource) {
266
- if (originalEvidenceSource === undefined) delete liveDiscovery.evidenceSource
267
- else liveDiscovery.evidenceSource = originalEvidenceSource
268
- }
269
- }, 'vision-router: private model registry sources')
280
+ ctx?.effect?.(() => restore, 'vision-router: private model registry sources')
270
281
  } catch {
271
- // The manager itself is disposed with the plugin; restoration is hygiene.
282
+ // The manager can outlive this failed composition attempt. Do not leave
283
+ // generation-specific method projections installed without an owner.
284
+ restore()
272
285
  }
273
286
  return liveDiscovery
274
287
  }
@@ -1,77 +1,76 @@
1
- import {
2
- ERROR_RESPONSE_MAX_BYTES,
3
- MODEL_RESPONSE_MAX_BYTES,
4
- readResponseJsonBounded,
5
- readResponseTextBounded,
6
- } from './http-body-limit.js'
7
- import { effectiveProxyUrlForUndici } from './proxy-url-compat.js'
8
- import { createProxyDispatcherPool } from './proxy-dispatcher-pool.js'
9
- import {
10
- createPerHopProxyDispatcher,
11
- proxyDispatcherLike,
12
- proxyHostMatchesAny,
13
- visionProxyOverrideConfigured,
14
- } from './proxy-routing.js'
15
-
1
+ import { ERROR_RESPONSE_MAX_BYTES, MODEL_RESPONSE_MAX_BYTES, readResponseJsonBounded, readResponseTextBounded, } from './http-body-limit.js';
2
+ import { effectiveProxyUrlForUndici } from './proxy-url-compat.js';
3
+ import { createProxyDispatcherPool } from './proxy-dispatcher-pool.js';
4
+ import { createPerHopProxyDispatcher, proxyDispatcherLike, proxyHostMatchesAny, visionProxyOverrideConfigured, } from './proxy-routing.js';
16
5
  const DEFAULT_PROXY_HOSTS = Object.freeze([
17
- 'api.openrouter.ai',
18
- 'openrouter.ai',
19
- 'api.openai.com',
20
- 'api.anthropic.com',
21
- 'api.groq.com',
22
- 'api.mistral.ai',
23
- 'api.together.xyz',
24
- 'generativelanguage.googleapis.com',
25
- 'api.x.ai',
26
- ])
27
-
28
- // Capture before core.apply installs the legacy process-wide fetch patch. The
29
- // Router-owned transport uses this original function explicitly, so direct
30
- // provider calls are not coupled to later globalThis.fetch mutation.
31
- const moduleFetch =
32
- typeof globalThis.fetch === 'function'
6
+ 'api.openrouter.ai',
7
+ 'openrouter.ai',
8
+ 'api.openai.com',
9
+ 'api.anthropic.com',
10
+ 'api.groq.com',
11
+ 'api.mistral.ai',
12
+ 'api.together.xyz',
13
+ 'generativelanguage.googleapis.com',
14
+ 'api.x.ai',
15
+ ]);
16
+ const capturedHostFetch = typeof globalThis.fetch === 'function'
33
17
  ? globalThis.fetch.bind(globalThis)
34
- : undefined
35
-
18
+ : undefined;
19
+ const moduleFetch = capturedHostFetch
20
+ ? (input, init) => capturedHostFetch(input, init)
21
+ : undefined;
22
+ function objectRecord(value) {
23
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
24
+ ? value
25
+ : undefined;
26
+ }
36
27
  function live(value) {
37
- return typeof value === 'function' ? value() : value
28
+ return typeof value === 'function' ? value() : value;
38
29
  }
39
-
40
30
  function asObject(value) {
41
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
31
+ return objectRecord(value) ?? {};
42
32
  }
43
-
44
33
  function asUrl(input) {
45
- try {
46
- return input instanceof URL ? input : new URL(typeof input === 'string' ? input : input?.url)
47
- } catch {
48
- return undefined
49
- }
34
+ try {
35
+ if (input instanceof URL)
36
+ return input;
37
+ if (typeof input === 'string')
38
+ return new URL(input);
39
+ const raw = objectRecord(input)?.url;
40
+ return typeof raw === 'string' ? new URL(raw) : undefined;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
50
45
  }
51
-
52
46
  function proxySettings(config) {
53
- const value = asObject(live(config))
54
- const proxy = visionProxyOverrideConfigured(value) ? value.proxy.trim() : undefined
55
- const proxyHosts = Array.isArray(value.proxyHosts)
56
- ? value.proxyHosts.filter((host) => typeof host === 'string' && host.trim() !== '').map((host) => host.trim())
57
- : [...DEFAULT_PROXY_HOSTS]
58
- return { proxy, proxyHosts }
47
+ const value = asObject(live(config));
48
+ const rawProxy = value.proxy;
49
+ const proxy = visionProxyOverrideConfigured(value) && typeof rawProxy === 'string'
50
+ ? rawProxy.trim()
51
+ : undefined;
52
+ const proxyHosts = Array.isArray(value.proxyHosts)
53
+ ? value.proxyHosts
54
+ .filter((host) => typeof host === 'string' && host.trim() !== '')
55
+ .map((host) => host.trim())
56
+ : [...DEFAULT_PROXY_HOSTS];
57
+ return { proxy, proxyHosts };
59
58
  }
60
-
61
59
  function credentialService(ctx) {
62
- try {
63
- return ctx?.get?.('credentials')
64
- } catch {
65
- return undefined
66
- }
60
+ try {
61
+ const service = ctx?.get?.('credentials');
62
+ return service !== null && (typeof service === 'object' || typeof service === 'function')
63
+ ? service
64
+ : undefined;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
67
69
  }
68
-
69
70
  function envCredential(ref) {
70
- if (typeof process === 'undefined' || !process.env) return undefined
71
- const value = process.env[ref]
72
- return typeof value === 'string' && value !== '' ? value : undefined
71
+ const value = process.env[ref];
72
+ return typeof value === 'string' && value !== '' ? value : undefined;
73
73
  }
74
-
75
74
  /**
76
75
  * Router-owned provider transport.
77
76
  *
@@ -82,94 +81,85 @@ function envCredential(ref) {
82
81
  * policy remains authoritative. It intentionally does not cover update checks,
83
82
  * GitHub/npm maintenance traffic or Host-owned LLM adapter requests.
84
83
  */
85
- export function createVisionProviderTransport({
86
- ctx,
87
- config = {},
88
- fetchImpl = moduleFetch,
89
- importUndici = () => import('undici'),
90
- } = {}) {
91
- if (typeof fetchImpl !== 'function') {
92
- throw new TypeError('vision provider transport requires fetch')
93
- }
94
-
95
- let disposed = false
96
- let disposePromise
97
- const dispatcherPool = createProxyDispatcherPool({
98
- importUndici,
99
- label: 'vision provider transport',
100
- })
101
-
102
- const fetchProvider = async (input, init = {}, context = {}) => {
103
- if (disposed) return fetchImpl(input, init)
104
- const url = asUrl(input)
105
- const { proxy, proxyHosts } = proxySettings(config)
106
- const effectiveProxyUrl = proxy ? effectiveProxyUrlForUndici(proxy) : undefined
107
- dispatcherPool.reconcile(effectiveProxyUrl)
108
- if (!url || !effectiveProxyUrl || context.allowProxy === false || !proxyHostMatchesAny(url.hostname, proxyHosts)) {
109
- return fetchImpl(input, init)
110
- }
111
- const lease = await dispatcherPool.acquire(effectiveProxyUrl)
112
- try {
113
- const fallbackDispatcher = proxyDispatcherLike(init?.dispatcher)
114
- ? init.dispatcher
115
- : lease.getGlobalDispatcher()
116
- const dispatcher = createPerHopProxyDispatcher(lease.dispatcher, fallbackDispatcher, proxyHosts)
117
- return await fetchImpl(input, { ...init, dispatcher })
118
- } finally {
119
- lease.release()
84
+ export function createVisionProviderTransport({ ctx, config = {}, fetchImpl = moduleFetch, importUndici = () => import('undici'), } = {}) {
85
+ if (typeof fetchImpl !== 'function') {
86
+ throw new TypeError('vision provider transport requires fetch');
120
87
  }
121
- }
122
-
123
- const dispose = () => {
124
- if (disposePromise) return disposePromise
125
- disposed = true
126
- disposePromise = dispatcherPool.dispose()
127
- return disposePromise
128
- }
129
-
130
- return Object.freeze({
131
- fetch: fetchProvider,
132
- dispose,
133
-
134
- async resolveCredential(ref) {
135
- const name = typeof ref === 'string' ? ref.trim() : ''
136
- if (!name) return undefined
137
- const credentials = credentialService(ctx)
138
- if (credentials && typeof credentials.resolve === 'function') {
88
+ let disposed = false;
89
+ let disposePromise;
90
+ const dispatcherPool = createProxyDispatcherPool({
91
+ importUndici,
92
+ label: 'vision provider transport',
93
+ });
94
+ const fetchProvider = async (input, init = {}, context = {}) => {
95
+ if (disposed)
96
+ return fetchImpl(input, init);
97
+ const url = asUrl(input);
98
+ const { proxy, proxyHosts } = proxySettings(config);
99
+ const effectiveProxyUrl = proxy ? effectiveProxyUrlForUndici(proxy) : undefined;
100
+ dispatcherPool.reconcile(effectiveProxyUrl);
101
+ if (!url
102
+ || !effectiveProxyUrl
103
+ || context.allowProxy === false
104
+ || !proxyHostMatchesAny(url.hostname, proxyHosts)) {
105
+ return fetchImpl(input, init);
106
+ }
107
+ const lease = await dispatcherPool.acquire(effectiveProxyUrl);
139
108
  try {
140
- const hit = await credentials.resolve(name)
141
- if (hit && typeof hit.value === 'string' && hit.value !== '') return hit.value
142
- } catch {
143
- // Environment remains the compatibility fallback.
109
+ const fallbackDispatcher = proxyDispatcherLike(init.dispatcher)
110
+ ? init.dispatcher
111
+ : lease.getGlobalDispatcher();
112
+ const dispatcher = createPerHopProxyDispatcher(lease.dispatcher, fallbackDispatcher, proxyHosts);
113
+ return await fetchImpl(input, { ...init, dispatcher });
114
+ }
115
+ finally {
116
+ lease.release();
144
117
  }
145
- }
146
- return envCredential(name)
147
- },
148
-
149
- readErrorText(response, options = {}) {
150
- return readResponseTextBounded(
151
- response,
152
- options.maxBytes ?? ERROR_RESPONSE_MAX_BYTES,
153
- { label: options.label ?? 'vision provider error response' },
154
- )
155
- },
156
-
157
- readModelJson(response, options = {}) {
158
- return readResponseJsonBounded(
159
- response,
160
- options.maxBytes ?? MODEL_RESPONSE_MAX_BYTES,
161
- { label: options.label ?? 'vision provider response' },
162
- )
163
- },
164
-
165
- proxyDecision(input) {
166
- const url = asUrl(input)
167
- const { proxy, proxyHosts } = proxySettings(config)
168
- return Object.freeze({
169
- proxied: Boolean(url && proxy && proxyHostMatchesAny(url.hostname, proxyHosts)),
170
- proxy,
171
- hostname: url?.hostname,
172
- })
173
- },
174
- })
118
+ };
119
+ const dispose = () => {
120
+ if (disposePromise)
121
+ return disposePromise;
122
+ disposed = true;
123
+ disposePromise = dispatcherPool.dispose();
124
+ return disposePromise;
125
+ };
126
+ return Object.freeze({
127
+ fetch: fetchProvider,
128
+ dispose,
129
+ async resolveCredential(ref) {
130
+ const name = typeof ref === 'string' ? ref.trim() : '';
131
+ if (!name)
132
+ return undefined;
133
+ const credentials = credentialService(ctx);
134
+ try {
135
+ const resolve = credentials?.resolve;
136
+ if (typeof resolve === 'function') {
137
+ const hit = await Reflect.apply(resolve, credentials, [name]);
138
+ const value = objectRecord(hit)?.value;
139
+ if (typeof value === 'string' && value !== '')
140
+ return value;
141
+ }
142
+ }
143
+ catch {
144
+ // A missing/partial/throwing Host credential service must not suppress
145
+ // the supported environment-variable compatibility fallback.
146
+ }
147
+ return envCredential(name);
148
+ },
149
+ readErrorText(response, options = {}) {
150
+ return readResponseTextBounded(response, options.maxBytes ?? ERROR_RESPONSE_MAX_BYTES, { label: options.label ?? 'vision provider error response' });
151
+ },
152
+ readModelJson(response, options = {}) {
153
+ return readResponseJsonBounded(response, options.maxBytes ?? MODEL_RESPONSE_MAX_BYTES, { label: options.label ?? 'vision provider response' });
154
+ },
155
+ proxyDecision(input) {
156
+ const url = asUrl(input);
157
+ const { proxy, proxyHosts } = proxySettings(config);
158
+ return Object.freeze({
159
+ proxied: Boolean(url && proxy && proxyHostMatchesAny(url.hostname, proxyHosts)),
160
+ proxy,
161
+ hostname: url?.hostname,
162
+ });
163
+ },
164
+ });
175
165
  }
@@ -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
  }