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
|
@@ -1,73 +1,128 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
import { capabilityBenchmarkFingerprint } from './vision-capability-
|
|
3
|
-
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { capabilityBenchmarkFingerprint } from './vision-capability-fingerprint.js';
|
|
3
|
+
function propertyBag(value) {
|
|
4
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function')
|
|
5
|
+
? value
|
|
6
|
+
: undefined;
|
|
7
|
+
}
|
|
4
8
|
function nonEmpty(value) {
|
|
5
|
-
|
|
9
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
10
|
+
}
|
|
11
|
+
function contextGetter(ctx) {
|
|
12
|
+
const get = propertyBag(ctx)?.get;
|
|
13
|
+
return typeof get === 'function'
|
|
14
|
+
? (name) => get.call(ctx, name)
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
function credentialValue(value) {
|
|
18
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
6
19
|
}
|
|
7
|
-
|
|
8
20
|
export function visionCredentialFingerprint(value) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
const text = typeof value === 'string' ? value : '';
|
|
22
|
+
if (text === '')
|
|
23
|
+
return 'none';
|
|
24
|
+
return `cred_${createHash('sha256').update(text).digest('hex').slice(0, 24)}`;
|
|
12
25
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
config,
|
|
24
|
-
} = {}) {
|
|
25
|
-
return capabilityBenchmarkFingerprint({ provider, model, endpoint, config })
|
|
26
|
+
/**
|
|
27
|
+
* Capability identity deliberately excludes credential identity.
|
|
28
|
+
*
|
|
29
|
+
* Credentials decide whether a route can be accessed right now; they do not
|
|
30
|
+
* define what the same model deployment is capable of. Deployment/project
|
|
31
|
+
* identity belongs in endpoint/config when it actually changes the served
|
|
32
|
+
* model, never in secret material.
|
|
33
|
+
*/
|
|
34
|
+
export function capabilityEvidenceFingerprint({ provider, model, endpoint, config, } = {}) {
|
|
35
|
+
return capabilityBenchmarkFingerprint({ provider, model, endpoint, config });
|
|
26
36
|
}
|
|
27
|
-
|
|
28
37
|
export async function resolveVisionCredential(ctx, ref) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
const key = nonEmpty(ref);
|
|
39
|
+
if (key === undefined) {
|
|
40
|
+
return {
|
|
41
|
+
required: false,
|
|
42
|
+
value: undefined,
|
|
43
|
+
fingerprint: 'none',
|
|
44
|
+
source: 'none',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
let credentials;
|
|
35
48
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
credentials = contextGetter(ctx)?.('credentials');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
credentials = undefined;
|
|
53
|
+
}
|
|
54
|
+
if (credentials !== undefined) {
|
|
55
|
+
try {
|
|
56
|
+
const resolve = propertyBag(credentials)?.resolve;
|
|
57
|
+
const hit = typeof resolve === 'function'
|
|
58
|
+
? await resolve.call(credentials, key)
|
|
59
|
+
: undefined;
|
|
60
|
+
const value = credentialValue(propertyBag(hit)?.value);
|
|
61
|
+
return {
|
|
62
|
+
required: true,
|
|
63
|
+
value,
|
|
64
|
+
fingerprint: value === undefined
|
|
65
|
+
? 'unresolved'
|
|
66
|
+
: visionCredentialFingerprint(value),
|
|
67
|
+
source: value === undefined ? 'credentials-miss' : 'credentials',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return {
|
|
72
|
+
required: true,
|
|
73
|
+
value: undefined,
|
|
74
|
+
fingerprint: 'unresolved',
|
|
75
|
+
source: 'credentials-error',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let launchEnvironment;
|
|
80
|
+
try {
|
|
81
|
+
launchEnvironment = contextGetter(ctx)?.('launchEnvironment');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return {
|
|
85
|
+
required: true,
|
|
86
|
+
value: undefined,
|
|
87
|
+
fingerprint: 'unresolved',
|
|
88
|
+
source: 'launch-environment-error',
|
|
89
|
+
};
|
|
46
90
|
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const launchEnvironment = ctx?.get?.('launchEnvironment')
|
|
51
91
|
if (launchEnvironment !== undefined) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
try {
|
|
93
|
+
const get = propertyBag(launchEnvironment)?.get;
|
|
94
|
+
const hit = typeof get === 'function'
|
|
95
|
+
? get.call(launchEnvironment, key)
|
|
96
|
+
: undefined;
|
|
97
|
+
const value = credentialValue(propertyBag(hit)?.value);
|
|
98
|
+
return {
|
|
99
|
+
required: true,
|
|
100
|
+
value,
|
|
101
|
+
fingerprint: value === undefined
|
|
102
|
+
? 'unresolved'
|
|
103
|
+
: visionCredentialFingerprint(value),
|
|
104
|
+
source: value === undefined
|
|
105
|
+
? 'launch-environment-miss'
|
|
106
|
+
: 'launch-environment',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return {
|
|
111
|
+
required: true,
|
|
112
|
+
value: undefined,
|
|
113
|
+
fingerprint: 'unresolved',
|
|
114
|
+
source: 'launch-environment-error',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const ambient = process.env[key];
|
|
119
|
+
const value = typeof ambient === 'string' && ambient !== '' ? ambient : undefined;
|
|
120
|
+
return {
|
|
55
121
|
required: true,
|
|
56
122
|
value,
|
|
57
|
-
fingerprint: value === undefined
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return { required: true, value: undefined, fingerprint: 'unresolved', source: 'launch-environment-error' }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const ambient = process.env[key]
|
|
66
|
-
const value = typeof ambient === 'string' && ambient !== '' ? ambient : undefined
|
|
67
|
-
return {
|
|
68
|
-
required: true,
|
|
69
|
-
value,
|
|
70
|
-
fingerprint: value === undefined ? 'unresolved' : visionCredentialFingerprint(value),
|
|
71
|
-
source: value === undefined ? 'ambient-miss' : 'ambient',
|
|
72
|
-
}
|
|
123
|
+
fingerprint: value === undefined
|
|
124
|
+
? 'unresolved'
|
|
125
|
+
: visionCredentialFingerprint(value),
|
|
126
|
+
source: value === undefined ? 'ambient-miss' : 'ambient',
|
|
127
|
+
};
|
|
73
128
|
}
|
|
@@ -5,26 +5,18 @@
|
|
|
5
5
|
// health only gates temporary availability. Model names/families never create
|
|
6
6
|
// capability scores and unmeasured tasks never reorder the user's chain.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export const BENCHMARK_AXES = Object.freeze([
|
|
22
|
-
'structured',
|
|
23
|
-
'ocr',
|
|
24
|
-
'document',
|
|
25
|
-
'grounding',
|
|
26
|
-
'general',
|
|
27
|
-
])
|
|
8
|
+
import {
|
|
9
|
+
BENCHMARK_AXES,
|
|
10
|
+
VISION_INTENTS,
|
|
11
|
+
benchmarkAxisForVisionIntent,
|
|
12
|
+
normalizeVisionIntent,
|
|
13
|
+
} from './vision-capability-evidence.js'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
BENCHMARK_AXES,
|
|
17
|
+
VISION_INTENTS,
|
|
18
|
+
benchmarkAxisForVisionIntent,
|
|
19
|
+
} from './vision-capability-evidence.js'
|
|
28
20
|
|
|
29
21
|
// Internal compatibility vocabulary. `privacy` is the historical scorer name
|
|
30
22
|
// for the user-facing `local` preference; unlike the old implementation it is
|
|
@@ -33,14 +25,6 @@ export const VISION_STRATEGIES = Object.freeze(['quality', 'balanced', 'speed',
|
|
|
33
25
|
|
|
34
26
|
export const AUTO_REORDER_MIN_ADVANTAGE = 0.08
|
|
35
27
|
|
|
36
|
-
const DIRECT_TASK_AXIS = Object.freeze({
|
|
37
|
-
structured: 'structured',
|
|
38
|
-
ocr: 'ocr',
|
|
39
|
-
document: 'document',
|
|
40
|
-
grounding: 'grounding',
|
|
41
|
-
general: 'general',
|
|
42
|
-
})
|
|
43
|
-
|
|
44
28
|
const BOOTSTRAP_VISUAL_INTENT = Object.freeze({
|
|
45
29
|
chat: 'ui',
|
|
46
30
|
document: 'document',
|
|
@@ -60,7 +44,7 @@ function clamp01(value, fallback = 0) {
|
|
|
60
44
|
}
|
|
61
45
|
|
|
62
46
|
function normalizedIntent(intent) {
|
|
63
|
-
return
|
|
47
|
+
return normalizeVisionIntent(intent)
|
|
64
48
|
}
|
|
65
49
|
|
|
66
50
|
function normalizedStrategy(strategy) {
|
|
@@ -72,10 +56,6 @@ function cleanEnum(value, allowed) {
|
|
|
72
56
|
return allowed.has(text) ? text : 'unknown'
|
|
73
57
|
}
|
|
74
58
|
|
|
75
|
-
export function benchmarkAxisForVisionIntent(intent) {
|
|
76
|
-
return DIRECT_TASK_AXIS[normalizedIntent(intent)]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
59
|
export function inferBootstrapVisionIntents(bootstrap = {}) {
|
|
80
60
|
const source = bootstrap && typeof bootstrap === 'object' && !Array.isArray(bootstrap) ? bootstrap : {}
|
|
81
61
|
const visualKind = cleanEnum(source.visual_kind, BOOTSTRAP_VISUAL_KINDS)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
function propertyBag(value) {
|
|
2
|
+
return value !== null && typeof value === 'object'
|
|
3
|
+
? value
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
6
|
+
function failureMessage(failure) {
|
|
7
|
+
const message = propertyBag(failure)?.message;
|
|
8
|
+
return message ? String(message) : String(failure);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Minimal DVR text assembler over the public DSH StreamChunk contract.
|
|
12
|
+
*
|
|
13
|
+
* Current DSH failures arrive inside the terminal finish reason. The two
|
|
14
|
+
* standalone terminal shapes remain explicit compatibility input for released
|
|
15
|
+
* historical/partial adapters. Runtime shape checks stay in place because JS
|
|
16
|
+
* callers may still provide malformed chunks.
|
|
17
|
+
*/
|
|
18
|
+
export function createChunkAssembler() {
|
|
19
|
+
const parts = new Map();
|
|
20
|
+
const order = [];
|
|
21
|
+
let finishKind;
|
|
22
|
+
let failure;
|
|
23
|
+
const pushUnknown = (value) => {
|
|
24
|
+
const chunk = propertyBag(value);
|
|
25
|
+
if (chunk === undefined || typeof chunk.type !== 'string')
|
|
26
|
+
return;
|
|
27
|
+
switch (chunk.type) {
|
|
28
|
+
case 'block-start': {
|
|
29
|
+
if (!parts.has(chunk.index)) {
|
|
30
|
+
order.push(chunk.index);
|
|
31
|
+
parts.set(chunk.index, { type: chunk.blockType, text: '' });
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'text-delta': {
|
|
36
|
+
const part = parts.get(chunk.index);
|
|
37
|
+
if (part !== undefined)
|
|
38
|
+
part.text += (chunk.text ?? '');
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'reasoning-delta':
|
|
42
|
+
case 'tool-call-delta':
|
|
43
|
+
case 'usage':
|
|
44
|
+
break;
|
|
45
|
+
case 'block-end': {
|
|
46
|
+
const part = parts.get(chunk.index);
|
|
47
|
+
const block = propertyBag(chunk.block);
|
|
48
|
+
if (part !== undefined && typeof block?.text === 'string') {
|
|
49
|
+
part.text = block.text;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case 'finish': {
|
|
54
|
+
const reason = propertyBag(chunk.reason);
|
|
55
|
+
if (reason !== undefined
|
|
56
|
+
&& (reason.kind === 'error' || reason.kind === 'aborted')) {
|
|
57
|
+
failure = reason.failure;
|
|
58
|
+
}
|
|
59
|
+
finishKind =
|
|
60
|
+
reason !== undefined && reason.kind
|
|
61
|
+
? String(reason.kind)
|
|
62
|
+
: 'stop';
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case 'error':
|
|
66
|
+
case 'aborted':
|
|
67
|
+
failure = chunk.failure;
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
push(chunk) {
|
|
75
|
+
pushUnknown(chunk);
|
|
76
|
+
},
|
|
77
|
+
finish() {
|
|
78
|
+
if (failure)
|
|
79
|
+
throw new Error(failureMessage(failure));
|
|
80
|
+
if (finishKind !== undefined
|
|
81
|
+
&& finishKind !== 'stop'
|
|
82
|
+
&& finishKind !== 'max-tokens') {
|
|
83
|
+
throw new Error(`vision call finished with "${finishKind}"`);
|
|
84
|
+
}
|
|
85
|
+
return order
|
|
86
|
+
.map((index) => parts.get(index))
|
|
87
|
+
.filter((part) => part !== undefined && part.type === 'text')
|
|
88
|
+
.map((part) => part.text)
|
|
89
|
+
.join('')
|
|
90
|
+
.trim();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const _officialFailureAuthority = undefined;
|
|
95
|
+
void _officialFailureAuthority;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
currentLegacyGlobalProxyFetch,
|
|
3
|
+
legacyGlobalProxyPairFor,
|
|
4
|
+
runWithLegacyGlobalProxyScope,
|
|
5
|
+
} from './legacy-global-proxy-boundary.js'
|
|
6
|
+
import { probeLocalBackends } from './local-connection-probe.js'
|
|
7
|
+
import {
|
|
8
|
+
METADATA_RESPONSE_MAX_BYTES,
|
|
9
|
+
readResponseJsonBounded,
|
|
10
|
+
} from './http-body-limit.js'
|
|
11
|
+
|
|
12
|
+
const moduleFetch = typeof globalThis.fetch === 'function'
|
|
13
|
+
? globalThis.fetch.bind(globalThis)
|
|
14
|
+
: undefined
|
|
15
|
+
|
|
16
|
+
export const VISION_TEST_CONNECTION_PATH = '/_dsh/vision-router/test-connection'
|
|
17
|
+
export const VISION_MODEL_CAPABILITIES_PATH = '/_dsh/vision-router/model-capabilities'
|
|
18
|
+
|
|
19
|
+
function adapterAvailable(llm, provider) {
|
|
20
|
+
if (!llm || typeof llm.registration !== 'function') return false
|
|
21
|
+
try {
|
|
22
|
+
llm.registration(provider)
|
|
23
|
+
return true
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function probeModels(fetchImpl, baseURL, expectedModel, startedAt) {
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetchImpl(`${String(baseURL).replace(/\/$/, '')}/models`, {
|
|
32
|
+
method: 'GET',
|
|
33
|
+
signal: AbortSignal.timeout(8000),
|
|
34
|
+
})
|
|
35
|
+
const latencyMs = Date.now() - startedAt
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
return { ok: false, latencyMs, status: response.status, error: `HTTP ${response.status}` }
|
|
38
|
+
}
|
|
39
|
+
const data = await readResponseJsonBounded(
|
|
40
|
+
response,
|
|
41
|
+
METADATA_RESPONSE_MAX_BYTES,
|
|
42
|
+
{ label: 'vision backend /models response' },
|
|
43
|
+
).catch(() => undefined)
|
|
44
|
+
const models = data && Array.isArray(data.data) ? data.data : undefined
|
|
45
|
+
const count = models ? models.length : undefined
|
|
46
|
+
if (
|
|
47
|
+
typeof expectedModel === 'string' &&
|
|
48
|
+
expectedModel !== '' &&
|
|
49
|
+
models &&
|
|
50
|
+
!models.some((entry) => entry && String(entry.id) === expectedModel)
|
|
51
|
+
) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
latencyMs,
|
|
55
|
+
status: response.status,
|
|
56
|
+
models: count,
|
|
57
|
+
endpoint: baseURL,
|
|
58
|
+
error: `configured model "${expectedModel}" was not returned by /models`,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, latencyMs, status: response.status, models: count, endpoint: baseURL }
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
latencyMs: Date.now() - startedAt,
|
|
66
|
+
error: error && error.message ? error.message : String(error),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getArray(factory) {
|
|
72
|
+
const value = typeof factory === 'function' ? factory() : factory
|
|
73
|
+
return Array.isArray(value) ? value : []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Product diagnostics/settings-support routes kept outside visual Core.
|
|
78
|
+
*
|
|
79
|
+
* The caller supplies three coherent domain faces instead of a Core facade:
|
|
80
|
+
* connection candidates/probes, capability snapshot production, and the
|
|
81
|
+
* Host-ownership state rendered in the connection response. Authentication
|
|
82
|
+
* and remote/local capability policy stay outside this module in the existing
|
|
83
|
+
* scoped WebServer boundary.
|
|
84
|
+
*/
|
|
85
|
+
export function installVisionDiagnosticsRoutes(ctx, options = {}) {
|
|
86
|
+
if (!ctx || typeof ctx.inject !== 'function') return ctx
|
|
87
|
+
|
|
88
|
+
const connection = options.connection ?? {}
|
|
89
|
+
const capabilities = options.capabilities ?? {}
|
|
90
|
+
const ownership = options.ownership ?? {}
|
|
91
|
+
const resolveCapability = connection.resolveCapability
|
|
92
|
+
const collectCapabilities = capabilities.collect
|
|
93
|
+
if (typeof resolveCapability !== 'function') {
|
|
94
|
+
throw new TypeError('vision diagnostics routes require a connection capability resolver')
|
|
95
|
+
}
|
|
96
|
+
if (typeof collectCapabilities !== 'function') {
|
|
97
|
+
throw new TypeError('vision diagnostics routes require a capability collector')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const llm = ctx.llm
|
|
101
|
+
const httpRoute = typeof connection.httpRoute === 'string' && connection.httpRoute !== ''
|
|
102
|
+
? connection.httpRoute
|
|
103
|
+
: 'vision-http'
|
|
104
|
+
// The proxy boundary installs its wrapper after apply(), so a capture taken here
|
|
105
|
+
// would make "test connection" report failures that real vision turns (which run
|
|
106
|
+
// inside the adapter's proxy scope) do not have. Use the fetch the boundary
|
|
107
|
+
// currently owns, with the module-load capture as the fallback.
|
|
108
|
+
const injectedFetch = typeof options.fetchImpl === 'function' ? options.fetchImpl : undefined
|
|
109
|
+
const resolveFetch = () => injectedFetch ?? currentLegacyGlobalProxyFetch() ?? moduleFetch
|
|
110
|
+
const connectionConfig = () => {
|
|
111
|
+
try {
|
|
112
|
+
const live = ctx?.get?.('settings')?.get?.('vision-router')
|
|
113
|
+
if (live !== null && typeof live === 'object' && !Array.isArray(live)) return live
|
|
114
|
+
} catch {
|
|
115
|
+
// No settings namespace on this Host: probes stay unscoped, as before.
|
|
116
|
+
}
|
|
117
|
+
return {}
|
|
118
|
+
}
|
|
119
|
+
const scopedProbe = (providerName, baseURL, expectedModel, startedAt) => {
|
|
120
|
+
const work = () => probeModels(resolveFetch(), baseURL, expectedModel, startedAt)
|
|
121
|
+
const pair = legacyGlobalProxyPairFor(connectionConfig(), providerName)
|
|
122
|
+
return pair === undefined
|
|
123
|
+
? work()
|
|
124
|
+
: runWithLegacyGlobalProxyScope(pair.provider, pair.model, work)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const probeConnection = async () => {
|
|
128
|
+
const started = Date.now()
|
|
129
|
+
let first
|
|
130
|
+
for (const pair of getArray(connection.candidatePairs)) {
|
|
131
|
+
if (!pair) continue
|
|
132
|
+
if (pair.provider !== httpRoute && !adapterAvailable(llm, pair.provider)) continue
|
|
133
|
+
const capability = await resolveCapability(pair.provider, pair.model)
|
|
134
|
+
if (capability?.attemptable !== false) {
|
|
135
|
+
first = pair
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const localProbe = await probeLocalBackends(
|
|
141
|
+
getArray(connection.localBackends),
|
|
142
|
+
(provider) => scopedProbe(provider.name ?? provider.id, provider.baseURL, provider.model, started),
|
|
143
|
+
started,
|
|
144
|
+
)
|
|
145
|
+
if (localProbe !== undefined) return localProbe
|
|
146
|
+
|
|
147
|
+
const httpBackends = getArray(connection.httpBackends)
|
|
148
|
+
if (first !== undefined && first.provider === httpRoute) {
|
|
149
|
+
const entry = httpBackends.find((provider) => `${provider.name}/${provider.model}` === first.model)
|
|
150
|
+
if (entry !== undefined) return scopedProbe(entry.name, entry.baseURL, entry.model, started)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (first !== undefined) {
|
|
154
|
+
try {
|
|
155
|
+
await llm.resolveModelInfo(first.provider, first.model)
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
latencyMs: Date.now() - started,
|
|
159
|
+
detail: `${first.provider}/${first.model} metadata resolved (no network call)`,
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
latencyMs: Date.now() - started,
|
|
165
|
+
error: error && error.message ? error.message : String(error),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const httpFirst = httpBackends[0]
|
|
171
|
+
if (httpFirst !== undefined) return scopedProbe(httpFirst.provider ?? httpFirst.name ?? httpRoute, httpFirst.baseURL, httpFirst.model, started)
|
|
172
|
+
return { ok: false, error: 'no usable vision provider configured' }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
176
|
+
webCtx.effect(() => {
|
|
177
|
+
return webCtx.webServer.register({
|
|
178
|
+
kind: 'exact',
|
|
179
|
+
path: VISION_TEST_CONNECTION_PATH,
|
|
180
|
+
handler: async (req, res) => {
|
|
181
|
+
if (req.method !== 'GET') {
|
|
182
|
+
res.setHeader('Allow', 'GET')
|
|
183
|
+
res.writeHead(405)
|
|
184
|
+
res.end()
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const result = await probeConnection()
|
|
189
|
+
const officialRouteAvailable = adapterAvailable(llm, 'deepseek-official')
|
|
190
|
+
result.stealth = {
|
|
191
|
+
configured: ownership.stealthConfigured,
|
|
192
|
+
active: false,
|
|
193
|
+
reason: ownership.hostOwnsOfficialDeepSeek && !officialRouteAvailable
|
|
194
|
+
? 'host-owned-official-unavailable'
|
|
195
|
+
: undefined,
|
|
196
|
+
hostOwned: ownership.hostOwnsOfficialDeepSeek,
|
|
197
|
+
}
|
|
198
|
+
res.writeHead(result.ok ? 200 : 502, { 'content-type': 'application/json' })
|
|
199
|
+
res.end(JSON.stringify(result))
|
|
200
|
+
} catch (error) {
|
|
201
|
+
res.writeHead(500, { 'content-type': 'application/json' })
|
|
202
|
+
res.end(JSON.stringify({
|
|
203
|
+
ok: false,
|
|
204
|
+
error: error && error.message ? error.message : String(error),
|
|
205
|
+
}))
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
}, 'vision-router: test-connection route')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
213
|
+
webCtx.effect(
|
|
214
|
+
() => webCtx.webServer.register({
|
|
215
|
+
kind: 'exact',
|
|
216
|
+
path: VISION_MODEL_CAPABILITIES_PATH,
|
|
217
|
+
handler: async (req, res) => {
|
|
218
|
+
if (req.method !== 'GET') {
|
|
219
|
+
res.setHeader('Allow', 'GET')
|
|
220
|
+
res.writeHead(405)
|
|
221
|
+
res.end()
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const snapshot = await collectCapabilities()
|
|
226
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
227
|
+
res.end(JSON.stringify({
|
|
228
|
+
capabilities: snapshot,
|
|
229
|
+
builtinFallback: getArray(capabilities.builtinFallback),
|
|
230
|
+
anonymousRpmPerModel: 2,
|
|
231
|
+
}))
|
|
232
|
+
} catch (error) {
|
|
233
|
+
res.writeHead(500, { 'content-type': 'application/json' })
|
|
234
|
+
res.end(JSON.stringify({
|
|
235
|
+
capabilities: {},
|
|
236
|
+
error: error && error.message ? error.message : String(error),
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
'vision-router: model capabilities route',
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
return ctx
|
|
246
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { visionRouterMessageSource } from './session-message-source-compat.js'
|
|
2
|
+
|
|
1
3
|
export const VISION_SUCCESS_GUIDANCE =
|
|
2
4
|
'Vision Router trusted execution metadata: the preceding vision_describe call SUCCEEDED. ' +
|
|
3
5
|
'This metadata is not image content. If that successful evidence resolves the visual fact needed for the current task, stop making additional vision calls for that fact and continue the remaining user task from this evidence. ' +
|
|
@@ -34,6 +36,6 @@ export function visionDescribeSuccessContext(exec, result) {
|
|
|
34
36
|
role: 'user',
|
|
35
37
|
id: `vision-router-evidence-success-${String(exec.callId)}`,
|
|
36
38
|
content: [{ type: 'text', text: VISION_SUCCESS_GUIDANCE }],
|
|
37
|
-
source:
|
|
39
|
+
source: visionRouterMessageSource(exec.agent.session),
|
|
38
40
|
}
|
|
39
41
|
}
|