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,15 +1,19 @@
1
- import { createHash } from 'node:crypto'
2
- import process from 'node:process'
3
1
  import { VISION_INTENTS } from './vision-capability-router.js'
2
+ import {
3
+ CAPABILITY_BENCHMARK_RENDERER_SCOPE,
4
+ CAPABILITY_BENCHMARK_SUITE_REVISION,
5
+ capabilityBenchmarkFingerprint,
6
+ } from './vision-capability-fingerprint.js'
7
+
8
+ export {
9
+ CAPABILITY_BENCHMARK_RENDERER_SCOPE,
10
+ CAPABILITY_BENCHMARK_SUITE_REVISION,
11
+ capabilityBenchmarkFingerprint,
12
+ } from './vision-capability-fingerprint.js'
4
13
 
5
14
  const WIDTH = 768
6
15
  const HEIGHT = 512
7
16
 
8
- export const CAPABILITY_BENCHMARK_SUITE_REVISION = 5
9
- // Renderer-only fixes must invalidate prior evidence even when the scoring and
10
- // prompt contract (the suite revision) is unchanged.
11
- export const CAPABILITY_BENCHMARK_RENDERER_SCOPE = `${process.platform}/${process.arch}-proof-badge-v2`
12
-
13
17
  function esc(value) {
14
18
  return String(value).replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[ch])
15
19
  }
@@ -160,60 +164,6 @@ export function listCapabilityBenchmarkFixtures(intents = CORE_BENCHMARK_INTENTS
160
164
  return fixtures
161
165
  }
162
166
 
