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
package/lib/http-compat.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { currentVisionProviderTransport } from './vision-provider-transport.js'
|
|
2
1
|
|
|
3
2
|
const GLM_4V_FLASH = /^glm-4v-flash(?:$|[-_.])/i
|
|
4
3
|
export const DEFAULT_HTTP_ERROR_DETAIL_MAX_BYTES = 64 * 1024
|
|
@@ -278,7 +277,7 @@ function nextCompatibilityBody(body, detail) {
|
|
|
278
277
|
*/
|
|
279
278
|
export async function fetchWithOpenAICompatibility(fetchImpl, input, init, context = {}) {
|
|
280
279
|
if (typeof fetchImpl !== 'function') throw new TypeError('fetch implementation is required')
|
|
281
|
-
const transport = context.active === true ?
|
|
280
|
+
const transport = context.active === true ? context.providerTransport : undefined
|
|
282
281
|
const requestFetch = transport
|
|
283
282
|
? (nextInput, nextInit) => transport.fetch(nextInput, nextInit, {
|
|
284
283
|
providerName: context.providerName,
|
|
@@ -1,25 +1,34 @@
|
|
|
1
|
+
function objectRecord(value) {
|
|
2
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
1
6
|
function boundedAttachmentId(block) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
const attachment = objectRecord(objectRecord(block)?.attachment);
|
|
8
|
+
const raw = attachment?.attachmentId ?? attachment?.id;
|
|
9
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
10
|
+
return undefined;
|
|
11
|
+
return raw.trim().slice(0, 256);
|
|
6
12
|
}
|
|
7
|
-
|
|
8
13
|
/** DSH 0.1.6+ marks durable request-limit omissions on the ImageBlock itself. */
|
|
9
14
|
export function isOffloadedImageBlock(block) {
|
|
10
|
-
|
|
15
|
+
const value = objectRecord(block);
|
|
16
|
+
return value?.type === 'image' && value.offloaded === true;
|
|
11
17
|
}
|
|
12
|
-
|
|
13
18
|
export function blocksHaveRetainedImage(content) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
if (!Array.isArray(content))
|
|
20
|
+
return false;
|
|
21
|
+
for (const block of content) {
|
|
22
|
+
const value = objectRecord(block);
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
if (value.type === 'image' && value.offloaded !== true)
|
|
26
|
+
return true;
|
|
27
|
+
if (Array.isArray(value.content) && blocksHaveRetainedImage(value.content))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
21
31
|
}
|
|
22
|
-
|
|
23
32
|
/**
|
|
24
33
|
* Dependency-free model-facing placeholder for an image DSH already offloaded.
|
|
25
34
|
*
|
|
@@ -29,10 +38,10 @@ export function blocksHaveRetainedImage(content) {
|
|
|
29
38
|
* read or resend its bytes once the Host projection marks this occurrence.
|
|
30
39
|
*/
|
|
31
40
|
export function offloadedImagePlaceholder(block) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
const id = boundedAttachmentId(block);
|
|
42
|
+
if (id === undefined) {
|
|
43
|
+
return '[image omitted to fit request image limits; attachment id unavailable]';
|
|
44
|
+
}
|
|
45
|
+
return `[image omitted to fit request image limits; attachment ${id}. ` +
|
|
46
|
+
`To inspect it again, call vision_describe with attachmentIds: [${JSON.stringify(id)}].]`;
|
|
38
47
|
}
|
|
@@ -1,254 +1,236 @@
|
|
|
1
|
-
import { currentVisionTurnBudgetSignal } from './turn-budget-context.js'
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
export const DEFAULT_IMAGE_RESOURCE_MAX_BYPASSES = 2
|
|
5
|
-
|
|
1
|
+
import { currentVisionTurnBudgetSignal } from './turn-budget-context.js';
|
|
2
|
+
const MIB = 1024 * 1024;
|
|
3
|
+
export const DEFAULT_IMAGE_RESOURCE_MAX_BYPASSES = 2;
|
|
6
4
|
function abortError() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const error = new Error('image resource wait aborted');
|
|
6
|
+
error.name = 'AbortError';
|
|
7
|
+
error.code = 'ABORT_ERR';
|
|
8
|
+
return error;
|
|
11
9
|
}
|
|
12
|
-
|
|
13
10
|
function effectiveSignal(explicit) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const ambient = currentVisionTurnBudgetSignal();
|
|
12
|
+
if (!explicit)
|
|
13
|
+
return ambient;
|
|
14
|
+
if (!ambient || ambient === explicit)
|
|
15
|
+
return explicit;
|
|
16
|
+
return AbortSignal.any([explicit, ambient]);
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
const h = Math.max(0, Math.floor(Number(height) || 0))
|
|
27
|
-
const c = Math.max(1, Number(channels) || 4)
|
|
28
|
-
const n = Math.max(1, Number(copies) || 1)
|
|
29
|
-
const factor = Math.max(1, Number(safetyFactor) || 1)
|
|
30
|
-
return Math.ceil(w * h * c * n * factor)
|
|
18
|
+
export function estimateDecodedBytes(width, height, { channels = 4, copies = 1, safetyFactor = 1.25, } = {}) {
|
|
19
|
+
const w = Math.max(0, Math.floor(Number(width) || 0));
|
|
20
|
+
const h = Math.max(0, Math.floor(Number(height) || 0));
|
|
21
|
+
const c = Math.max(1, Number(channels) || 4);
|
|
22
|
+
const n = Math.max(1, Number(copies) || 1);
|
|
23
|
+
const factor = Math.max(1, Number(safetyFactor) || 1);
|
|
24
|
+
return Math.ceil(w * h * c * n * factor);
|
|
31
25
|
}
|
|
32
|
-
|
|
33
26
|
export function estimateImageOperationBytes(operation, width, height) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
27
|
+
switch (operation) {
|
|
28
|
+
case 'metadata':
|
|
29
|
+
return 1 * MIB;
|
|
30
|
+
case 'preview':
|
|
31
|
+
case 'annotation':
|
|
32
|
+
return estimateDecodedBytes(width, height, { copies: 1, safetyFactor: 1.5 });
|
|
33
|
+
case 'tile':
|
|
34
|
+
case 'crop':
|
|
35
|
+
return estimateDecodedBytes(width, height, { copies: 1, safetyFactor: 1.35 });
|
|
36
|
+
case 'raw':
|
|
37
|
+
return estimateDecodedBytes(width, height, { copies: 2, safetyFactor: 1.5 });
|
|
38
|
+
case 'pixel-diff':
|
|
39
|
+
return estimateDecodedBytes(width, height, { copies: 3, safetyFactor: 1.5 });
|
|
40
|
+
default:
|
|
41
|
+
return estimateDecodedBytes(width, height, { copies: 1, safetyFactor: 1.5 });
|
|
42
|
+
}
|
|
50
43
|
}
|
|
51
|
-
|
|
52
44
|
export function scaledDimensions(width, height, maxPixels) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
45
|
+
const w = Math.max(0, Math.floor(Number(width) || 0));
|
|
46
|
+
const h = Math.max(0, Math.floor(Number(height) || 0));
|
|
47
|
+
const limit = Math.max(1, Math.floor(Number(maxPixels) || 1));
|
|
48
|
+
if (w <= 0 || h <= 0)
|
|
49
|
+
return { width: w, height: h, scale: 1 };
|
|
50
|
+
const pixels = w * h;
|
|
51
|
+
if (pixels <= limit)
|
|
52
|
+
return { width: w, height: h, scale: 1 };
|
|
53
|
+
const scale = Math.sqrt(limit / pixels);
|
|
54
|
+
return {
|
|
55
|
+
width: Math.max(1, Math.floor(w * scale)),
|
|
56
|
+
height: Math.max(1, Math.floor(h * scale)),
|
|
57
|
+
scale,
|
|
58
|
+
};
|
|
65
59
|
}
|
|
66
|
-
|
|
67
60
|
export function scaleBox(box, fromWidth, fromHeight, toWidth, toHeight) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
const sx = fromWidth > 0 ? toWidth / fromWidth : 1;
|
|
62
|
+
const sy = fromHeight > 0 ? toHeight / fromHeight : 1;
|
|
63
|
+
return {
|
|
64
|
+
x1: Math.max(0, Math.round(box.x1 * sx)),
|
|
65
|
+
y1: Math.max(0, Math.round(box.y1 * sy)),
|
|
66
|
+
x2: Math.min(toWidth, Math.max(1, Math.round(box.x2 * sx))),
|
|
67
|
+
y2: Math.min(toHeight, Math.max(1, Math.round(box.y2 * sy))),
|
|
68
|
+
};
|
|
76
69
|
}
|
|
77
|
-
|
|
78
70
|
/**
|
|
79
71
|
* Cover a long/large image with bounded tiles in reading order. Normal narrow
|
|
80
72
|
* screenshots keep one full-width strip. Ultra-wide inputs split horizontally
|
|
81
73
|
* as well so width * height never exceeds maxTilePixels.
|
|
82
74
|
*/
|
|
83
|
-
export function boundedOcrTiles(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const bottom = Math.min(h, top + tileHeight)
|
|
102
|
-
for (let left = 0; left < w; left += tileWidth) {
|
|
103
|
-
const right = Math.min(w, left + tileWidth)
|
|
104
|
-
tiles.push({ left, right, top, bottom })
|
|
75
|
+
export function boundedOcrTiles(width, height, { chunkHeight = 1200, overlap = 120, maxTilePixels = 4_000_000, } = {}) {
|
|
76
|
+
const w = Math.max(1, Math.floor(Number(width) || 1));
|
|
77
|
+
const h = Math.max(1, Math.floor(Number(height) || 1));
|
|
78
|
+
const requestedHeight = Math.max(1, Math.min(h, Math.floor(Number(chunkHeight) || 1200)));
|
|
79
|
+
const pixelLimit = Math.max(1, Math.floor(Number(maxTilePixels) || 4_000_000));
|
|
80
|
+
const tileWidth = Math.min(w, Math.max(1, Math.floor(pixelLimit / requestedHeight)));
|
|
81
|
+
const tileHeight = Math.min(requestedHeight, Math.max(1, Math.floor(pixelLimit / tileWidth)));
|
|
82
|
+
const verticalOverlap = Math.min(Math.max(0, Math.floor(Number(overlap) || 0)), Math.max(0, Math.floor(tileHeight / 2)));
|
|
83
|
+
const verticalStep = Math.max(1, tileHeight - verticalOverlap);
|
|
84
|
+
const tiles = [];
|
|
85
|
+
for (let top = 0; top < h; top += verticalStep) {
|
|
86
|
+
const bottom = Math.min(h, top + tileHeight);
|
|
87
|
+
for (let left = 0; left < w; left += tileWidth) {
|
|
88
|
+
const right = Math.min(w, left + tileWidth);
|
|
89
|
+
tiles.push({ left, right, top, bottom });
|
|
90
|
+
}
|
|
91
|
+
if (bottom >= h)
|
|
92
|
+
break;
|
|
105
93
|
}
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
return tiles
|
|
94
|
+
return tiles;
|
|
109
95
|
}
|
|
110
|
-
|
|
111
96
|
export class ImageResourceGovernor {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.queue = []
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
_normalize(bytes, exclusive) {
|
|
127
|
-
const requested = Math.max(1, Math.ceil(Number(bytes) || 1))
|
|
128
|
-
const wantsExclusive = exclusive === true || requested >= this.maxBytes
|
|
129
|
-
return {
|
|
130
|
-
requested,
|
|
131
|
-
charged: Math.min(requested, this.maxBytes),
|
|
132
|
-
exclusive: wantsExclusive,
|
|
97
|
+
maxBytes;
|
|
98
|
+
maxConcurrent;
|
|
99
|
+
maxBypasses;
|
|
100
|
+
activeBytes = 0;
|
|
101
|
+
activeCount = 0;
|
|
102
|
+
activeExclusive = false;
|
|
103
|
+
queue = [];
|
|
104
|
+
constructor({ maxBytes = 256 * MIB, maxConcurrent = 2, maxBypasses = DEFAULT_IMAGE_RESOURCE_MAX_BYPASSES, } = {}) {
|
|
105
|
+
this.maxBytes = Math.max(1, Math.floor(Number(maxBytes) || 256 * MIB));
|
|
106
|
+
this.maxConcurrent = Math.max(1, Math.floor(Number(maxConcurrent) || 2));
|
|
107
|
+
this.maxBypasses = Math.max(0, Math.floor(Number(maxBypasses) || 0));
|
|
133
108
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
_grant(item) {
|
|
146
|
-
if (item.settled) return
|
|
147
|
-
item.settled = true
|
|
148
|
-
if (item.signal && item.abortHandler) {
|
|
149
|
-
item.signal.removeEventListener('abort', item.abortHandler)
|
|
109
|
+
_normalize(bytes, exclusive) {
|
|
110
|
+
const requested = Math.max(1, Math.ceil(Number(bytes) || 1));
|
|
111
|
+
const wantsExclusive = exclusive === true || requested >= this.maxBytes;
|
|
112
|
+
return {
|
|
113
|
+
requested,
|
|
114
|
+
charged: Math.min(requested, this.maxBytes),
|
|
115
|
+
exclusive: wantsExclusive,
|
|
116
|
+
};
|
|
150
117
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.activeCount = Math.max(0, this.activeCount - 1)
|
|
159
|
-
this.activeBytes = Math.max(0, this.activeBytes - item.request.charged)
|
|
160
|
-
if (item.request.exclusive) this.activeExclusive = false
|
|
161
|
-
this._drain()
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Find work that fits the current byte/concurrency window without letting a
|
|
167
|
-
* large request permanently pin every smaller request behind it. Each blocked
|
|
168
|
-
* non-exclusive request may be bypassed only a small, fixed number of times;
|
|
169
|
-
* after that it becomes a fairness barrier until capacity is released.
|
|
170
|
-
* Exclusive work is always a barrier immediately.
|
|
171
|
-
*/
|
|
172
|
-
_nextRunnable() {
|
|
173
|
-
const bypassed = []
|
|
174
|
-
for (let index = 0; index < this.queue.length; index++) {
|
|
175
|
-
const item = this.queue[index]
|
|
176
|
-
if (!item || item.settled) continue
|
|
177
|
-
if (this._canRun(item.request)) return { index, item, bypassed }
|
|
178
|
-
if (item.request.exclusive || item.bypassCount >= this.maxBypasses) return undefined
|
|
179
|
-
bypassed.push(item)
|
|
118
|
+
_canRun(request) {
|
|
119
|
+
if (this.activeExclusive)
|
|
120
|
+
return false;
|
|
121
|
+
if (request.exclusive)
|
|
122
|
+
return this.activeCount === 0;
|
|
123
|
+
return (this.activeCount < this.maxConcurrent &&
|
|
124
|
+
this.activeBytes + request.charged <= this.maxBytes);
|
|
180
125
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
126
|
+
_grant(item) {
|
|
127
|
+
if (item.settled)
|
|
128
|
+
return;
|
|
129
|
+
item.settled = true;
|
|
130
|
+
if (item.signal && item.abortHandler) {
|
|
131
|
+
item.signal.removeEventListener('abort', item.abortHandler);
|
|
132
|
+
}
|
|
133
|
+
this.activeCount += 1;
|
|
134
|
+
this.activeBytes += item.request.charged;
|
|
135
|
+
if (item.request.exclusive)
|
|
136
|
+
this.activeExclusive = true;
|
|
137
|
+
let released = false;
|
|
138
|
+
item.resolve(() => {
|
|
139
|
+
if (released)
|
|
140
|
+
return;
|
|
141
|
+
released = true;
|
|
142
|
+
this.activeCount = Math.max(0, this.activeCount - 1);
|
|
143
|
+
this.activeBytes = Math.max(0, this.activeBytes - item.request.charged);
|
|
144
|
+
if (item.request.exclusive)
|
|
145
|
+
this.activeExclusive = false;
|
|
146
|
+
this._drain();
|
|
147
|
+
});
|
|
194
148
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
signal: combinedSignal,
|
|
209
|
-
resolve,
|
|
210
|
-
reject,
|
|
211
|
-
settled: false,
|
|
212
|
-
abortHandler: undefined,
|
|
213
|
-
bypassCount: 0,
|
|
214
|
-
}
|
|
215
|
-
item.abortHandler = () => {
|
|
216
|
-
if (item.settled) return
|
|
217
|
-
item.settled = true
|
|
218
|
-
const index = this.queue.indexOf(item)
|
|
219
|
-
if (index >= 0) this.queue.splice(index, 1)
|
|
220
|
-
reject(abortError())
|
|
221
|
-
this._drain()
|
|
222
|
-
}
|
|
223
|
-
if (combinedSignal) combinedSignal.addEventListener('abort', item.abortHandler, { once: true })
|
|
224
|
-
if (this.queue.length === 0 && this._canRun(request)) {
|
|
225
|
-
this._grant(item)
|
|
226
|
-
} else {
|
|
227
|
-
this.queue.push(item)
|
|
228
|
-
this._drain()
|
|
229
|
-
}
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async withBudget(bytes, options, fn) {
|
|
234
|
-
const release = await this.acquire(bytes, options)
|
|
235
|
-
try {
|
|
236
|
-
return await fn()
|
|
237
|
-
} finally {
|
|
238
|
-
release()
|
|
149
|
+
_nextRunnable() {
|
|
150
|
+
const bypassed = [];
|
|
151
|
+
for (let index = 0; index < this.queue.length; index++) {
|
|
152
|
+
const item = this.queue[index];
|
|
153
|
+
if (!item || item.settled)
|
|
154
|
+
continue;
|
|
155
|
+
if (this._canRun(item.request))
|
|
156
|
+
return { index, item, bypassed };
|
|
157
|
+
if (item.request.exclusive || item.bypassCount >= this.maxBypasses)
|
|
158
|
+
return undefined;
|
|
159
|
+
bypassed.push(item);
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
239
162
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
163
|
+
_drain() {
|
|
164
|
+
while (this.queue.length > 0) {
|
|
165
|
+
while (this.queue[0]?.settled)
|
|
166
|
+
this.queue.shift();
|
|
167
|
+
if (this.queue.length === 0)
|
|
168
|
+
return;
|
|
169
|
+
const selected = this._nextRunnable();
|
|
170
|
+
if (!selected)
|
|
171
|
+
return;
|
|
172
|
+
this.queue.splice(selected.index, 1);
|
|
173
|
+
for (const item of selected.bypassed)
|
|
174
|
+
item.bypassCount += 1;
|
|
175
|
+
this._grant(selected.item);
|
|
176
|
+
if (selected.item.request.exclusive)
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
acquire(bytes, { signal, exclusive = false } = {}) {
|
|
181
|
+
const combinedSignal = effectiveSignal(signal);
|
|
182
|
+
if (combinedSignal?.aborted)
|
|
183
|
+
return Promise.reject(abortError());
|
|
184
|
+
const request = this._normalize(bytes, exclusive);
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
const item = {
|
|
187
|
+
request,
|
|
188
|
+
signal: combinedSignal,
|
|
189
|
+
resolve,
|
|
190
|
+
reject,
|
|
191
|
+
settled: false,
|
|
192
|
+
abortHandler: undefined,
|
|
193
|
+
bypassCount: 0,
|
|
194
|
+
};
|
|
195
|
+
item.abortHandler = () => {
|
|
196
|
+
if (item.settled)
|
|
197
|
+
return;
|
|
198
|
+
item.settled = true;
|
|
199
|
+
const index = this.queue.indexOf(item);
|
|
200
|
+
if (index >= 0)
|
|
201
|
+
this.queue.splice(index, 1);
|
|
202
|
+
reject(abortError());
|
|
203
|
+
this._drain();
|
|
204
|
+
};
|
|
205
|
+
if (combinedSignal)
|
|
206
|
+
combinedSignal.addEventListener('abort', item.abortHandler, { once: true });
|
|
207
|
+
if (this.queue.length === 0 && this._canRun(request)) {
|
|
208
|
+
this._grant(item);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.queue.push(item);
|
|
212
|
+
this._drain();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async withBudget(bytes, options, fn) {
|
|
217
|
+
const release = await this.acquire(bytes, options);
|
|
218
|
+
try {
|
|
219
|
+
return await fn();
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
release();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
stats() {
|
|
226
|
+
return {
|
|
227
|
+
maxBytes: this.maxBytes,
|
|
228
|
+
maxConcurrent: this.maxConcurrent,
|
|
229
|
+
activeBytes: this.activeBytes,
|
|
230
|
+
activeCount: this.activeCount,
|
|
231
|
+
queued: this.queue.filter((item) => !item.settled).length,
|
|
232
|
+
exclusive: this.activeExclusive,
|
|
233
|
+
};
|
|
250
234
|
}
|
|
251
|
-
}
|
|
252
235
|
}
|
|
253
|
-
|
|
254
|
-
export const defaultImageResourceGovernor = new ImageResourceGovernor()
|
|
236
|
+
export const defaultImageResourceGovernor = new ImageResourceGovernor();
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
} from './proxy-routing.js'
|
|
16
16
|
|
|
17
17
|
const legacyProxyScope = new AsyncLocalStorage()
|
|
18
|
+
// The one process-wide fetch this boundary currently owns. Consumers that must
|
|
19
|
+
// follow the same egress path ask for it here instead of reading `globalThis.fetch`
|
|
20
|
+
// at call time (which the lifecycle invariant forbids and which would re-admit
|
|
21
|
+
// whatever wrapper happens to be installed later).
|
|
22
|
+
let installedScopedFetch
|
|
18
23
|
|
|
19
24
|
export const LEGACY_GLOBAL_PROXY_REMOVAL_CONDITION =
|
|
20
25
|
'Remove the remaining wrapper when explicit Vision Router proxy overrides no longer need Host-owned adapter interception and the legacy direct whole-turn fallback is retired by product policy.'
|
|
@@ -86,6 +91,36 @@ export function legacyGlobalProxyScopeAllows(config = {}, state = currentLegacyG
|
|
|
86
91
|
return configuredHostOwnedPair(config, state.provider, state.model)
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
// Probe paths (live model discovery, the settings "test connection" route) do not
|
|
95
|
+
// stream a provider turn, so they have no adapter call to open a scope for them.
|
|
96
|
+
// Without a scope, `scopedFetch` falls through to the original fetch and a
|
|
97
|
+
// configured `proxy` silently does not apply — while real vision turns to the same
|
|
98
|
+
// provider are proxied. Expose the exact pair lookup and a plain-promise scope
|
|
99
|
+
// runner so those probes follow the same egress path as the traffic they describe.
|
|
100
|
+
export function currentLegacyGlobalProxyFetch() {
|
|
101
|
+
return installedScopedFetch
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function legacyGlobalProxyPairFor(config = {}, provider) {
|
|
105
|
+
if (!visionProxyOverrideConfigured(config) || typeof provider !== 'string' || provider === '') return undefined
|
|
106
|
+
return configuredPairs(config).find(
|
|
107
|
+
(pair) => pair.provider === provider && !routerOwnedProvider(pair.provider, config),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function runWithLegacyGlobalProxyScope(provider, model, work) {
|
|
112
|
+
if (typeof work !== 'function') throw new TypeError('legacy proxy scope requires work')
|
|
113
|
+
if (!validPair(provider, model)) return work()
|
|
114
|
+
const state = { provider, model, active: true }
|
|
115
|
+
return legacyProxyScope.run(state, async () => {
|
|
116
|
+
try {
|
|
117
|
+
return await work()
|
|
118
|
+
} finally {
|
|
119
|
+
state.active = false
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
89
124
|
// AsyncIterable work is lazy: the adapter normally opens its network request on
|
|
90
125
|
// iterator.next(), not when ctx.llm.stream() returns. Keep the scope active for
|
|
91
126
|
// every iterator operation and retire it as soon as the stream ends/fails.
|
|
@@ -229,19 +264,27 @@ export function installLegacyGlobalProxyBoundary(ctx, config = {}, options = {})
|
|
|
229
264
|
}
|
|
230
265
|
|
|
231
266
|
const restoreFetch = installFetchWrapper(scopedFetch)
|
|
267
|
+
installedScopedFetch = scopedFetch
|
|
232
268
|
let disposed = false
|
|
269
|
+
let disposePromise
|
|
233
270
|
const dispose = () => {
|
|
234
|
-
if (
|
|
271
|
+
if (disposePromise) return disposePromise
|
|
272
|
+
if (disposed) return Promise.resolve()
|
|
235
273
|
disposed = true
|
|
236
274
|
active = false
|
|
237
|
-
|
|
275
|
+
if (installedScopedFetch === scopedFetch) installedScopedFetch = undefined
|
|
276
|
+
// Process-wide fetch ownership is released synchronously; the returned
|
|
277
|
+
// Promise owns only DVR's private dispatcher retirement and is awaitable by
|
|
278
|
+
// the Cordis fiber during HMR/unload.
|
|
238
279
|
restoreFetch()
|
|
280
|
+
disposePromise = dispatcherPool.dispose()
|
|
281
|
+
return disposePromise
|
|
239
282
|
}
|
|
240
283
|
if (typeof ctx?.effect === 'function') {
|
|
241
284
|
try {
|
|
242
285
|
ctx.effect(() => dispose, 'vision-router: scoped legacy proxy fetch')
|
|
243
286
|
} catch (error) {
|
|
244
|
-
dispose()
|
|
287
|
+
void dispose().catch(() => {})
|
|
245
288
|
throw error
|
|
246
289
|
}
|
|
247
290
|
}
|
|
@@ -503,12 +503,20 @@ export const LIVE_MODEL_CLIENT_PRELUDE = String.raw`(function(){
|
|
|
503
503
|
}
|
|
504
504
|
});
|
|
505
505
|
if (contexts) contexts.set(ctx, wrapped);
|
|
506
|
+
var disposeLifecycle = function() {
|
|
507
|
+
live.dispose();
|
|
508
|
+
if (contexts && contexts.get(ctx) === wrapped) contexts.delete(ctx);
|
|
509
|
+
};
|
|
510
|
+
var lifecycleOwned = false;
|
|
506
511
|
try {
|
|
507
|
-
if (typeof ctx.effect === 'function')
|
|
512
|
+
if (typeof ctx.effect === 'function') {
|
|
513
|
+
ctx.effect(function(){ return disposeLifecycle; }, 'vision-router: live model catalog client');
|
|
514
|
+
lifecycleOwned = true;
|
|
515
|
+
}
|
|
508
516
|
} catch (_) {}
|
|
509
|
-
|
|
510
|
-
//
|
|
511
|
-
void live.refresh(true);
|
|
517
|
+
if (!lifecycleOwned) disposeLifecycle();
|
|
518
|
+
// Warm the Host cache only after Cordis owns this client generation.
|
|
519
|
+
if (lifecycleOwned) void live.refresh(true);
|
|
512
520
|
return wrapped;
|
|
513
521
|
}
|
|
514
522
|
|