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
@@ -1,61 +1,51 @@
1
1
  function pairIdentity(pair) {
2
- const provider = typeof pair?.provider === 'string' ? pair.provider.trim() : ''
3
- const model = typeof pair?.model === 'string' ? pair.model.trim() : ''
4
- return provider !== '' && model !== '' ? `${provider}\u0000${model}` : undefined
2
+ const value = pair !== null && typeof pair === 'object'
3
+ ? pair
4
+ : {};
5
+ const provider = typeof value.provider === 'string' ? value.provider.trim() : '';
6
+ const model = typeof value.model === 'string' ? value.model.trim() : '';
7
+ return provider !== '' && model !== '' ? `${provider}\u0000${model}` : undefined;
5
8
  }
6
-
7
- /**
8
- * Apply an explicit P1 execution order to a base pair list without changing
9
- * the base set of executable routes.
10
- *
11
- * This is intentionally a stable reorder, not replacement:
12
- * - a scoped pair not present in `basePairs` is ignored (cannot invent a route);
13
- * - every base pair omitted by the planner is appended in its original order
14
- * (cannot delete configured/local/discovered fallback);
15
- * - duplicate base/scoped identities collapse to the first base occurrence;
16
- * - no scoped order means byte-for-shape legacy behavior: a detached copy of
17
- * the base list in the same order.
18
- *
19
- * The caller remains responsible for constructing `basePairs` with its normal
20
- * adapter/HTTP/local availability rules. This helper owns ordering only.
21
- */
22
9
  export function applyVisionExecutionOrder(basePairs, scopedOrder) {
23
- const base = Array.isArray(basePairs)
24
- ? basePairs.filter((pair) => pairIdentity(pair) !== undefined)
25
- : []
26
- const baseById = new Map()
27
- for (const pair of base) {
28
- const id = pairIdentity(pair)
29
- if (!baseById.has(id)) baseById.set(id, pair)
30
- }
31
-
32
- if (!Array.isArray(scopedOrder) || scopedOrder.length === 0) {
33
- return [...baseById.values()]
34
- }
35
-
36
- const out = []
37
- const seen = new Set()
38
- const addBasePair = (pair) => {
39
- const id = pairIdentity(pair)
40
- if (id === undefined || seen.has(id)) return
41
- seen.add(id)
42
- out.push(pair)
43
- }
44
-
45
- for (const requested of scopedOrder) {
46
- const id = pairIdentity(requested)
47
- if (id === undefined) continue
48
- const pair = baseById.get(id)
49
- if (pair !== undefined) addBasePair(pair)
50
- }
51
- for (const pair of baseById.values()) addBasePair(pair)
52
- return out
10
+ const base = Array.isArray(basePairs)
11
+ ? basePairs.filter((pair) => pairIdentity(pair) !== undefined)
12
+ : [];
13
+ const baseById = new Map();
14
+ for (const pair of base) {
15
+ const id = pairIdentity(pair);
16
+ if (id !== undefined && !baseById.has(id))
17
+ baseById.set(id, pair);
18
+ }
19
+ if (!Array.isArray(scopedOrder) || scopedOrder.length === 0) {
20
+ return [...baseById.values()];
21
+ }
22
+ const out = [];
23
+ const seen = new Set();
24
+ const addBasePair = (pair) => {
25
+ const id = pairIdentity(pair);
26
+ if (id === undefined || seen.has(id))
27
+ return;
28
+ seen.add(id);
29
+ out.push(pair);
30
+ };
31
+ for (const requested of scopedOrder) {
32
+ const id = pairIdentity(requested);
33
+ if (id === undefined)
34
+ continue;
35
+ const pair = baseById.get(id);
36
+ if (pair !== undefined)
37
+ addBasePair(pair);
38
+ }
39
+ for (const pair of baseById.values())
40
+ addBasePair(pair);
41
+ return out;
53
42
  }
54
-
55
43
  export function sameVisionPairOrder(left, right) {
56
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false
57
- for (let index = 0; index < left.length; index += 1) {
58
- if (pairIdentity(left[index]) !== pairIdentity(right[index])) return false
59
- }
60
- return true
44
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
45
+ return false;
46
+ for (let index = 0; index < left.length; index += 1) {
47
+ if (pairIdentity(left[index]) !== pairIdentity(right[index]))
48
+ return false;
49
+ }
50
+ return true;
61
51
  }
@@ -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,79 +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
-
36
- const installed = []
37
-
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
+ }
38
27
  function live(value) {
39
- return typeof value === 'function' ? value() : value
28
+ return typeof value === 'function' ? value() : value;
40
29
  }
41
-
42
30
  function asObject(value) {
43
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
31
+ return objectRecord(value) ?? {};
44
32
  }
45
-
46
33
  function asUrl(input) {
47
- try {
48
- return input instanceof URL ? input : new URL(typeof input === 'string' ? input : input?.url)
49
- } catch {
50
- return undefined
51
- }
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
+ }
52
45
  }
53
-
54
46
  function proxySettings(config) {
55
- const value = asObject(live(config))
56
- const proxy = visionProxyOverrideConfigured(value) ? value.proxy.trim() : undefined
57
- const proxyHosts = Array.isArray(value.proxyHosts)
58
- ? value.proxyHosts.filter((host) => typeof host === 'string' && host.trim() !== '').map((host) => host.trim())
59
- : [...DEFAULT_PROXY_HOSTS]
60
- 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 };
61
58
  }
