dsh-vision-router 2.1.5 → 2.1.7

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 (36) hide show
  1. package/README.md +15 -6
  2. package/README.zh.md +15 -6
  3. package/cordis.patch.yml +14 -0
  4. package/docs/architecture/2x-contract-ledger.md +1 -1
  5. package/docs/architecture/compat-inventory.md +10 -9
  6. package/docs/architecture/dsh-compatibility-matrix.md +2 -2
  7. package/docs/architecture/dsh-support-window.md +4 -2
  8. package/docs/architecture/host-first-proxy-convergence.md +91 -0
  9. package/docs/architecture/p3-compat-retirement.md +1 -1
  10. package/docs/architecture/p3-host-native-seams.md +1 -1
  11. package/docs/releases/v2.1.6.md +30 -0
  12. package/docs/releases/v2.1.7.md +13 -0
  13. package/index.js +62 -94
  14. package/lib/adversarial-hardening.js +0 -46
  15. package/lib/client.js +282 -39
  16. package/lib/core-primitives.js +9 -3
  17. package/lib/dsh-support-window.js +2 -2
  18. package/lib/legacy-global-proxy-boundary.js +181 -60
  19. package/lib/live-model-discovery.js +125 -15
  20. package/lib/proxy-dispatcher-pool.js +96 -0
  21. package/lib/proxy-routing.js +76 -0
  22. package/lib/proxy-url-compat.js +12 -0
  23. package/lib/public-entry.js +14 -6
  24. package/lib/remote-settings-bridge.js +10 -1
  25. package/lib/replay-delegation.js +35 -29
  26. package/lib/runtime-config-normalizer.js +271 -1
  27. package/lib/runtime-reliability.js +0 -68
  28. package/lib/settings-ia-client-prelude.js +2 -2
  29. package/lib/vision-background-stop-store.js +30 -20
  30. package/lib/vision-capability-benchmark-service.js +13 -8
  31. package/lib/vision-capability-probe.js +24 -14
  32. package/lib/vision-image-input-verdict.js +22 -7
  33. package/lib/vision-provider-transport.js +44 -37
  34. package/lib/vision-tool-runtime-boundary.js +0 -25
  35. package/lib/web-capability-boundary.js +85 -22
  36. package/package.json +8 -6
package/index.js CHANGED
@@ -12,9 +12,10 @@
12
12
  // are downscaled with sharp, results are cached by content hash + question,
13
13
  // and an optional JSON mode validates structured output.
14
14
  //
15
- // Proxy: an optional `proxy` config (e.g. http://127.0.0.1:10808) patches the
16
- // process fetch to route only the `proxyHosts` domains through it; everything
17
- // else (DeepSeek and the rest) stays on the direct connection.
15
+ // Proxy: network egress is Host-owned by default. A blank `proxy` leaves fetch
16
+ // entirely on DSH/Host's current network path. An explicit plugin proxy is an
17
+ // advanced vision-only override for `proxyHosts`; Host-owned visual adapters use
18
+ // a scoped compatibility wrapper, never configuration-wide process routing.
18
19
 
19
20
  // Compatibility shim: dsh 0.1.2-alpha.4 removed `session.events` in favor of
20
21
  // `session.snapshotEvents()`. This helper returns an array (or undefined) that
@@ -105,6 +106,7 @@ import {
105
106
  } from './lib/http-body-limit.js'
106
107
  import { writeArtifactFile } from './lib/artifact-boundary.js'
107
108
  import { stripTrailingSlashes } from './lib/string-normalization.js'
109
+ import { streamWithLegacyGlobalProxyScope } from './lib/legacy-global-proxy-boundary.js'
108
110
  import { parseVersionComparator } from './lib/version-range.js'
109
111
  import { createCoalescingRunner } from './lib/adapter-update-coalescer.js'
110
112
  import { captureWindowsDesktop } from './lib/windows-desktop-capture.js'