163
- function isSensitiveKey(key) {
164
- return /(^|[_-])(api[_-]?key|key|token|secret|password|authorization|auth|signature|sig)([_-]|$)/i.test(String(key ?? ''))
165
- }
166
-
167
- function normalizeEndpoint(value) {
168
- const raw = String(value ?? '').trim()
169
- if (raw === '') return ''
170
- try {
171
- const url = new URL(raw)
172
- url.username = ''
173
- url.password = ''
174
- url.hash = ''
175
- for (const key of [...url.searchParams.keys()]) {
176
- if (isSensitiveKey(key)) url.searchParams.delete(key)
177
- }
178
- url.searchParams.sort()
179
- const pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '')
180
- return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${pathname}${url.search}`
181
- } catch {
182
- return raw.replace(/\/+$/, '')
183
- }
184
- }
185
-
186
- function sanitizeFingerprintValue(value, keyHint = '') {
187
- if (value === null || value === undefined) return value ?? null
188
- if (Array.isArray(value)) return value.map((item) => sanitizeFingerprintValue(item))
189
- if (typeof value === 'object') {
190
- return Object.fromEntries(
191
- Object.entries(value)
192
- .filter(([key]) => !isSensitiveKey(key))
193
- .sort(([a], [b]) => a.localeCompare(b))
194
- .map(([key, child]) => [key, sanitizeFingerprintValue(child, key)]),
195
- )
196
- }
197
- if (typeof value === 'string' && /(endpoint|base[_-]?url|url)$/i.test(keyHint)) return normalizeEndpoint(value)
198
- if (typeof value === 'bigint') return value.toString()
199
- if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value
200
- return String(value)
201
- }
202
-
203
- export function capabilityBenchmarkFingerprint({ provider, model, endpoint, config, credentialFingerprint } = {}) {
204
- const payload = JSON.stringify({
205
- schema: 'vision-capability-endpoint-v2',
206
- suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
207
- rendererScope: CAPABILITY_BENCHMARK_RENDERER_SCOPE,
208
- provider: String(provider ?? '').trim(),
209
- model: String(model ?? '').trim(),
210
- endpoint: normalizeEndpoint(endpoint),
211
- config: sanitizeFingerprintValue(config && typeof config === 'object' ? config : null),
212
- credentialFingerprint: String(credentialFingerprint ?? 'none'),
213
- })
214
- return `ep2_${createHash('sha256').update(payload).digest('hex').slice(0, 32)}`
215
- }
216
-
217
167
  function clamp01(value) {
218
168
  return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0))
219
169
  }
@@ -0,0 +1,35 @@
1
+ export const VISION_INTENTS = [
2
+ 'structured',
3
+ 'ocr',
4
+ 'document',
5
+ 'ui',
6
+ 'grounding',
7
+ 'detection',
8
+ 'general',
9
+ 'chart_diagram',
10
+ 'code_screenshot',
11
+ 'visual_compare',
12
+ ];
13
+ export const BENCHMARK_AXES = [
14
+ 'structured',
15
+ 'ocr',
16
+ 'document',
17
+ 'grounding',
18
+ 'general',
19
+ ];
20
+ const DIRECT_TASK_AXIS = Object.freeze({
21
+ structured: 'structured',
22
+ ocr: 'ocr',
23
+ document: 'document',
24
+ grounding: 'grounding',
25
+ general: 'general',
26
+ });
27
+ export function normalizeVisionIntent(intent) {
28
+ return typeof intent === 'string'
29
+ && VISION_INTENTS.includes(intent)
30
+ ? intent
31
+ : 'general';
32
+ }
33
+ export function benchmarkAxisForVisionIntent(intent) {
34
+ return DIRECT_TASK_AXIS[normalizeVisionIntent(intent)];
35
+ }
@@ -0,0 +1,72 @@
1
+ import { createHash } from 'node:crypto';
2
+ import process from 'node:process';
3
+ export const CAPABILITY_BENCHMARK_SUITE_REVISION = 5;
4
+ // Renderer-only fixes must invalidate prior evidence even when the scoring and
5
+ // prompt contract (the suite revision) is unchanged.
6
+ export const CAPABILITY_BENCHMARK_RENDERER_SCOPE = `${process.platform}/${process.arch}-proof-badge-v2`;
7
+ function isSensitiveKey(key) {
8
+ return /(^|[_-])(api[_-]?key|key|token|secret|password|authorization|auth|signature|sig)([_-]|$)/i
9
+ .test(String(key ?? ''));
10
+ }
11
+ function normalizeEndpoint(value) {
12
+ const raw = String(value ?? '').trim();
13
+ if (raw === '')
14
+ return '';
15
+ try {
16
+ const url = new URL(raw);
17
+ url.username = '';
18
+ url.password = '';
19
+ url.hash = '';
20
+ for (const key of [...url.searchParams.keys()]) {
21
+ if (isSensitiveKey(key))
22
+ url.searchParams.delete(key);
23
+ }
24
+ url.searchParams.sort();
25
+ const pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
26
+ return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${pathname}${url.search}`;
27
+ }
28
+ catch {
29
+ return raw.replace(/\/+$/, '');
30
+ }
31
+ }
32
+ function sanitizeFingerprintValue(value, keyHint = '') {
33
+ if (value === null || value === undefined)
34
+ return value ?? null;
35
+ if (Array.isArray(value)) {
36
+ return value.map((item) => sanitizeFingerprintValue(item));
37
+ }
38
+ if (typeof value === 'object') {
39
+ return Object.fromEntries(Object.entries(value)
40
+ .filter(([key]) => !isSensitiveKey(key))
41
+ .sort(([a], [b]) => a.localeCompare(b))
42
+ .map(([key, child]) => [
43
+ key,
44
+ sanitizeFingerprintValue(child, key),
45
+ ]));
46
+ }
47
+ if (typeof value === 'string'
48
+ && /(endpoint|base[_-]?url|url)$/i.test(keyHint)) {
49
+ return normalizeEndpoint(value);
50
+ }
51
+ if (typeof value === 'bigint')
52
+ return value.toString();
53
+ if (typeof value === 'number'
54
+ || typeof value === 'boolean'
55
+ || typeof value === 'string') {
56
+ return value;
57
+ }
58
+ return String(value);
59
+ }
60
+ export function capabilityBenchmarkFingerprint({ provider, model, endpoint, config, credentialFingerprint, } = {}) {
61
+ const payload = JSON.stringify({
62
+ schema: 'vision-capability-endpoint-v2',
63
+ suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
64
+ rendererScope: CAPABILITY_BENCHMARK_RENDERER_SCOPE,
65
+ provider: String(provider ?? '').trim(),
66
+ model: String(model ?? '').trim(),
67
+ endpoint: normalizeEndpoint(endpoint),
68
+ config: sanitizeFingerprintValue(config && typeof config === 'object' ? config : null),
69
+ credentialFingerprint: String(credentialFingerprint ?? 'none'),
70
+ });
71
+ return `ep2_${createHash('sha256').update(payload).digest('hex').slice(0, 32)}`;
72
+ }
@@ -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;
@@ -1,9 +1,18 @@
1
+ import {
2
+ currentLegacyGlobalProxyFetch,
3
+ legacyGlobalProxyPairFor,
4
+ runWithLegacyGlobalProxyScope,
5
+ } from './legacy-global-proxy-boundary.js'
1
6
  import { probeLocalBackends } from './local-connection-probe.js'
