dsh-vision-router 2.3.0 → 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 +12 -3
- package/README.zh.md +12 -3
- 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/dsh-support-window.md +5 -3
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +1 -1
- package/index.js +108 -71
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/core-apply-lifecycle.js +37 -43
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +3 -143
- 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/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +26 -230
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- 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/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-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +1 -3
- 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 +23 -44
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +17 -4
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +22 -4
- 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-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 +25 -107
- 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/structured-flow-hardening.js +22 -4
- 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 +16 -0
- 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.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 +34 -4
- 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 -147
- 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-tool-runtime-boundary.js +99 -30
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +4 -7
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +37 -11
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const VISION_INTENTS = [
|
|
2
|
+
'structured',
|
|
3
|
+
'ocr',
|
|
4
|
+
'document',
|
|
5
|
+
'ui',
|
|
6
|
+
'grounding',
|
|
7
|
+
'detection',
|
|
8
|
+
'general',
|
|
9
|
+
'chart_diagram',
|
|
10
|
+
'code_screenshot',
|
|
11
|
+
'visual_compare',
|
|
12
|
+
];
|
|
13
|
+
export const BENCHMARK_AXES = [
|
|
14
|
+
'structured',
|
|
15
|
+
'ocr',
|
|
16
|
+
'document',
|
|
17
|
+
'grounding',
|
|
18
|
+
'general',
|
|
19
|
+
];
|
|
20
|
+
const DIRECT_TASK_AXIS = Object.freeze({
|
|
21
|
+
structured: 'structured',
|
|
22
|
+
ocr: 'ocr',
|
|
23
|
+
document: 'document',
|
|
24
|
+
grounding: 'grounding',
|
|
25
|
+
general: 'general',
|
|
26
|
+
});
|
|
27
|
+
export function normalizeVisionIntent(intent) {
|
|
28
|
+
return typeof intent === 'string'
|
|
29
|
+
&& VISION_INTENTS.includes(intent)
|
|
30
|
+
? intent
|
|
31
|
+
: 'general';
|
|
32
|
+
}
|
|
33
|
+
export function benchmarkAxisForVisionIntent(intent) {
|
|
34
|
+
return DIRECT_TASK_AXIS[normalizeVisionIntent(intent)];
|
|
35
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
export const CAPABILITY_BENCHMARK_SUITE_REVISION = 5;
|
|
4
|
+
// Renderer-only fixes must invalidate prior evidence even when the scoring and
|
|
5
|
+
// prompt contract (the suite revision) is unchanged.
|
|
6
|
+
export const CAPABILITY_BENCHMARK_RENDERER_SCOPE = `${process.platform}/${process.arch}-proof-badge-v2`;
|
|
7
|
+
function isSensitiveKey(key) {
|
|
8
|
+
return /(^|[_-])(api[_-]?key|key|token|secret|password|authorization|auth|signature|sig)([_-]|$)/i
|
|
9
|
+
.test(String(key ?? ''));
|
|
10
|
+
}
|
|
11
|
+
function normalizeEndpoint(value) {
|
|
12
|
+
const raw = String(value ?? '').trim();
|
|
13
|
+
if (raw === '')
|
|
14
|
+
return '';
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(raw);
|
|
17
|
+
url.username = '';
|
|
18
|
+
url.password = '';
|
|
19
|
+
url.hash = '';
|
|
20
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
21
|
+
if (isSensitiveKey(key))
|
|
22
|
+
url.searchParams.delete(key);
|
|
23
|
+
}
|
|
24
|
+
url.searchParams.sort();
|
|
25
|
+
const pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
|
|
26
|
+
return `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}${pathname}${url.search}`;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return raw.replace(/\/+$/, '');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function sanitizeFingerprintValue(value, keyHint = '') {
|
|
33
|
+
if (value === null || value === undefined)
|
|
34
|
+
return value ?? null;
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
return value.map((item) => sanitizeFingerprintValue(item));
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'object') {
|
|
39
|
+
return Object.fromEntries(Object.entries(value)
|
|
40
|
+
.filter(([key]) => !isSensitiveKey(key))
|
|
41
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
42
|
+
.map(([key, child]) => [
|
|
43
|
+
key,
|
|
44
|
+
sanitizeFingerprintValue(child, key),
|
|
45
|
+
]));
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === 'string'
|
|
48
|
+
&& /(endpoint|base[_-]?url|url)$/i.test(keyHint)) {
|
|
49
|
+
return normalizeEndpoint(value);
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'bigint')
|
|
52
|
+
return value.toString();
|
|
53
|
+
if (typeof value === 'number'
|
|
54
|
+
|| typeof value === 'boolean'
|
|
55
|
+
|| typeof value === 'string') {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
export function capabilityBenchmarkFingerprint({ provider, model, endpoint, config, credentialFingerprint, } = {}) {
|
|
61
|
+
const payload = JSON.stringify({
|
|
62
|
+
schema: 'vision-capability-endpoint-v2',
|
|
63
|
+
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
64
|
+
rendererScope: CAPABILITY_BENCHMARK_RENDERER_SCOPE,
|
|
65
|
+
provider: String(provider ?? '').trim(),
|
|
66
|
+
model: String(model ?? '').trim(),
|
|
67
|
+
endpoint: normalizeEndpoint(endpoint),
|
|
68
|
+
config: sanitizeFingerprintValue(config && typeof config === 'object' ? config : null),
|
|
69
|
+
credentialFingerprint: String(credentialFingerprint ?? 'none'),
|
|
70
|
+
});
|
|
71
|
+
return `ep2_${createHash('sha256').update(payload).digest('hex').slice(0, 32)}`;
|
|
72
|
+
}
|
|
@@ -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;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
currentLegacyGlobalProxyFetch,
|
|
3
|
+
legacyGlobalProxyPairFor,
|
|
4
|
+
runWithLegacyGlobalProxyScope,
|
|
5
|
+
} from './legacy-global-proxy-boundary.js'
|
|
1
6
|
import { probeLocalBackends } from './local-connection-probe.js'
|
|
2
7
|
import {
|
|
3
8
|
METADATA_RESPONSE_MAX_BYTES,
|
|
4
9
|
readResponseJsonBounded,
|
|
5
10
|
} from './http-body-limit.js'
|
|
6
11
|
|
|
12
|
+
const moduleFetch = typeof globalThis.fetch === 'function'
|
|
13
|
+
? globalThis.fetch.bind(globalThis)
|
|
14
|
+
: undefined
|
|
15
|
+
|
|
7
16
|
export const VISION_TEST_CONNECTION_PATH = '/_dsh/vision-router/test-connection'
|
|
8
17
|
export const VISION_MODEL_CAPABILITIES_PATH = '/_dsh/vision-router/model-capabilities'
|
|
9
18
|
|
|
@@ -92,7 +101,28 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
|
|
|
92
101
|
const httpRoute = typeof connection.httpRoute === 'string' && connection.httpRoute !== ''
|
|
93
102
|
? connection.httpRoute
|
|
94
103
|
: 'vision-http'
|
|
95
|
-
|
|
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
|
+
}
|
|
96
126
|
|
|
97
127
|
const probeConnection = async () => {
|
|
98
128
|
const started = Date.now()
|
|
@@ -109,7 +139,7 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
|
|
|
109
139
|
|
|
110
140
|
const localProbe = await probeLocalBackends(
|
|
111
141
|
getArray(connection.localBackends),
|
|
112
|
-
(provider) =>
|
|
142
|
+
(provider) => scopedProbe(provider.name ?? provider.id, provider.baseURL, provider.model, started),
|
|
113
143
|
started,
|
|
114
144
|
)
|
|
115
145
|
if (localProbe !== undefined) return localProbe
|
|
@@ -117,7 +147,7 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
|
|
|
117
147
|
const httpBackends = getArray(connection.httpBackends)
|
|
118
148
|
if (first !== undefined && first.provider === httpRoute) {
|
|
119
149
|
const entry = httpBackends.find((provider) => `${provider.name}/${provider.model}` === first.model)
|
|
120
|
-
if (entry !== undefined) return
|
|
150
|
+
if (entry !== undefined) return scopedProbe(entry.name, entry.baseURL, entry.model, started)
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
if (first !== undefined) {
|
|
@@ -138,7 +168,7 @@ export function installVisionDiagnosticsRoutes(ctx, options = {}) {
|
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
const httpFirst = httpBackends[0]
|
|
141
|
-
if (httpFirst !== undefined) return
|
|
171
|
+
if (httpFirst !== undefined) return scopedProbe(httpFirst.provider ?? httpFirst.name ?? httpRoute, httpFirst.baseURL, httpFirst.model, started)
|
|
142
172
|
return { ok: false, error: 'no usable vision provider configured' }
|
|
143
173
|
}
|
|
144
174
|
|
|
@@ -1,61 +1,51 @@
|
|
|
1
1
|
function pairIdentity(pair) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
const value = pair !== null && typeof pair === 'object'
|
|
3
|
+
? pair
|
|
4
|
+
: {};
|
|
5
|
+
const provider = typeof value.provider === 'string' ? value.provider.trim() : '';
|
|
6
|
+
const model = typeof value.model === 'string' ? value.model.trim() : '';
|
|
7
|
+
return provider !== '' && model !== '' ? `${provider}\u0000${model}` : undefined;
|
|
5
8
|
}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Apply an explicit P1 execution order to a base pair list without changing
|
|
9
|
-
* the base set of executable routes.
|
|
10
|
-
*
|
|
11
|
-
* This is intentionally a stable reorder, not replacement:
|
|
12
|
-
* - a scoped pair not present in `basePairs` is ignored (cannot invent a route);
|
|
13
|
-
* - every base pair omitted by the planner is appended in its original order
|
|
14
|
-
* (cannot delete configured/local/discovered fallback);
|
|
15
|
-
* - duplicate base/scoped identities collapse to the first base occurrence;
|
|
16
|
-
* - no scoped order means byte-for-shape legacy behavior: a detached copy of
|
|
17
|
-
* the base list in the same order.
|
|
18
|
-
*
|
|
19
|
-
* The caller remains responsible for constructing `basePairs` with its normal
|
|
20
|
-
* adapter/HTTP/local availability rules. This helper owns ordering only.
|
|
21
|
-
*/
|
|
22
9
|
export function applyVisionExecutionOrder(basePairs, scopedOrder) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
10
|
+
const base = Array.isArray(basePairs)
|
|
11
|
+
? basePairs.filter((pair) => pairIdentity(pair) !== undefined)
|
|
12
|
+
: [];
|
|
13
|
+
const baseById = new Map();
|
|
14
|
+
for (const pair of base) {
|
|
15
|
+
const id = pairIdentity(pair);
|
|
16
|
+
if (id !== undefined && !baseById.has(id))
|
|
17
|
+
baseById.set(id, pair);
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(scopedOrder) || scopedOrder.length === 0) {
|
|
20
|
+
return [...baseById.values()];
|
|
21
|
+
}
|
|
22
|
+
const out = [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const addBasePair = (pair) => {
|
|
25
|
+
const id = pairIdentity(pair);
|
|
26
|
+
if (id === undefined || seen.has(id))
|
|
27
|
+
return;
|
|
28
|
+
seen.add(id);
|
|
29
|
+
out.push(pair);
|
|
30
|
+
};
|
|
31
|
+
for (const requested of scopedOrder) {
|
|
32
|
+
const id = pairIdentity(requested);
|
|
33
|
+
if (id === undefined)
|
|
34
|
+
continue;
|
|
35
|
+
const pair = baseById.get(id);
|
|
36
|
+
if (pair !== undefined)
|
|
37
|
+
addBasePair(pair);
|
|
38
|
+
}
|
|
39
|
+
for (const pair of baseById.values())
|
|
40
|
+
addBasePair(pair);
|
|
41
|
+
return out;
|
|
53
42
|
}
|
|
54
|
-
|
|
55
43
|
export function sameVisionPairOrder(left, right) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
45
|
+
return false;
|
|
46
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
47
|
+
if (pairIdentity(left[index]) !== pairIdentity(right[index]))
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
61
51
|
}
|