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,73 +1,128 @@
1
- import { createHash } from 'node:crypto'
2
- import { capabilityBenchmarkFingerprint } from './vision-capability-benchmark.js'
3
-
1
+ import { createHash } from 'node:crypto';
2
+ import { capabilityBenchmarkFingerprint } from './vision-capability-fingerprint.js';
3
+ function propertyBag(value) {
4
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
5
+ ? value
6
+ : undefined;
7
+ }
4
8
  function nonEmpty(value) {
5
- return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
9
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
10
+ }
11
+ function contextGetter(ctx) {
12
+ const get = propertyBag(ctx)?.get;
13
+ return typeof get === 'function'
14
+ ? (name) => get.call(ctx, name)
15
+ : undefined;
16
+ }
17
+ function credentialValue(value) {
18
+ return typeof value === 'string' && value !== '' ? value : undefined;
6
19
  }
7
-
8
20
  export function visionCredentialFingerprint(value) {
9
- const text = typeof value === 'string' ? value : ''
10
- if (text === '') return 'none'
11
- return `cred_${createHash('sha256').update(text).digest('hex').slice(0, 24)}`
21
+ const text = typeof value === 'string' ? value : '';
22
+ if (text === '')
23
+ return 'none';
24
+ return `cred_${createHash('sha256').update(text).digest('hex').slice(0, 24)}`;
12
25
  }
