@mengruo/dsh-vision-toolkit 0.1.3 → 0.1.4
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 +4 -0
- package/README.zh.md +4 -0
- 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 +22 -7
- package/lib/client.js.map +1 -1
- package/lib/config.js +34 -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/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 +80 -12
- 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 +6 -1
- 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 +22 -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/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 +19 -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.map +1 -1
- package/lib/types/web.d.ts.map +1 -1
- package/lib/upstream.js +31 -8
- package/lib/upstream.js.map +1 -1
- package/lib/web.js +9 -3
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/src/artifact-access.ts +22 -2
- package/src/client/index.tsx +18 -3
- package/src/client/paste-images.tsx +14 -4
- package/src/config.ts +61 -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/paste-images.ts +81 -19
- package/src/paths.ts +249 -28
- package/src/runtime-manager.ts +93 -10
- package/src/runtime.ts +79 -11
- package/src/storage-history.ts +172 -0
- package/src/upstream.ts +32 -7
- package/src/web.ts +9 -2
package/src/runtime.ts
CHANGED
|
@@ -475,6 +475,9 @@ export interface VisionToolkitHealthResult {
|
|
|
475
475
|
python: HealthCheck
|
|
476
476
|
dependencies: HealthCheck
|
|
477
477
|
chrome: HealthCheck
|
|
478
|
+
credential: HealthCheck
|
|
479
|
+
artifactDirectory: HealthCheck
|
|
480
|
+
tempDirectory: HealthCheck
|
|
478
481
|
service: HealthCheck
|
|
479
482
|
model: HealthCheck
|
|
480
483
|
}
|
|
@@ -537,8 +540,15 @@ const FORMAT_BY_EXTENSION = new Map([
|
|
|
537
540
|
])
|
|
538
541
|
const HEX_COLOR_PATTERN = /^#[0-9A-F]{6}$/
|
|
539
542
|
|
|
540
|
-
/**
|
|
541
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Error codes a provider retries against the SAME provider within its
|
|
545
|
+
* `attempts` budget. Only transient failures are worth re-requesting: a
|
|
546
|
+
* timeout may clear on the next attempt and a 5xx / network drop is usually
|
|
547
|
+
* ephemeral. Deterministic failures (auth, quota, rate_limit, invalid_request,
|
|
548
|
+
* region, tos) must fail over to the next provider immediately instead of
|
|
549
|
+
* re-requesting a backend that cannot succeed with the same input.
|
|
550
|
+
*/
|
|
551
|
+
const RETRYABLE_CODES: ReadonlySet<VisionToolkitErrorCode> = new Set(['timeout', 'server', 'network'])
|
|
542
552
|
|
|
543
553
|
/** Resolve as soon as `signal` aborts (or immediately when already aborted). */
|
|
544
554
|
function untilAbort(signal: AbortSignal): Promise<void> {
|
|
@@ -771,6 +781,7 @@ export class VisionToolkitRuntime {
|
|
|
771
781
|
private readonly ctx: Context,
|
|
772
782
|
private readonly config: ResolvedVisionToolkitConfig,
|
|
773
783
|
adapter?: UpstreamAdapter,
|
|
784
|
+
private readonly readableStorageDirs: readonly string[] = [],
|
|
774
785
|
) {
|
|
775
786
|
this.adapter = adapter ?? new UpstreamAdapter(ctx, config)
|
|
776
787
|
}
|
|
@@ -785,6 +796,11 @@ export class VisionToolkitRuntime {
|
|
|
785
796
|
return this.config.sessionMaxConcurrency
|
|
786
797
|
}
|
|
787
798
|
|
|
799
|
+
/** Shared storage root belonging to this immutable runtime generation. */
|
|
800
|
+
get storageDirectory(): string | undefined {
|
|
801
|
+
return this.config.storageDir
|
|
802
|
+
}
|
|
803
|
+
|
|
788
804
|
/** Stable identity for persisted image descriptions produced by this runtime. */
|
|
789
805
|
get evidenceFingerprint(): string {
|
|
790
806
|
return evidenceRuntimeFingerprint(this.config, undefined, process.env.VISION_SSL_VERIFY?.trim())
|
|
@@ -1010,13 +1026,13 @@ export class VisionToolkitRuntime {
|
|
|
1010
1026
|
}
|
|
1011
1027
|
|
|
1012
1028
|
private pathPolicy(workspace: string): Promise<PathPolicy> {
|
|
1013
|
-
return createPathPolicy(workspace, this.config.allowedDirs)
|
|
1029
|
+
return createPathPolicy(workspace, this.config.allowedDirs, this.config.storageDir, this.readableStorageDirs)
|
|
1014
1030
|
}
|
|
1015
1031
|
|
|
1016
1032
|
private async compressedImageRoot(policy: PathPolicy): Promise<string> {
|
|
1017
|
-
const root = join(policy.
|
|
1018
|
-
let current = policy.
|
|
1019
|
-
for (const segment of ['
|
|
1033
|
+
const root = join(policy.storageRoot, 'tmp', 'compressed-images')
|
|
1034
|
+
let current = policy.storageRoot
|
|
1035
|
+
for (const segment of ['tmp', 'compressed-images']) {
|
|
1020
1036
|
current = join(current, segment)
|
|
1021
1037
|
try {
|
|
1022
1038
|
await mkdir(current, { mode: 0o700 })
|
|
@@ -1027,13 +1043,13 @@ export class VisionToolkitRuntime {
|
|
|
1027
1043
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
1028
1044
|
throw new VisionToolkitError('path', `compressed-image cache path is not a real directory: ${current}`)
|
|
1029
1045
|
}
|
|
1030
|
-
if (!isWithin(policy.
|
|
1031
|
-
throw new VisionToolkitError('path', `compressed-image cache path escaped
|
|
1046
|
+
if (!isWithin(policy.storageRoot, current)) {
|
|
1047
|
+
throw new VisionToolkitError('path', `compressed-image cache path escaped plugin storage: ${current}`)
|
|
1032
1048
|
}
|
|
1033
1049
|
}
|
|
1034
1050
|
const canonical = await realpath(root)
|
|
1035
|
-
if (!isWithin(policy.
|
|
1036
|
-
throw new VisionToolkitError('path', 'compressed-image cache resolved outside
|
|
1051
|
+
if (!isWithin(policy.storageRoot, canonical)) {
|
|
1052
|
+
throw new VisionToolkitError('path', 'compressed-image cache resolved outside plugin storage')
|
|
1037
1053
|
}
|
|
1038
1054
|
return canonical
|
|
1039
1055
|
}
|
|
@@ -1383,6 +1399,22 @@ export class VisionToolkitRuntime {
|
|
|
1383
1399
|
return Math.max(0, Math.min(t2Remaining, globalRemaining))
|
|
1384
1400
|
}
|
|
1385
1401
|
|
|
1402
|
+
/**
|
|
1403
|
+
* Advance to the next provider after one provider reached a terminal
|
|
1404
|
+
* failure. This is what makes failover work for FAST failures too: the
|
|
1405
|
+
* hedge timer only launches the next provider when the current one is SLOW
|
|
1406
|
+
* (crosses t1), so a quick auth/5xx/network failure must explicitly launch
|
|
1407
|
+
* the successor. Never advances when a higher-priority provider superseded
|
|
1408
|
+
* this task, when the whole operation was cancelled, or when the global
|
|
1409
|
+
* deadline has too little room left for another request. `launch` is
|
|
1410
|
+
* idempotent, so an earlier hedge timer cannot cause a double launch.
|
|
1411
|
+
*/
|
|
1412
|
+
private advanceAfterFailure(task: ProviderTask, operation: OperationContext, launchNext: () => void): void {
|
|
1413
|
+
if (task.abort.signal.aborted || operation.signal.aborted) return
|
|
1414
|
+
if (operation.deadlineAt - Date.now() < this.config.minAvailableSeconds * 1000) return
|
|
1415
|
+
launchNext()
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1386
1418
|
/**
|
|
1387
1419
|
* Run one provider to a terminal state: retryable errors retry within
|
|
1388
1420
|
* `attempts`, a single request crossing t1 hedges the next provider, and the
|
|
@@ -1415,6 +1447,7 @@ export class VisionToolkitRuntime {
|
|
|
1415
1447
|
if (budget < minAvailableMs) {
|
|
1416
1448
|
task.status = 'failed'
|
|
1417
1449
|
task.error = new VisionToolkitError('timeout', `${tool}: ${provider.name} has insufficient remaining time`)
|
|
1450
|
+
this.advanceAfterFailure(task, operation, launchNext)
|
|
1418
1451
|
return
|
|
1419
1452
|
}
|
|
1420
1453
|
const reqDeadline = createDeadline(AbortSignal.any([operation.signal, task.abort.signal]), budget)
|
|
@@ -1453,6 +1486,7 @@ export class VisionToolkitRuntime {
|
|
|
1453
1486
|
if (reqDeadline.timedOut) {
|
|
1454
1487
|
task.status = 'failed'
|
|
1455
1488
|
task.error = new VisionToolkitError('timeout', `${tool}: ${provider.name} exhausted its t2 budget`)
|
|
1489
|
+
this.advanceAfterFailure(task, operation, launchNext)
|
|
1456
1490
|
return
|
|
1457
1491
|
}
|
|
1458
1492
|
if (classified.code === 'rate_limit') {
|
|
@@ -1467,6 +1501,7 @@ export class VisionToolkitRuntime {
|
|
|
1467
1501
|
}
|
|
1468
1502
|
task.status = 'failed'
|
|
1469
1503
|
task.error = classified
|
|
1504
|
+
this.advanceAfterFailure(task, operation, launchNext)
|
|
1470
1505
|
return
|
|
1471
1506
|
} finally {
|
|
1472
1507
|
reqDeadline.cleanup()
|
|
@@ -2453,6 +2488,18 @@ export class VisionToolkitRuntime {
|
|
|
2453
2488
|
})
|
|
2454
2489
|
}
|
|
2455
2490
|
|
|
2491
|
+
private async writableDirectoryCheck(path: string, label: string): Promise<HealthCheck> {
|
|
2492
|
+
const probe = join(path, `.vision-toolkit-health-${randomUUID()}`)
|
|
2493
|
+
try {
|
|
2494
|
+
await writeFile(probe, 'ok\n', { encoding: 'utf8', flag: 'wx' })
|
|
2495
|
+
await rm(probe, { force: true })
|
|
2496
|
+
return { status: 'ok', detail: `${label} is writable: ${path}` }
|
|
2497
|
+
} catch {
|
|
2498
|
+
await rm(probe, { force: true }).catch(() => {})
|
|
2499
|
+
return { status: 'error', detail: `${label} is not writable: ${path}` }
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2456
2503
|
/** Health: inspect local readiness, and optionally probe one provider's `/models` plus one real multimodal request. */
|
|
2457
2504
|
async health(testConnection: boolean, options: ToolCallOptions, testModel = false, provider?: ResolvedProvider): Promise<VisionToolkitHealthResult> {
|
|
2458
2505
|
return this.runOperation('vision_toolkit_health', options, async (operation) => {
|
|
@@ -2474,6 +2521,27 @@ export class VisionToolkitRuntime {
|
|
|
2474
2521
|
if (operation.signal.aborted) throw new VisionToolkitError('cancelled', 'vision_toolkit_health: cancelled')
|
|
2475
2522
|
chrome = { status: 'error', detail: 'Chrome availability probe failed' }
|
|
2476
2523
|
}
|
|
2524
|
+
let resolvedCredential: ResolvedCredential | undefined
|
|
2525
|
+
let credential: HealthCheck
|
|
2526
|
+
try {
|
|
2527
|
+
resolvedCredential = isBuiltInFreeVisionProvider(this.config.provider)
|
|
2528
|
+
? { value: BUILT_IN_FREE_VISION_KEY, source: 'built-in' }
|
|
2529
|
+
: await this.ctx.credentials.resolve(this.config.provider.credential)
|
|
2530
|
+
credential = resolvedCredential === undefined
|
|
2531
|
+
? { status: 'error', detail: `credential ${this.config.provider.credential} is not configured` }
|
|
2532
|
+
: { status: 'ok', detail: `credential ${this.config.provider.credential} is resolvable` }
|
|
2533
|
+
} catch {
|
|
2534
|
+
credential = { status: 'error', detail: `credential ${this.config.provider.credential} could not be resolved` }
|
|
2535
|
+
}
|
|
2536
|
+
let artifactDirectory: HealthCheck
|
|
2537
|
+
try {
|
|
2538
|
+
// allowedDirs are session input roots; they do not affect output readiness.
|
|
2539
|
+
const policy = await createPathPolicy(options.workspace, [], this.config.storageDir)
|
|
2540
|
+
artifactDirectory = await this.writableDirectoryCheck(policy.outputDir, 'Artifact directory')
|
|
2541
|
+
} catch {
|
|
2542
|
+
artifactDirectory = { status: 'error', detail: 'Artifact directory could not be prepared' }
|
|
2543
|
+
}
|
|
2544
|
+
const tempDirectory = await this.writableDirectoryCheck(info.runtimeHome, 'Runtime temp directory')
|
|
2477
2545
|
let service: HealthCheck = {
|
|
2478
2546
|
status: 'not_tested',
|
|
2479
2547
|
detail: 'Connection was not tested; use the per-provider API test',
|
|
@@ -2564,7 +2632,7 @@ export class VisionToolkitRuntime {
|
|
|
2564
2632
|
}
|
|
2565
2633
|
}
|
|
2566
2634
|
}
|
|
2567
|
-
const checks = { python, dependencies, chrome, service, model }
|
|
2635
|
+
const checks = { python, dependencies, chrome, credential, artifactDirectory, tempDirectory, service, model }
|
|
2568
2636
|
const healthy = Object.values(checks).every(check => check.status !== 'error')
|
|
2569
2637
|
return {
|
|
2570
2638
|
pluginVersion: PLUGIN_VERSION,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** Durable configured-storage history used to authorize persisted image paths after Profile restarts. */
|
|
2
|
+
|
|
3
|
+
import type { Context, Fiber } from '@deepseek-ai/cordis'
|
|
4
|
+
import { defineDomain } from '@deepseek-ai/dsh-storage-domain'
|
|
5
|
+
import type { DomainGlobal } from '@deepseek-ai/dsh-storage-domain'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { resolveConfig, type VisionToolkitConfig } from './config.ts'
|
|
8
|
+
|
|
9
|
+
const storageHistoryStateSchema = z.object({
|
|
10
|
+
roots: z.array(z.string().min(1)),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
type StorageHistoryState = z.infer<typeof storageHistoryStateSchema>
|
|
14
|
+
|
|
15
|
+
/** Plugin-owned storage roots that survive Settings-provider and Profile restarts. */
|
|
16
|
+
export const storageHistoryDomainSpec = defineDomain({
|
|
17
|
+
name: 'vision_toolkit_storage',
|
|
18
|
+
version: 0,
|
|
19
|
+
global: {
|
|
20
|
+
schema: storageHistoryStateSchema,
|
|
21
|
+
initial: { roots: [] },
|
|
22
|
+
},
|
|
23
|
+
tables: {},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
interface StorageBinding {
|
|
27
|
+
accepting: boolean
|
|
28
|
+
global: DomainGlobal<StorageHistoryState>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sameRoots(left: readonly string[], right: readonly string[]): boolean {
|
|
32
|
+
return left.length === right.length && left.every((root, index) => root === right[index])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Return every configured root that must remain readable, including the active root.
|
|
37
|
+
* @param config - Settings generation to summarize.
|
|
38
|
+
* @returns normalized configured roots in retention order.
|
|
39
|
+
*/
|
|
40
|
+
export function configuredStorageRoots(config: VisionToolkitConfig): string[] {
|
|
41
|
+
const resolved = resolveConfig(config)
|
|
42
|
+
return [...new Set([
|
|
43
|
+
...resolved.storageHistory,
|
|
44
|
+
...(resolved.storageDir === undefined ? [] : [resolved.storageDir]),
|
|
45
|
+
])]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merge plugin-owned roots into a Settings generation without retaining its active root as history.
|
|
50
|
+
* @param config - Settings generation being restored.
|
|
51
|
+
* @param durableRoots - roots loaded from the plugin-owned sidecar.
|
|
52
|
+
* @returns the original generation when unchanged, otherwise a generation with restored history.
|
|
53
|
+
*/
|
|
54
|
+
export function restoreDurableStorageHistory(
|
|
55
|
+
config: VisionToolkitConfig,
|
|
56
|
+
durableRoots: readonly string[],
|
|
57
|
+
): VisionToolkitConfig {
|
|
58
|
+
const resolved = resolveConfig(config)
|
|
59
|
+
const storageHistory = [...new Set([...resolved.storageHistory, ...durableRoots])]
|
|
60
|
+
.filter(root => root !== resolved.storageDir)
|
|
61
|
+
if (sameRoots(storageHistory, resolved.storageHistory)) return config
|
|
62
|
+
return { ...config, storageHistory }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Optional storage-domain sidecar for storage roots that Settings cannot persist itself. */
|
|
66
|
+
export class StorageHistoryStore {
|
|
67
|
+
private storage: StorageBinding | undefined
|
|
68
|
+
private storageFiber: (Fiber & PromiseLike<Fiber>) | undefined
|
|
69
|
+
private storageReady: Promise<void> | undefined
|
|
70
|
+
private mutationTail: Promise<void> = Promise.resolve()
|
|
71
|
+
private desiredRoots: readonly string[] | undefined
|
|
72
|
+
private persistenceTicket = 0
|
|
73
|
+
private warned = false
|
|
74
|
+
|
|
75
|
+
constructor(private readonly ctx: Context) {
|
|
76
|
+
if (typeof ctx.inject !== 'function') return
|
|
77
|
+
this.storageFiber = ctx.inject(['storageDomain'], async (storageCtx: Context) => {
|
|
78
|
+
const domain = await storageCtx.storageDomain.open(storageHistoryDomainSpec)
|
|
79
|
+
const binding: StorageBinding = { accepting: true, global: domain.global }
|
|
80
|
+
this.storage = binding
|
|
81
|
+
try {
|
|
82
|
+
if (this.desiredRoots !== undefined) {
|
|
83
|
+
await this.write(binding, this.desiredRoots, this.persistenceTicket)
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this.storage = undefined
|
|
87
|
+
await domain.close()
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
return async () => {
|
|
91
|
+
binding.accepting = false
|
|
92
|
+
if (this.storage === binding) this.storage = undefined
|
|
93
|
+
await this.mutationTail
|
|
94
|
+
await domain.close()
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
this.storageReady = Promise.resolve(this.storageFiber).then(
|
|
98
|
+
() => undefined,
|
|
99
|
+
(error: unknown) => { this.warnOnce(error) },
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Restore durable roots into one Settings generation before runtime preparation.
|
|
105
|
+
* @param config - Settings generation to restore.
|
|
106
|
+
* @returns the generation with available durable roots merged into its history.
|
|
107
|
+
*/
|
|
108
|
+
async restore(config: VisionToolkitConfig): Promise<VisionToolkitConfig> {
|
|
109
|
+
const binding = await this.prepareStorage()
|
|
110
|
+
return restoreDurableStorageHistory(config, binding?.global.get().roots ?? [])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Persist the active and historical configured roots.
|
|
115
|
+
* @param config - validated generation whose roots must survive restart.
|
|
116
|
+
* @returns false when no storage-domain is available; true after persistence or when there are no roots.
|
|
117
|
+
*/
|
|
118
|
+
async persist(config: VisionToolkitConfig): Promise<boolean> {
|
|
119
|
+
const roots = configuredStorageRoots(config)
|
|
120
|
+
const ticket = ++this.persistenceTicket
|
|
121
|
+
this.desiredRoots = roots
|
|
122
|
+
if (roots.length === 0) return true
|
|
123
|
+
const binding = await this.prepareStorage()
|
|
124
|
+
if (binding === undefined) return false
|
|
125
|
+
await this.write(binding, roots, ticket)
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Release the optional storage-domain binding with the plugin lifecycle. */
|
|
130
|
+
dispose(): void {
|
|
131
|
+
const fiber = this.storageFiber
|
|
132
|
+
this.storageFiber = undefined
|
|
133
|
+
this.storageReady = undefined
|
|
134
|
+
if (fiber !== undefined) void fiber.dispose().catch(error => { this.warnOnce(error) })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async prepareStorage(): Promise<StorageBinding | undefined> {
|
|
138
|
+
const current = this.activeStorage()
|
|
139
|
+
if (current !== undefined) return current
|
|
140
|
+
if (this.ctx.get('storageDomain') === undefined) return undefined
|
|
141
|
+
await this.storageReady
|
|
142
|
+
return this.activeStorage()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private activeStorage(): StorageBinding | undefined {
|
|
146
|
+
return this.storage?.accepting === true ? this.storage : undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private write(binding: StorageBinding, roots: readonly string[], ticket: number): Promise<void> {
|
|
150
|
+
return this.enqueueMutation(async () => {
|
|
151
|
+
if (!binding.accepting) throw new Error('the storage-domain provider changed while storage history was pending')
|
|
152
|
+
if (ticket !== this.persistenceTicket) return
|
|
153
|
+
if (sameRoots(binding.global.get().roots, roots)) return
|
|
154
|
+
await binding.global.set({ roots: [...roots] })
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private enqueueMutation(operation: () => Promise<void>): Promise<void> {
|
|
159
|
+
const result = this.mutationTail.then(operation)
|
|
160
|
+
this.mutationTail = result.then(() => undefined, () => undefined)
|
|
161
|
+
return result
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private warnOnce(error: unknown): void {
|
|
165
|
+
if (this.warned) return
|
|
166
|
+
this.warned = true
|
|
167
|
+
this.ctx.logger?.warn(
|
|
168
|
+
'dsh-vision-toolkit: configured storage history sidecar is unavailable. %s',
|
|
169
|
+
(error instanceof Error ? error.message : String(error)).slice(0, 500),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/upstream.ts
CHANGED
|
@@ -1059,22 +1059,47 @@ export class UpstreamAdapter {
|
|
|
1059
1059
|
return new VisionToolkitError('output', `${tool}: upstream output exceeded the capture limit`)
|
|
1060
1060
|
}
|
|
1061
1061
|
const message = upstreamFailureMessage(tool, result.stderr, options.secrets ?? [])
|
|
1062
|
-
|
|
1063
|
-
|
|
1062
|
+
const stderr = result.stderr
|
|
1063
|
+
|
|
1064
|
+
// Remote provider failures, classified into a machine-routable taxonomy so
|
|
1065
|
+
// the failover loop retries transient errors and fails over immediately on
|
|
1066
|
+
// deterministic ones. Order matters: auth/quota/rate-limit/region/tos are
|
|
1067
|
+
// checked before the broader server/network/request-shape patterns.
|
|
1068
|
+
if (/HTTP 401|\b401\b|HTTP 403|\b403\b|Unauthorized|Forbidden|authentication|Invalid API-key|invalid api key|check the api key/i.test(stderr)) {
|
|
1069
|
+
return new VisionToolkitError('auth', `${message}; verify the configured credential`)
|
|
1070
|
+
}
|
|
1071
|
+
if (/HTTP 402|\b402\b|Payment Required|insufficient balance|insufficient quota|insufficient credits|billing|out of credits|out of quota/i.test(stderr)) {
|
|
1072
|
+
return new VisionToolkitError('quota', `${message}; the provider account is out of quota or unpaid`)
|
|
1064
1073
|
}
|
|
1065
|
-
if (/HTTP 429|\b429\b|rate limit|
|
|
1074
|
+
if (/HTTP 429|\b429\b|rate ?limit|too many requests/i.test(stderr)) {
|
|
1066
1075
|
return new VisionToolkitError('rate_limit', `${message}; retry later or reduce concurrency`)
|
|
1067
1076
|
}
|
|
1068
|
-
if (/
|
|
1077
|
+
if (/not available in your region|prohibited region|unsupported region|region is not supported/i.test(stderr)) {
|
|
1078
|
+
return new VisionToolkitError('region', `${message}; the provider is not available in this region`)
|
|
1079
|
+
}
|
|
1080
|
+
if (/terms of service|\btos\b|content policy|safety system/i.test(stderr)) {
|
|
1081
|
+
return new VisionToolkitError('tos', `${message}; the request was rejected by the provider content policy`)
|
|
1082
|
+
}
|
|
1083
|
+
if (/HTTP 5\d\d|\b500\b|\b502\b|\b503\b|\b504\b|bad gateway|service unavailable|internal server error/i.test(stderr)) {
|
|
1084
|
+
return new VisionToolkitError('server', `${message}; the provider returned a server error`)
|
|
1085
|
+
}
|
|
1086
|
+
if (/connection refused|connection reset|ECONN|ENOTFOUND|EAI_AGAIN|getaddrinfo|fetch failed|name resolution|network error|network is unreachable/i.test(stderr)) {
|
|
1087
|
+
return new VisionToolkitError('network', `${message}; network failure reaching the provider`)
|
|
1088
|
+
}
|
|
1089
|
+
if (/HTTP 400|\b400\b|HTTP 404|\b404\b|HTTP 422|\b422\b|invalid request|invalid model|no such model|model not exist|unknown model/i.test(stderr)) {
|
|
1090
|
+
return new VisionToolkitError('invalid_request', `${message}; the request or model is not supported by this provider`)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (/Missing config VISION_/i.test(stderr)) {
|
|
1069
1094
|
return new VisionToolkitError('config', message)
|
|
1070
1095
|
}
|
|
1071
|
-
if (/maxImagePixels|exceed(?:s|ing).*pixels/i.test(
|
|
1096
|
+
if (/maxImagePixels|exceed(?:s|ing).*pixels/i.test(stderr)) {
|
|
1072
1097
|
return new VisionToolkitError('capacity', message)
|
|
1073
1098
|
}
|
|
1074
|
-
if (/not found|only PNG|unsupported|cannot open|empty region|must be|expects|invalid colour|needs at least/i.test(
|
|
1099
|
+
if (/not found|only PNG|unsupported|cannot open|empty region|must be|expects|invalid colour|needs at least/i.test(stderr)) {
|
|
1075
1100
|
return new VisionToolkitError('input', message)
|
|
1076
1101
|
}
|
|
1077
|
-
if (/requires Pillow|requires numpy|requires vtracer|no Chrome|capture failed/i.test(
|
|
1102
|
+
if (/requires Pillow|requires numpy|requires vtracer|no Chrome|capture failed/i.test(stderr)) {
|
|
1078
1103
|
return new VisionToolkitError('runtime', message)
|
|
1079
1104
|
}
|
|
1080
1105
|
return new VisionToolkitError(
|
package/src/web.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from './paste-images.ts'
|
|
23
23
|
import {
|
|
24
24
|
resolveConfig,
|
|
25
|
+
retainedStorageHistory,
|
|
25
26
|
isBuiltInFreeVisionProvider,
|
|
26
27
|
VISION_TOOLKIT_SETTINGS_NAMESPACE,
|
|
27
28
|
type ResolvedProvider,
|
|
@@ -346,16 +347,22 @@ export class VisionToolkitWebBackend {
|
|
|
346
347
|
|
|
347
348
|
private async save(request: SaveRequest): Promise<VisionToolkitSettingsSnapshot> {
|
|
348
349
|
if (!this.ctx.settings.writable) throw new Error('settings provider is read-only')
|
|
350
|
+
const current = resolveConfig(descriptorOf(this.ctx).value as VisionToolkitConfig)
|
|
351
|
+
const storageHistory = retainedStorageHistory(request.value, current)
|
|
352
|
+
const value: VisionToolkitConfig = {
|
|
353
|
+
...request.value,
|
|
354
|
+
...(storageHistory.length === 0 ? {} : { storageHistory }),
|
|
355
|
+
}
|
|
349
356
|
let candidate: PreparedRuntimeGeneration
|
|
350
357
|
try {
|
|
351
|
-
candidate = await this.manager.prepareCandidate(
|
|
358
|
+
candidate = await this.manager.prepareCandidate(value)
|
|
352
359
|
} catch (error) {
|
|
353
360
|
this.manager.recordFailure(error)
|
|
354
361
|
throw error
|
|
355
362
|
}
|
|
356
363
|
await this.ctx.settings.replace(
|
|
357
364
|
VISION_TOOLKIT_SETTINGS_NAMESPACE,
|
|
358
|
-
|
|
365
|
+
value as object,
|
|
359
366
|
request.expectedRevision,
|
|
360
367
|
)
|
|
361
368
|
this.manager.activateCandidate(candidate)
|