dsh-vision-router 2.1.0 → 2.1.2
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/docs/releases/v2.1.1.md +19 -0
- package/docs/releases/v2.1.2.md +30 -0
- package/lib/agent-request-route-authority.js +94 -0
- package/lib/client-host-compat-prelude.js +29 -17
- package/lib/delegated-call-config.js +27 -0
- package/lib/doctor.js +45 -4
- package/lib/grounding-coordinate-frame.js +93 -0
- package/lib/grounding-coordinate-runtime.js +246 -0
- package/lib/legacy-core-vision-policy-bridge.js +17 -146
- package/lib/runtime-composition.js +43 -10
- package/lib/session-surface-policy.js +59 -32
- package/lib/session-vision-mode-authority.js +254 -0
- package/lib/session-vision-mode-boundary.js +408 -0
- package/lib/twin-image-capability-fallback.js +428 -0
- package/lib/vision-attachment-handle-runtime.js +212 -0
- package/lib/vision-tool-runtime-boundary.js +224 -5
- package/package.json +6 -5
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# v2.1.1
|
|
2
|
+
|
|
3
|
+
This is an urgent browser-startup hotfix for DVR 2.1.0.
|
|
4
|
+
|
|
5
|
+
## Fixed
|
|
6
|
+
|
|
7
|
+
- Fixes #367: DSH Web could fail while applying `dsh-vision-router` with `cannot get property "remote.session" without inject`.
|
|
8
|
+
- The 2.1.0 client Host compatibility prelude no longer probes the optional `remote.session` namespace through the traceable `ctx.remote` proxy. It resolves the optional namespace through `ctx.get('remote.session')` first and keeps a guarded fallback for older/non-Cordis harnesses.
|
|
9
|
+
- The new Host model-catalog bridge still uses `remote.session.modelCatalog()` when available, while older supported Hosts keep the original `connection.api.llm.models()` path and exact legacy Remote/Connection surfaces.
|
|
10
|
+
|
|
11
|
+
## Regression coverage
|
|
12
|
+
|
|
13
|
+
- Adds a production-shaped regression where reading `ctx.remote.session` throws the exact #367 Cordis error while `ctx.get('remote.session')` succeeds.
|
|
14
|
+
- Adds the same test to the default Node 22/24 suite and the Web compatibility suite.
|
|
15
|
+
- Keeps a fail-closed legacy-host regression so the hotfix does not turn the optional alpha namespace into a hard browser dependency.
|
|
16
|
+
|
|
17
|
+
## Upgrade
|
|
18
|
+
|
|
19
|
+
Users on 2.1.0 who hit the browser loader failure should upgrade to 2.1.1 and restart the DSH Web/Desktop process. No settings migration is required.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# v2.1.2
|
|
2
|
+
|
|
3
|
+
DVR 2.1.2 is a reliability hotfix for image-session ownership, delegated vision routing, text-only attachment projection, and composer Vision-mode authority. It also closes CI blind spots that could allow valid regression tests to exist without being executed by the normal release gate.
|
|
4
|
+
|
|
5
|
+
## Fixed
|
|
6
|
+
|
|
7
|
+
- Keeps uploaded image blocks as durable Session facts. Vision Router no longer rewrites the current user image into internal `[attached image: sha256:…]` marker text through the legacy pre-step path. Model-specific projection now stays at adapter/request boundaries instead of leaking into the conversation transcript.
|
|
8
|
+
- Fixes #374 and the wider delegated call-config class: when Vision Router actually hands a request to another provider/model, source-route `maxTokens`, `reasoningEffort`, sampling and stop settings no longer override the target adapter's own defaults. This fixes GLM routes such as `glm-4v-flash` rejecting a Router-supplied 4096-token limit when the configured model cap is 1024.
|
|
9
|
+
- Refines that handoff rule so transparent Vision Router twins keep caller-owned generation settings, while tool, Vision Chain and `agent/request` provider/model handoffs restore target-route authority. Reasoning memory is scoped to the relevant session and route instead of becoming cross-route state.
|
|
10
|
+
- Keeps the `vision_describe` cache as a successful-answer cache. Structured Vision Router failures (`ok: false` with `VISION_*` errors) are no longer retained across turns, so repaired credentials or recovered backends can be retried immediately.
|
|
11
|
+
- Recovers safely when a source adapter is incorrectly catalogued as image-capable. Generated Vision Router twins now recognize explicit runtime image-capability rejection before model-visible output, record bounded negative proof for the exact source route, and re-enter the canonical text-only fallback path instead of repeatedly sending raw pixels to a text-only backend. Generic 4xx/5xx, auth, rate-limit, media-format, cancellation and post-output failures do not trigger this fallback.
|
|
12
|
+
- Resolves DSH text-only projected image handles such as `sha256:deadbeef` back to a canonical attachment only inside the current Session. Unique prefixes resolve, unknown or ambiguous prefixes fail closed, bounded cache eviction cannot erase ambiguity because durable Session history remains authoritative, and attachment-shaped input can no longer fall through into a guessed local filesystem path.
|
|
13
|
+
- Makes the composer `👁 Vision` state the authoritative per-Session permission for Vision Router orchestration. Ordinary text-only and native-multimodal routes no longer receive Router-owned `vision_*` schemas, structured bootstrap, automatic deep-tool mounting or instant visual work while Vision is OFF. Native multimodal pixels remain Host-owned and continue to work normally without Router intervention.
|
|
14
|
+
- Keeps the current Agent step internally stable if the model picker changes while it is running. The new selection is applied at the next real prompt-assembly boundary, while diagnostic/out-of-band prompt renders cannot clear or mutate the active step authority.
|
|
15
|
+
- Keeps Settings `tool=false` as an independent global capability gate. Selecting a Vision Router wrapper cannot re-enable tools that the user explicitly disabled.
|
|
16
|
+
|
|
17
|
+
## Regression and CI hardening
|
|
18
|
+
|
|
19
|
+
- Adds production-shaped coverage for delegated target defaults, transparent wrapper behavior, `agent/request` route handoffs and ordinary non-delegated LLM calls.
|
|
20
|
+
- Adds transcript ownership coverage ensuring accepted user images remain raw Session content and the retired pre-step bridge cannot synthesize attachment markers into durable history.
|
|
21
|
+
- Adds false-multimodal metadata regressions across generated twins, custom wrappers, lazy source registration, route changes and negative-proof isolation.
|
|
22
|
+
- Adds attachment-handle regressions for exact IDs, DSH short projected handles, cache eviction, same-Session prefix collisions, cross-Session isolation, malformed handles and attachment-vs-filesystem dispatch.
|
|
23
|
+
- Adds Session Vision-mode regressions for OFF/ON isolation, native multimodal non-intervention, prompt-assembly visibility, PTC/scoped-tool behavior, mid-step picker changes, diagnostic assemblies, foreign `vision_*` tools and Settings `tool=false`.
|
|
24
|
+
- Makes the default test manifest closed-world: every `tests/**/*.test.js` file must either run in the normal suite or have a documented PR-triggered specialist workflow owner.
|
|
25
|
+
- Verifies specialist workflow ownership against the workflow source so exclusions cannot silently become stale or merely appear in a path trigger without being executed.
|
|
26
|
+
- The final release head passes Node 22/24, DSH rc.6/rc.7/rc.8 contracts, P1/P2/P3 architecture gates, native multimodal cold/process restart coverage, Ubuntu/macOS/Windows host-sharp checks, and CodeQL with no new alerts.
|
|
27
|
+
|
|
28
|
+
## Upgrade
|
|
29
|
+
|
|
30
|
+
Upgrade to 2.1.2 and restart the DSH Web/Desktop process so the new runtime boundaries are loaded. No settings migration is required.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { projectDelegatedCallConfig } from './delegated-call-config.js'
|
|
2
|
+
import { contextWithTwinImageCapabilityFallback } from './twin-image-capability-fallback.js'
|
|
3
|
+
|
|
4
|
+
const routeAuthorityOptionsByContext = new WeakMap()
|
|
5
|
+
|
|
6
|
+
function isObject(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function routeOf(value) {
|
|
11
|
+
if (!isObject(value)) return undefined
|
|
12
|
+
const provider = typeof value.provider === 'string' && value.provider !== '' ? value.provider : undefined
|
|
13
|
+
const model = typeof value.model === 'string' && value.model !== '' ? value.model : undefined
|
|
14
|
+
return provider !== undefined && model !== undefined ? { provider, model } : undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A completed `agent/request` route change is an authority handoff: generation
|
|
19
|
+
* defaults from the source provider/model must not be mistaken for explicit
|
|
20
|
+
* target-model config. Payload/lifecycle fields remain untouched.
|
|
21
|
+
*
|
|
22
|
+
* Fail closed on incomplete route evidence. If either side lacks an exact
|
|
23
|
+
* provider+model tuple, preserve the handler result rather than guessing.
|
|
24
|
+
*/
|
|
25
|
+
export function projectAgentRequestRouteHandoff(sourceConfig, resultConfig) {
|
|
26
|
+
const source = routeOf(sourceConfig)
|
|
27
|
+
const target = routeOf(resultConfig)
|
|
28
|
+
if (source === undefined || target === undefined) return resultConfig
|
|
29
|
+
if (source.provider === target.provider && source.model === target.model) return resultConfig
|
|
30
|
+
return projectDelegatedCallConfig(resultConfig)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Runtime composition can publish route identities before Core.apply without
|
|
35
|
+
* changing the mature `contextWithAgentRequestRouteAuthority(backendRuntimeCtx)`
|
|
36
|
+
* boundary shape. The WeakMap keeps this per-context and garbage-collectable.
|
|
37
|
+
*/
|
|
38
|
+
export function configureAgentRequestRouteAuthority(ctx, options = {}) {
|
|
39
|
+
if (!isObject(ctx)) return ctx
|
|
40
|
+
routeAuthorityOptionsByContext.set(ctx, {
|
|
41
|
+
wrapperRoute: options.wrapperRoute,
|
|
42
|
+
chainRoute: options.chainRoute,
|
|
43
|
+
logger: options.logger,
|
|
44
|
+
})
|
|
45
|
+
return ctx
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Private Core-facing context. It wraps only `agent/request` handlers and only
|
|
50
|
+
* projects a result when that handler demonstrably changed provider/model.
|
|
51
|
+
* Ordinary request handlers and direct LLM calls retain caller identity.
|
|
52
|
+
*
|
|
53
|
+
* This is also the final Core-facing adapter-registration boundary, so it owns
|
|
54
|
+
* the generated-twin runtime capability correction: if a `<provider>-vision`
|
|
55
|
+
* twin trusted image metadata but the live source explicitly rejects raw image
|
|
56
|
+
* input before producing output, the twin is re-entered once through Core's
|
|
57
|
+
* existing text bridge instead of leaking that 400 to the user. Keeping the
|
|
58
|
+
* fallback here means the deeper prepareCall/ownership layers still observe the
|
|
59
|
+
* final wrapped stream and retain their existing contracts.
|
|
60
|
+
*/
|
|
61
|
+
export function contextWithAgentRequestRouteAuthority(ctx, options) {
|
|
62
|
+
if (!isObject(ctx)) return ctx
|
|
63
|
+
const fallbackOptions = isObject(options)
|
|
64
|
+
? options
|
|
65
|
+
: routeAuthorityOptionsByContext.get(ctx) ?? {}
|
|
66
|
+
const routeAuthorityCtx = new Proxy(ctx, {
|
|
67
|
+
get(target, property) {
|
|
68
|
+
if (property !== 'on') {
|
|
69
|
+
const value = Reflect.get(target, property, target)
|
|
70
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
71
|
+
}
|
|
72
|
+
const on = Reflect.get(target, property, target)
|
|
73
|
+
if (typeof on !== 'function') return on
|
|
74
|
+
return (event, handler, ...rest) => {
|
|
75
|
+
if (event !== 'agent/request' || typeof handler !== 'function') {
|
|
76
|
+
return on.call(target, event, handler, ...rest)
|
|
77
|
+
}
|
|
78
|
+
return on.call(target, event, async function routeAuthorityAwareRequest(payload, next) {
|
|
79
|
+
let sourceConfig
|
|
80
|
+
const captureNext = typeof next === 'function'
|
|
81
|
+
? async (...args) => {
|
|
82
|
+
const value = await next(...args)
|
|
83
|
+
if (sourceConfig === undefined) sourceConfig = value
|
|
84
|
+
return value
|
|
85
|
+
}
|
|
86
|
+
: next
|
|
87
|
+
const result = await handler.call(this, payload, captureNext)
|
|
88
|
+
return projectAgentRequestRouteHandoff(sourceConfig, result)
|
|
89
|
+
}, ...rest)
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
return contextWithTwinImageCapabilityFallback(routeAuthorityCtx, fallbackOptions)
|
|
94
|
+
}
|
|
@@ -20,11 +20,22 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
|
|
|
20
20
|
var LEGACY_CREDENTIAL_EVENT = 'credentials/updated';
|
|
21
21
|
var HOST_CREDENTIAL_EVENT = 'credentials/reference-updated';
|
|
22
22
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
function optionalHostSession(ctx, remote) {
|
|
24
|
+
try {
|
|
25
|
+
if (ctx && typeof ctx.get === 'function') {
|
|
26
|
+
var session = ctx.get('remote.session');
|
|
27
|
+
if (session) return session;
|
|
28
|
+
}
|
|
29
|
+
} catch (_) {}
|
|
30
|
+
try {
|
|
31
|
+
return remote && remote.session;
|
|
32
|
+
} catch (_) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasHostCatalog(session) {
|
|
38
|
+
return !!(session && typeof session.modelCatalog === 'function');
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
function wrapCatalogResult(value) {
|
|
@@ -37,23 +48,23 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
|
|
|
37
48
|
return value;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
function catalogModels(
|
|
51
|
+
function catalogModels(session) {
|
|
41
52
|
return function models() {
|
|
42
|
-
return Promise.resolve(
|
|
53
|
+
return Promise.resolve(session.modelCatalog()).then(wrapCatalogResult);
|
|
43
54
|
};
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
function compatibleApi(originalApi,
|
|
57
|
+
function compatibleApi(originalApi, session) {
|
|
47
58
|
var originalLlm = originalApi && originalApi.llm;
|
|
48
59
|
var llm = originalLlm && (typeof originalLlm === 'object' || typeof originalLlm === 'function')
|
|
49
60
|
? new Proxy(originalLlm, {
|
|
50
61
|
get: function(target, property) {
|
|
51
|
-
if (property === 'models') return catalogModels(
|
|
62
|
+
if (property === 'models') return catalogModels(session);
|
|
52
63
|
var value = Reflect.get(target, property, target);
|
|
53
64
|
return typeof value === 'function' ? value.bind(target) : value;
|
|
54
65
|
}
|
|
55
66
|
})
|
|
56
|
-
: { models: catalogModels(
|
|
67
|
+
: { models: catalogModels(session) };
|
|
57
68
|
if (originalApi && (typeof originalApi === 'object' || typeof originalApi === 'function')) {
|
|
58
69
|
return new Proxy(originalApi, {
|
|
59
70
|
get: function(target, property) {
|
|
@@ -66,10 +77,10 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
|
|
|
66
77
|
return { llm: llm };
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
function compatibleConnection(connection,
|
|
70
|
-
if (!hasHostCatalog(
|
|
80
|
+
function compatibleConnection(connection, session) {
|
|
81
|
+
if (!hasHostCatalog(session)) return connection;
|
|
71
82
|
if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
|
|
72
|
-
var api = compatibleApi(connection.api,
|
|
83
|
+
var api = compatibleApi(connection.api, session);
|
|
73
84
|
return new Proxy(connection, {
|
|
74
85
|
get: function(target, property) {
|
|
75
86
|
if (property === 'api') return api;
|
|
@@ -79,8 +90,8 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
|
|
|
79
90
|
});
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
function compatibleRemote(remote) {
|
|
83
|
-
if (!hasHostCatalog(
|
|
93
|
+
function compatibleRemote(remote, session) {
|
|
94
|
+
if (!hasHostCatalog(session)) return remote;
|
|
84
95
|
if (!remote || (typeof remote !== 'object' && typeof remote !== 'function')) return remote;
|
|
85
96
|
return new Proxy(remote, {
|
|
86
97
|
get: function(target, property) {
|
|
@@ -140,7 +151,8 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
|
|
|
140
151
|
|
|
141
152
|
function compatibleContext(ctx) {
|
|
142
153
|
if (!ctx || typeof ctx !== 'object' || typeof Proxy !== 'function') return ctx;
|
|
143
|
-
var
|
|
154
|
+
var session = optionalHostSession(ctx, ctx.remote);
|
|
155
|
+
var remote = compatibleRemote(ctx.remote, session);
|
|
144
156
|
var locale = compatibleLocale(ctx.locale);
|
|
145
157
|
return new Proxy(ctx, {
|
|
146
158
|
get: function(target, property) {
|
|
@@ -151,7 +163,7 @@ export const CLIENT_HOST_COMPAT_PRELUDE = String.raw`(function(){
|
|
|
151
163
|
if (typeof get !== 'function') return get;
|
|
152
164
|
return function(name) {
|
|
153
165
|
var value = get.call(target, name);
|
|
154
|
-
return name === 'connection' ? compatibleConnection(value,
|
|
166
|
+
return name === 'connection' ? compatibleConnection(value, session) : value;
|
|
155
167
|
};
|
|
156
168
|
}
|
|
157
169
|
var value = Reflect.get(target, property, target);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const ROUTE_OWNED_CALL_FIELDS = Object.freeze([
|
|
2
|
+
'reasoningEffort',
|
|
3
|
+
'temperature',
|
|
4
|
+
'maxTokens',
|
|
5
|
+
'stop',
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Project an internal Vision Router delegation onto a different provider/model.
|
|
10
|
+
*
|
|
11
|
+
* The delegated route owns its own call defaults and compatibility. Carry the
|
|
12
|
+
* request payload and lifecycle through, but never leak source-route sampling,
|
|
13
|
+
* reasoning or output-limit state into the target adapter. DSH will materialize
|
|
14
|
+
* the target adapter's configured defaults when these fields are absent.
|
|
15
|
+
*
|
|
16
|
+
* Direct transports owned by Vision Router do not use this projection; their
|
|
17
|
+
* wire compatibility remains the Router's responsibility.
|
|
18
|
+
*/
|
|
19
|
+
export function projectDelegatedCallConfig(options) {
|
|
20
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) return options
|
|
21
|
+
if (!ROUTE_OWNED_CALL_FIELDS.some((field) => Object.hasOwn(options, field))) return options
|
|
22
|
+
const next = { ...options }
|
|
23
|
+
for (const field of ROUTE_OWNED_CALL_FIELDS) delete next[field]
|
|
24
|
+
return next
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const delegatedRouteOwnedCallFields = ROUTE_OWNED_CALL_FIELDS
|
package/lib/doctor.js
CHANGED
|
@@ -303,12 +303,53 @@ function stripYamlComment(line) {
|
|
|
303
303
|
return line
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
function yamlIndent(line) {
|
|
307
|
+
return /^(\s*)/.exec(line)?.[1].length ?? 0
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function rowIsInsideInsert(lines, rowIndex, rowIndent) {
|
|
311
|
+
for (let cursor = rowIndex - 1; cursor >= 0; cursor -= 1) {
|
|
312
|
+
const candidate = lines[cursor]
|
|
313
|
+
if (/^\s*$/.test(candidate)) continue
|
|
314
|
+
const indent = yamlIndent(candidate)
|
|
315
|
+
if (indent >= rowIndent) continue
|
|
316
|
+
return /^\s*-?\s*insert\s*:\s*$/.test(candidate)
|
|
317
|
+
}
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function inspectVisionRouterPatchRows(lines) {
|
|
322
|
+
let mountRows = 0
|
|
323
|
+
let overrideRows = 0
|
|
324
|
+
|
|
325
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
326
|
+
const idMatch = /^(\s*)-\s+id\s*:\s*['"]?([^'"\s]+)['"]?\s*$/.exec(lines[index])
|
|
327
|
+
if (!idMatch) continue
|
|
328
|
+
const rowIndent = idMatch[1].length
|
|
329
|
+
let pluginName
|
|
330
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
331
|
+
const candidate = lines[cursor]
|
|
332
|
+
if (/^\s*$/.test(candidate)) continue
|
|
333
|
+
const indent = yamlIndent(candidate)
|
|
334
|
+
if (indent <= rowIndent) break
|
|
335
|
+
const nameMatch = /^\s*name\s*:\s*['"]?([^'"\s]+)['"]?\s*$/.exec(candidate)
|
|
336
|
+
if (nameMatch) pluginName = nameMatch[1]
|
|
337
|
+
}
|
|
338
|
+
if (idMatch[2] !== 'vision-router' && pluginName !== PLUGIN_NAME) continue
|
|
339
|
+
if (rowIsInsideInsert(lines, index, rowIndent)) mountRows += 1
|
|
340
|
+
else overrideRows += 1
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { mountRows, overrideRows }
|
|
344
|
+
}
|
|
345
|
+
|
|
306
346
|
export function inspectProfilePatch(patchPath) {
|
|
307
347
|
if (!existsSync(patchPath)) {
|
|
308
348
|
return {
|
|
309
349
|
path: patchPath,
|
|
310
350
|
exists: false,
|
|
311
351
|
visionRouterRows: 0,
|
|
352
|
+
visionRouterOverrideRows: 0,
|
|
312
353
|
manualVisionRouter: false,
|
|
313
354
|
disablesOfficialDeepSeek: false,
|
|
314
355
|
}
|
|
@@ -317,14 +358,13 @@ export function inspectProfilePatch(patchPath) {
|
|
|
317
358
|
try {
|
|
318
359
|
text = readFileBounded(patchPath).toString('utf8')
|
|
319
360
|
} catch (error) {
|
|
320
|
-
return { path: patchPath, exists: true, visionRouterRows: 0, manualVisionRouter: false, disablesOfficialDeepSeek: false, error: error instanceof Error ? error.message : String(error), oversized: error?.code === 'DOCTOR_INPUT_TOO_LARGE' }
|
|
361
|
+
return { path: patchPath, exists: true, visionRouterRows: 0, visionRouterOverrideRows: 0, manualVisionRouter: false, disablesOfficialDeepSeek: false, error: error instanceof Error ? error.message : String(error), oversized: error?.code === 'DOCTOR_INPUT_TOO_LARGE' }
|
|
321
362
|
}
|
|
322
363
|
const lines = text.split(/\r?\n/).map(stripYamlComment)
|
|
323
|
-
|
|
364
|
+
const { mountRows: visionRouterRows, overrideRows: visionRouterOverrideRows } = inspectVisionRouterPatchRows(lines)
|
|
324
365
|
let disablesOfficialDeepSeek = false
|
|
325
366
|
for (let index = 0; index < lines.length; index += 1) {
|
|
326
367
|
const line = lines[index]
|
|
327
|
-
if (/^\s*name\s*:\s*['"]?dsh-vision-router['"]?\s*$/.test(line)) visionRouterRows += 1
|
|
328
368
|
if (!/^\s*-?\s*id\s*:\s*['"]?llm-deepseek['"]?\s*$/.test(line)) continue
|
|
329
369
|
for (let cursor = index + 1; cursor < Math.min(lines.length, index + 12); cursor += 1) {
|
|
330
370
|
if (/^\s*-\s+id\s*:/.test(lines[cursor])) break
|
|
@@ -338,6 +378,7 @@ export function inspectProfilePatch(patchPath) {
|
|
|
338
378
|
path: patchPath,
|
|
339
379
|
exists: true,
|
|
340
380
|
visionRouterRows,
|
|
381
|
+
visionRouterOverrideRows,
|
|
341
382
|
manualVisionRouter: visionRouterRows > 0,
|
|
342
383
|
disablesOfficialDeepSeek,
|
|
343
384
|
}
|
|
@@ -581,4 +622,4 @@ export function doctorProfiles({ dshHome = resolveDshHome(), profile, fix = fals
|
|
|
581
622
|
&& applicable.every(profileHealthy)
|
|
582
623
|
&& (log.settingsSaveFailures?.length ?? 0) === 0,
|
|
583
624
|
}
|
|
584
|
-
}
|
|
625
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const GROUNDING_FRAME_SIZE = 1000
|
|
2
|
+
|
|
3
|
+
function finitePositiveInteger(value, name) {
|
|
4
|
+
const number = Number(value)
|
|
5
|
+
if (!Number.isFinite(number) || number <= 0) {
|
|
6
|
+
throw new TypeError(`${name} must be a positive finite number`)
|
|
7
|
+
}
|
|
8
|
+
return Math.max(1, Math.round(number))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build the exact geometry used to letterbox a source raster into the
|
|
13
|
+
* grounding protocol's square frame. The rendered dimensions are integers so
|
|
14
|
+
* callers can resize to these exact values before extending the canvas; the
|
|
15
|
+
* inverse therefore never depends on a backend's resize policy.
|
|
16
|
+
*/
|
|
17
|
+
export function createGroundingFrame(width, height, frameSize = GROUNDING_FRAME_SIZE) {
|
|
18
|
+
const sourceWidth = finitePositiveInteger(width, 'width')
|
|
19
|
+
const sourceHeight = finitePositiveInteger(height, 'height')
|
|
20
|
+
const size = finitePositiveInteger(frameSize, 'frameSize')
|
|
21
|
+
const scale = Math.min(size / sourceWidth, size / sourceHeight)
|
|
22
|
+
const renderedWidth = Math.max(1, Math.min(size, Math.round(sourceWidth * scale)))
|
|
23
|
+
const renderedHeight = Math.max(1, Math.min(size, Math.round(sourceHeight * scale)))
|
|
24
|
+
const left = Math.floor((size - renderedWidth) / 2)
|
|
25
|
+
const top = Math.floor((size - renderedHeight) / 2)
|
|
26
|
+
const right = size - renderedWidth - left
|
|
27
|
+
const bottom = size - renderedHeight - top
|
|
28
|
+
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
frameWidth: size,
|
|
31
|
+
frameHeight: size,
|
|
32
|
+
sourceWidth,
|
|
33
|
+
sourceHeight,
|
|
34
|
+
renderedWidth,
|
|
35
|
+
renderedHeight,
|
|
36
|
+
left,
|
|
37
|
+
top,
|
|
38
|
+
right,
|
|
39
|
+
bottom,
|
|
40
|
+
scaleX: renderedWidth / sourceWidth,
|
|
41
|
+
scaleY: renderedHeight / sourceHeight,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function finiteCoordinate(value) {
|
|
46
|
+
const number = Number(value)
|
|
47
|
+
return Number.isFinite(number) ? number : undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clamp(value, min, max) {
|
|
51
|
+
return Math.max(min, Math.min(value, max))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Map a box reported in the explicit square grounding frame back to source pixels. */
|
|
55
|
+
export function groundingFrameBoxToSource(box, frame) {
|
|
56
|
+
if (!box || !frame) return undefined
|
|
57
|
+
const x1 = finiteCoordinate(box.x1)
|
|
58
|
+
const y1 = finiteCoordinate(box.y1)
|
|
59
|
+
const x2 = finiteCoordinate(box.x2)
|
|
60
|
+
const y2 = finiteCoordinate(box.y2)
|
|
61
|
+
if ([x1, y1, x2, y2].some((value) => value === undefined)) return undefined
|
|
62
|
+
|
|
63
|
+
const raw = {
|
|
64
|
+
x1: (x1 - frame.left) / frame.scaleX,
|
|
65
|
+
y1: (y1 - frame.top) / frame.scaleY,
|
|
66
|
+
x2: (x2 - frame.left) / frame.scaleX,
|
|
67
|
+
y2: (y2 - frame.top) / frame.scaleY,
|
|
68
|
+
}
|
|
69
|
+
const mapped = {
|
|
70
|
+
x1: Math.floor(clamp(raw.x1, 0, frame.sourceWidth)),
|
|
71
|
+
y1: Math.floor(clamp(raw.y1, 0, frame.sourceHeight)),
|
|
72
|
+
x2: Math.ceil(clamp(raw.x2, 0, frame.sourceWidth)),
|
|
73
|
+
y2: Math.ceil(clamp(raw.y2, 0, frame.sourceHeight)),
|
|
74
|
+
}
|
|
75
|
+
if (mapped.x2 <= mapped.x1 || mapped.y2 <= mapped.y1) return undefined
|
|
76
|
+
return mapped
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Map source-pixel geometry into the square frame, useful for protocol tests. */
|
|
80
|
+
export function sourceBoxToGroundingFrame(box, frame) {
|
|
81
|
+
if (!box || !frame) return undefined
|
|
82
|
+
const x1 = finiteCoordinate(box.x1)
|
|
83
|
+
const y1 = finiteCoordinate(box.y1)
|
|
84
|
+
const x2 = finiteCoordinate(box.x2)
|
|
85
|
+
const y2 = finiteCoordinate(box.y2)
|
|
86
|
+
if ([x1, y1, x2, y2].some((value) => value === undefined)) return undefined
|
|
87
|
+
return {
|
|
88
|
+
x1: frame.left + x1 * frame.scaleX,
|
|
89
|
+
y1: frame.top + y1 * frame.scaleY,
|
|
90
|
+
x2: frame.left + x2 * frame.scaleX,
|
|
91
|
+
y2: frame.top + y2 * frame.scaleY,
|
|
92
|
+
}
|
|
93
|
+
}
|