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.
- package/README.md +16 -7
- package/README.zh.md +16 -7
- package/docs/architecture/2.3.0-compat-retirement.md +147 -0
- package/docs/architecture/2.3.0-execution-plan.md +410 -0
- package/docs/architecture/2.3.0-final-audit.md +279 -0
- package/docs/architecture/2.3.0-reopened-plan.md +720 -0
- package/docs/architecture/3.0-architecture-plan.md +252 -0
- package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
- package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
- package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
- package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
- package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
- package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
- package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
- package/docs/architecture/3.0-r7-public-api.md +151 -0
- package/docs/architecture/3.0-r8-ci-release.md +248 -0
- package/docs/architecture/3.0-r9-final-audit.md +370 -0
- package/docs/architecture/compat-inventory.md +26 -34
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +30 -22
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +9 -57
- package/index.js +268 -761
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/catalog-corrections.js +1 -2
- package/lib/client-maintenance-actions.js +145 -0
- package/lib/client-presentation-boundary-main.js +75 -17
- package/lib/client-presentation-boundary.js +53 -28
- package/lib/client.js +246 -144
- package/lib/core-apply-lifecycle.js +41 -0
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +91 -278
- package/lib/core-vision-surface-domain.js +63 -0
- package/lib/core-vision-surface.js +11 -77
- package/lib/delegated-call-config.js +15 -24
- package/lib/desktop-screenshot-tool.js +262 -0
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +37 -256
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/dsh-support-window.js +3 -3
- package/lib/file-logger.js +8 -3
- package/lib/grounding-coordinate-runtime.js +1 -2
- package/lib/http-body-limit.js +97 -79
- package/lib/http-compat.js +1 -2
- package/lib/image-offload-compat.js +30 -21
- package/lib/image-resource-governor.js +210 -228
- package/lib/legacy-global-proxy-boundary.js +46 -3
- package/lib/live-model-client-prelude.js +12 -4
- package/lib/live-model-discovery.js +134 -43
- package/lib/local-remote-settings-permission.js +33 -7
- package/lib/local-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +134 -0
- package/lib/native-image-coexistence.js +15 -3
- package/lib/ollama-cold-start.js +87 -23
- package/lib/pi-ai-bridge-wire-compat.js +3 -2
- package/lib/proxy-dispatcher-pool.js +151 -92
- package/lib/proxy-routing.js +75 -51
- package/lib/proxy-url-compat.js +3 -10
- package/lib/public-config.js +36 -0
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +19 -11
- package/lib/remote-settings-bridge.js +11 -0
- package/lib/remote-settings-risk-client-boundary.js +155 -0
- package/lib/remote-settings-risk-confirmation.js +17 -105
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +127 -34
- package/lib/runtime-i18n-boundary.js +10 -11
- package/lib/screenshot-source-boundary.js +2 -2
- package/lib/session-affinity-runtime.js +113 -97
- package/lib/session-affinity.js +89 -76
- package/lib/session-event-history-compat.js +30 -0
- package/lib/session-message-source-compat.js +7 -3
- package/lib/session-query-readers.js +263 -0
- package/lib/session-surface-compat.js +30 -31
- package/lib/session-surface-policy-domain.js +85 -0
- package/lib/session-surface-policy.js +2 -111
- package/lib/session-turn-resolver.js +2 -6
- package/lib/session-vision-attachment-index.js +143 -0
- package/lib/session-vision-delegation-boundary.js +34 -0
- package/lib/session-vision-delegation.js +22 -0
- package/lib/session-vision-index.js +179 -116
- package/lib/session-vision-mode-authority.js +229 -192
- package/lib/session-vision-mode-boundary.js +65 -22
- package/lib/session-vision-policy-domain.js +304 -0
- package/lib/session-vision-policy.js +104 -0
- package/lib/session-vision-state.js +368 -378
- package/lib/settings-client-017-compat.js +9 -7
- package/lib/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +25 -6
- package/lib/tesseract-exec-compat.js +9 -1
- package/lib/turn-budget-context.js +47 -41
- package/lib/update-check.js +11 -2
- package/lib/vision-attachment-handle-runtime.js +176 -156
- package/lib/vision-backend-runtime-policy.js +19 -2
- package/lib/vision-background-benchmark.js +41 -4
- package/lib/vision-background-failure-policy.js +41 -41
- package/lib/vision-background-stop-store-core.js +217 -0
- package/lib/vision-background-stop-store.js +8 -179
- package/lib/vision-capability-benchmark-service.js +1 -0
- package/lib/vision-capability-benchmark.js +11 -61
- package/lib/vision-capability-evidence.js +35 -0
- package/lib/vision-capability-fingerprint.js +72 -0
- package/lib/vision-capability-identity.js +116 -61
- package/lib/vision-capability-router.js +13 -33
- package/lib/vision-chunk-assembler.js +95 -0
- package/lib/vision-diagnostics-routes.js +246 -0
- package/lib/vision-evidence-guidance.js +3 -1
- package/lib/vision-execution-order-apply.js +45 -55
- package/lib/vision-execution-order.js +19 -19
- package/lib/vision-execution-policy.js +12 -7
- package/lib/vision-model-registry.js +30 -17
- package/lib/vision-provider-transport.js +137 -176
- package/lib/vision-routing-authority.js +47 -48
- package/lib/vision-routing-product.js +26 -29
- package/lib/vision-routing-settings-prelude.js +8 -3
- package/lib/vision-runtime-performance-store.js +166 -0
- package/lib/vision-runtime-performance.js +13 -142
- package/lib/vision-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +104 -31
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +6 -9
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +56 -20
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -397
package/lib/core-primitives.js
CHANGED
|
@@ -1,94 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { DeepSeekAdapter, resolveAdapterOptions } from '@deepseek-ai/dsh-llm-deepseek'
|
|
6
|
-
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
|
|
4
|
+
|
|
7
5
|
import { existsSync } from 'node:fs'
|
|
8
6
|
import { execFile } from 'node:child_process'
|
|
9
7
|
import { Worker } from 'node:worker_threads'
|
|
10
8
|
import { createRequire } from 'node:module'
|
|
11
9
|
import { pathToFileURL } from 'node:url'
|
|
12
10
|
import { promisify } from 'node:util'
|
|
13
|
-
import {
|
|
11
|
+
import { fetchWithOpenAICompatibility } from './http-compat.js'
|
|
14
12
|
import {
|
|
15
13
|
directSessionAffinityHeaders,
|
|
16
|
-
isOfficialOpenCodeGoUrl,
|
|
17
|
-
openCodeSessionAffinityHeaderForUrl,
|
|
18
14
|
rawSessionIdentity,
|
|
19
|
-
sessionIdentityOf,
|
|
20
15
|
} from './session-affinity.js'
|
|
21
|
-
import { runWithVisionSessionAffinity
|
|
16
|
+
import { runWithVisionSessionAffinity } from './session-affinity-runtime.js'
|
|
22
17
|
import {
|
|
23
|
-
routingCorrectionFor,
|
|
24
|
-
toAnthropicMessages,
|
|
25
18
|
callAnthropicCompatible,
|
|
26
19
|
anthropicMediaType,
|
|
27
20
|
} from './catalog-corrections.js'
|
|
28
|
-
|
|
29
|
-
import { probeLocalBackends } from './local-connection-probe.js'
|
|
30
|
-
import { detectDshSelfUpdatePlan, runDshPluginUpdate } from './self-update.js'
|
|
21
|
+
|
|
31
22
|
import {
|
|
32
|
-
classifyVisionFailure,
|
|
33
|
-
createDeadline,
|
|
34
23
|
combineSignals,
|
|
35
|
-
createVisionCircuitBreaker,
|
|
36
|
-
createVisionTurnMemory,
|
|
37
|
-
buildVisionFailure,
|
|
38
|
-
ensureSentencePunctuation,
|
|
39
|
-
resultCodeForKinds,
|
|
40
24
|
qwenKeyEndpointHint,
|
|
41
25
|
kindForHttpStatus,
|
|
42
|
-
VISION_FAILURE_KINDS,
|
|
43
|
-
VISION_RESULT_CODES,
|
|
44
26
|
} from './vision-resilience.js'
|
|
45
|
-
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
import {
|
|
49
|
-
normalizeStructuredBootstrapResult,
|
|
50
|
-
structuredBootstrapMemory,
|
|
51
|
-
structuredBootstrapQuestion,
|
|
52
|
-
} from './structured-bootstrap.js'
|
|
53
|
-
import { planMixedBranches, renderMixedGuidance } from './mixed-router.js'
|
|
54
|
-
import { renderDepthGuidance } from './depth-guidance.js'
|
|
55
|
-
import { assertNoRepetitionLoop } from './repetition-guard.js'
|
|
56
|
-
import { compareRgbaStreams } from './pixel-diff-stream.js'
|
|
27
|
+
|
|
28
|
+
import { createHash } from 'node:crypto'
|
|
29
|
+
|
|
57
30
|
import {
|
|
58
|
-
boundedOcrTiles,
|
|
59
31
|
defaultImageResourceGovernor,
|
|
60
32
|
estimateImageOperationBytes,
|
|
61
33
|
scaleBox,
|
|
62
34
|
scaledDimensions,
|
|
63
35
|
} from './image-resource-governor.js'
|
|
64
|
-
|
|
65
|
-
import { createSessionVisionStateStore } from './session-vision-state.js'
|
|
36
|
+
|
|
66
37
|
import {
|
|
67
38
|
ERROR_RESPONSE_MAX_BYTES,
|
|
68
|
-
METADATA_RESPONSE_MAX_BYTES,
|
|
69
39
|
MODEL_RESPONSE_MAX_BYTES,
|
|
70
40
|
readResponseJsonBounded,
|
|
71
41
|
readResponseTextBounded,
|
|
72
42
|
} from './http-body-limit.js'
|
|
73
|
-
|
|
43
|
+
|
|
74
44
|
import { stripTrailingSlashes } from './string-normalization.js'
|
|
75
45
|
import { isOffloadedImageBlock, offloadedImagePlaceholder } from './image-offload-compat.js'
|
|
76
46
|
import { proxyHostMatchesAny } from './proxy-routing.js'
|
|
77
|
-
import { parseVersionComparator } from './version-range.js'
|
|
78
|
-
import { captureWindowsDesktop } from './windows-desktop-capture.js'
|
|
79
|
-
import {
|
|
80
|
-
sharpPromise,
|
|
81
|
-
sharpWarningHook,
|
|
82
|
-
registerSharpWarningHook,
|
|
83
|
-
warnSharp,
|
|
84
|
-
parseVersionParts,
|
|
85
|
-
compareVersionParts,
|
|
86
|
-
versionSatisfies,
|
|
87
|
-
sharpPeerRangeCache,
|
|
88
|
-
sharpPeerRange,
|
|
89
|
-
loadSharp,
|
|
90
|
-
} from './sharp-runtime.js'
|
|
91
47
|
|
|
48
|
+
import { loadSharp } from './sharp-runtime.js'
|
|
92
49
|
export const IMAGE_EXTENSIONS = {
|
|
93
50
|
png: 'image/png',
|
|
94
51
|
jpg: 'image/jpeg',
|
|
@@ -109,7 +66,6 @@ export function mediaTypeOf(path) {
|
|
|
109
66
|
*/
|
|
110
67
|
export { depthLimitFor } from './depth-guidance.js'
|
|
111
68
|
|
|
112
|
-
|
|
113
69
|
/**
|
|
114
70
|
* Detect the image format from magic bytes instead of the file extension.
|
|
115
71
|
* Attachments are stored as content-addressed files WITHOUT an extension,
|
|
@@ -655,77 +611,7 @@ export function extractJson(text) {
|
|
|
655
611
|
return undefined
|
|
656
612
|
}
|
|
657
613
|
|
|
658
|
-
|
|
659
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return value.byteLength
|
|
660
|
-
if (typeof value === 'string') return Buffer.byteLength(value, 'utf8')
|
|
661
|
-
try {
|
|
662
|
-
const encoded = JSON.stringify(value)
|
|
663
|
-
return Buffer.byteLength(encoded === undefined ? String(value) : encoded, 'utf8')
|
|
664
|
-
} catch {
|
|
665
|
-
return Buffer.byteLength(String(value), 'utf8')
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/** LRU+TTL cache bounded by BOTH entry count and retained bytes. */
|
|
670
|
-
export function createCache(maxEntries, ttlMs, options = {}) {
|
|
671
|
-
const entries = new Map()
|
|
672
|
-
const entryLimit = Math.max(0, Math.floor(Number(maxEntries) || 0))
|
|
673
|
-
const maxBytes = Number.isFinite(Number(options.maxBytes)) && Number(options.maxBytes) >= 0
|
|
674
|
-
? Math.floor(Number(options.maxBytes))
|
|
675
|
-
: 8 * 1024 * 1024
|
|
676
|
-
const maxEntryBytes = Number.isFinite(Number(options.maxEntryBytes)) && Number(options.maxEntryBytes) >= 0
|
|
677
|
-
? Math.floor(Number(options.maxEntryBytes))
|
|
678
|
-
: Math.min(maxBytes, 1024 * 1024)
|
|
679
|
-
let retainedBytes = 0
|
|
680
|
-
|
|
681
|
-
const remove = (key) => {
|
|
682
|
-
const entry = entries.get(key)
|
|
683
|
-
if (!entry) return
|
|
684
|
-
retainedBytes = Math.max(0, retainedBytes - entry.weight)
|
|
685
|
-
entries.delete(key)
|
|
686
|
-
}
|
|
687
|
-
const evict = () => {
|
|
688
|
-
while (entries.size > entryLimit || retainedBytes > maxBytes) {
|
|
689
|
-
const oldest = entries.keys().next().value
|
|
690
|
-
if (oldest === undefined) break
|
|
691
|
-
remove(oldest)
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return {
|
|
696
|
-
get(key) {
|
|
697
|
-
const entry = entries.get(key)
|
|
698
|
-
if (!entry) return undefined
|
|
699
|
-
if (entry.expiresAt <= Date.now()) {
|
|
700
|
-
remove(key)
|
|
701
|
-
return undefined
|
|
702
|
-
}
|
|
703
|
-
entries.delete(key)
|
|
704
|
-
entries.set(key, entry)
|
|
705
|
-
return entry.value
|
|
706
|
-
},
|
|
707
|
-
set(key, value) {
|
|
708
|
-
const normalizedKey = String(key)
|
|
709
|
-
const weight = Buffer.byteLength(normalizedKey, 'utf8') + cacheWeight(value)
|
|
710
|
-
remove(normalizedKey)
|
|
711
|
-
if (entryLimit === 0 || maxBytes === 0 || weight > maxEntryBytes || weight > maxBytes) return false
|
|
712
|
-
entries.set(normalizedKey, {
|
|
713
|
-
value,
|
|
714
|
-
weight,
|
|
715
|
-
expiresAt: ttlMs <= 0 ? Infinity : Date.now() + ttlMs,
|
|
716
|
-
})
|
|
717
|
-
retainedBytes += weight
|
|
718
|
-
evict()
|
|
719
|
-
return entries.has(normalizedKey)
|
|
720
|
-
},
|
|
721
|
-
get size() {
|
|
722
|
-
return entries.size
|
|
723
|
-
},
|
|
724
|
-
get bytes() {
|
|
725
|
-
return retainedBytes
|
|
726
|
-
},
|
|
727
|
-
}
|
|
728
|
-
}
|
|
614
|
+
export { cacheWeight, createCache } from './core-cache.js'
|
|
729
615
|
|
|
730
616
|
/** True when the harness llm service has a registered adapter for the provider route. */
|
|
731
617
|
export function adapterAvailable(llm, provider) {
|
|
@@ -1268,29 +1154,52 @@ export function bitmapOfGray(raw, width, height, threshold = 128) {
|
|
|
1268
1154
|
return out
|
|
1269
1155
|
}
|
|
1270
1156
|
|
|
1157
|
+
function visionAbortReason(signal, fallback = 'vision operation aborted') {
|
|
1158
|
+
if (signal?.reason instanceof Error) return signal.reason
|
|
1159
|
+
const error = new Error(fallback)
|
|
1160
|
+
error.name = 'AbortError'
|
|
1161
|
+
error.code = 'ABORT_ERR'
|
|
1162
|
+
return error
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function throwIfVisionAborted(signal, fallback) {
|
|
1166
|
+
if (signal?.aborted) throw visionAbortReason(signal, fallback)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function isVisionAbort(error, signal) {
|
|
1170
|
+
return signal?.aborted === true || error?.name === 'AbortError' || error?.code === 'ABORT_ERR'
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1271
1173
|
/** Vectorize an image buffer into an SVG string via potrace posterization. */
|
|
1272
|
-
export function posterizeSvg(bytes, steps = 4, fillStrategy = 'dominant', timeoutMs = 60000) {
|
|
1174
|
+
export function posterizeSvg(bytes, steps = 4, fillStrategy = 'dominant', timeoutMs = 60000, options = {}) {
|
|
1273
1175
|
// potrace is CPU-bound and runs its computation in long synchronous
|
|
1274
1176
|
// chunks: on the main thread it blocks the whole dsh process (other
|
|
1275
1177
|
// sessions time out) and a setTimeout-based timeout can NEVER fire while
|
|
1276
1178
|
// the loop is blocked. Run it in a worker thread instead — the main loop
|
|
1277
1179
|
// stays responsive, and a timeout hard-terminates the worker.
|
|
1180
|
+
const signal = options?.signal
|
|
1278
1181
|
return new Promise((resolve, reject) => {
|
|
1279
1182
|
let settled = false
|
|
1280
1183
|
let worker
|
|
1184
|
+
let timer
|
|
1185
|
+
let abortListener
|
|
1281
1186
|
const finish = (error, svg) => {
|
|
1282
1187
|
if (settled) return
|
|
1283
1188
|
settled = true
|
|
1284
|
-
clearTimeout(timer)
|
|
1189
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
1190
|
+
if (signal && abortListener) signal.removeEventListener?.('abort', abortListener)
|
|
1285
1191
|
void worker?.terminate()
|
|
1286
1192
|
if (error) reject(error)
|
|
1287
1193
|
else resolve(svg)
|
|
1288
1194
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1195
|
+
abortListener = () => finish(visionAbortReason(signal, 'potrace aborted'))
|
|
1196
|
+
if (signal?.aborted) {
|
|
1197
|
+
finish(visionAbortReason(signal, 'potrace aborted'))
|
|
1198
|
+
return
|
|
1199
|
+
}
|
|
1200
|
+
signal?.addEventListener?.('abort', abortListener, { once: true })
|
|
1201
|
+
timer = setTimeout(() => {
|
|
1202
|
+
finish(
|
|
1294
1203
|
new Error(
|
|
1295
1204
|
'potrace timed out — the image is too large or too complex; crop it to the target region first',
|
|
1296
1205
|
),
|
|
@@ -1347,23 +1256,30 @@ export function posterizeSvg(bytes, steps = 4, fillStrategy = 'dominant', timeou
|
|
|
1347
1256
|
* @param palette - [{ hex, count, share }] from quantizeColors, ordered by
|
|
1348
1257
|
* share descending.
|
|
1349
1258
|
*/
|
|
1350
|
-
export function posterizeSvgColor(data, info, palette, timeoutMs = 60000) {
|
|
1259
|
+
export function posterizeSvgColor(data, info, palette, timeoutMs = 60000, options = {}) {
|
|
1260
|
+
const signal = options?.signal
|
|
1351
1261
|
return new Promise((resolve, reject) => {
|
|
1352
1262
|
let settled = false
|
|
1353
1263
|
let worker
|
|
1264
|
+
let timer
|
|
1265
|
+
let abortListener
|
|
1354
1266
|
const finish = (error, svg) => {
|
|
1355
1267
|
if (settled) return
|
|
1356
1268
|
settled = true
|
|
1357
|
-
clearTimeout(timer)
|
|
1269
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
1270
|
+
if (signal && abortListener) signal.removeEventListener?.('abort', abortListener)
|
|
1358
1271
|
void worker?.terminate()
|
|
1359
1272
|
if (error) reject(error)
|
|
1360
1273
|
else resolve(svg)
|
|
1361
1274
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1275
|
+
abortListener = () => finish(visionAbortReason(signal, 'color trace aborted'))
|
|
1276
|
+
if (signal?.aborted) {
|
|
1277
|
+
finish(visionAbortReason(signal, 'color trace aborted'))
|
|
1278
|
+
return
|
|
1279
|
+
}
|
|
1280
|
+
signal?.addEventListener?.('abort', abortListener, { once: true })
|
|
1281
|
+
timer = setTimeout(() => {
|
|
1282
|
+
finish(
|
|
1367
1283
|
new Error(
|
|
1368
1284
|
'color trace timed out — the image is too large or too complex; crop it to the target region first',
|
|
1369
1285
|
),
|
|
@@ -1473,13 +1389,21 @@ export function resolveVisionOcrEngine(requestedEngine, configuredEngine = 'auto
|
|
|
1473
1389
|
}
|
|
1474
1390
|
|
|
1475
1391
|
/** OCR image bytes with a local tesseract binary (chi_sim+eng) when available. */
|
|
1476
|
-
export async function ocrWithTesseract(bytes, timeoutMs = 60000) {
|
|
1477
|
-
const exec = promisify(execFile)
|
|
1392
|
+
export async function ocrWithTesseract(bytes, timeoutMs = 60000, options = {}) {
|
|
1393
|
+
const exec = typeof options.exec === 'function' ? options.exec : promisify(execFile)
|
|
1394
|
+
const signal = options.signal
|
|
1395
|
+
throwIfVisionAborted(signal, 'tesseract OCR aborted')
|
|
1478
1396
|
const { stdout } = await exec(
|
|
1479
1397
|
'tesseract',
|
|
1480
1398
|
['stdin', 'stdout', '-l', 'chi_sim+eng', '--psm', '6'],
|
|
1481
|
-
{
|
|
1399
|
+
{
|
|
1400
|
+
timeout: Math.min(timeoutMs, 60000),
|
|
1401
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
1402
|
+
input: bytes,
|
|
1403
|
+
...(signal === undefined ? {} : { signal }),
|
|
1404
|
+
},
|
|
1482
1405
|
)
|
|
1406
|
+
throwIfVisionAborted(signal, 'tesseract OCR aborted')
|
|
1483
1407
|
return String(stdout ?? '')
|
|
1484
1408
|
}
|
|
1485
1409
|
|
|
@@ -1543,6 +1467,7 @@ export function selectTesseractOcrCandidate(candidates) {
|
|
|
1543
1467
|
|
|
1544
1468
|
export async function ocrWithTesseractAdaptive(bytes, timeoutMs = 12000, options = {}) {
|
|
1545
1469
|
const exec = typeof options.exec === 'function' ? options.exec : promisify(execFile)
|
|
1470
|
+
const signal = options.signal
|
|
1546
1471
|
const requestedBudget = Number(timeoutMs)
|
|
1547
1472
|
const budgetMs = Number.isFinite(requestedBudget) && requestedBudget > 0
|
|
1548
1473
|
? Math.min(60000, requestedBudget)
|
|
@@ -1551,20 +1476,25 @@ export async function ocrWithTesseractAdaptive(bytes, timeoutMs = 12000, options
|
|
|
1551
1476
|
const attemptedPsms = []
|
|
1552
1477
|
const remaining = () => Math.max(1, budgetMs - (Date.now() - started))
|
|
1553
1478
|
const run = async (psm) => {
|
|
1479
|
+
throwIfVisionAborted(signal, 'adaptive tesseract OCR aborted')
|
|
1554
1480
|
attemptedPsms.push(psm)
|
|
1555
1481
|
const common = ['stdin', 'stdout', '-l', 'chi_sim+eng', '--psm', String(psm)]
|
|
1556
1482
|
const execOptions = () => ({
|
|
1557
1483
|
timeout: remaining(),
|
|
1558
1484
|
maxBuffer: 32 * 1024 * 1024,
|
|
1559
1485
|
input: bytes,
|
|
1486
|
+
...(signal === undefined ? {} : { signal }),
|
|
1560
1487
|
})
|
|
1561
1488
|
const { stdout: plain } = await exec('tesseract', common, execOptions())
|
|
1489
|
+
throwIfVisionAborted(signal, 'adaptive tesseract OCR aborted')
|
|
1562
1490
|
const text = String(plain ?? '').trim()
|
|
1563
1491
|
if (text === '') return assessTesseractOcr('', '', psm)
|
|
1564
1492
|
try {
|
|
1565
1493
|
const { stdout: tsv } = await exec('tesseract', [...common, 'tsv'], execOptions())
|
|
1494
|
+
throwIfVisionAborted(signal, 'adaptive tesseract OCR aborted')
|
|
1566
1495
|
return assessTesseractOcr(text, String(tsv ?? ''), psm)
|
|
1567
|
-
} catch {
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
if (isVisionAbort(error, signal)) throw error
|
|
1568
1498
|
return { psm, text, words: 0, quality: 0, riskyTokens: [], uncertain: true }
|
|
1569
1499
|
}
|
|
1570
1500
|
}
|
|
@@ -1577,7 +1507,8 @@ export async function ocrWithTesseractAdaptive(bytes, timeoutMs = 12000, options
|
|
|
1577
1507
|
if (Date.now() - started >= budgetMs) break
|
|
1578
1508
|
try {
|
|
1579
1509
|
candidates.push(await run(psm))
|
|
1580
|
-
} catch {
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
if (isVisionAbort(error, signal)) throw error
|
|
1581
1512
|
// Keep the first successful local OCR result when an optional review pass fails.
|
|
1582
1513
|
}
|
|
1583
1514
|
}
|
|
@@ -2096,6 +2027,7 @@ export async function callLocalBackend(provider, messages, options = {}) {
|
|
|
2096
2027
|
maxTokens,
|
|
2097
2028
|
signal: options.signal,
|
|
2098
2029
|
allowKeyless: true,
|
|
2030
|
+
providerTransport: options.providerTransport,
|
|
2099
2031
|
system: system.join('\n').trim(),
|
|
2100
2032
|
...(rawSessionIdentity(options.sessionId) === undefined
|
|
2101
2033
|
? {}
|
|
@@ -2109,6 +2041,7 @@ export async function callLocalBackend(provider, messages, options = {}) {
|
|
|
2109
2041
|
}
|
|
2110
2042
|
return callOpenAICompatible(provider, messages, {
|
|
2111
2043
|
maxTokens,
|
|
2044
|
+
providerTransport: options.providerTransport,
|
|
2112
2045
|
signal: options.signal,
|
|
2113
2046
|
...(rawSessionIdentity(options.sessionId) === undefined
|
|
2114
2047
|
? {}
|
|
@@ -2291,7 +2224,7 @@ export async function callOpenAICompatible(provider, messages, options = {}) {
|
|
|
2291
2224
|
body: JSON.stringify(body),
|
|
2292
2225
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
2293
2226
|
},
|
|
2294
|
-
{ active: true, providerName: provider.name },
|
|
2227
|
+
{ active: true, providerName: provider.name, providerTransport: options.providerTransport },
|
|
2295
2228
|
)
|
|
2296
2229
|
const response = await request()
|
|
2297
2230
|
if (!response.ok) {
|
|
@@ -2332,77 +2265,8 @@ export async function callOpenAICompatible(provider, messages, options = {}) {
|
|
|
2332
2265
|
return text
|
|
2333
2266
|
}
|
|
2334
2267
|
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
* `llm/stream` chunk protocol and produces the final text of text blocks.
|
|
2338
|
-
* Terminal failures throw; a `max-tokens` finish returns the partial text.
|
|
2339
|
-
*/
|
|
2340
|
-
export function createChunkAssembler() {
|
|
2341
|
-
const parts = new Map()
|
|
2342
|
-
const order = []
|
|
2343
|
-
let finishKind
|
|
2344
|
-
let failure
|
|
2345
|
-
|
|
2346
|
-
const push = (chunk) => {
|
|
2347
|
-
if (!chunk || typeof chunk.type !== 'string') return
|
|
2348
|
-
switch (chunk.type) {
|
|
2349
|
-
case 'block-start': {
|
|
2350
|
-
if (!parts.has(chunk.index)) {
|
|
2351
|
-
order.push(chunk.index)
|
|
2352
|
-
parts.set(chunk.index, { type: chunk.blockType, text: '' })
|
|
2353
|
-
}
|
|
2354
|
-
break
|
|
2355
|
-
}
|
|
2356
|
-
case 'text-delta': {
|
|
2357
|
-
const part = parts.get(chunk.index)
|
|
2358
|
-
if (part) part.text += chunk.text ?? ''
|
|
2359
|
-
break
|
|
2360
|
-
}
|
|
2361
|
-
case 'reasoning-delta':
|
|
2362
|
-
case 'tool-call-delta':
|
|
2363
|
-
case 'usage':
|
|
2364
|
-
break
|
|
2365
|
-
case 'block-end': {
|
|
2366
|
-
const part = parts.get(chunk.index)
|
|
2367
|
-
if (part && chunk.block && typeof chunk.block.text === 'string') {
|
|
2368
|
-
part.text = chunk.block.text
|
|
2369
|
-
}
|
|
2370
|
-
break
|
|
2371
|
-
}
|
|
2372
|
-
case 'finish': {
|
|
2373
|
-
const reason = chunk.reason
|
|
2374
|
-
if (reason && (reason.kind === 'error' || reason.kind === 'aborted')) {
|
|
2375
|
-
failure = reason.failure
|
|
2376
|
-
}
|
|
2377
|
-
finishKind = reason && reason.kind ? reason.kind : 'stop'
|
|
2378
|
-
break
|
|
2379
|
-
}
|
|
2380
|
-
case 'error':
|
|
2381
|
-
case 'aborted':
|
|
2382
|
-
failure = chunk.failure
|
|
2383
|
-
break
|
|
2384
|
-
default:
|
|
2385
|
-
break
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
const finish = () => {
|
|
2390
|
-
if (failure) {
|
|
2391
|
-
throw new Error(failure && failure.message ? failure.message : String(failure))
|
|
2392
|
-
}
|
|
2393
|
-
if (finishKind !== undefined && finishKind !== 'stop' && finishKind !== 'max-tokens') {
|
|
2394
|
-
throw new Error(`vision call finished with "${finishKind}"`)
|
|
2395
|
-
}
|
|
2396
|
-
return order
|
|
2397
|
-
.map((index) => parts.get(index))
|
|
2398
|
-
.filter((part) => part && part.type === 'text')
|
|
2399
|
-
.map((part) => part.text)
|
|
2400
|
-
.join('')
|
|
2401
|
-
.trim()
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
return { push, finish }
|
|
2405
|
-
}
|
|
2268
|
+
import { createChunkAssembler } from './vision-chunk-assembler.js'
|
|
2269
|
+
export { createChunkAssembler }
|
|
2406
2270
|
|
|
2407
2271
|
async function visionAnswer(llm, options) {
|
|
2408
2272
|
return runWithVisionSessionAffinity(options?.sessionId, async () => {
|
|
@@ -2414,61 +2278,6 @@ async function visionAnswer(llm, options) {
|
|
|
2414
2278
|
})
|
|
2415
2279
|
}
|
|
2416
2280
|
|
|
2417
|
-
/** Environment shim for `resolveAdapterOptions`: `{ get: (name) => ({ value }) }`. */
|
|
2418
|
-
export function launchEnvironmentLike(env) {
|
|
2419
|
-
const map = env ?? {}
|
|
2420
|
-
return {
|
|
2421
|
-
get(name) {
|
|
2422
|
-
return Object.prototype.hasOwnProperty.call(map, name) ? { value: map[name] } : undefined
|
|
2423
|
-
},
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
|
|
2427
|
-
/**
|
|
2428
|
-
* Legacy-only DeepSeek reconstruction for Host contracts that do not own the
|
|
2429
|
-
* official provider lifecycle. Modern Hosts keep `deepseek-official` Host-owned
|
|
2430
|
-
* because request-local attachment/file/image-access seams cannot be cloned
|
|
2431
|
-
* safely here. Remove this helper when the legacy takeover window is retired.
|
|
2432
|
-
*/
|
|
2433
|
-
export function createNativeDeepSeekAdapter(ctx) {
|
|
2434
|
-
const env = launchEnvironmentLike(
|
|
2435
|
-
typeof process !== 'undefined' && process.env ? process.env : {},
|
|
2436
|
-
)
|
|
2437
|
-
const options = () => {
|
|
2438
|
-
let raw
|
|
2439
|
-
try {
|
|
2440
|
-
const settings = ctx.get('settings')
|
|
2441
|
-
raw = settings && settings.get ? settings.get('llm-deepseek') : undefined
|
|
2442
|
-
} catch {
|
|
2443
|
-
raw = undefined
|
|
2444
|
-
}
|
|
2445
|
-
return resolveAdapterOptions(raw ?? {}, env)
|
|
2446
|
-
}
|
|
2447
|
-
const resolveApiKey = async (connection) => {
|
|
2448
|
-
const ref = connection.apiKeyEnv
|
|
2449
|
-
const credentials = ctx.get('credentials')
|
|
2450
|
-
if (credentials !== undefined) {
|
|
2451
|
-
try {
|
|
2452
|
-
const hit = await credentials.resolve(ref)
|
|
2453
|
-
if (hit && typeof hit.value === 'string' && hit.value.length > 0) return hit.value
|
|
2454
|
-
} catch {
|
|
2455
|
-
/* fall through to the environment */
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
const ambient = env.get(ref)
|
|
2459
|
-
if (ambient !== undefined && typeof ambient.value === 'string' && ambient.value.length > 0) {
|
|
2460
|
-
return ambient.value
|
|
2461
|
-
}
|
|
2462
|
-
throw new Error(`vision-router: no API key for the native DeepSeek route (${ref})`)
|
|
2463
|
-
}
|
|
2464
|
-
let userId
|
|
2465
|
-
const resolveUserId = () => {
|
|
2466
|
-
if (userId === undefined) userId = getOrCreateAnonymousUserId()
|
|
2467
|
-
return userId
|
|
2468
|
-
}
|
|
2469
|
-
return new DeepSeekAdapter({ options, resolveApiKey, resolveUserId })
|
|
2470
|
-
}
|
|
2471
|
-
|
|
2472
2281
|
/**
|
|
2473
2282
|
* dsh-vision 并入:本地识别提示模板。
|
|
2474
2283
|
* `plain` = 平铺描述;`structured` = 结构化识别(【初步判断】/【细节】/
|
|
@@ -2595,7 +2404,11 @@ export async function buildInstantLocalMap(ctx, messages, provider, options = {}
|
|
|
2595
2404
|
const text = await callLocalBackend(
|
|
2596
2405
|
currentProvider,
|
|
2597
2406
|
[{ role: 'user', content }],
|
|
2598
|
-
{
|
|
2407
|
+
{
|
|
2408
|
+
maxTokens: currentProvider.maxTokens ?? 2048,
|
|
2409
|
+
signal,
|
|
2410
|
+
providerTransport: options.providerTransport,
|
|
2411
|
+
},
|
|
2599
2412
|
)
|
|
2600
2413
|
return { id, ok: true, text, elapsedMs: Date.now() - startedAt }
|
|
2601
2414
|
} catch (error) {
|
|
@@ -2673,7 +2486,7 @@ export async function buildInstantLocalMap(ctx, messages, provider, options = {}
|
|
|
2673
2486
|
* 标记;provider/style/timeout 也可以是 getter,让设置页保存后下一次 stream
|
|
2674
2487
|
* 立即读取新值,无需重启或重建 adapter。
|
|
2675
2488
|
*/
|
|
2676
|
-
export function createWrapperStreamBody(ctx, { imageMemory, delegateProvider, preserveImageInput, instantLocal, instantLocalStyle, instantLocalTimeoutMs, instantLocalMaxPixels }) {
|
|
2489
|
+
export function createWrapperStreamBody(ctx, { imageMemory, delegateProvider, preserveImageInput, instantLocal, instantLocalStyle, instantLocalTimeoutMs, instantLocalMaxPixels, providerTransport }) {
|
|
2677
2490
|
// issue #103: the reasoning level is a per-session picker choice (the chat
|
|
2678
2491
|
// page's bottom-right selector), but the host can drop reasoningEffort from
|
|
2679
2492
|
// the later steps of a multi-step turn once the twin metadata lacks a
|
|
@@ -2709,6 +2522,7 @@ export function createWrapperStreamBody(ctx, { imageMemory, delegateProvider, pr
|
|
|
2709
2522
|
memory: imageMemory,
|
|
2710
2523
|
timeoutMs: liveValue(instantLocalTimeoutMs),
|
|
2711
2524
|
downscaleMaxPixels: liveValue(instantLocalMaxPixels),
|
|
2525
|
+
providerTransport,
|
|
2712
2526
|
})
|
|
2713
2527
|
: undefined
|
|
2714
2528
|
// Rewrite image blocks ANYWHERE in the model input — including inside
|
|
@@ -2801,7 +2615,7 @@ export function createWrapperStreamBody(ctx, { imageMemory, delegateProvider, pr
|
|
|
2801
2615
|
* route). Any other route name (e.g. the `deepseek-vision` alias) advertises
|
|
2802
2616
|
* no models, so it stays functional but invisible in the picker.
|
|
2803
2617
|
*/
|
|
2804
|
-
export function createStealthAdapter(ctx, { native, imageMemory, pairs, chainRoute, delegateProvider, instantLocal, instantLocalStyle, instantLocalTimeoutMs, instantLocalMaxPixels }) {
|
|
2618
|
+
export function createStealthAdapter(ctx, { native, imageMemory, pairs, chainRoute, delegateProvider, instantLocal, instantLocalStyle, instantLocalTimeoutMs, instantLocalMaxPixels, providerTransport }) {
|
|
2805
2619
|
return {
|
|
2806
2620
|
providerInfo(provider) {
|
|
2807
2621
|
return { id: provider, name: 'DeepSeek' }
|
|
@@ -2822,7 +2636,7 @@ export function createStealthAdapter(ctx, { native, imageMemory, pairs, chainRou
|
|
|
2822
2636
|
const base = await native.resolveModel(provider, model, signal)
|
|
2823
2637
|
return { ...base, provider, inputModalities: ['text', 'image'] }
|
|
2824
2638
|
},
|
|
2825
|
-
...createWrapperStreamBody(ctx, { imageMemory, delegateProvider, instantLocal, instantLocalStyle, instantLocalTimeoutMs, instantLocalMaxPixels }),
|
|
2639
|
+
...createWrapperStreamBody(ctx, { imageMemory, delegateProvider, instantLocal, instantLocalStyle, instantLocalTimeoutMs, instantLocalMaxPixels, providerTransport }),
|
|
2826
2640
|
}
|
|
2827
2641
|
}
|
|
2828
2642
|
|
|
@@ -3019,7 +2833,6 @@ export {
|
|
|
3019
2833
|
FAILURE_ADVICE,
|
|
3020
2834
|
PERSISTED_GUARD_STOP_SURFACE_ID,
|
|
3021
2835
|
imageMarker,
|
|
3022
|
-
cacheWeight,
|
|
3023
2836
|
visionAnswer,
|
|
3024
2837
|
NON_GENERATIVE_VISION_MODEL_HINTS,
|
|
3025
2838
|
VISION_MODEL_NAME_HINTS,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { resolveSessionSurfacePolicy, } from './session-surface-policy-domain.js';
|
|
2
|
+
export const CORE_VISION_SURFACE_KEYS = [
|
|
3
|
+
'tool',
|
|
4
|
+
'rewriteImages',
|
|
5
|
+
'instantDescribe',
|
|
6
|
+
'autoActivateOnImage',
|
|
7
|
+
'structuredVisionBootstrap',
|
|
8
|
+
];
|
|
9
|
+
function objectRecord(value) {
|
|
10
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: undefined;
|
|
13
|
+
}
|
|
14
|
+
function own(object, key) {
|
|
15
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
16
|
+
}
|
|
17
|
+
function coreFlags(values) {
|
|
18
|
+
return {
|
|
19
|
+
toolAvailable: values.tool !== false,
|
|
20
|
+
rewriteEnabled: values.rewriteImages !== false,
|
|
21
|
+
instantDescribe: values.instantDescribe === true,
|
|
22
|
+
autoActivateOnImage: values.autoActivateOnImage !== false,
|
|
23
|
+
structuredBootstrap: values.structuredVisionBootstrap === true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Explicit Core-facing projection of SessionSurfacePolicy.
|
|
28
|
+
*
|
|
29
|
+
* Only the five historical Core switches cross this boundary. Unrelated plugin
|
|
30
|
+
* Settings cannot masquerade as a Core config/Settings object.
|
|
31
|
+
*/
|
|
32
|
+
export function projectCoreVisionSurface(config = {}, sessionPolicy) {
|
|
33
|
+
const value = objectRecord(config) ?? {};
|
|
34
|
+
const policyRecord = objectRecord(sessionPolicy);
|
|
35
|
+
const policy = policyRecord !== undefined
|
|
36
|
+
? policyRecord
|
|
37
|
+
: resolveSessionSurfacePolicy({ config: value });
|
|
38
|
+
const overrides = objectRecord(policy.legacyConfigOverrides) ?? {};
|
|
39
|
+
const projected = {};
|
|
40
|
+
for (const key of CORE_VISION_SURFACE_KEYS) {
|
|
41
|
+
projected[key] = own(overrides, key)
|
|
42
|
+
? overrides[key]
|
|
43
|
+
: value[key];
|
|
44
|
+
}
|
|
45
|
+
const values = Object.freeze(projected);
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
ownership: typeof policy.ownership === 'string'
|
|
48
|
+
? policy.ownership
|
|
49
|
+
: undefined,
|
|
50
|
+
preserveRawImages: policy.preserveRawImages === true,
|
|
51
|
+
rewriteCurrentImages: policy.rewriteCurrentImages === true,
|
|
52
|
+
...coreFlags(values),
|
|
53
|
+
values,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function resolveCoreVisionSurface({ visionPolicy, config = {}, schemaBootstrapping = false, } = {}) {
|
|
57
|
+
const value = objectRecord(config) ?? {};
|
|
58
|
+
return projectCoreVisionSurface(value, resolveSessionSurfacePolicy({
|
|
59
|
+
visionPolicy,
|
|
60
|
+
config: value,
|
|
61
|
+
schemaBootstrapping,
|
|
62
|
+
}));
|
|
63
|
+
}
|