@@ -1129,8 +1131,8 @@ export function apply(ctx, config = {}, runtime = {}) {
1129
1131
  // session model does not declare image input, and the DeepSeek adapter
1130
1132
  // hardcodes text-only. This wrapper route (`deepseek-vision` by default)
1131
1133
  // declares image input so the admission passes, shows up in the model
1132
- // picker as "DeepSeek + 自动识图", and delegates to the real text-provider
1133
- // adapter for anything the waterfalls did not rewrite.
1134
+ // picker as "DeepSeek + 自动识图", and delegates only to the official
1135
+ // DeepSeek adapter (or the hidden native route during stealth takeover).
1134
1136
  //
1135
1137
  // The adapter is built unconditionally; whether (and under which name) it
1136
1138
  // mounts is reconciled reactively against the resolved settings document by
@@ -1138,12 +1140,16 @@ export function apply(ctx, config = {}, runtime = {}) {
1138
1140
  // take effect without a restart.
1139
1141
  let wrapperAdapter
1140
1142
  {
1141
- const WRAPPER_MODEL_IDS = ['deepseek-v4-pro', 'deepseek-v4-flash']
1142
1143
  const wrapName = (name) => name ?? 'DeepSeek'
1143
- const textProviderRoute = () => (stealthActive ? nativeRoute : textProvider().provider)
1144
+ // The row is explicitly branded as DeepSeek, so its metadata and network
1145
+ // authority must come from DeepSeek as well. `textProvider` is legacy
1146
+ // configuration and must never let an arbitrary relay masquerade behind
1147
+ // the special wrapper. During stealth takeover old wrapper sessions keep
1148
+ // delegating to the hidden native DeepSeek route.
1149
+ const wrapperDelegateRoute = () => (stealthActive ? nativeRoute : 'deepseek-official')
1144
1150
  const delegateAdapter = () => {
1145
1151
  try {
1146
- return ctx.llm.registration(textProviderRoute()).adapter
1152
+ return ctx.llm.registration(wrapperDelegateRoute()).adapter
1147
1153
  } catch {
1148
1154
  return undefined
1149
1155
  }
@@ -1154,7 +1160,7 @@ export function apply(ctx, config = {}, runtime = {}) {
1154
1160
  },
1155
1161
  providerRetryPolicy() {
1156
1162
  try {
1157
- return ctx.llm.registration(textProviderRoute()).retryPolicy
1163
+ return ctx.llm.registration(wrapperDelegateRoute()).retryPolicy
1158
1164
  } catch {
1159
1165
  return undefined
1160
1166
  }
@@ -1165,12 +1171,12 @@ export function apply(ctx, config = {}, runtime = {}) {
1165
1171
  if (stealthActive) return []
1166
1172
  const entries = []
1167
1173
  const real = delegateAdapter()
1168
- if (real !== undefined) {
1174
+ if (real !== undefined && typeof real.listModels === 'function') {
1169
1175
  try {
1170
- const listed = await real.listModels(textProviderRoute())
1176
+ const listed = await real.listModels('deepseek-official')
1171
1177
  entries.push(
1172
- ...listed
1173
- .filter((model) => WRAPPER_MODEL_IDS.includes(model.id))
1178
+ ...(Array.isArray(listed) ? listed : [])
1179
+ .filter((model) => model && typeof model.id === 'string' && model.id !== '')
1174
1180
  .map((model) => ({
1175
1181
  ...model,
1176
1182
  provider: wrapperRoute(),
@@ -1219,14 +1225,29 @@ export function apply(ctx, config = {}, runtime = {}) {
1219
1225
  inputModalities: ['text', 'image'],
1220
1226
  }
1221
1227
  } catch {
1222
- /* fall through to the text-provider path */
1228
+ /* fall through to the official DeepSeek path */
1223
1229
  }
1224
1230
  }
1225
1231
  const real = delegateAdapter()
1226
- if (real === undefined) {
1227
- throw new Error('vision-router: the text provider adapter is not available')
1232
+ if (real === undefined || typeof real.resolveModel !== 'function') {
1233
+ throw new Error('vision-router: the official DeepSeek adapter is not available')
1228
1234
  }
1229
- const base = await real.resolveModel(textProviderRoute(), model)
1235
+ // Outside stealth mode, accept only models the live official catalog
1236
+ // actually publishes. Some adapters can resolve arbitrary ids; that is
1237
+ // not permission to expose them under the DeepSeek product identity.
1238
+ if (!stealthActive) {
1239
+ if (typeof real.listModels !== 'function') {
1240
+ throw new Error('vision-router: the official DeepSeek catalog is not available')
1241
+ }
1242
+ const listed = await real.listModels('deepseek-official')
1243
+ const admitted = Array.isArray(listed) && listed.some(
1244
+ (entry) => entry && entry.id === model,
1245
+ )
1246
+ if (!admitted) {
1247
+ throw new Error(`vision-router: DeepSeek model "${model}" is not in the live official catalog`)
1248
+ }
1249
+ }
1250
+ const base = await real.resolveModel(wrapperDelegateRoute(), model)
1230
1251
  return {
1231
1252
  ...base,
1232
1253
  provider: wrapperRoute(),
@@ -1236,7 +1257,7 @@ export function apply(ctx, config = {}, runtime = {}) {
1236
1257
  },
1237
1258
  ...createWrapperStreamBody(ctx, {
1238
1259
  imageMemory,
1239
- delegateProvider: textProviderRoute(),
1260
+ delegateProvider: wrapperDelegateRoute,
1240
1261
  instantLocal: instantLocalProvider,
1241
1262
  instantLocalStyle,
1242
1263
  instantLocalTimeoutMs: timeoutMs,
@@ -1765,7 +1786,15 @@ export function apply(ctx, config = {}, runtime = {}) {
1765
1786
  const corrected = await correctedVisionAnswer(pair, messages, options)
1766
1787
  if (corrected !== undefined) return corrected
1767
1788
  assertOpenCodeGoAffinityForPair(pair, options.sessionId)
1768
- return visionAnswer(ctx.llm, {
1789
+ return visionAnswer({
1790
+ stream(streamOptions) {
1791
+ return streamWithLegacyGlobalProxyScope(
1792
+ pair.provider,
1793
+ pair.model,
1794
+ () => ctx.llm.stream(streamOptions),
1795
+ )
1796
+ },
1797
+ }, {
1769
1798
  provider: pair.provider,
1770
1799
  model: pair.model,
1771
1800
  messages,
@@ -2079,14 +2108,18 @@ export function apply(ctx, config = {}, runtime = {}) {
2079
2108
  })
2080
2109
  if (text === undefined) {
2081
2110
  assertOpenCodeGoAffinityForPair(pair, options.sessionId)
2082
- yield* streamWithVisionSessionAffinity(options.sessionId, () => ctx.llm.stream({
2083
- ...options,
2084
- provider: pair.provider,
2085
- model: pair.model,
2086
- reasoningEffort: undefined,
2087
- messages,
2088
- signal: attemptSignal,
2089
- }))
2111
+ yield* streamWithLegacyGlobalProxyScope(
2112
+ pair.provider,
2113
+ pair.model,
2114
+ () => streamWithVisionSessionAffinity(options.sessionId, () => ctx.llm.stream({
2115
+ ...options,
2116
+ provider: pair.provider,
2117
+ model: pair.model,
2118
+ reasoningEffort: undefined,
2119
+ messages,
2120
+ signal: attemptSignal,
2121
+ })),
2122
+ )
2090
2123
  return
2091
2124
  }
2092
2125
  if (text !== '') {
@@ -2227,73 +2260,8 @@ export function apply(ctx, config = {}, runtime = {}) {
2227
2260
  // #208: attachment refs, description memory and the event-log cursor are
2228
2261
  // owned by the same bounded SessionVisionStateStore above.
2229
2262
 
2230
- // ── optional fetch proxy for the vision provider hosts ─────────────────────
2231
- //
2232
- // Resolved per request from the live settings section (`current()`), so the
2233
- // Web settings panel can change the proxy URL and host list without a
2234
- // restart. The fetch patcher itself is installed once for the plugin fiber.
2235
-
2236
- const currentProxyUrl = () => {
2237
- const value = current().proxy
2238
- return typeof value === 'string' && value !== '' ? value : undefined
2239
- }
2240
- const currentProxyHosts = () => {
2241
- const value = current().proxyHosts
2242
- return Array.isArray(value)
2243
- ? value.filter((host) => typeof host === 'string' && host !== '')
2244
- : []
2245
- }
2246
-
2247
- {
2248
- const originalFetch = globalThis.fetch
2249
- let cachedAgentUrl
2250
- let cachedAgentPromise
2251
- const agentFor = (url) => {
2252
- if (cachedAgentUrl === url && cachedAgentPromise !== undefined) return cachedAgentPromise
2253
- cachedAgentUrl = url
2254
- // Do not import userland Undici at plugin/module load time. Loading a
2255
- // different Undici major can disturb the dispatcher used by the host's
2256
- // built-in fetch even when Vision Router's own proxy setting is empty.
2257
- // Load ProxyAgent only when this plugin's selective proxy is actually used.
2258
- cachedAgentPromise = import('undici')
2259
- .then(({ ProxyAgent }) => {
2260
- if (typeof ProxyAgent !== 'function') {
2261
- throw new Error('dsh-vision-router: undici ProxyAgent is unavailable')
2262
- }
2263
- return new ProxyAgent(url)
2264
- })
2265
- .catch((error) => {
2266
- if (cachedAgentUrl === url) {
2267
- cachedAgentUrl = undefined
2268
- cachedAgentPromise = undefined
2269
- }
2270
- throw error
2271
- })
2272
- return cachedAgentPromise
2273
- }
2274
- const patchedFetch = (input, init) => {
2275
- const proxyUrl = currentProxyUrl()
2276
- if (proxyUrl === undefined) return originalFetch(input, init)
2277
- let url
2278
- try {
2279
- url = new URL(
2280
- typeof input === 'string' ? input : input && input.url ? input.url : String(input),
2281
- )
2282
- } catch {
2283
- return originalFetch(input, init)
2284
- }
2285
- if (!hostMatchesAny(url.hostname, currentProxyHosts())) return originalFetch(input, init)
2286
- return agentFor(proxyUrl).then((dispatcher) =>
2287
- originalFetch(input, { ...(init ?? {}), dispatcher }),
2288
- )
2289
- }
2290
- ctx.effect(() => {
2291
- globalThis.fetch = patchedFetch
2292
- return () => {
2293
- globalThis.fetch = originalFetch
2294
- }
2295
- }, 'vision-router: proxy fetch')
2296
- }
2263
+ // Host-owned proxy overrides are scoped by lib/legacy-global-proxy-boundary.js.
2264
+ // Core no longer owns or installs a process-wide proxy fetch implementation.
2297
2265
 
2298
2266
  const lookupAttachment = (session, id) => sessionVisionIndex.lookupAttachment(session, id)
2299
2267
 
@@ -470,52 +470,6 @@ export function installAdversarialHardening(ctx, config = {}, core) {
470
470
  const wrapped = new Proxy(ctx, {
471
471
  get(target, property) {
472
472
  if (property === 'tools') return wrapTools(target.tools, wrapped, core, hardenedConfig)
473
- if (property === 'effect') {
474
- const effect = Reflect.get(target, property, target)
475
- if (typeof effect !== 'function') return effect
476
- return (factory, label) => {
477
- if (label !== 'vision-router: proxy fetch') return effect.call(target, factory, label)
478
- return effect.call(target, () => {
479
- const originalFetch = globalThis.fetch
480
- const disposer = factory()
481
- let installedFetch = globalThis.fetch
482
- let active = true
483
- // Later plugins may capture this function. Making it an inert
484
- // delegator on unload removes Vision Router from the chain even if
485
- // another plugin still sits above it, and prevents it resurfacing
486
- // when that later plugin restores the fetch value it captured.
487
- const guardedFetch = (...args) => {
488
- const fetchImpl = active && typeof installedFetch === 'function'
489
- ? installedFetch
490
- : originalFetch
491
- return fetchImpl(...args)
492
- }
493
- globalThis.fetch = guardedFetch
494
- return () => {
495
- if (!active) return
496
- active = false
497
- // Drop the raw Vision Router fetch closure as soon as ownership
498
- // ends. This releases its cached ProxyAgent/undici closure even
499
- // when a later plugin still retains guardedFetch.
500
- installedFetch = undefined
501
- const currentFetch = globalThis.fetch
502
- try {
503
- // Always run the underlying effect disposer. It may own more
504
- // than the process-level pointer; skipping it merely because a
505
- // later plugin wrapped fetch leaks that lifecycle forever.
506
- if (typeof disposer === 'function') disposer()
507
- } finally {
508
- // The core disposer currently restores originalFetch
509
- // unconditionally. Preserve any later plugin that was above us,
510
- // while an outermost Vision Router cleanup returns to the host.
511
- globalThis.fetch = currentFetch === guardedFetch
512
- ? originalFetch
513
- : currentFetch
514
- }
515
- }
516
- }, label)
517
- }
518
- }
519
473
  // Deliberately do NOT wrap ctx.inject or injected child contexts. DSH
520
474
  // rc.6 ties route/effect ownership to the exact child-context identity;
521
475
  // replacing it with a Proxy previously made host routes disappear (#160).