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,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
  }
@@ -419,6 +419,7 @@ export const VISION_MODEL_VISIBILITY_PRELUDE = String.raw`(function(){
419
419
  function create() {
420
420
  var result = originalCreate.apply(this, arguments);
421
421
  patchLoader(loader);
422
+ if (result && result !== loader) patchLoader(result);
422
423
  return result;
423
424
  }
424
425
  try { Object.defineProperty(create, '__visionRouterModelVisibility', { value: true }); } catch (_) {}
@@ -427,31 +428,29 @@ export const VISION_MODEL_VISIBILITY_PRELUDE = String.raw`(function(){
427
428
  }
428
429
 
429
430
  function install() {
430
- if (window.__ModuleLoader__) {
431
- patchLoader(window.__ModuleLoader__);
432
- return;
433
- }
434
431
  var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
432
+ var current;
433
+ try { current = window.__ModuleLoader__; } catch (_) { current = undefined; }
434
+ if (current) patchLoader(current);
435
435
  if (descriptor && descriptor.configurable === false) return;
436
- var stored;
436
+ var previousGet = descriptor && descriptor.get;
437
+ var previousSet = descriptor && descriptor.set;
438
+ var stored = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
439
+ ? descriptor.value
440
+ : current;
437
441
  Object.defineProperty(window, '__ModuleLoader__', {
438
442
  configurable: true,
439
- enumerable: true,
440
- get: function(){ return stored; },
443
+ enumerable: !descriptor || descriptor.enumerable !== false,
444
+ get: function(){ return previousGet ? previousGet.call(window) : stored; },
441
445
  set: function(value) {
442
- stored = value;
443
- patchLoader(value);
444
- Object.defineProperty(window, '__ModuleLoader__', {
445
- configurable: true,
446
- enumerable: true,
447
- writable: true,
448
- value: stored
449
- });
446
+ if (previousSet) previousSet.call(window, value); else stored = value;
447
+ try { patchLoader(previousGet ? previousGet.call(window) : value); } catch (_) {}
450
448
  }
451
449
  });
450
+ if (stored) patchLoader(stored);
452
451
  }
453
452
 
454
- install();
453
+ try { install(); } catch (_) {}
455
454
  })();`
456
455
 
457
456
  export function injectVisionModelVisibilityBoundary(html) {
@@ -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
  }