dsh-vision-router 2.0.0 → 2.1.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.
- package/README.md +33 -16
- package/README.zh.md +34 -17
- package/cordis.patch.yml +19 -9
- package/docs/architecture/2x-contract-ledger.md +165 -0
- package/docs/architecture/compat-inventory.md +121 -0
- package/docs/architecture/dsh-compatibility-matrix.md +57 -0
- package/docs/architecture/dsh-support-window.md +50 -0
- package/docs/architecture/final-closure-audit.md +148 -0
- package/docs/architecture/p2-jobs-feasibility.md +94 -0
- package/docs/architecture/p3-compat-retirement.md +46 -0
- package/docs/architecture/p3-host-native-seams.md +43 -0
- package/docs/architecture/p3-native-recovery-evaluation.md +65 -0
- package/docs/architecture/runtime-boundaries.md +124 -0
- package/docs/releases/v2.0.1.md +17 -0
- package/docs/releases/v2.1.0-support-window.md +29 -0
- package/docs/releases/v2.1.0.md +83 -0
- package/entry.js +13 -307
- package/index.js +85 -352
- package/lib/abort-signal-compat.js +191 -0
- package/lib/adapter-update-coalescer.js +113 -29
- package/lib/adversarial-hardening.js +24 -9
- package/lib/artifact-boundary.js +16 -175
- package/lib/artifact-io.js +220 -0
- package/lib/artifact-retention.js +305 -53
- package/lib/catalog-corrections.js +6 -1
- package/lib/client-host-compat-prelude.js +249 -0
- package/lib/client-presentation-boundary.js +61 -3
- package/lib/client.js +71 -72
- package/lib/core-vision-surface.js +124 -0
- package/lib/depth-guidance.js +114 -90
- package/lib/doctor-cli-p0.js +168 -0
- package/lib/doctor-cli.js +7 -0
- package/lib/doctor-vision-limits.js +72 -0
- package/lib/dsh-contract-compat.js +207 -39
- package/lib/dsh-host-capabilities.js +107 -0
- package/lib/dsh-support-window.js +48 -0
- package/lib/http-compat.js +22 -6
- package/lib/image-resource-governor.js +35 -10
- package/lib/legacy-core-vision-policy-bridge.js +25 -226
- package/lib/legacy-global-proxy-boundary.js +127 -0
- package/lib/live-model-client-prelude.js +0 -12
- package/lib/mixed-router.js +81 -81
- package/lib/public-entry.js +42 -4
- package/lib/runtime-composition.js +348 -0
- package/lib/runtime-i18n-boundary.js +683 -0
- package/lib/runtime-i18n-core-scope.js +56 -0
- package/lib/runtime-i18n-core.js +34 -0
- package/lib/runtime-i18n.js +121 -0
- package/lib/session-surface-policy.js +97 -0
- package/lib/session-vision-index.js +282 -0
- package/lib/session-vision-runtime.js +48 -0
- package/lib/settings-factory-lifecycle.js +144 -0
- package/lib/settings-ia-client-prelude.js +74 -2
- package/lib/settings-limit-client-prelude.js +110 -0
- package/lib/settings-native-card-layout.js +297 -0
- package/lib/settings-number-contract.js +29 -0
- package/lib/structured-flow-hardening.js +207 -76
- package/lib/tesseract-exec-compat.js +58 -32
- package/lib/update-check.js +17 -5
- package/lib/v2-settings-ia-integration.js +9 -4
- package/lib/vision-artifact-store.js +77 -0
- package/lib/vision-backend-runtime-policy.js +6 -2
- package/lib/vision-background-benchmark.js +130 -41
- package/lib/vision-background-failure-policy.js +70 -0
- package/lib/vision-background-stop-store.js +54 -9
- package/lib/vision-capability-benchmark-client.js +57 -66
- package/lib/vision-capability-benchmark-presentation.js +97 -0
- package/lib/vision-capability-benchmark-service.js +3 -3
- package/lib/vision-execution-order-apply.js +61 -0
- package/lib/vision-execution-order-plan.js +64 -0
- package/lib/vision-execution-order.js +38 -0
- package/lib/vision-limit-diagnostics.js +202 -0
- package/lib/vision-product-presentation.js +284 -0
- package/lib/vision-provider-transport.js +197 -0
- package/lib/vision-resilience.js +87 -45
- package/lib/vision-routing-evidence.js +366 -0
- package/lib/vision-routing-runtime.js +278 -0
- package/lib/vision-tool-runtime-boundary.js +27 -21
- package/lib/vision-turn-budget-client-prelude.js +9 -9
- package/lib/web/benchmark-panel.js +6 -0
- package/lib/web/diagnostics-panel.js +44 -0
- package/lib/web/index.js +60 -0
- package/lib/web/model-picker.js +22 -0
- package/lib/web/onboarding.js +6 -0
- package/lib/web/product-state.js +59 -0
- package/lib/web/remote-settings-client.js +8 -0
- package/lib/web/routing-section.js +6 -0
- package/lib/web/settings-controller.js +6 -0
- package/lib/web-capability-boundary.js +65 -9
- package/lib/windows-screenshot-dpi-compat.js +148 -0
- package/package.json +14 -6
- package/lib/vision-capability-shadow.js +0 -576
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
const LIMIT_LINE = /vision-router: effective vision limits\s+taskTimeoutMs=(\d+)\s+taskSource=([^\s]+)\s+turnBudgetMs=(\d+)\s+turnSource=([^\s]+)/i
|
|
4
|
+
const EXHAUSTED_LINE = /vision-router: vision turn deadline exhausted\s+turn=([^\s]+)\s+budgetMs=(\d+)\s+elapsedMs=([^\s]+)/i
|
|
5
|
+
|
|
6
|
+
function readTail(file, maxBytes = 2 * 1024 * 1024) {
|
|
7
|
+
if (!file || !existsSync(file)) return ''
|
|
8
|
+
let fd
|
|
9
|
+
try {
|
|
10
|
+
const stat = statSync(file)
|
|
11
|
+
const size = Math.min(stat.size, maxBytes)
|
|
12
|
+
if (size <= 0) return ''
|
|
13
|
+
const buffer = Buffer.alloc(size)
|
|
14
|
+
fd = openSync(file, 'r')
|
|
15
|
+
readSync(fd, buffer, 0, size, Math.max(0, stat.size - size))
|
|
16
|
+
return buffer.toString('utf8')
|
|
17
|
+
} catch {
|
|
18
|
+
try { return readFileSync(file, 'utf8') } catch { return '' }
|
|
19
|
+
} finally {
|
|
20
|
+
if (fd !== undefined) closeSync(fd)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseVisionLimitLog(text) {
|
|
25
|
+
let latest
|
|
26
|
+
let latestExhaustion
|
|
27
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
28
|
+
const limit = LIMIT_LINE.exec(line)
|
|
29
|
+
if (limit) {
|
|
30
|
+
latest = {
|
|
31
|
+
taskTimeoutMs: Number(limit[1]),
|
|
32
|
+
taskSource: limit[2],
|
|
33
|
+
turnBudgetMs: Number(limit[3]),
|
|
34
|
+
turnSource: limit[4],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const exhausted = EXHAUSTED_LINE.exec(line)
|
|
38
|
+
if (exhausted) {
|
|
39
|
+
latestExhaustion = {
|
|
40
|
+
turn: exhausted[1],
|
|
41
|
+
budgetMs: Number(exhausted[2]),
|
|
42
|
+
elapsedMs: exhausted[3] === 'unknown' ? undefined : Number(exhausted[3]),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!latest && !latestExhaustion) return undefined
|
|
47
|
+
return {
|
|
48
|
+
...latest,
|
|
49
|
+
latestExhaustion,
|
|
50
|
+
explicitTurnLimit: Number(latest?.turnBudgetMs) > 0,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function inspectDoctorVisionLimits(logFile) {
|
|
55
|
+
return parseVisionLimitLog(readTail(logFile))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatDoctorVisionLimits(limits) {
|
|
59
|
+
if (!limits) return []
|
|
60
|
+
const task = Number.isFinite(limits.taskTimeoutMs) ? `${limits.taskTimeoutMs / 1000}s` : 'unknown'
|
|
61
|
+
const turn = Number.isFinite(limits.turnBudgetMs)
|
|
62
|
+
? limits.turnBudgetMs === 0 ? 'unlimited' : `${limits.turnBudgetMs / 1000}s`
|
|
63
|
+
: 'unknown'
|
|
64
|
+
const lines = [
|
|
65
|
+
`Vision task timeout: ${task}${limits.taskSource ? ` (${limits.taskSource})` : ''}`,
|
|
66
|
+
`Vision turn deadline: ${turn}${limits.turnSource ? ` (${limits.turnSource})` : ''}`,
|
|
67
|
+
]
|
|
68
|
+
if (limits.explicitTurnLimit) {
|
|
69
|
+
lines.push('WARN: an explicit whole-turn vision deadline is active; v2 defaults to unlimited and long multi-image tasks may stop after this deadline.')
|
|
70
|
+
}
|
|
71
|
+
return lines
|
|
72
|
+
}
|
|
@@ -1,9 +1,30 @@
|
|
|
1
|
+
import { inspectDshHostCapabilities } from './dsh-host-capabilities.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compatibility inventory
|
|
5
|
+
* Reason: preserve Vision Router's released behavior across the supported DSH Host window.
|
|
6
|
+
* Host gap: attachment ownership, settings-section and adapter-ownership seams differ across supported Hosts.
|
|
7
|
+
* First needed for: rc.6 minimum support and the later batch-attachment/settings migration.
|
|
8
|
+
* Feature detection: concrete attachment/settings/LLM methods only; never a DSH version string.
|
|
9
|
+
* Removal condition: the minimum supported Host natively provides every seam used below and the legacy profile overlay is outside the support window.
|
|
10
|
+
* Tests: rc6-rc7-compat, rc6-real-settings-persistence, attachment-admission-policy, DSH contract CI.
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
const HOST_OWNED_ROUTES = new Set(['deepseek-official', 'deepseek-official-native'])
|
|
2
14
|
|
|
3
15
|
const LEGACY_VISION_ATTACHMENT_BYTES = 20 * 1024 * 1024
|
|
4
16
|
const LEGACY_VISION_ATTACHMENT_PIXELS = 100_000_000
|
|
5
17
|
const DSH_RC8_DEFAULT_IMAGE_DIMENSION = 2000
|
|
18
|
+
const DSH_ALPHA_DEFAULT_IMAGE_DIMENSION = 8192
|
|
6
19
|
const VISION_ROUTER_IMAGE_DIMENSION = 10_000
|
|
20
|
+
const DSH_ALPHA_DEFAULT_NORMALIZED_PIXELS = 2048 * 2048
|
|
21
|
+
const DSH_ALPHA_DEFAULT_NORMALIZED_DIMENSION = 8192
|
|
22
|
+
const DSH_ALPHA_DEFAULT_NORMALIZED_BYTES = 4 * 1024 * 1024
|
|
23
|
+
const VISION_ROUTER_NORMALIZED_PIXELS = LEGACY_VISION_ATTACHMENT_PIXELS
|
|
24
|
+
const VISION_ROUTER_NORMALIZED_DIMENSION = VISION_ROUTER_IMAGE_DIMENSION
|
|
25
|
+
const VISION_ROUTER_NORMALIZED_BYTES = LEGACY_VISION_ATTACHMENT_BYTES
|
|
26
|
+
const HOST_CAPABILITIES_PATH = '/_dsh/vision-router/host-capabilities'
|
|
27
|
+
const capabilityRouteInstalls = new WeakSet()
|
|
7
28
|
|
|
8
29
|
function attachmentStoreOf(ctx) {
|
|
9
30
|
if (!ctx || typeof ctx !== 'object') return undefined
|
|
@@ -23,6 +44,61 @@ function attachmentStoreOf(ctx) {
|
|
|
23
44
|
}
|
|
24
45
|
}
|
|
25
46
|
|
|
47
|
+
function sendCapabilityJson(res, status, body) {
|
|
48
|
+
res.writeHead(status, {
|
|
49
|
+
'content-type': 'application/json; charset=utf-8',
|
|
50
|
+
'cache-control': 'no-store',
|
|
51
|
+
})
|
|
52
|
+
res.end(JSON.stringify(body))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Expose the same side-effect-free Host capability snapshot Doctor consumes.
|
|
57
|
+
* The endpoint is GET-only and contains no settings values, credentials,
|
|
58
|
+
* provider ids or version-derived guesses. Failing to install it is advisory.
|
|
59
|
+
*/
|
|
60
|
+
export function installDshHostCapabilityDiagnostics(ctx) {
|
|
61
|
+
if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return false
|
|
62
|
+
if (capabilityRouteInstalls.has(ctx)) return true
|
|
63
|
+
capabilityRouteInstalls.add(ctx)
|
|
64
|
+
try {
|
|
65
|
+
if (typeof ctx.inject !== 'function') return false
|
|
66
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
67
|
+
try {
|
|
68
|
+
webCtx.effect(
|
|
69
|
+
() => webCtx.webServer.register({
|
|
70
|
+
kind: 'exact',
|
|
71
|
+
path: HOST_CAPABILITIES_PATH,
|
|
72
|
+
handler: async (req, res) => {
|
|
73
|
+
if (req.method !== 'GET') {
|
|
74
|
+
res.setHeader('Allow', 'GET')
|
|
75
|
+
sendCapabilityJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
let capabilities
|
|
79
|
+
try {
|
|
80
|
+
capabilities = inspectDshHostCapabilities(webCtx)
|
|
81
|
+
} catch {
|
|
82
|
+
capabilities = undefined
|
|
83
|
+
}
|
|
84
|
+
sendCapabilityJson(res, 200, {
|
|
85
|
+
ok: true,
|
|
86
|
+
capabilities: capabilities ?? {},
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
'vision-router: host capability diagnostics route',
|
|
91
|
+
)
|
|
92
|
+
} catch {
|
|
93
|
+
// Diagnostics must never veto webServer injection or plugin startup.
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
return true
|
|
97
|
+
} catch {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
26
102
|
/**
|
|
27
103
|
* Detect the released batch-attachment contract directly. DSH rc.6 exposes
|
|
28
104
|
* validateImage/saveImage/readImage only; rc.7+ adds AttachmentStore.saveImages().
|
|
@@ -35,22 +111,58 @@ export function hasBatchAttachmentContract(ctx) {
|
|
|
35
111
|
return !!store && typeof store.saveImages === 'function'
|
|
36
112
|
}
|
|
37
113
|
|
|
114
|
+
function exactAlphaNormalizationDefaults(policy) {
|
|
115
|
+
return !!policy && typeof policy === 'object' &&
|
|
116
|
+
policy.maxPixels === DSH_ALPHA_DEFAULT_NORMALIZED_PIXELS &&
|
|
117
|
+
policy.maxDimension === DSH_ALPHA_DEFAULT_NORMALIZED_DIMENSION &&
|
|
118
|
+
policy.maxBytes === DSH_ALPHA_DEFAULT_NORMALIZED_BYTES
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function knownVisionRouterAdmissionShape(limits) {
|
|
122
|
+
if (!limits || typeof limits !== 'object') return false
|
|
123
|
+
if (
|
|
124
|
+
limits.maxImageBytes !== LEGACY_VISION_ATTACHMENT_BYTES ||
|
|
125
|
+
limits.maxImagePixels !== LEGACY_VISION_ATTACHMENT_PIXELS
|
|
126
|
+
) return false
|
|
127
|
+
return limits.maxImageDimension === DSH_RC8_DEFAULT_IMAGE_DIMENSION ||
|
|
128
|
+
limits.maxImageDimension === DSH_ALPHA_DEFAULT_IMAGE_DIMENSION
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function replaceFrozenPolicy(store, property, next) {
|
|
132
|
+
let changed = false
|
|
133
|
+
try {
|
|
134
|
+
changed = Reflect.set(store, property, next)
|
|
135
|
+
} catch {
|
|
136
|
+
changed = false
|
|
137
|
+
}
|
|
138
|
+
return changed && store[property] === next
|
|
139
|
+
}
|
|
140
|
+
|
|
38
141
|
/**
|
|
39
|
-
* Repair
|
|
142
|
+
* Repair historical Vision Router profile overlays across supported DSH Hosts.
|
|
143
|
+
*
|
|
144
|
+
* DSH patch rows replace the WHOLE config object instead of deep-merging it.
|
|
145
|
+
* Old Vision Router profiles therefore retain a later `attachment-local` row
|
|
146
|
+
* containing only the released 20MiB / 100MP admission pair. On rc.8 that
|
|
147
|
+
* shadowed the later 10000px bundle value and rematerialized the Host's 2000px
|
|
148
|
+
* default. On 0.1.2-alpha.1 the same stale row additionally omits the new
|
|
149
|
+
* normalization fields, rematerializing 8192px admission plus the Host's
|
|
150
|
+
* 4.2MP / 4MiB canonical-image defaults.
|
|
40
151
|
*
|
|
41
|
-
*
|
|
42
|
-
* attachment
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
152
|
+
* This migration never edits a user's profile file and never creates another
|
|
153
|
+
* attachment owner. LocalAttachmentStore exposes the resolved immutable
|
|
154
|
+
* `imageLimits` and (alpha+) `normalizationPolicy` objects, and reads those
|
|
155
|
+
* properties for every later validate/save. We replace only those policy
|
|
156
|
+
* objects on one exact historical fingerprint:
|
|
46
157
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
158
|
+
* - admission bytes === 20MiB and pixels === 100MP; and
|
|
159
|
+
* - dimension is rc.8's 2000px default or alpha's 8192px default; and
|
|
160
|
+
* - normalization, when repaired, is EXACTLY alpha.1's 4.2MP/8192px/4MiB
|
|
161
|
+
* default tuple.
|
|
162
|
+
*
|
|
163
|
+
* Any custom admission dimension or any custom normalization tuple is left
|
|
164
|
+
* untouched. This is capability/shape detection only; no DSH version string is
|
|
165
|
+
* consulted.
|
|
54
166
|
*/
|
|
55
167
|
export function ensureVisionAttachmentAdmissionPolicy(ctx, logger, options = {}) {
|
|
56
168
|
const store = attachmentStoreOf(ctx)
|
|
@@ -62,41 +174,96 @@ export function ensureVisionAttachmentAdmissionPolicy(ctx, logger, options = {})
|
|
|
62
174
|
const targetDimension = Number.isInteger(options.maxImageDimension)
|
|
63
175
|
? Math.max(1, options.maxImageDimension)
|
|
64
176
|
: VISION_ROUTER_IMAGE_DIMENSION
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
177
|
+
const targetNormalizedPixels = Number.isInteger(options.normalizedImageMaxPixels)
|
|
178
|
+
? Math.max(1, options.normalizedImageMaxPixels)
|
|
179
|
+
: VISION_ROUTER_NORMALIZED_PIXELS
|
|
180
|
+
const targetNormalizedDimension = Number.isInteger(options.normalizedImageMaxDimension)
|
|
181
|
+
? Math.max(1, options.normalizedImageMaxDimension)
|
|
182
|
+
: VISION_ROUTER_NORMALIZED_DIMENSION
|
|
183
|
+
const targetNormalizedBytes = Number.isInteger(options.normalizedImageMaxBytes)
|
|
184
|
+
? Math.max(1, options.normalizedImageMaxBytes)
|
|
185
|
+
: VISION_ROUTER_NORMALIZED_BYTES
|
|
69
186
|
|
|
70
|
-
if (!
|
|
187
|
+
if (!knownVisionRouterAdmissionShape(limits)) {
|
|
188
|
+
return { changed: false, reason: 'not-legacy-overlay', limits }
|
|
189
|
+
}
|
|
71
190
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
191
|
+
const repairNormalization =
|
|
192
|
+
limits.maxImageDimension === DSH_ALPHA_DEFAULT_IMAGE_DIMENSION &&
|
|
193
|
+
exactAlphaNormalizationDefaults(store.normalizationPolicy)
|
|
194
|
+
const repairDimension =
|
|
195
|
+
limits.maxImageDimension === DSH_RC8_DEFAULT_IMAGE_DIMENSION ||
|
|
196
|
+
repairNormalization
|
|
197
|
+
|
|
198
|
+
if (!repairDimension && !repairNormalization) {
|
|
199
|
+
return { changed: false, reason: 'not-legacy-overlay', limits }
|
|
78
200
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
201
|
+
|
|
202
|
+
if (repairDimension) {
|
|
203
|
+
const nextLimits = Object.freeze({ ...limits, maxImageDimension: targetDimension })
|
|
204
|
+
if (!replaceFrozenPolicy(store, 'imageLimits', nextLimits)) {
|
|
205
|
+
try {
|
|
206
|
+
logger?.warn?.(
|
|
207
|
+
'vision-router: detected the legacy attachment-local admission overlay but could not repair maxImageDimension; DSH may still enforce the Host default',
|
|
208
|
+
)
|
|
209
|
+
} catch {
|
|
210
|
+
// Diagnostics must never break Host startup.
|
|
211
|
+
}
|
|
212
|
+
return { changed: false, reason: 'limits-readonly', limits }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (repairNormalization) {
|
|
217
|
+
const nextNormalization = Object.freeze({
|
|
218
|
+
maxPixels: targetNormalizedPixels,
|
|
219
|
+
maxDimension: targetNormalizedDimension,
|
|
220
|
+
maxBytes: targetNormalizedBytes,
|
|
221
|
+
})
|
|
222
|
+
if (!replaceFrozenPolicy(store, 'normalizationPolicy', nextNormalization)) {
|
|
223
|
+
// If admission was already repaired above, leave that safe improvement in
|
|
224
|
+
// place but report the unresolved canonical-image blocker explicitly.
|
|
225
|
+
try {
|
|
226
|
+
logger?.warn?.(
|
|
227
|
+
'vision-router: detected the legacy attachment-local alpha normalization defaults but could not repair normalizationPolicy; canonical images may still be downscaled',
|
|
228
|
+
)
|
|
229
|
+
} catch {
|
|
230
|
+
// Diagnostics must never break Host startup.
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
changed: repairDimension,
|
|
234
|
+
reason: 'normalization-policy-readonly',
|
|
235
|
+
limits: store.imageLimits,
|
|
236
|
+
normalizationPolicy: store.normalizationPolicy,
|
|
237
|
+
}
|
|
86
238
|
}
|
|
87
|
-
return { changed: false, reason: 'limits-readonly', limits }
|
|
88
239
|
}
|
|
89
240
|
|
|
90
241
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
242
|
+
if (repairNormalization) {
|
|
243
|
+
logger?.info?.(
|
|
244
|
+
'vision-router: repaired legacy attachment-local policy — admission %dpx; canonical image %dpx/%dpx/%d bytes',
|
|
245
|
+
store.imageLimits.maxImageDimension,
|
|
246
|
+
store.normalizationPolicy.maxPixels,
|
|
247
|
+
store.normalizationPolicy.maxDimension,
|
|
248
|
+
store.normalizationPolicy.maxBytes,
|
|
249
|
+
)
|
|
250
|
+
} else {
|
|
251
|
+
logger?.info?.(
|
|
252
|
+
'vision-router: repaired legacy attachment-local overlay — maxImageDimension %d -> %d',
|
|
253
|
+
DSH_RC8_DEFAULT_IMAGE_DIMENSION,
|
|
254
|
+
targetDimension,
|
|
255
|
+
)
|
|
256
|
+
}
|
|
96
257
|
} catch {
|
|
97
258
|
// Diagnostics must never break Host startup.
|
|
98
259
|
}
|
|
99
|
-
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
changed: true,
|
|
263
|
+
reason: repairNormalization ? 'legacy-alpha-policy-repaired' : 'legacy-overlay-repaired',
|
|
264
|
+
limits: store.imageLimits,
|
|
265
|
+
...(repairNormalization ? { normalizationPolicy: store.normalizationPolicy } : {}),
|
|
266
|
+
}
|
|
100
267
|
}
|
|
101
268
|
|
|
102
269
|
/**
|
|
@@ -269,6 +436,7 @@ export function installHostSettingsCompatibility(ctx, entryConfig, options = {})
|
|
|
269
436
|
* host context passes through untouched.
|
|
270
437
|
*/
|
|
271
438
|
export function attachmentContextForContract(ctx, logger, options = {}) {
|
|
439
|
+
installDshHostCapabilityDiagnostics(ctx)
|
|
272
440
|
if (hasBatchAttachmentContract(ctx)) return ctx
|
|
273
441
|
const install = options.installAndroidAttachmentCompat
|
|
274
442
|
if (typeof install !== 'function') return ctx
|
|
@@ -280,4 +448,4 @@ export function attachmentContextForContract(ctx, logger, options = {}) {
|
|
|
280
448
|
// releases do not accrete `if (rc8) / if (rc9)` branches.
|
|
281
449
|
export const isRc7ContractRuntime = hasBatchAttachmentContract
|
|
282
450
|
export const protectRc7ProviderOwnership = protectHostProviderOwnership
|
|
283
|
-
export const installRc7SettingsCompatibility = installHostSettingsCompatibility
|
|
451
|
+
export const installRc7SettingsCompatibility = installHostSettingsCompatibility
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const UNKNOWN = 'unknown'
|
|
2
|
+
|
|
3
|
+
function serviceOf(ctx, name) {
|
|
4
|
+
if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return undefined
|
|
5
|
+
try {
|
|
6
|
+
if (typeof ctx.get === 'function') {
|
|
7
|
+
const service = ctx.get(name)
|
|
8
|
+
if (service !== undefined && service !== null) return service
|
|
9
|
+
}
|
|
10
|
+
} catch {
|
|
11
|
+
// Capability diagnostics are advisory and must not break plugin startup.
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const service = ctx[name]
|
|
15
|
+
return service === undefined || service === null ? undefined : service
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasFunction(value, name) {
|
|
22
|
+
try {
|
|
23
|
+
return !!value && typeof value[name] === 'function'
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function known(value) {
|
|
30
|
+
return value === true || value === false ? value : UNKNOWN
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return a side-effect-free capability snapshot of the mounted DSH Host.
|
|
35
|
+
*
|
|
36
|
+
* The probe deliberately never registers adapters, tools, jobs, settings or
|
|
37
|
+
* client surfaces just to learn whether a feature exists. If a capability
|
|
38
|
+
* cannot be proven from a stable readable seam, it is reported as `unknown`
|
|
39
|
+
* rather than inferred from a DSH version string.
|
|
40
|
+
*/
|
|
41
|
+
export function inspectDshHostCapabilities(ctx) {
|
|
42
|
+
const attachments = serviceOf(ctx, 'attachments')
|
|
43
|
+
const llm = serviceOf(ctx, 'llm')
|
|
44
|
+
const jobs = serviceOf(ctx, 'jobs')
|
|
45
|
+
const settings = serviceOf(ctx, 'settings')
|
|
46
|
+
const tools = serviceOf(ctx, 'tools')
|
|
47
|
+
|
|
48
|
+
let maxImageDimension = false
|
|
49
|
+
try {
|
|
50
|
+
const value = attachments?.imageLimits?.maxImageDimension
|
|
51
|
+
maxImageDimension = Number.isFinite(Number(value)) && Number(value) > 0
|
|
52
|
+
} catch {
|
|
53
|
+
maxImageDimension = false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const adapterRegistration = hasFunction(llm, 'registerAdapter')
|
|
57
|
+
const prepareCall = hasFunction(llm, 'prepareCall')
|
|
58
|
+
const toolRegistration = hasFunction(tools, 'register') || hasFunction(tools, 'registerTool')
|
|
59
|
+
const toolExecution = hasFunction(tools, 'call') || hasFunction(tools, 'execute') || hasFunction(tools, 'invoke')
|
|
60
|
+
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
batchAttachments: hasFunction(attachments, 'saveImages'),
|
|
63
|
+
maxImageDimension,
|
|
64
|
+
adapterRegistration,
|
|
65
|
+
// replace() lives on the handle returned by registerAdapter(). A read-only
|
|
66
|
+
// Doctor cannot prove the handle shape without mutating Host topology, so
|
|
67
|
+
// an observable registration seam means "unknown", not "no".
|
|
68
|
+
registrationReplace: adapterRegistration ? UNKNOWN : false,
|
|
69
|
+
jobs: jobs === undefined ? false : true,
|
|
70
|
+
// No stable read-only client-surface capability flag exists across the
|
|
71
|
+
// supported Host window yet.
|
|
72
|
+
surfaceReplacement: UNKNOWN,
|
|
73
|
+
// DSH SettingsProvider.register() is the public live-namespace seam; get()
|
|
74
|
+
// and watch() belong to the returned SettingsScope rather than the service.
|
|
75
|
+
// Detect the stable service method and do not manufacture a registration.
|
|
76
|
+
settingsLiveNamespace: hasFunction(settings, 'register'),
|
|
77
|
+
// Browser exposure is a presentation capability and is not safely implied
|
|
78
|
+
// by the Host settings service alone.
|
|
79
|
+
settingsWebExposure: UNKNOWN,
|
|
80
|
+
prepareCall,
|
|
81
|
+
toolRegistration,
|
|
82
|
+
toolExecution,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function normalizeDshHostCapabilities(value) {
|
|
87
|
+
const source = value && typeof value === 'object' ? value : {}
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
batchAttachments: known(source.batchAttachments),
|
|
90
|
+
maxImageDimension: known(source.maxImageDimension),
|
|
91
|
+
adapterRegistration: known(source.adapterRegistration),
|
|
92
|
+
registrationReplace: known(source.registrationReplace),
|
|
93
|
+
jobs: known(source.jobs),
|
|
94
|
+
surfaceReplacement: known(source.surfaceReplacement),
|
|
95
|
+
settingsLiveNamespace: known(source.settingsLiveNamespace),
|
|
96
|
+
settingsWebExposure: known(source.settingsWebExposure),
|
|
97
|
+
prepareCall: known(source.prepareCall),
|
|
98
|
+
toolRegistration: known(source.toolRegistration),
|
|
99
|
+
toolExecution: known(source.toolExecution),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatDshHostCapability(value) {
|
|
104
|
+
if (value === true) return 'yes'
|
|
105
|
+
if (value === false) return 'no'
|
|
106
|
+
return UNKNOWN
|
|
107
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const DSH_SUPPORT_WINDOW = Object.freeze({
|
|
2
|
+
dvrTrain: '2.1.x',
|
|
3
|
+
minimum: '0.1.0-rc.8',
|
|
4
|
+
previous: '0.1.1-rc.1',
|
|
5
|
+
current: '0.1.1-rc.2',
|
|
6
|
+
canary: '0.1.2-alpha.4',
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export function supportWindowUpgradeAdvice(capabilities = {}) {
|
|
10
|
+
const batchAttachments = capabilities?.batchAttachments
|
|
11
|
+
const maxImageDimension = capabilities?.maxImageDimension
|
|
12
|
+
|
|
13
|
+
if (batchAttachments === false || maxImageDimension === false) {
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
level: 'required',
|
|
16
|
+
code: 'HOST_BELOW_CURRENT_FLOOR_CAPABILITIES',
|
|
17
|
+
message: `Host lacks attachment capabilities expected by DVR ${DSH_SUPPORT_WINDOW.dvrTrain}; the minimum supported Host is DSH ${DSH_SUPPORT_WINDOW.minimum}`,
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (batchAttachments === 'unknown' || maxImageDimension === 'unknown') {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
level: 'unknown',
|
|
24
|
+
code: 'HOST_CURRENT_FLOOR_UNKNOWN',
|
|
25
|
+
message: `DVR ${DSH_SUPPORT_WINDOW.dvrTrain} requires DSH ${DSH_SUPPORT_WINDOW.minimum} or newer, but this read-only probe cannot prove the relevant Host capabilities`,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
level: 'ok',
|
|
31
|
+
code: 'HOST_CURRENT_FLOOR_CAPABLE',
|
|
32
|
+
message: `Host exposes the attachment capabilities expected by the DVR ${DSH_SUPPORT_WINDOW.dvrTrain} support floor`,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatDshSupportWindowLines(capabilities = {}) {
|
|
37
|
+
const advice = supportWindowUpgradeAdvice(capabilities)
|
|
38
|
+
return Object.freeze([
|
|
39
|
+
'DSH Host support window:',
|
|
40
|
+
` DVR train: ${DSH_SUPPORT_WINDOW.dvrTrain}`,
|
|
41
|
+
` minimum: ${DSH_SUPPORT_WINDOW.minimum}`,
|
|
42
|
+
` previous: ${DSH_SUPPORT_WINDOW.previous}`,
|
|
43
|
+
` current: ${DSH_SUPPORT_WINDOW.current}`,
|
|
44
|
+
` canary only: ${DSH_SUPPORT_WINDOW.canary}`,
|
|
45
|
+
` support floor: DVR ${DSH_SUPPORT_WINDOW.dvrTrain} -> DSH ${DSH_SUPPORT_WINDOW.minimum}`,
|
|
46
|
+
` upgrade advice: ${advice.level} (${advice.code}) — ${advice.message}`,
|
|
47
|
+
])
|
|
48
|
+
}
|
package/lib/http-compat.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { currentVisionProviderTransport } from './vision-provider-transport.js'
|
|
2
|
+
|
|
1
3
|
const GLM_4V_FLASH = /^glm-4v-flash(?:$|[-_.])/i
|
|
2
4
|
export const DEFAULT_HTTP_ERROR_DETAIL_MAX_BYTES = 64 * 1024
|
|
3
5
|
|
|
@@ -262,6 +264,11 @@ function nextCompatibilityBody(body, detail) {
|
|
|
262
264
|
/**
|
|
263
265
|
* Fetch wrapper for vision-router's own OpenAI-compatible requests only.
|
|
264
266
|
*
|
|
267
|
+
* When the P2 provider transport is installed, every active Router-owned call
|
|
268
|
+
* goes through that facade. The `fetchImpl` parameter remains as a compatibility
|
|
269
|
+
* fallback for direct/unit callers, so the behavior outside the public plugin
|
|
270
|
+
* composition is unchanged.
|
|
271
|
+
*
|
|
265
272
|
* Layer 1: generic request shape (works for unknown/new models).
|
|
266
273
|
* Layer 2: known model-family presets.
|
|
267
274
|
* Layer 3: bounded, evidence-driven retries when the server reports an output
|
|
@@ -270,18 +277,27 @@ function nextCompatibilityBody(body, detail) {
|
|
|
270
277
|
* sequentially without creating an unbounded retry loop.
|
|
271
278
|
*/
|
|
272
279
|
export async function fetchWithOpenAICompatibility(fetchImpl, input, init, context = {}) {
|
|
273
|
-
if (typeof fetchImpl !== 'function'
|
|
274
|
-
|
|
280
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('fetch implementation is required')
|
|
281
|
+
const transport = context.active === true ? currentVisionProviderTransport() : undefined
|
|
282
|
+
const requestFetch = transport
|
|
283
|
+
? (nextInput, nextInit) => transport.fetch(nextInput, nextInit, {
|
|
284
|
+
providerName: context.providerName,
|
|
285
|
+
allowProxy: context.allowProxy !== false,
|
|
286
|
+
})
|
|
287
|
+
: fetchImpl
|
|
288
|
+
|
|
289
|
+
if (context.active !== true) {
|
|
290
|
+
return requestFetch(input, init)
|
|
275
291
|
}
|
|
276
292
|
if (!isCompatibleChatCompletionRequest(input, init)) {
|
|
277
|
-
return
|
|
293
|
+
return requestFetch(input, init)
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
let parsed
|
|
281
297
|
try {
|
|
282
298
|
parsed = JSON.parse(init.body)
|
|
283
299
|
} catch {
|
|
284
|
-
return
|
|
300
|
+
return requestFetch(input, init)
|
|
285
301
|
}
|
|
286
302
|
|
|
287
303
|
const url = typeof input === 'string' || input instanceof URL ? input : input?.url
|
|
@@ -291,7 +307,7 @@ export async function fetchWithOpenAICompatibility(fetchImpl, input, init, conte
|
|
|
291
307
|
prompt: context.prompt,
|
|
292
308
|
})
|
|
293
309
|
let requestBody = prepared.body
|
|
294
|
-
let response = await
|
|
310
|
+
let response = await requestFetch(input, { ...init, body: JSON.stringify(requestBody) })
|
|
295
311
|
|
|
296
312
|
for (let correction = 0; correction < 2; correction++) {
|
|
297
313
|
if (response.ok || (response.status !== 400 && response.status !== 422)) return response
|
|
@@ -308,7 +324,7 @@ export async function fetchWithOpenAICompatibility(fetchImpl, input, init, conte
|
|
|
308
324
|
}
|
|
309
325
|
|
|
310
326
|
requestBody = retryBody
|
|
311
|
-
response = await
|
|
327
|
+
response = await requestFetch(input, { ...init, body: JSON.stringify(requestBody) })
|
|
312
328
|
}
|
|
313
329
|
|
|
314
330
|
return response
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { currentVisionTurnBudgetSignal } from './turn-budget-context.js'
|
|
2
2
|
|
|
3
3
|
const MIB = 1024 * 1024
|
|
4
|
+
export const DEFAULT_IMAGE_RESOURCE_MAX_BYPASSES = 2
|
|
4
5
|
|
|
5
6
|
function abortError() {
|
|
6
7
|
const error = new Error('image resource wait aborted')
|
|
@@ -108,9 +109,14 @@ export function boundedOcrTiles(
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
export class ImageResourceGovernor {
|
|
111
|
-
constructor({
|
|
112
|
+
constructor({
|
|
113
|
+
maxBytes = 256 * MIB,
|
|
114
|
+
maxConcurrent = 2,
|
|
115
|
+
maxBypasses = DEFAULT_IMAGE_RESOURCE_MAX_BYPASSES,
|
|
116
|
+
} = {}) {
|
|
112
117
|
this.maxBytes = Math.max(1, Math.floor(Number(maxBytes) || 256 * MIB))
|
|
113
118
|
this.maxConcurrent = Math.max(1, Math.floor(Number(maxConcurrent) || 2))
|
|
119
|
+
this.maxBypasses = Math.max(0, Math.floor(Number(maxBypasses) || 0))
|
|
114
120
|
this.activeBytes = 0
|
|
115
121
|
this.activeCount = 0
|
|
116
122
|
this.activeExclusive = false
|
|
@@ -156,17 +162,35 @@ export class ImageResourceGovernor {
|
|
|
156
162
|
})
|
|
157
163
|
}
|
|
158
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Find work that fits the current byte/concurrency window without letting a
|
|
167
|
+
* large request permanently pin every smaller request behind it. Each blocked
|
|
168
|
+
* non-exclusive request may be bypassed only a small, fixed number of times;
|
|
169
|
+
* after that it becomes a fairness barrier until capacity is released.
|
|
170
|
+
* Exclusive work is always a barrier immediately.
|
|
171
|
+
*/
|
|
172
|
+
_nextRunnable() {
|
|
173
|
+
const bypassed = []
|
|
174
|
+
for (let index = 0; index < this.queue.length; index++) {
|
|
175
|
+
const item = this.queue[index]
|
|
176
|
+
if (!item || item.settled) continue
|
|
177
|
+
if (this._canRun(item.request)) return { index, item, bypassed }
|
|
178
|
+
if (item.request.exclusive || item.bypassCount >= this.maxBypasses) return undefined
|
|
179
|
+
bypassed.push(item)
|
|
180
|
+
}
|
|
181
|
+
return undefined
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
_drain() {
|
|
160
185
|
while (this.queue.length > 0) {
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.
|
|
168
|
-
|
|
169
|
-
if (item.request.exclusive) break
|
|
186
|
+
while (this.queue[0]?.settled) this.queue.shift()
|
|
187
|
+
if (this.queue.length === 0) return
|
|
188
|
+
const selected = this._nextRunnable()
|
|
189
|
+
if (!selected) return
|
|
190
|
+
this.queue.splice(selected.index, 1)
|
|
191
|
+
for (const item of selected.bypassed) item.bypassCount += 1
|
|
192
|
+
this._grant(selected.item)
|
|
193
|
+
if (selected.item.request.exclusive) return
|
|
170
194
|
}
|
|
171
195
|
}
|
|
172
196
|
|
|
@@ -186,6 +210,7 @@ export class ImageResourceGovernor {
|
|
|
186
210
|
reject,
|
|
187
211
|
settled: false,
|
|
188
212
|
abortHandler: undefined,
|
|
213
|
+
bypassCount: 0,
|
|
189
214
|
}
|
|
190
215
|
item.abortHandler = () => {
|
|
191
216
|
if (item.settled) return
|