2
7
  import {
3
8
  METADATA_RESPONSE_MAX_BYTES,
4
9
  readResponseJsonBounded,
5
10
  } from './http-body-limit.js'
6
11
 
12
+ const moduleFetch = typeof globalThis.fetch === 'function'
13
+ ? globalThis.fetch.bind(globalThis)
14
+ : undefined
15
+
7
16
  export const VISION_TEST_CONNECTION_PATH = '/_dsh/vision-router/test-connection'
8
17
  export const VISION_MODEL_CAPABILITIES_PATH = '/_dsh/vision-router/model-capabilities'
9
18
 
@@ -92,7 +101,36 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
92
101
  const httpRoute = typeof connection.httpRoute === 'string' && connection.httpRoute !== ''
93
102
  ? connection.httpRoute
94
103
  : 'vision-http'
95
- const fetchImpl = typeof options.fetchImpl === 'function' ? options.fetchImpl : globalThis.fetch
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
+ // Probes must describe the egress real vision turns use. That egress is the
110
+ // Router-owned provider transport, which also owns the proxy/proxyHosts decision;
111
+ // the legacy pair scope below only covers non-router-owned providers, so a
112
+ // vision-http direct backend would otherwise probe with ambient fetch.
113
+ const transport = options.transport
114
+ const transportFetch = typeof transport?.fetch === 'function'
115
+ ? (input, init) => transport.fetch(input, init, { allowProxy: true })
116
+ : undefined
117
+ const resolveFetch = () => injectedFetch ?? transportFetch ?? currentLegacyGlobalProxyFetch() ?? moduleFetch
118
+ const connectionConfig = () => {
119
+ try {
120
+ const live = ctx?.get?.('settings')?.get?.('vision-router')
121
+ if (live !== null && typeof live === 'object' && !Array.isArray(live)) return live
122
+ } catch {
123
+ // No settings namespace on this Host: probes stay unscoped, as before.
124
+ }
125
+ return {}
126
+ }
127
+ const scopedProbe = (providerName, baseURL, expectedModel, startedAt) => {
128
+ const work = () => probeModels(resolveFetch(), baseURL, expectedModel, startedAt)
129
+ const pair = legacyGlobalProxyPairFor(connectionConfig(), providerName)
130
+ return pair === undefined
131
+ ? work()
132
+ : runWithLegacyGlobalProxyScope(pair.provider, pair.model, work)
133
+ }
96
134
 
97
135
  const probeConnection = async () => {
98
136
  const started = Date.now()
@@ -109,7 +147,7 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
109
147
 
110
148
  const localProbe = await probeLocalBackends(
111
149
  getArray(connection.localBackends),
112
- (provider) => probeModels(fetchImpl, provider.baseURL, provider.model, started),
150
+ (provider) => scopedProbe(provider.name ?? provider.id, provider.baseURL, provider.model, started),
113
151
  started,
114
152
  )
115
153
  if (localProbe !== undefined) return localProbe
@@ -117,7 +155,7 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
117
155
  const httpBackends = getArray(connection.httpBackends)
118
156
  if (first !== undefined && first.provider === httpRoute) {
119
157
  const entry = httpBackends.find((provider) => `${provider.name}/${provider.model}` === first.model)
120
- if (entry !== undefined) return probeModels(fetchImpl, entry.baseURL, entry.model, started)
158
+ if (entry !== undefined) return scopedProbe(entry.name, entry.baseURL, entry.model, started)
121
159
  }
122
160
 
123
161
  if (first !== undefined) {
@@ -138,7 +176,7 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
138
176
  }
139
177
 
140
178
  const httpFirst = httpBackends[0]
141
- if (httpFirst !== undefined) return probeModels(fetchImpl, httpFirst.baseURL, httpFirst.model, started)
179
+ if (httpFirst !== undefined) return scopedProbe(httpFirst.provider ?? httpFirst.name ?? httpRoute, httpFirst.baseURL, httpFirst.model, started)
142
180
  return { ok: false, error: 'no usable vision provider configured' }
143
181
  }
144
182