dsh-vision-router 2.2.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +16 -7
  2. package/README.zh.md +16 -7
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/3.0-architecture-plan.md +252 -0
  8. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  9. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  10. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  11. package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
  12. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  13. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  14. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  15. package/docs/architecture/3.0-r7-public-api.md +151 -0
  16. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  17. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  18. package/docs/architecture/compat-inventory.md +26 -34
  19. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  20. package/docs/architecture/dsh-support-window.md +30 -22
  21. package/docs/architecture/final-closure-audit.md +2 -0
  22. package/docs/releases/v2.3.0.md +53 -0
  23. package/docs/releases/v3.0.0.md +85 -0
  24. package/entry.js +9 -57
  25. package/index.js +268 -761
  26. package/lib/agent-request-route-authority.js +1 -20
  27. package/lib/catalog-corrections.js +1 -2
  28. package/lib/client-maintenance-actions.js +145 -0
  29. package/lib/client-presentation-boundary-main.js +75 -17
  30. package/lib/client-presentation-boundary.js +53 -28
  31. package/lib/client.js +246 -144
  32. package/lib/core-apply-lifecycle.js +41 -0
  33. package/lib/core-cache.js +87 -0
  34. package/lib/core-primitives.js +91 -278
  35. package/lib/core-vision-surface-domain.js +63 -0
  36. package/lib/core-vision-surface.js +11 -77
  37. package/lib/delegated-call-config.js +15 -24
  38. package/lib/desktop-screenshot-tool.js +262 -0
  39. package/lib/doctor-cli.js +0 -0
  40. package/lib/doctor-runtime.js +4 -1
  41. package/lib/dsh-contract-compat.js +37 -256
  42. package/lib/dsh-host-capabilities.js +3 -9
  43. package/lib/dsh-settings-017-compat.js +17 -10
  44. package/lib/dsh-support-window.js +3 -3
  45. package/lib/file-logger.js +8 -3
  46. package/lib/grounding-coordinate-runtime.js +1 -2
  47. package/lib/http-body-limit.js +97 -79
  48. package/lib/http-compat.js +1 -2
  49. package/lib/image-offload-compat.js +30 -21
  50. package/lib/image-resource-governor.js +210 -228
  51. package/lib/legacy-global-proxy-boundary.js +46 -3
  52. package/lib/live-model-client-prelude.js +12 -4
  53. package/lib/live-model-discovery.js +134 -43
  54. package/lib/local-remote-settings-permission.js +33 -7
  55. package/lib/local-vision-stabilizer.js +31 -17
  56. package/lib/maintenance-routes.js +134 -0
  57. package/lib/native-image-coexistence.js +15 -3
  58. package/lib/ollama-cold-start.js +87 -23
  59. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  60. package/lib/proxy-dispatcher-pool.js +151 -92
  61. package/lib/proxy-routing.js +75 -51
  62. package/lib/proxy-url-compat.js +3 -10
  63. package/lib/public-config.js +36 -0
  64. package/lib/public-entry.d.ts +148 -0
  65. package/lib/public-entry.js +19 -11
  66. package/lib/remote-settings-bridge.js +11 -0
  67. package/lib/remote-settings-risk-client-boundary.js +155 -0
  68. package/lib/remote-settings-risk-confirmation.js +17 -105
  69. package/lib/replay-delegation.js +12 -7
  70. package/lib/replay-envelope-v2-compat.js +12 -7
  71. package/lib/runtime-composition.js +127 -34
  72. package/lib/runtime-i18n-boundary.js +10 -11
  73. package/lib/screenshot-source-boundary.js +2 -2
  74. package/lib/session-affinity-runtime.js +113 -97
  75. package/lib/session-affinity.js +89 -76
  76. package/lib/session-event-history-compat.js +30 -0
  77. package/lib/session-message-source-compat.js +7 -3
  78. package/lib/session-query-readers.js +263 -0
  79. package/lib/session-surface-compat.js +30 -31
  80. package/lib/session-surface-policy-domain.js +85 -0
  81. package/lib/session-surface-policy.js +2 -111
  82. package/lib/session-turn-resolver.js +2 -6
  83. package/lib/session-vision-attachment-index.js +143 -0
  84. package/lib/session-vision-delegation-boundary.js +34 -0
  85. package/lib/session-vision-delegation.js +22 -0
  86. package/lib/session-vision-index.js +179 -116
  87. package/lib/session-vision-mode-authority.js +229 -192
  88. package/lib/session-vision-mode-boundary.js +65 -22
  89. package/lib/session-vision-policy-domain.js +304 -0
  90. package/lib/session-vision-policy.js +104 -0
  91. package/lib/session-vision-state.js +368 -378
  92. package/lib/settings-client-017-compat.js +9 -7
  93. package/lib/settings-client-loader-lifecycle.js +129 -0
  94. package/lib/sharp-runtime.js +0 -7
  95. package/lib/structured-flow-hardening.js +25 -6
  96. package/lib/tesseract-exec-compat.js +9 -1
  97. package/lib/turn-budget-context.js +47 -41
  98. package/lib/update-check.js +11 -2
  99. package/lib/vision-attachment-handle-runtime.js +176 -156
  100. package/lib/vision-backend-runtime-policy.js +19 -2
  101. package/lib/vision-background-benchmark.js +41 -4
  102. package/lib/vision-background-failure-policy.js +41 -41
  103. package/lib/vision-background-stop-store-core.js +217 -0
  104. package/lib/vision-background-stop-store.js +8 -179
  105. package/lib/vision-capability-benchmark-service.js +1 -0
  106. package/lib/vision-capability-benchmark.js +11 -61
  107. package/lib/vision-capability-evidence.js +35 -0
  108. package/lib/vision-capability-fingerprint.js +72 -0
  109. package/lib/vision-capability-identity.js +116 -61
  110. package/lib/vision-capability-router.js +13 -33
  111. package/lib/vision-chunk-assembler.js +95 -0
  112. package/lib/vision-diagnostics-routes.js +246 -0
  113. package/lib/vision-evidence-guidance.js +3 -1
  114. package/lib/vision-execution-order-apply.js +45 -55
  115. package/lib/vision-execution-order.js +19 -19
  116. package/lib/vision-execution-policy.js +12 -7
  117. package/lib/vision-model-registry.js +30 -17
  118. package/lib/vision-provider-transport.js +137 -176
  119. package/lib/vision-routing-authority.js +47 -48
  120. package/lib/vision-routing-product.js +26 -29
  121. package/lib/vision-routing-settings-prelude.js +8 -3
  122. package/lib/vision-runtime-performance-store.js +166 -0
  123. package/lib/vision-runtime-performance.js +13 -142
  124. package/lib/vision-toggle-root-hardening.js +22 -5
  125. package/lib/vision-tool-runtime-boundary.js +104 -31
  126. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  127. package/lib/web/remote-settings-client.js +6 -9
  128. package/lib/web-capability-boundary.js +17 -7
  129. package/lib/wrapper-directory.js +14 -23
  130. package/package.json +56 -20
  131. package/lib/android-attachment-compat.js +0 -244
  132. package/lib/settings-client-rc8-lifecycle.js +0 -397