13
-
14
- // Capability identity deliberately excludes credential identity. Credentials
15
- // decide whether a route can be accessed right now; they do not define what
16
- // the same model deployment is capable of. Deployment/project identity must
17
- // be represented explicitly in endpoint/config when it actually changes the
18
- // served model, rather than inferred from secret material.
19
- export function capabilityEvidenceFingerprint({
20
- provider,
21
- model,
22
- endpoint,
23
- config,
24
- } = {}) {
25
- return capabilityBenchmarkFingerprint({ provider, model, endpoint, config })
26
+ /**
27
+ * Capability identity deliberately excludes credential identity.
28
+ *
29
+ * Credentials decide whether a route can be accessed right now; they do not
30
+ * define what the same model deployment is capable of. Deployment/project
31
+ * identity belongs in endpoint/config when it actually changes the served
32
+ * model, never in secret material.
33
+ */
34
+ export function capabilityEvidenceFingerprint({ provider, model, endpoint, config, } = {}) {
35
+ return capabilityBenchmarkFingerprint({ provider, model, endpoint, config });
26
36
  }
27
-
28
37
  export async function resolveVisionCredential(ctx, ref) {
29
- const key = nonEmpty(ref)
30
- if (key === undefined) return { required: false, value: undefined, fingerprint: 'none', source: 'none' }
31
-
32
- let credentials
33
- try { credentials = ctx?.get?.('credentials') } catch { credentials = undefined }
34
- if (credentials !== undefined) {
38
+ const key = nonEmpty(ref);
39
+ if (key === undefined) {
40
+ return {
41
+ required: false,
42
+ value: undefined,
43
+ fingerprint: 'none',
44
+ source: 'none',
45
+ };
46
+ }
47
+ let credentials;
35
48
  try {
36
- const hit = await credentials?.resolve?.(key)
37
- const value = hit && typeof hit.value === 'string' && hit.value !== '' ? hit.value : undefined
38
- return {
39
- required: true,
40
- value,
41
- fingerprint: value === undefined ? 'unresolved' : visionCredentialFingerprint(value),
42
- source: value === undefined ? 'credentials-miss' : 'credentials',
43
- }
44
- } catch {
45
- return { required: true, value: undefined, fingerprint: 'unresolved', source: 'credentials-error' }
49
+ credentials = contextGetter(ctx)?.('credentials');
50
+ }
51
+ catch {
52
+ credentials = undefined;
53
+ }
54
+ if (credentials !== undefined) {
55
+ try {
56
+ const resolve = propertyBag(credentials)?.resolve;
57
+ const hit = typeof resolve === 'function'
58
+ ? await resolve.call(credentials, key)
59
+ : undefined;
60
+ const value = credentialValue(propertyBag(hit)?.value);
61
+ return {
62
+ required: true,
63
+ value,
64
+ fingerprint: value === undefined
65
+ ? 'unresolved'
66
+ : visionCredentialFingerprint(value),
67
+ source: value === undefined ? 'credentials-miss' : 'credentials',
68
+ };
69
+ }
70
+ catch {
71
+ return {
72
+ required: true,
73
+ value: undefined,
74
+ fingerprint: 'unresolved',
75
+ source: 'credentials-error',
76
+ };
77
+ }
78
+ }
79
+ let launchEnvironment;
80
+ try {
81
+ launchEnvironment = contextGetter(ctx)?.('launchEnvironment');
82
+ }
83
+ catch {
84
+ return {
85
+ required: true,
86
+ value: undefined,
87
+ fingerprint: 'unresolved',
88
+ source: 'launch-environment-error',
89
+ };
46
90
  }
47
- }
48
-
49
- try {
50
- const launchEnvironment = ctx?.get?.('launchEnvironment')
51
91
  if (launchEnvironment !== undefined) {
52
- const hit = launchEnvironment?.get?.(key)
53
- const value = hit && typeof hit.value === 'string' && hit.value !== '' ? hit.value : undefined
54
- return {
92
+ try {
93
+ const get = propertyBag(launchEnvironment)?.get;
94
+ const hit = typeof get === 'function'
95
+ ? get.call(launchEnvironment, key)
96
+ : undefined;
97
+ const value = credentialValue(propertyBag(hit)?.value);
98
+ return {
99
+ required: true,
100
+ value,
101
+ fingerprint: value === undefined
102
+ ? 'unresolved'
103
+ : visionCredentialFingerprint(value),
104
+ source: value === undefined
105
+ ? 'launch-environment-miss'
106
+ : 'launch-environment',
107
+ };
108
+ }
109
+ catch {
110
+ return {
111
+ required: true,
112
+ value: undefined,
113
+ fingerprint: 'unresolved',
114
+ source: 'launch-environment-error',
115
+ };
116
+ }
117
+ }
118
+ const ambient = process.env[key];
119
+ const value = typeof ambient === 'string' && ambient !== '' ? ambient : undefined;
120
+ return {
55
121
  required: true,
56
122
  value,
57
- fingerprint: value === undefined ? 'unresolved' : visionCredentialFingerprint(value),
58
- source: value === undefined ? 'launch-environment-miss' : 'launch-environment',
59
- }
60
- }
61
- } catch {
62
- return { required: true, value: undefined, fingerprint: 'unresolved', source: 'launch-environment-error' }
63
- }
64
-
65
- const ambient = process.env[key]
66
- const value = typeof ambient === 'string' && ambient !== '' ? ambient : undefined
67
- return {
68
- required: true,
69
- value,
70
- fingerprint: value === undefined ? 'unresolved' : visionCredentialFingerprint(value),
71
- source: value === undefined ? 'ambient-miss' : 'ambient',
72
- }
123
+ fingerprint: value === undefined
124
+ ? 'unresolved'
125
+ : visionCredentialFingerprint(value),
126
+ source: value === undefined ? 'ambient-miss' : 'ambient',
127
+ };
73
128
  }
@@ -5,26 +5,18 @@
5
5
  // health only gates temporary availability. Model names/families never create
6
6
  // capability scores and unmeasured tasks never reorder the user's chain.
7
7
 
8
- export const VISION_INTENTS = Object.freeze([
9
- 'structured',
10
- 'ocr',
11
- 'document',
12
- 'ui',
13
- 'grounding',
14
- 'detection',
15
- 'general',
16
- 'chart_diagram',
17
- 'code_screenshot',
18
- 'visual_compare',
19
- ])
20
-
21
- export const BENCHMARK_AXES = Object.freeze([
22
- 'structured',
23
- 'ocr',
24
- 'document',
25
- 'grounding',
26
- 'general',
27
- ])
8
+ import {
9
+ BENCHMARK_AXES,
10
+ VISION_INTENTS,
11
+ benchmarkAxisForVisionIntent,
12
+ normalizeVisionIntent,
13
+ } from './vision-capability-evidence.js'
14
+
15
+ export {
16
+ BENCHMARK_AXES,
17
+ VISION_INTENTS,
18
+ benchmarkAxisForVisionIntent,
19
+ } from './vision-capability-evidence.js'
28
20
 
