@mengruo/dsh-vision-toolkit 0.1.3 → 0.1.5
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.i18n.yaml +2 -2
- package/README.md +15 -70
- package/README.zh.md +15 -69
- package/docs/requirements-traceability/README.i18n.yaml +2 -2
- package/docs/requirements-traceability/README.md +1 -1
- package/docs/requirements-traceability/README.zh.md +1 -1
- package/lib/artifact-access.js +20 -2
- package/lib/artifact-access.js.map +1 -1
- package/lib/client.js +128 -12
- package/lib/client.js.map +1 -1
- package/lib/config.js +69 -0
- package/lib/config.js.map +1 -1
- package/lib/errors.js +25 -2
- package/lib/errors.js.map +1 -1
- package/lib/evidence-cache.js +2 -1
- package/lib/evidence-cache.js.map +1 -1
- package/lib/exposure.js +14 -1
- package/lib/exposure.js.map +1 -1
- package/lib/image-input-variants.js +22 -12
- package/lib/image-input-variants.js.map +1 -1
- package/lib/index.js +53 -6
- package/lib/index.js.map +1 -1
- package/lib/object-storage.js +141 -0
- package/lib/object-storage.js.map +1 -0
- package/lib/paste-images.js +67 -19
- package/lib/paste-images.js.map +1 -1
- package/lib/paths.js +214 -28
- package/lib/paths.js.map +1 -1
- package/lib/runtime-manager.js +76 -10
- package/lib/runtime-manager.js.map +1 -1
- package/lib/runtime.js +219 -38
- package/lib/runtime.js.map +1 -1
- package/lib/storage-history.js +154 -0
- package/lib/storage-history.js.map +1 -0
- package/lib/types/artifact-access.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +45 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paste-images.d.ts +2 -0
- package/lib/types/client/paste-images.d.ts.map +1 -1
- package/lib/types/config.d.ts +55 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/errors.d.ts +18 -2
- package/lib/types/errors.d.ts.map +1 -1
- package/lib/types/evidence-cache.d.ts +1 -1
- package/lib/types/evidence-cache.d.ts.map +1 -1
- package/lib/types/exposure.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +5 -3
- package/lib/types/image-input-variants.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/object-storage.d.ts +54 -0
- package/lib/types/object-storage.d.ts.map +1 -0
- package/lib/types/paste-images.d.ts +12 -4
- package/lib/types/paste-images.d.ts.map +1 -1
- package/lib/types/paths.d.ts +31 -5
- package/lib/types/paths.d.ts.map +1 -1
- package/lib/types/runtime-manager.d.ts +28 -4
- package/lib/types/runtime-manager.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +31 -1
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/storage-history.d.ts +63 -0
- package/lib/types/storage-history.d.ts.map +1 -0
- package/lib/types/upstream.d.ts +1 -0
- package/lib/types/upstream.d.ts.map +1 -1
- package/lib/types/web.d.ts +7 -0
- package/lib/types/web.d.ts.map +1 -1
- package/lib/upstream.js +34 -8
- package/lib/upstream.js.map +1 -1
- package/lib/web.js +44 -9
- package/lib/web.js.map +1 -1
- package/package.json +3 -1
- package/src/artifact-access.ts +22 -2
- package/src/client/index.tsx +169 -9
- package/src/client/paste-images.tsx +14 -4
- package/src/config.ts +128 -0
- package/src/errors.ts +25 -2
- package/src/evidence-cache.ts +2 -1
- package/src/exposure.ts +16 -2
- package/src/image-input-variants.ts +21 -6
- package/src/index.ts +65 -6
- package/src/object-storage.ts +174 -0
- package/src/paste-images.ts +81 -19
- package/src/paths.ts +249 -28
- package/src/runtime-manager.ts +93 -10
- package/src/runtime.ts +215 -36
- package/src/storage-history.ts +172 -0
- package/src/upstream.ts +36 -7
- package/src/web.ts +54 -9
- package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +11 -11
- package/vendor/agent-vision-toolkit/__pycache__/detect.cpython-314.pyc +0 -0
- package/vendor/agent-vision-toolkit/__pycache__/ground.cpython-314.pyc +0 -0
- package/vendor/agent-vision-toolkit/__pycache__/vision_client.cpython-314.pyc +0 -0
- package/vendor/agent-vision-toolkit/bin/__pycache__/glancecpython-314.pyc +0 -0
- package/vendor/agent-vision-toolkit/bin/glance +8 -1
- package/vendor/agent-vision-toolkit/detect.py +13 -7
- package/vendor/agent-vision-toolkit/ground.py +43 -18
- package/vendor/agent-vision-toolkit/tests/test_vision_client.py +88 -0
- package/vendor/agent-vision-toolkit/vision_client.py +84 -6
- package/assets/community-group-qr.png +0 -0
- package/assets/logo_aihubmix.png +0 -0
- package/assets/logo_eapi_dark.png +0 -0
- package/assets/wechat-reward.png +0 -0
package/lib/runtime.js
CHANGED
|
@@ -15,6 +15,7 @@ import { isBuiltInFreeVisionProvider } from "./config.js";
|
|
|
15
15
|
import { BUILT_IN_FREE_VISION_KEY } from "./defaults.js";
|
|
16
16
|
import { evidenceRuntimeFingerprint } from "./evidence-cache.js";
|
|
17
17
|
import { VisionToolkitError } from "./errors.js";
|
|
18
|
+
import { ObjectStorageClient, splitObjectStorageCredential } from "./object-storage.js";
|
|
18
19
|
import { assertDistinctOutput, commitStagedDirectory, commitStagedOutput, createPathPolicy, createStagedDirectory, createStagedOutput, isWithin, resolveHtmlFile, resolveInputFile, resolveOutputDirectory, resolveOutputFile, seedStagedDirectory, } from "./paths.js";
|
|
19
20
|
import { parseCropOutput, parseDominantColorsOutput, parseExtractForegroundOutput, parseHtmlScreenshotOutput, parseLocationOutput, parsePixelDiffOutput, parseTraceOutput, UpstreamAdapter, } from "./upstream.js";
|
|
20
21
|
import { PLUGIN_VERSION } from "./version.js";
|
|
@@ -174,8 +175,25 @@ const FORMAT_BY_EXTENSION = new Map([
|
|
|
174
175
|
['.webp', 'webp'],
|
|
175
176
|
]);
|
|
176
177
|
const HEX_COLOR_PATTERN = /^#[0-9A-F]{6}$/;
|
|
177
|
-
/**
|
|
178
|
-
|
|
178
|
+
/** MIME type for one analyzed image format, used when uploading to object storage. */
|
|
179
|
+
function imageMimeType(format) {
|
|
180
|
+
switch (format) {
|
|
181
|
+
case 'png': return 'image/png';
|
|
182
|
+
case 'jpeg': return 'image/jpeg';
|
|
183
|
+
case 'gif': return 'image/gif';
|
|
184
|
+
case 'webp': return 'image/webp';
|
|
185
|
+
default: return 'application/octet-stream';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Error codes a provider retries against the SAME provider within its
|
|
190
|
+
* `attempts` budget. Only transient failures are worth re-requesting: a
|
|
191
|
+
* timeout may clear on the next attempt and a 5xx / network drop is usually
|
|
192
|
+
* ephemeral. Deterministic failures (auth, quota, rate_limit, invalid_request,
|
|
193
|
+
* region, tos) must fail over to the next provider immediately instead of
|
|
194
|
+
* re-requesting a backend that cannot succeed with the same input.
|
|
195
|
+
*/
|
|
196
|
+
const RETRYABLE_CODES = new Set(['timeout', 'server', 'network']);
|
|
179
197
|
/** Resolve as soon as `signal` aborts (or immediately when already aborted). */
|
|
180
198
|
function untilAbort(signal) {
|
|
181
199
|
if (signal.aborted)
|
|
@@ -331,13 +349,15 @@ export function parseRegion(region) {
|
|
|
331
349
|
export class VisionToolkitRuntime {
|
|
332
350
|
ctx;
|
|
333
351
|
config;
|
|
352
|
+
readableStorageDirs;
|
|
334
353
|
semaphores = new Map();
|
|
335
354
|
glanceCache = new WeakMap();
|
|
336
355
|
providerGates = new Map();
|
|
337
356
|
adapter;
|
|
338
|
-
constructor(ctx, config, adapter) {
|
|
357
|
+
constructor(ctx, config, adapter, readableStorageDirs = []) {
|
|
339
358
|
this.ctx = ctx;
|
|
340
359
|
this.config = config;
|
|
360
|
+
this.readableStorageDirs = readableStorageDirs;
|
|
341
361
|
this.adapter = adapter ?? new UpstreamAdapter(ctx, config);
|
|
342
362
|
}
|
|
343
363
|
/** Pinned and prepared upstream identity. */
|
|
@@ -348,6 +368,10 @@ export class VisionToolkitRuntime {
|
|
|
348
368
|
get sessionMaxConcurrency() {
|
|
349
369
|
return this.config.sessionMaxConcurrency;
|
|
350
370
|
}
|
|
371
|
+
/** Shared storage root belonging to this immutable runtime generation. */
|
|
372
|
+
get storageDirectory() {
|
|
373
|
+
return this.config.storageDir;
|
|
374
|
+
}
|
|
351
375
|
/** Stable identity for persisted image descriptions produced by this runtime. */
|
|
352
376
|
get evidenceFingerprint() {
|
|
353
377
|
return evidenceRuntimeFingerprint(this.config, undefined, process.env.VISION_SSL_VERIFY?.trim());
|
|
@@ -478,6 +502,7 @@ export class VisionToolkitRuntime {
|
|
|
478
502
|
VISION_API_PROTOCOL: provider.protocol === 'anthropic' ? 'anthropic' : 'chat_completions',
|
|
479
503
|
VISION_ANTHROPIC_THINKING: provider.anthropicThinking,
|
|
480
504
|
...(sslVerify === undefined ? {} : { VISION_SSL_VERIFY: sslVerify }),
|
|
505
|
+
...(provider.stream ? { VISION_STREAM: '1' } : {}),
|
|
481
506
|
VISION_USER_AGENT: provider.userAgent,
|
|
482
507
|
LANG: this.config.language,
|
|
483
508
|
};
|
|
@@ -493,6 +518,8 @@ export class VisionToolkitRuntime {
|
|
|
493
518
|
protocol: provider.protocol,
|
|
494
519
|
anthropicThinking: provider.anthropicThinking,
|
|
495
520
|
userAgent: provider.userAgent,
|
|
521
|
+
stream: provider.stream,
|
|
522
|
+
uploadViaUrl: provider.uploadViaUrl,
|
|
496
523
|
})
|
|
497
524
|
? { value: BUILT_IN_FREE_VISION_KEY, source: 'built-in' }
|
|
498
525
|
: await this.ctx.credentials.resolve(provider.credential);
|
|
@@ -528,12 +555,12 @@ export class VisionToolkitRuntime {
|
|
|
528
555
|
return entry.env;
|
|
529
556
|
}
|
|
530
557
|
pathPolicy(workspace) {
|
|
531
|
-
return createPathPolicy(workspace, this.config.allowedDirs);
|
|
558
|
+
return createPathPolicy(workspace, this.config.allowedDirs, this.config.storageDir, this.readableStorageDirs);
|
|
532
559
|
}
|
|
533
560
|
async compressedImageRoot(policy) {
|
|
534
|
-
const root = join(policy.
|
|
535
|
-
let current = policy.
|
|
536
|
-
for (const segment of ['
|
|
561
|
+
const root = join(policy.storageRoot, 'tmp', 'compressed-images');
|
|
562
|
+
let current = policy.storageRoot;
|
|
563
|
+
for (const segment of ['tmp', 'compressed-images']) {
|
|
537
564
|
current = join(current, segment);
|
|
538
565
|
try {
|
|
539
566
|
await mkdir(current, { mode: 0o700 });
|
|
@@ -546,13 +573,13 @@ export class VisionToolkitRuntime {
|
|
|
546
573
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
547
574
|
throw new VisionToolkitError('path', `compressed-image cache path is not a real directory: ${current}`);
|
|
548
575
|
}
|
|
549
|
-
if (!isWithin(policy.
|
|
550
|
-
throw new VisionToolkitError('path', `compressed-image cache path escaped
|
|
576
|
+
if (!isWithin(policy.storageRoot, current)) {
|
|
577
|
+
throw new VisionToolkitError('path', `compressed-image cache path escaped plugin storage: ${current}`);
|
|
551
578
|
}
|
|
552
579
|
}
|
|
553
580
|
const canonical = await realpath(root);
|
|
554
|
-
if (!isWithin(policy.
|
|
555
|
-
throw new VisionToolkitError('path', 'compressed-image cache resolved outside
|
|
581
|
+
if (!isWithin(policy.storageRoot, canonical)) {
|
|
582
|
+
throw new VisionToolkitError('path', 'compressed-image cache resolved outside plugin storage');
|
|
556
583
|
}
|
|
557
584
|
return canonical;
|
|
558
585
|
}
|
|
@@ -746,6 +773,83 @@ export class VisionToolkitRuntime {
|
|
|
746
773
|
operation.metrics.imageBytes += image.bytes;
|
|
747
774
|
operation.metrics.imagePixels += image.width * image.height;
|
|
748
775
|
}
|
|
776
|
+
/** Resolve the configured object storage into a usable client, or undefined. */
|
|
777
|
+
async resolveObjectStorageClient() {
|
|
778
|
+
const objectStorage = this.config.objectStorage;
|
|
779
|
+
if (objectStorage.credential === undefined || objectStorage.endpoint.length === 0 || objectStorage.bucket.length === 0) {
|
|
780
|
+
return undefined;
|
|
781
|
+
}
|
|
782
|
+
let resolved;
|
|
783
|
+
try {
|
|
784
|
+
resolved = await this.ctx.credentials.resolve(objectStorage.credential);
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
resolved = undefined;
|
|
788
|
+
}
|
|
789
|
+
if (resolved === undefined)
|
|
790
|
+
return undefined;
|
|
791
|
+
let accessKeyId;
|
|
792
|
+
let secretAccessKey;
|
|
793
|
+
try {
|
|
794
|
+
const split = splitObjectStorageCredential(resolved.value);
|
|
795
|
+
accessKeyId = split.accessKeyId;
|
|
796
|
+
secretAccessKey = split.secretAccessKey;
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
throw new VisionToolkitError('config', 'object storage credential is malformed', { cause: error });
|
|
800
|
+
}
|
|
801
|
+
const settings = {
|
|
802
|
+
endpoint: objectStorage.endpoint,
|
|
803
|
+
bucket: objectStorage.bucket,
|
|
804
|
+
accessKeyId,
|
|
805
|
+
secretAccessKey,
|
|
806
|
+
...(objectStorage.publicBase === undefined ? {} : { publicBase: objectStorage.publicBase }),
|
|
807
|
+
};
|
|
808
|
+
return new ObjectStorageClient(settings);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Upload the prepared images to object storage and return their URLs plus a
|
|
812
|
+
* cleanup callback, when the primary provider opts into URL transfer and
|
|
813
|
+
* object storage is configured. Returns undefined otherwise (base64 path).
|
|
814
|
+
*/
|
|
815
|
+
async maybeTransferImages(pool, images, operation) {
|
|
816
|
+
const primary = pool[0];
|
|
817
|
+
if (primary === undefined || primary.provider.uploadViaUrl !== true)
|
|
818
|
+
return undefined;
|
|
819
|
+
const client = await this.resolveObjectStorageClient();
|
|
820
|
+
if (client === undefined) {
|
|
821
|
+
throw new VisionToolkitError('config', 'uploadViaUrl is enabled but object storage is not configured');
|
|
822
|
+
}
|
|
823
|
+
const keys = [];
|
|
824
|
+
const urls = [];
|
|
825
|
+
try {
|
|
826
|
+
for (const image of images) {
|
|
827
|
+
if (operation.signal.aborted)
|
|
828
|
+
throw new VisionToolkitError('cancelled', 'vision image upload cancelled');
|
|
829
|
+
const uploaded = await client.uploadImage(image.path, imageMimeType(image.format));
|
|
830
|
+
keys.push(uploaded.key);
|
|
831
|
+
urls.push(uploaded.url);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
await Promise.allSettled(keys.map(key => client.deleteObject(key)));
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
urls,
|
|
840
|
+
cleanup: async () => {
|
|
841
|
+
await Promise.allSettled(keys.map(key => client.deleteObject(key)));
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/** Settings "test storage" probe: upload → head → delete a tiny marker object. */
|
|
846
|
+
async testObjectStorage() {
|
|
847
|
+
const client = await this.resolveObjectStorageClient();
|
|
848
|
+
if (client === undefined) {
|
|
849
|
+
throw new VisionToolkitError('config', 'object storage is not configured (endpoint, bucket, and credential are required)');
|
|
850
|
+
}
|
|
851
|
+
return client.test();
|
|
852
|
+
}
|
|
749
853
|
/** Stable gate key for one provider's in-flight request cap. */
|
|
750
854
|
providerGate(provider) {
|
|
751
855
|
const key = `${provider.baseUrl}\u0000${provider.model}\u0000${String(provider.credential)}`;
|
|
@@ -876,6 +980,23 @@ export class VisionToolkitRuntime {
|
|
|
876
980
|
const globalRemaining = operation.deadlineAt - Date.now();
|
|
877
981
|
return Math.max(0, Math.min(t2Remaining, globalRemaining));
|
|
878
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* Advance to the next provider after one provider reached a terminal
|
|
985
|
+
* failure. This is what makes failover work for FAST failures too: the
|
|
986
|
+
* hedge timer only launches the next provider when the current one is SLOW
|
|
987
|
+
* (crosses t1), so a quick auth/5xx/network failure must explicitly launch
|
|
988
|
+
* the successor. Never advances when a higher-priority provider superseded
|
|
989
|
+
* this task, when the whole operation was cancelled, or when the global
|
|
990
|
+
* deadline has too little room left for another request. `launch` is
|
|
991
|
+
* idempotent, so an earlier hedge timer cannot cause a double launch.
|
|
992
|
+
*/
|
|
993
|
+
advanceAfterFailure(task, operation, launchNext) {
|
|
994
|
+
if (task.abort.signal.aborted || operation.signal.aborted)
|
|
995
|
+
return;
|
|
996
|
+
if (operation.deadlineAt - Date.now() < this.config.minAvailableSeconds * 1000)
|
|
997
|
+
return;
|
|
998
|
+
launchNext();
|
|
999
|
+
}
|
|
879
1000
|
/**
|
|
880
1001
|
* Run one provider to a terminal state: retryable errors retry within
|
|
881
1002
|
* `attempts`, a single request crossing t1 hedges the next provider, and the
|
|
@@ -902,6 +1023,7 @@ export class VisionToolkitRuntime {
|
|
|
902
1023
|
if (budget < minAvailableMs) {
|
|
903
1024
|
task.status = 'failed';
|
|
904
1025
|
task.error = new VisionToolkitError('timeout', `${tool}: ${provider.name} has insufficient remaining time`);
|
|
1026
|
+
this.advanceAfterFailure(task, operation, launchNext);
|
|
905
1027
|
return;
|
|
906
1028
|
}
|
|
907
1029
|
const reqDeadline = createDeadline(AbortSignal.any([operation.signal, task.abort.signal]), budget);
|
|
@@ -943,6 +1065,7 @@ export class VisionToolkitRuntime {
|
|
|
943
1065
|
if (reqDeadline.timedOut) {
|
|
944
1066
|
task.status = 'failed';
|
|
945
1067
|
task.error = new VisionToolkitError('timeout', `${tool}: ${provider.name} exhausted its t2 budget`);
|
|
1068
|
+
this.advanceAfterFailure(task, operation, launchNext);
|
|
946
1069
|
return;
|
|
947
1070
|
}
|
|
948
1071
|
if (classified.code === 'rate_limit') {
|
|
@@ -957,6 +1080,7 @@ export class VisionToolkitRuntime {
|
|
|
957
1080
|
}
|
|
958
1081
|
task.status = 'failed';
|
|
959
1082
|
task.error = classified;
|
|
1083
|
+
this.advanceAfterFailure(task, operation, launchNext);
|
|
960
1084
|
return;
|
|
961
1085
|
}
|
|
962
1086
|
finally {
|
|
@@ -1058,6 +1182,8 @@ export class VisionToolkitRuntime {
|
|
|
1058
1182
|
protocol: env.VISION_API_PROTOCOL,
|
|
1059
1183
|
anthropicThinking: env.VISION_ANTHROPIC_THINKING,
|
|
1060
1184
|
sslVerify: env.VISION_SSL_VERIFY ?? null,
|
|
1185
|
+
stream: env.VISION_STREAM === '1',
|
|
1186
|
+
uploadViaUrl: provider.uploadViaUrl,
|
|
1061
1187
|
userAgent: env.VISION_USER_AGENT,
|
|
1062
1188
|
credentialSha256: createHash('sha256').update(env.VISION_API_KEY).digest('hex'),
|
|
1063
1189
|
maxImageBytes: provider.maxImageBytes,
|
|
@@ -1170,25 +1296,34 @@ export class VisionToolkitRuntime {
|
|
|
1170
1296
|
return cached.result;
|
|
1171
1297
|
}
|
|
1172
1298
|
}
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1299
|
+
const transfer = request.region === undefined
|
|
1300
|
+
? await this.maybeTransferImages(pool, images, operation)
|
|
1301
|
+
: undefined;
|
|
1302
|
+
try {
|
|
1303
|
+
const result = await this.runVisionHedge('glance', [
|
|
1304
|
+
...(transfer !== undefined ? transfer.urls : images.map(image => image.path)),
|
|
1305
|
+
...(transfer === undefined && request.region !== undefined ? ['--region', request.region] : []),
|
|
1306
|
+
...(request.ocr === true ? ['--ocr'] : []),
|
|
1307
|
+
...(request.query !== undefined ? ['-q', request.query] : []),
|
|
1308
|
+
], images, operation, pool);
|
|
1309
|
+
const answer = result.stdout.trim();
|
|
1310
|
+
if (answer.length === 0)
|
|
1311
|
+
throw new VisionToolkitError('output', 'glance: vision API returned an empty description');
|
|
1312
|
+
const value = {
|
|
1313
|
+
images,
|
|
1314
|
+
mode: request.ocr === true ? 'ocr' : request.query !== undefined ? 'qa' : 'describe',
|
|
1315
|
+
answer,
|
|
1316
|
+
truncated: false,
|
|
1317
|
+
};
|
|
1318
|
+
if (options.sessionScope !== undefined && cacheKey !== undefined && !operation.signal.aborted) {
|
|
1319
|
+
this.glanceCache.set(options.sessionScope, { key: cacheKey, result: value });
|
|
1320
|
+
}
|
|
1321
|
+
return value;
|
|
1322
|
+
}
|
|
1323
|
+
finally {
|
|
1324
|
+
if (transfer !== undefined)
|
|
1325
|
+
await transfer.cleanup();
|
|
1190
1326
|
}
|
|
1191
|
-
return value;
|
|
1192
1327
|
});
|
|
1193
1328
|
}
|
|
1194
1329
|
validateLocations(elements, width, height) {
|
|
@@ -1217,14 +1352,25 @@ export class VisionToolkitRuntime {
|
|
|
1217
1352
|
}
|
|
1218
1353
|
const image = await this.prepareVisionImage(request.image, pool.map(entry => entry.provider), policy, operation);
|
|
1219
1354
|
this.accountImage(image, operation);
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1355
|
+
const transfer = request.region === undefined
|
|
1356
|
+
? await this.maybeTransferImages(pool, [image], operation)
|
|
1357
|
+
: undefined;
|
|
1358
|
+
try {
|
|
1359
|
+
const result = await this.runVisionHedge(tool, transfer !== undefined
|
|
1360
|
+
? [transfer.urls[0], request.target, '--size', `${image.width}x${image.height}`]
|
|
1361
|
+
: [
|
|
1362
|
+
image.path,
|
|
1363
|
+
request.target,
|
|
1364
|
+
...(request.region !== undefined ? ['--region', request.region] : []),
|
|
1365
|
+
], [image], operation, pool);
|
|
1366
|
+
const elements = parseLocationOutput(result.stdout);
|
|
1367
|
+
this.validateLocations(elements, image.width, image.height);
|
|
1368
|
+
return { image, elements };
|
|
1369
|
+
}
|
|
1370
|
+
finally {
|
|
1371
|
+
if (transfer !== undefined)
|
|
1372
|
+
await transfer.cleanup();
|
|
1373
|
+
}
|
|
1228
1374
|
}
|
|
1229
1375
|
/** ground: locate one named target and return pixel boxes. */
|
|
1230
1376
|
async ground(request, options) {
|
|
@@ -1906,6 +2052,18 @@ export class VisionToolkitRuntime {
|
|
|
1906
2052
|
}
|
|
1907
2053
|
});
|
|
1908
2054
|
}
|
|
2055
|
+
async writableDirectoryCheck(path, label) {
|
|
2056
|
+
const probe = join(path, `.vision-toolkit-health-${randomUUID()}`);
|
|
2057
|
+
try {
|
|
2058
|
+
await writeFile(probe, 'ok\n', { encoding: 'utf8', flag: 'wx' });
|
|
2059
|
+
await rm(probe, { force: true });
|
|
2060
|
+
return { status: 'ok', detail: `${label} is writable: ${path}` };
|
|
2061
|
+
}
|
|
2062
|
+
catch {
|
|
2063
|
+
await rm(probe, { force: true }).catch(() => { });
|
|
2064
|
+
return { status: 'error', detail: `${label} is not writable: ${path}` };
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
1909
2067
|
/** Health: inspect local readiness, and optionally probe one provider's `/models` plus one real multimodal request. */
|
|
1910
2068
|
async health(testConnection, options, testModel = false, provider) {
|
|
1911
2069
|
return this.runOperation('vision_toolkit_health', options, async (operation) => {
|
|
@@ -1929,6 +2087,29 @@ export class VisionToolkitRuntime {
|
|
|
1929
2087
|
throw new VisionToolkitError('cancelled', 'vision_toolkit_health: cancelled');
|
|
1930
2088
|
chrome = { status: 'error', detail: 'Chrome availability probe failed' };
|
|
1931
2089
|
}
|
|
2090
|
+
let resolvedCredential;
|
|
2091
|
+
let credential;
|
|
2092
|
+
try {
|
|
2093
|
+
resolvedCredential = isBuiltInFreeVisionProvider(this.config.provider)
|
|
2094
|
+
? { value: BUILT_IN_FREE_VISION_KEY, source: 'built-in' }
|
|
2095
|
+
: await this.ctx.credentials.resolve(this.config.provider.credential);
|
|
2096
|
+
credential = resolvedCredential === undefined
|
|
2097
|
+
? { status: 'error', detail: `credential ${this.config.provider.credential} is not configured` }
|
|
2098
|
+
: { status: 'ok', detail: `credential ${this.config.provider.credential} is resolvable` };
|
|
2099
|
+
}
|
|
2100
|
+
catch {
|
|
2101
|
+
credential = { status: 'error', detail: `credential ${this.config.provider.credential} could not be resolved` };
|
|
2102
|
+
}
|
|
2103
|
+
let artifactDirectory;
|
|
2104
|
+
try {
|
|
2105
|
+
// allowedDirs are session input roots; they do not affect output readiness.
|
|
2106
|
+
const policy = await createPathPolicy(options.workspace, [], this.config.storageDir);
|
|
2107
|
+
artifactDirectory = await this.writableDirectoryCheck(policy.outputDir, 'Artifact directory');
|
|
2108
|
+
}
|
|
2109
|
+
catch {
|
|
2110
|
+
artifactDirectory = { status: 'error', detail: 'Artifact directory could not be prepared' };
|
|
2111
|
+
}
|
|
2112
|
+
const tempDirectory = await this.writableDirectoryCheck(info.runtimeHome, 'Runtime temp directory');
|
|
1932
2113
|
let service = {
|
|
1933
2114
|
status: 'not_tested',
|
|
1934
2115
|
detail: 'Connection was not tested; use the per-provider API test',
|
|
@@ -2026,7 +2207,7 @@ export class VisionToolkitRuntime {
|
|
|
2026
2207
|
}
|
|
2027
2208
|
}
|
|
2028
2209
|
}
|
|
2029
|
-
const checks = { python, dependencies, chrome, service, model };
|
|
2210
|
+
const checks = { python, dependencies, chrome, credential, artifactDirectory, tempDirectory, service, model };
|
|
2030
2211
|
const healthy = Object.values(checks).every(check => check.status !== 'error');
|
|
2031
2212
|
return {
|
|
2032
2213
|
pluginVersion: PLUGIN_VERSION,
|