@@ -1,94 +1,51 @@
1
- import z from '@deepseek-ai/schemastery'
2
- import { mkdir, writeFile, readFile, unlink } from 'node:fs/promises'
1
+
2
+
3
3
  import path from 'node:path'
4
- import { tmpdir } from 'node:os'
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 { appendPromptToImageOnlyMessage, fetchWithOpenAICompatibility } from './http-compat.js'
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, streamWithVisionSessionAffinity } from './session-affinity-runtime.js'
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
- import { createCachedUpdateChecker } from './update-check.js'
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
- import { currentVisionExecutionOrder } from './vision-execution-order.js'
46
- import { applyVisionExecutionOrder } from './vision-execution-order-apply.js'
47
- import { createHash, randomBytes } from 'node:crypto'
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
- import { createSessionVisionIndex } from './session-vision-index.js'
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
- import { writeArtifactFile } from './artifact-boundary.js'
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
- function cacheWeight(value) {
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
- const timer = setTimeout(() => {
1290
- if (settled) return
1291
- settled = true
1292
- void worker?.terminate()
1293
- reject(
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
- const timer = setTimeout(() => {
1363
- if (settled) return
1364
- settled = true
1365
- void worker?.terminate()
1366
- reject(
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
- { timeout: Math.min(timeoutMs, 60000), maxBuffer: 32 * 1024 * 1024, input: bytes },
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
- * Minimal harness-chunk assembler (no dsh imports required). Feeds the raw
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
- { maxTokens: currentProvider.maxTokens ?? 2048, signal },
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
+ }