62
-
63
59
  function credentialService(ctx) {
64
- try {
65
- return ctx?.get?.('credentials')
66
- } catch {
67
- return undefined
68
- }
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
+ }
69
69
  }
70
-
71
70
  function envCredential(ref) {
72
- if (typeof process === 'undefined' || !process.env) return undefined
73
- const value = process.env[ref]
74
- return typeof value === 'string' && value !== '' ? value : undefined
71
+ const value = process.env[ref];
72
+ return typeof value === 'string' && value !== '' ? value : undefined;
75
73
  }
76
-
77
74
  /**
78
75
  * Router-owned provider transport.
79
76
  *
@@ -84,121 +81,85 @@ function envCredential(ref) {
84
81
  * policy remains authoritative. It intentionally does not cover update checks,
85
82
  * GitHub/npm maintenance traffic or Host-owned LLM adapter requests.
86
83
  */
87
- export function createVisionProviderTransport({
88
- ctx,
89
- config = {},
90
- fetchImpl = moduleFetch,
91
- importUndici = () => import('undici'),
92
- } = {}) {
93
- if (typeof fetchImpl !== 'function') {
94
- throw new TypeError('vision provider transport requires fetch')
95
- }
96
-
97
- let disposed = false
98
- let disposePromise
99
- const dispatcherPool = createProxyDispatcherPool({
100
- importUndici,
101
- label: 'vision provider transport',
102
- })
103
-
104
- const fetchProvider = async (input, init = {}, context = {}) => {
105
- if (disposed) return fetchImpl(input, init)
106
- const url = asUrl(input)
107
- const { proxy, proxyHosts } = proxySettings(config)
108
- const effectiveProxyUrl = proxy ? effectiveProxyUrlForUndici(proxy) : undefined
109
- dispatcherPool.reconcile(effectiveProxyUrl)
110
- if (!url || !effectiveProxyUrl || context.allowProxy === false || !proxyHostMatchesAny(url.hostname, proxyHosts)) {
111
- return fetchImpl(input, init)
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');
112
87
  }
113
- const lease = await dispatcherPool.acquire(effectiveProxyUrl)
114
- try {
115
- const fallbackDispatcher = proxyDispatcherLike(init?.dispatcher)
116
- ? init.dispatcher
117
- : lease.getGlobalDispatcher()
118
- const dispatcher = createPerHopProxyDispatcher(lease.dispatcher, fallbackDispatcher, proxyHosts)
119
- return await fetchImpl(input, { ...init, dispatcher })
120
- } finally {
121
- lease.release()
122
- }
123
- }
124
-
125
- const dispose = () => {
126
- if (disposePromise) return disposePromise
127
- disposed = true
128
- disposePromise = dispatcherPool.dispose()
129
- return disposePromise
130
- }
131
-
132
- return Object.freeze({
133
- fetch: fetchProvider,
134
- dispose,
135
-
136
- async resolveCredential(ref) {
137
- const name = typeof ref === 'string' ? ref.trim() : ''
138
- if (!name) return undefined
139
- const credentials = credentialService(ctx)
140
- 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);
141
108
  try {
142
- const hit = await credentials.resolve(name)
143
- if (hit && typeof hit.value === 'string' && hit.value !== '') return hit.value
144
- } catch {
145
- // 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 });
146
114
  }
147
- }
148
- return envCredential(name)
149
- },
150
-
151
- readErrorText(response, options = {}) {
152
- return readResponseTextBounded(
153
- response,
154
- options.maxBytes ?? ERROR_RESPONSE_MAX_BYTES,
155
- { label: options.label ?? 'vision provider error response' },
156
- )
157
- },
158
-
159
- readModelJson(response, options = {}) {
160
- return readResponseJsonBounded(
161
- response,
162
- options.maxBytes ?? MODEL_RESPONSE_MAX_BYTES,
163
- { label: options.label ?? 'vision provider response' },
164
- )
165
- },
166
-
167
- proxyDecision(input) {
168
- const url = asUrl(input)
169
- const { proxy, proxyHosts } = proxySettings(config)
170
- return Object.freeze({
171
- proxied: Boolean(url && proxy && proxyHostMatchesAny(url.hostname, proxyHosts)),
172
- proxy,
173
- hostname: url?.hostname,
174
- })
175
- },
176
- })
177
- }
178
-
179
- /**
180
- * Scoped process/profile carrier for Router-owned compatibility calls whose
181
- * mature signatures do not yet accept an explicit VisionProviderTransport.
182
- * The registry never patches global fetch and is released with the public
183
- * plugin lifecycle. Remove it only after every production compatibility caller
184
- * receives the transport explicitly and no production code reads
185
- * currentVisionProviderTransport().
186
- */
187
- export function installVisionProviderTransport(transport) {
188
- if (!transport || typeof transport.fetch !== 'function') {
189
- throw new TypeError('invalid vision provider transport')
190
- }
191
- const token = Object.freeze({ transport })
192
- installed.push(token)
193
- let active = true
194
- return () => {
195
- if (!active) return
196
- active = false
197
- const index = installed.indexOf(token)
198
- if (index >= 0) installed.splice(index, 1)
199
- }
200
- }
201
-
202
- export function currentVisionProviderTransport() {
203
- return installed[installed.length - 1]?.transport
115
+ finally {
116
+ lease.release();
117
+ }
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
+ });
204
165
  }