29
21
  // Internal compatibility vocabulary. `privacy` is the historical scorer name
30
22
  // for the user-facing `local` preference; unlike the old implementation it is
@@ -33,14 +25,6 @@ export const VISION_STRATEGIES = Object.freeze(['quality', 'balanced', 'speed',
33
25
 
34
26
  export const AUTO_REORDER_MIN_ADVANTAGE = 0.08
35
27
 
36
- const DIRECT_TASK_AXIS = Object.freeze({
37
- structured: 'structured',
38
- ocr: 'ocr',
39
- document: 'document',
40
- grounding: 'grounding',
41
- general: 'general',
42
- })
43
-
44
28
  const BOOTSTRAP_VISUAL_INTENT = Object.freeze({
45
29
  chat: 'ui',
46
30
  document: 'document',
@@ -60,7 +44,7 @@ function clamp01(value, fallback = 0) {
60
44
  }
61
45
 
62
46
  function normalizedIntent(intent) {
63
- return VISION_INTENTS.includes(intent) ? intent : 'general'
47
+ return normalizeVisionIntent(intent)
64
48
  }
65
49
 
66
50
  function normalizedStrategy(strategy) {
@@ -72,10 +56,6 @@ function cleanEnum(value, allowed) {
72
56
  return allowed.has(text) ? text : 'unknown'
73
57
  }
74
58
 
75
- export function benchmarkAxisForVisionIntent(intent) {
76
- return DIRECT_TASK_AXIS[normalizedIntent(intent)]
77
- }
78
-
79
59
  export function inferBootstrapVisionIntents(bootstrap = {}) {
80
60
  const source = bootstrap && typeof bootstrap === 'object' && !Array.isArray(bootstrap) ? bootstrap : {}
81
61
  const visualKind = cleanEnum(source.visual_kind, BOOTSTRAP_VISUAL_KINDS)
@@ -0,0 +1,95 @@
1
+ function propertyBag(value) {
2
+ return value !== null && typeof value === 'object'
3
+ ? value
4
+ : undefined;
5
+ }
6
+ function failureMessage(failure) {
7
+ const message = propertyBag(failure)?.message;
8
+ return message ? String(message) : String(failure);
9
+ }
10
+ /**
11
+ * Minimal DVR text assembler over the public DSH StreamChunk contract.
12
+ *
13
+ * Current DSH failures arrive inside the terminal finish reason. The two
14
+ * standalone terminal shapes remain explicit compatibility input for released
15
+ * historical/partial adapters. Runtime shape checks stay in place because JS
16
+ * callers may still provide malformed chunks.
17
+ */
18
+ export function createChunkAssembler() {
19
+ const parts = new Map();
20
+ const order = [];
21
+ let finishKind;
22
+ let failure;
23
+ const pushUnknown = (value) => {
24
+ const chunk = propertyBag(value);
25
+ if (chunk === undefined || typeof chunk.type !== 'string')
26
+ return;
27
+ switch (chunk.type) {
28
+ case 'block-start': {
29
+ if (!parts.has(chunk.index)) {
30
+ order.push(chunk.index);
31
+ parts.set(chunk.index, { type: chunk.blockType, text: '' });
32
+ }
33
+ break;
34
+ }
35
+ case 'text-delta': {
36
+ const part = parts.get(chunk.index);
37
+ if (part !== undefined)
38
+ part.text += (chunk.text ?? '');
39
+ break;
40
+ }
41
+ case 'reasoning-delta':
42
+ case 'tool-call-delta':
43
+ case 'usage':
44
+ break;
45
+ case 'block-end': {
46
+ const part = parts.get(chunk.index);
47
+ const block = propertyBag(chunk.block);
48
+ if (part !== undefined && typeof block?.text === 'string') {
49
+ part.text = block.text;
50
+ }
51
+ break;
52
+ }
53
+ case 'finish': {
54
+ const reason = propertyBag(chunk.reason);
55
+ if (reason !== undefined
56
+ && (reason.kind === 'error' || reason.kind === 'aborted')) {
57
+ failure = reason.failure;
58
+ }
59
+ finishKind =
60
+ reason !== undefined && reason.kind
61
+ ? String(reason.kind)
62
+ : 'stop';
63
+ break;
64
+ }
65
+ case 'error':
66
+ case 'aborted':
67
+ failure = chunk.failure;
68
+ break;
69
+ default:
70
+ break;
71
+ }
72
+ };
73
+ return {
74
+ push(chunk) {
75
+ pushUnknown(chunk);
76
+ },
77
+ finish() {
78
+ if (failure)
79
+ throw new Error(failureMessage(failure));
80
+ if (finishKind !== undefined
81
+ && finishKind !== 'stop'
82
+ && finishKind !== 'max-tokens') {
83
+ throw new Error(`vision call finished with "${finishKind}"`);
84
+ }
85
+ return order
86
+ .map((index) => parts.get(index))
87
+ .filter((part) => part !== undefined && part.type === 'text')
88
+ .map((part) => part.text)
89
+ .join('')
90
+ .trim();
91
+ },
92
+ };
93
+ }
94
+ const _officialFailureAuthority = undefined;
95
+ void _officialFailureAuthority;
@@ -0,0 +1,246 @@
1
+ import {
2
+ currentLegacyGlobalProxyFetch,
3
+ legacyGlobalProxyPairFor,
4
+ runWithLegacyGlobalProxyScope,
5
+ } from './legacy-global-proxy-boundary.js'
6
+ import { probeLocalBackends } from './local-connection-probe.js'
7
+ import {
8
+ METADATA_RESPONSE_MAX_BYTES,
9
+ readResponseJsonBounded,
10
+ } from './http-body-limit.js'
11
+
12
+ const moduleFetch = typeof globalThis.fetch === 'function'
13
+ ? globalThis.fetch.bind(globalThis)
14
+ : undefined
15
+
16
+ export const VISION_TEST_CONNECTION_PATH = '/_dsh/vision-router/test-connection'
17
+ export const VISION_MODEL_CAPABILITIES_PATH = '/_dsh/vision-router/model-capabilities'
18
+
19
+ function adapterAvailable(llm, provider) {
20
+ if (!llm || typeof llm.registration !== 'function') return false
21
+ try {
22
+ llm.registration(provider)
23
+ return true
24
+ } catch {
25
+ return false
26
+ }
27
+ }
28
+
29
+ async function probeModels(fetchImpl, baseURL, expectedModel, startedAt) {
30
+ try {
31
+ const response = await fetchImpl(`${String(baseURL).replace(/\/$/, '')}/models`, {
32
+ method: 'GET',
33
+ signal: AbortSignal.timeout(8000),
34
+ })
35
+ const latencyMs = Date.now() - startedAt
36
+ if (!response.ok) {
37
+ return { ok: false, latencyMs, status: response.status, error: `HTTP ${response.status}` }
38
+ }
39
+ const data = await readResponseJsonBounded(
40
+ response,
41
+ METADATA_RESPONSE_MAX_BYTES,
42
+ { label: 'vision backend /models response' },
43
+ ).catch(() => undefined)
44
+ const models = data && Array.isArray(data.data) ? data.data : undefined
45
+ const count = models ? models.length : undefined
46
+ if (
47
+ typeof expectedModel === 'string' &&
48
+ expectedModel !== '' &&
49
+ models &&
50
+ !models.some((entry) => entry && String(entry.id) === expectedModel)
51
+ ) {
52
+ return {
53
+ ok: false,
54
+ latencyMs,
55
+ status: response.status,
56
+ models: count,
57
+ endpoint: baseURL,
58
+ error: `configured model "${expectedModel}" was not returned by /models`,
59
+ }
60
+ }
61
+ return { ok: true, latencyMs, status: response.status, models: count, endpoint: baseURL }
62
+ } catch (error) {
63
+ return {
64
+ ok: false,
65
+ latencyMs: Date.now() - startedAt,
66
+ error: error && error.message ? error.message : String(error),
67
+ }
68
+ }
69
+ }
70
+
71
+ function getArray(factory) {
72
+ const value = typeof factory === 'function' ? factory() : factory
73
+ return Array.isArray(value) ? value : []
74
+ }
75
+
76
+ /**
77
+ * Product diagnostics/settings-support routes kept outside visual Core.
78
+ *
79
+ * The caller supplies three coherent domain faces instead of a Core facade:
80
+ * connection candidates/probes, capability snapshot production, and the
81
+ * Host-ownership state rendered in the connection response. Authentication
82
+ * and remote/local capability policy stay outside this module in the existing
83
+ * scoped WebServer boundary.
84
+ */
85
+ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
86
+ if (!ctx || typeof ctx.inject !== 'function') return ctx
87
+
88
+ const connection = options.connection ?? {}
89
+ const capabilities = options.capabilities ?? {}
90
+ const ownership = options.ownership ?? {}
91
+ const resolveCapability = connection.resolveCapability
92
+ const collectCapabilities = capabilities.collect
93
+ if (typeof resolveCapability !== 'function') {
94
+ throw new TypeError('vision diagnostics routes require a connection capability resolver')
95
+ }
96
+ if (typeof collectCapabilities !== 'function') {
97
+ throw new TypeError('vision diagnostics routes require a capability collector')
98
+ }
99
+
100
+ const llm = ctx.llm
101
+ const httpRoute = typeof connection.httpRoute === 'string' && connection.httpRoute !== ''
102
+ ? connection.httpRoute
103
+ : 'vision-http'
104
+ // The proxy boundary installs its wrapper after apply(), so a capture taken here
105
+ // would make "test connection" report failures that real vision turns (which run
106
+ // inside the adapter's proxy scope) do not have. Use the fetch the boundary
107
+ // currently owns, with the module-load capture as the fallback.
108
+ const injectedFetch = typeof options.fetchImpl === 'function' ? options.fetchImpl : undefined
109
+ const resolveFetch = () => injectedFetch ?? currentLegacyGlobalProxyFetch() ?? moduleFetch
110
+ const connectionConfig = () => {
111
+ try {
112
+ const live = ctx?.get?.('settings')?.get?.('vision-router')
113
+ if (live !== null && typeof live === 'object' && !Array.isArray(live)) return live
114
+ } catch {
115
+ // No settings namespace on this Host: probes stay unscoped, as before.
116
+ }
117
+ return {}
118
+ }
119
+ const scopedProbe = (providerName, baseURL, expectedModel, startedAt) => {
120
+ const work = () => probeModels(resolveFetch(), baseURL, expectedModel, startedAt)
121
+ const pair = legacyGlobalProxyPairFor(connectionConfig(), providerName)
122
+ return pair === undefined
123
+ ? work()
124
+ : runWithLegacyGlobalProxyScope(pair.provider, pair.model, work)
125
+ }
126
+
127
+ const probeConnection = async () => {
128
+ const started = Date.now()
129
+ let first
130
+ for (const pair of getArray(connection.candidatePairs)) {
131
+ if (!pair) continue
132
+ if (pair.provider !== httpRoute && !adapterAvailable(llm, pair.provider)) continue
133
+ const capability = await resolveCapability(pair.provider, pair.model)
134
+ if (capability?.attemptable !== false) {
135
+ first = pair
136
+ break
137
+ }
138
+ }
139
+
140
+ const localProbe = await probeLocalBackends(
141
+ getArray(connection.localBackends),
142
+ (provider) => scopedProbe(provider.name ?? provider.id, provider.baseURL, provider.model, started),
143
+ started,
144
+ )
145
+ if (localProbe !== undefined) return localProbe
146
+
147
+ const httpBackends = getArray(connection.httpBackends)
148
+ if (first !== undefined && first.provider === httpRoute) {
149
+ const entry = httpBackends.find((provider) => `${provider.name}/${provider.model}` === first.model)
150
+ if (entry !== undefined) return scopedProbe(entry.name, entry.baseURL, entry.model, started)
151
+ }
152
+
153
+ if (first !== undefined) {
154
+ try {
155
+ await llm.resolveModelInfo(first.provider, first.model)
156
+ return {
157
+ ok: true,
158
+ latencyMs: Date.now() - started,
159
+ detail: `${first.provider}/${first.model} metadata resolved (no network call)`,
160
+ }
161
+ } catch (error) {
162
+ return {
163
+ ok: false,
164
+ latencyMs: Date.now() - started,
165
+ error: error && error.message ? error.message : String(error),
166
+ }
167
+ }
168
+ }
169
+
170
+ const httpFirst = httpBackends[0]
171
+ if (httpFirst !== undefined) return scopedProbe(httpFirst.provider ?? httpFirst.name ?? httpRoute, httpFirst.baseURL, httpFirst.model, started)
172
+ return { ok: false, error: 'no usable vision provider configured' }
173
+ }
174
+
175
+ ctx.inject(['webServer'], (webCtx) => {
176
+ webCtx.effect(() => {
177
+ return webCtx.webServer.register({
178
+ kind: 'exact',
179
+ path: VISION_TEST_CONNECTION_PATH,
180
+ handler: async (req, res) => {
181
+ if (req.method !== 'GET') {
182
+ res.setHeader('Allow', 'GET')
183
+ res.writeHead(405)
184
+ res.end()
185
+ return
186
+ }
187
+ try {
188
+ const result = await probeConnection()
189
+ const officialRouteAvailable = adapterAvailable(llm, 'deepseek-official')
190
+ result.stealth = {
191
+ configured: ownership.stealthConfigured,
192
+ active: false,
193
+ reason: ownership.hostOwnsOfficialDeepSeek && !officialRouteAvailable
194
+ ? 'host-owned-official-unavailable'
195
+ : undefined,
196
+ hostOwned: ownership.hostOwnsOfficialDeepSeek,
197
+ }
198
+ res.writeHead(result.ok ? 200 : 502, { 'content-type': 'application/json' })
199
+ res.end(JSON.stringify(result))
200
+ } catch (error) {
201
+ res.writeHead(500, { 'content-type': 'application/json' })
202
+ res.end(JSON.stringify({
203
+ ok: false,
204
+ error: error && error.message ? error.message : String(error),
205
+ }))
206
+ }
207
+ },
208
+ })
209
+ }, 'vision-router: test-connection route')
210
+ })
211
+
212
+ ctx.inject(['webServer'], (webCtx) => {
213
+ webCtx.effect(
214
+ () => webCtx.webServer.register({
215
+ kind: 'exact',
216
+ path: VISION_MODEL_CAPABILITIES_PATH,
217
+ handler: async (req, res) => {
218
+ if (req.method !== 'GET') {
219
+ res.setHeader('Allow', 'GET')
220
+ res.writeHead(405)
221
+ res.end()
222
+ return
223
+ }
224
+ try {
225
+ const snapshot = await collectCapabilities()
226
+ res.writeHead(200, { 'content-type': 'application/json' })
227
+ res.end(JSON.stringify({
228
+ capabilities: snapshot,
229
+ builtinFallback: getArray(capabilities.builtinFallback),
230
+ anonymousRpmPerModel: 2,
231
+ }))
232
+ } catch (error) {
233
+ res.writeHead(500, { 'content-type': 'application/json' })
234
+ res.end(JSON.stringify({
235
+ capabilities: {},
236
+ error: error && error.message ? error.message : String(error),
237
+ }))
238
+ }
239
+ },
240
+ }),
241
+ 'vision-router: model capabilities route',
242
+ )
243
+ })
244
+
245
+ return ctx
246
+ }
@@ -1,3 +1,5 @@
1
+ import { visionRouterMessageSource } from './session-message-source-compat.js'
2
+
1
3
  export const VISION_SUCCESS_GUIDANCE =
2
4
  'Vision Router trusted execution metadata: the preceding vision_describe call SUCCEEDED. ' +
3
5
  'This metadata is not image content. If that successful evidence resolves the visual fact needed for the current task, stop making additional vision calls for that fact and continue the remaining user task from this evidence. ' +
@@ -34,6 +36,6 @@ export function visionDescribeSuccessContext(exec, result) {
34
36
  role: 'user',
35
37
  id: `vision-router-evidence-success-${String(exec.callId)}`,
36
38
  content: [{ type: 'text', text: VISION_SUCCESS_GUIDANCE }],
37
- source: { kind: 'plugin', plugin: 'dsh-vision-router' },
39
+ source: visionRouterMessageSource(exec.agent.session),
38
40
  }
39
41
  }