@mengruo/dsh-vision-toolkit 0.1.2 → 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.
Files changed (85) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +4 -0
  3. package/README.zh.md +4 -0
  4. package/docs/requirements-traceability/README.i18n.yaml +2 -2
  5. package/docs/requirements-traceability/README.md +1 -1
  6. package/docs/requirements-traceability/README.zh.md +1 -1
  7. package/lib/artifact-access.js +20 -2
  8. package/lib/artifact-access.js.map +1 -1
  9. package/lib/client.js +59 -19
  10. package/lib/client.js.map +1 -1
  11. package/lib/config.js +64 -12
  12. package/lib/config.js.map +1 -1
  13. package/lib/errors.js +25 -1
  14. package/lib/errors.js.map +1 -1
  15. package/lib/evidence-cache.js +5 -2
  16. package/lib/evidence-cache.js.map +1 -1
  17. package/lib/exposure.js +14 -1
  18. package/lib/exposure.js.map +1 -1
  19. package/lib/image-input-variants.js +22 -12
  20. package/lib/image-input-variants.js.map +1 -1
  21. package/lib/index.js +53 -6
  22. package/lib/index.js.map +1 -1
  23. package/lib/paste-images.js +67 -19
  24. package/lib/paste-images.js.map +1 -1
  25. package/lib/paths.js +214 -28
  26. package/lib/paths.js.map +1 -1
  27. package/lib/runtime-manager.js +76 -10
  28. package/lib/runtime-manager.js.map +1 -1
  29. package/lib/runtime.js +389 -104
  30. package/lib/runtime.js.map +1 -1
  31. package/lib/storage-history.js +154 -0
  32. package/lib/storage-history.js.map +1 -0
  33. package/lib/tools.js +68 -35
  34. package/lib/tools.js.map +1 -1
  35. package/lib/types/artifact-access.d.ts.map +1 -1
  36. package/lib/types/client/index.d.ts +23 -5
  37. package/lib/types/client/index.d.ts.map +1 -1
  38. package/lib/types/client/paste-images.d.ts +2 -0
  39. package/lib/types/client/paste-images.d.ts.map +1 -1
  40. package/lib/types/config.d.ts +38 -7
  41. package/lib/types/config.d.ts.map +1 -1
  42. package/lib/types/errors.d.ts +18 -2
  43. package/lib/types/errors.d.ts.map +1 -1
  44. package/lib/types/evidence-cache.d.ts +1 -1
  45. package/lib/types/evidence-cache.d.ts.map +1 -1
  46. package/lib/types/exposure.d.ts.map +1 -1
  47. package/lib/types/image-input-variants.d.ts +5 -3
  48. package/lib/types/image-input-variants.d.ts.map +1 -1
  49. package/lib/types/index.d.ts.map +1 -1
  50. package/lib/types/paste-images.d.ts +12 -4
  51. package/lib/types/paste-images.d.ts.map +1 -1
  52. package/lib/types/paths.d.ts +31 -5
  53. package/lib/types/paths.d.ts.map +1 -1
  54. package/lib/types/runtime-manager.d.ts +28 -4
  55. package/lib/types/runtime-manager.d.ts.map +1 -1
  56. package/lib/types/runtime.d.ts +75 -12
  57. package/lib/types/runtime.d.ts.map +1 -1
  58. package/lib/types/storage-history.d.ts +63 -0
  59. package/lib/types/storage-history.d.ts.map +1 -0
  60. package/lib/types/tools.d.ts +1 -0
  61. package/lib/types/tools.d.ts.map +1 -1
  62. package/lib/types/upstream.d.ts.map +1 -1
  63. package/lib/types/web.d.ts.map +1 -1
  64. package/lib/upstream.js +31 -8
  65. package/lib/upstream.js.map +1 -1
  66. package/lib/web.js +9 -3
  67. package/lib/web.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/artifact-access.ts +22 -2
  70. package/src/client/index.tsx +72 -20
  71. package/src/client/paste-images.tsx +14 -4
  72. package/src/config.ts +107 -20
  73. package/src/errors.ts +25 -1
  74. package/src/evidence-cache.ts +5 -2
  75. package/src/exposure.ts +16 -2
  76. package/src/image-input-variants.ts +21 -6
  77. package/src/index.ts +65 -6
  78. package/src/paste-images.ts +81 -19
  79. package/src/paths.ts +249 -28
  80. package/src/runtime-manager.ts +93 -10
  81. package/src/runtime.ts +431 -115
  82. package/src/storage-history.ts +172 -0
  83. package/src/tools.ts +78 -46
  84. package/src/upstream.ts +33 -8
  85. package/src/web.ts +9 -2
@@ -71,15 +71,28 @@ const en = {
71
71
  anthropicThinkingHint: 'omit has the broadest compatibility. Use disabled or adaptive only when the selected model documents that mode; restore omit first after HTTP 400.',
72
72
  userAgent: 'User-Agent',
73
73
  language: 'Output language',
74
- limits: 'Limits',
74
+ limits: 'Timeout and concurrency',
75
75
  timeout: 'Request timeout (ms)',
76
+ t1: 't1 — single-request hedge (s)',
77
+ t1Hint: 'When a single request exceeds t1, keep it running and start the next model in parallel.',
78
+ t2: 't2 — cumulative cutoff (s)',
79
+ t2Hint: 'Terminate the model once its accumulated request time reaches t2.',
80
+ hardTimeout: 'Global hard timeout (s)',
81
+ hardTimeoutHint: 'Wall-clock ceiling for one tool call; on timeout every request is cancelled.',
82
+ sessionConcurrency: 'Per-session max concurrency',
83
+ sessionConcurrencyHint: 'Maximum concurrent vision calls in one session; excess is rejected immediately.',
84
+ minAvailable: 'Minimum available time (s)',
85
+ minAvailableHint: 'No new request is issued once the remaining budget drops below this value.',
76
86
  maxBytes: 'Maximum image bytes',
77
87
  maxPixels: 'Maximum image pixels',
78
- concurrency: 'Concurrent calls per session',
88
+ concurrency: 'Concurrency (per model)',
79
89
  runtime: 'Runtime',
80
90
  runtimeMode: 'Runtime mode',
81
91
  toolkitPath: 'Pinned checkout path',
82
92
  python: 'Python override',
93
+ storage: 'Local files',
94
+ storageDir: 'Default save directory',
95
+ storageDirHint: 'Leave blank to keep .dsh-vision-toolkit in each workspace. On POSIX systems, set an absolute shared root such as /tmp/dsh-vision-toolkit to store artifacts, pasted images, and caches in an automatically generated per-user, workspace-specific child directory. Windows shared roots remain disabled until their ACLs can be verified safely.',
83
96
  allowedDirs: 'Additional allowed directories',
84
97
  allowedDirsHint: 'One path per line. The session workspace is always allowed.',
85
98
  save: 'Save and apply',
@@ -102,7 +115,7 @@ const en = {
102
115
  connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.',
103
116
  saveBeforeTesting: 'Save service changes before testing the connection.',
104
117
  advanced: 'Advanced settings',
105
- advancedHint: 'Credential name, provider compatibility, output language, resource limits, runtime source, Python, and additional readable directories.',
118
+ advancedHint: 'Credential name, provider compatibility, output language, resource limits, default save directory, runtime source, Python, and additional readable directories.',
106
119
  imageInput: 'Image input',
107
120
  hiddenVariants: 'Transparent variant routing',
108
121
  hiddenVariantsLabel: 'Keep the original model names and enable images automatically',
@@ -274,15 +287,28 @@ const zh: Record<LocaleKey, string> = {
274
287
  anthropicThinkingHint: 'omit 兼容性最好。仅当所选模型明确支持时使用 disabled 或 adaptive;遇到 HTTP 400 时先恢复 omit。',
275
288
  userAgent: 'User-Agent',
276
289
  language: '结果语言',
277
- limits: '资源与并发限制',
290
+ limits: '超时与并发限制',
278
291
  timeout: '单次请求超时(毫秒)',
292
+ t1: 't1(单次请求阈值,秒)',
293
+ t1Hint: '单次请求超过 t1 时,继续请求并并行调用下一个模型。',
294
+ t2: 't2(累计终止阈值,秒)',
295
+ t2Hint: '该模型累计请求时长达到 t2 时终止。',
296
+ hardTimeout: '全局硬超时(秒)',
297
+ hardTimeoutHint: '单次工具调用的墙钟上限,超时终止所有任务。',
298
+ sessionConcurrency: '单会话最大并发',
299
+ sessionConcurrencyHint: '同一会话最多同时运行的视觉任务数,超出直接返回错误。',
300
+ minAvailable: '最低可用时间(秒)',
301
+ minAvailableHint: '剩余时间额度低于该值时不再发起新请求。',
279
302
  maxBytes: '单张图片大小上限(字节)',
280
303
  maxPixels: '单张图片最大像素数',
281
- concurrency: '单个会话最多并发任务数',
304
+ concurrency: '并发(每个模型)',
282
305
  runtime: '工具运行环境',
283
306
  runtimeMode: '环境来源',
284
307
  toolkitPath: 'agent-vision-toolkit 目录',
285
308
  python: 'Python 解释器(可选)',
309
+ storage: '本地文件',
310
+ storageDir: '默认保存目录',
311
+ storageDirHint: '留空时继续在各工作区创建 .dsh-vision-toolkit。在 POSIX 系统上填写绝对路径共享根目录(例如 /tmp/dsh-vision-toolkit)后,产物、粘贴图片和缓存会保存到自动生成的当前用户、工作区专属子目录中。Windows 共享目录会保持禁用,直到能够安全校验 ACL 权限。',
286
312
  allowedDirs: '允许读取的其他目录',
287
313
  allowedDirsHint: '每行填写一个目录。当前会话的工作目录始终可以读取,无需重复填写。',
288
314
  save: '保存设置',
@@ -305,7 +331,7 @@ const zh: Record<LocaleKey, string> = {
305
331
  connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。',
306
332
  saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。',
307
333
  advanced: '高级设置',
308
- advancedHint: '凭据名称、服务兼容参数、结果语言、资源限制、运行环境来源、Python 和额外可读目录。一般无需修改。',
334
+ advancedHint: '凭据名称、服务兼容参数、结果语言、资源限制、默认保存目录、运行环境来源、Python 和额外可读目录。一般无需修改。',
309
335
  imageInput: '图片输入',
310
336
  hiddenVariants: '透明变体路由',
311
337
  hiddenVariantsLabel: '保留原模型名并自动启用图片能力',
@@ -501,7 +527,8 @@ interface ProviderValue {
501
527
  protocol?: 'openai' | 'anthropic'
502
528
  anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
503
529
  userAgent?: string
504
- timeoutMs?: number
530
+ t1Seconds?: number
531
+ t2Seconds?: number
505
532
  maxImageBytes?: number
506
533
  maxImagePixels?: number
507
534
  concurrency?: number
@@ -519,11 +546,15 @@ interface SettingsValue {
519
546
  }
520
547
  providers?: ProviderValue[]
521
548
  language?: 'zh' | 'en'
522
- timeoutMs?: number
549
+ hardTimeoutSeconds?: number
550
+ sessionMaxConcurrency?: number
551
+ minAvailableSeconds?: number
523
552
  maxImageBytes?: number
524
553
  maxImagePixels?: number
525
554
  concurrency?: number
526
555
  runtime?: { mode?: 'managed' | 'external'; agentVisionToolkitPath?: string; python?: string }
556
+ storageDir?: string
557
+ storageHistory?: string[]
527
558
  allowedDirs?: string[]
528
559
  imageInputVariants?: {
529
560
  enabled?: boolean
@@ -1140,7 +1171,8 @@ interface ProviderDraft {
1140
1171
  protocol: 'openai' | 'anthropic'
1141
1172
  anthropicThinking: 'omit' | 'disabled' | 'adaptive'
1142
1173
  userAgent: string
1143
- timeoutMs: string
1174
+ t1Seconds: string
1175
+ t2Seconds: string
1144
1176
  maxImageBytes: string
1145
1177
  maxImagePixels: string
1146
1178
  concurrency: string
@@ -1150,13 +1182,16 @@ interface ProviderDraft {
1150
1182
  interface Draft {
1151
1183
  providers: ProviderDraft[]
1152
1184
  language: 'zh' | 'en'
1153
- timeoutMs: string
1185
+ hardTimeoutSeconds: string
1186
+ sessionMaxConcurrency: string
1187
+ minAvailableSeconds: string
1154
1188
  maxImageBytes: string
1155
1189
  maxImagePixels: string
1156
1190
  concurrency: string
1157
1191
  runtimeMode: 'managed' | 'external'
1158
1192
  toolkitPath: string
1159
1193
  python: string
1194
+ storageDir: string
1160
1195
  allowedDirs: string
1161
1196
  hiddenVariants: boolean
1162
1197
  variantEnabled: boolean
@@ -1179,7 +1214,6 @@ function autoCredentialName(id: string): string {
1179
1214
  }
1180
1215
 
1181
1216
  interface ProviderDefaults {
1182
- timeoutMs: number
1183
1217
  maxImageBytes: number
1184
1218
  maxImagePixels: number
1185
1219
  concurrency: number
@@ -1197,7 +1231,8 @@ function emptyProviderDraft(defaults: ProviderDefaults): ProviderDraft {
1197
1231
  protocol: 'openai',
1198
1232
  anthropicThinking: 'omit',
1199
1233
  userAgent: DEFAULT_USER_AGENT,
1200
- timeoutMs: String(defaults.timeoutMs),
1234
+ t1Seconds: '90',
1235
+ t2Seconds: '90',
1201
1236
  maxImageBytes: String(defaults.maxImageBytes),
1202
1237
  maxImagePixels: String(defaults.maxImagePixels),
1203
1238
  concurrency: String(defaults.concurrency),
@@ -1218,7 +1253,8 @@ function providerDraftOf(value: ProviderValue | undefined, fallback: ProviderVal
1218
1253
  protocol: source?.protocol ?? 'openai',
1219
1254
  anthropicThinking: source?.anthropicThinking ?? 'omit',
1220
1255
  userAgent: source?.userAgent ?? DEFAULT_USER_AGENT,
1221
- timeoutMs: String(source?.timeoutMs ?? defaults.timeoutMs),
1256
+ t1Seconds: String(source?.t1Seconds ?? 90),
1257
+ t2Seconds: String(source?.t2Seconds ?? 90),
1222
1258
  maxImageBytes: String(source?.maxImageBytes ?? defaults.maxImageBytes),
1223
1259
  maxImagePixels: String(source?.maxImagePixels ?? defaults.maxImagePixels),
1224
1260
  concurrency: String(source?.concurrency ?? defaults.concurrency),
@@ -1228,7 +1264,6 @@ function providerDraftOf(value: ProviderValue | undefined, fallback: ProviderVal
1228
1264
 
1229
1265
  function draftOf(value: SettingsValue): Draft {
1230
1266
  const globalDefaults: ProviderDefaults = {
1231
- timeoutMs: value.timeoutMs ?? 30000,
1232
1267
  maxImageBytes: value.maxImageBytes ?? 4194304,
1233
1268
  maxImagePixels: value.maxImagePixels ?? 20000000,
1234
1269
  concurrency: value.concurrency ?? 4,
@@ -1240,13 +1275,16 @@ function draftOf(value: SettingsValue): Draft {
1240
1275
  return {
1241
1276
  providers,
1242
1277
  language: value.language ?? 'zh',
1243
- timeoutMs: String(globalDefaults.timeoutMs),
1278
+ hardTimeoutSeconds: String(value.hardTimeoutSeconds ?? 180),
1279
+ sessionMaxConcurrency: String(value.sessionMaxConcurrency ?? 6),
1280
+ minAvailableSeconds: String(value.minAvailableSeconds ?? 20),
1244
1281
  maxImageBytes: String(globalDefaults.maxImageBytes),
1245
1282
  maxImagePixels: String(globalDefaults.maxImagePixels),
1246
1283
  concurrency: String(globalDefaults.concurrency),
1247
1284
  runtimeMode: value.runtime?.mode ?? 'managed',
1248
1285
  toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
1249
1286
  python: value.runtime?.python ?? '',
1287
+ storageDir: value.storageDir ?? '',
1250
1288
  allowedDirs: (value.allowedDirs ?? []).join('\n'),
1251
1289
  hiddenVariants: value.imageInputVariants?.hidden ?? true,
1252
1290
  variantEnabled: value.imageInputVariants?.enabled ?? true,
@@ -1282,7 +1320,8 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1282
1320
  protocol: provider.protocol,
1283
1321
  anthropicThinking: provider.anthropicThinking,
1284
1322
  ...(provider.userAgent.trim() === DEFAULT_USER_AGENT ? {} : { userAgent: provider.userAgent.trim() }),
1285
- ...(provider.timeoutMs.trim().length === 0 ? {} : { timeoutMs: positiveInteger(provider.timeoutMs, t('timeout'), t) }),
1323
+ ...(provider.t1Seconds.trim().length === 0 ? {} : { t1Seconds: positiveInteger(provider.t1Seconds, t('t1'), t) }),
1324
+ ...(provider.t2Seconds.trim().length === 0 ? {} : { t2Seconds: positiveInteger(provider.t2Seconds, t('t2'), t) }),
1286
1325
  ...(provider.maxImageBytes.trim().length === 0 ? {} : { maxImageBytes: positiveInteger(provider.maxImageBytes, t('maxBytes'), t) }),
1287
1326
  ...(provider.maxImagePixels.trim().length === 0 ? {} : { maxImagePixels: positiveInteger(provider.maxImagePixels, t('maxPixels'), t) }),
1288
1327
  ...(provider.concurrency.trim().length === 0 ? {} : { concurrency: positiveInteger(provider.concurrency, t('concurrency'), t) }),
@@ -1300,7 +1339,9 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1300
1339
  },
1301
1340
  providers,
1302
1341
  language: draft.language,
1303
- timeoutMs: positiveInteger(draft.timeoutMs, t('timeout'), t),
1342
+ hardTimeoutSeconds: positiveInteger(draft.hardTimeoutSeconds, t('hardTimeout'), t),
1343
+ sessionMaxConcurrency: positiveInteger(draft.sessionMaxConcurrency, t('sessionConcurrency'), t),
1344
+ minAvailableSeconds: positiveInteger(draft.minAvailableSeconds, t('minAvailable'), t),
1304
1345
  maxImageBytes: positiveInteger(draft.maxImageBytes, t('maxBytes'), t),
1305
1346
  maxImagePixels: positiveInteger(draft.maxImagePixels, t('maxPixels'), t),
1306
1347
  concurrency: positiveInteger(draft.concurrency, t('concurrency'), t),
@@ -1309,6 +1350,7 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1309
1350
  ...(draft.runtimeMode === 'external' ? { agentVisionToolkitPath: draft.toolkitPath.trim() } : {}),
1310
1351
  ...(draft.python.trim().length === 0 ? {} : { python: draft.python.trim() }),
1311
1352
  },
1353
+ ...(draft.storageDir.trim().length === 0 ? {} : { storageDir: draft.storageDir.trim() }),
1312
1354
  allowedDirs: draft.allowedDirs.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
1313
1355
  imageInputVariants: {
1314
1356
  ...(draft.variantEnabled ? {} : { enabled: false }),
@@ -1488,7 +1530,6 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1488
1530
  setDraft(current => current === undefined ? current : {
1489
1531
  ...current,
1490
1532
  providers: [...current.providers, emptyProviderDraft({
1491
- timeoutMs: Number(current.timeoutMs) || 30000,
1492
1533
  maxImageBytes: Number(current.maxImageBytes) || 4194304,
1493
1534
  maxImagePixels: Number(current.maxImagePixels) || 20000000,
1494
1535
  concurrency: Number(current.concurrency) || 4,
@@ -1626,7 +1667,8 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1626
1667
  <Field label={t('protocol')}><select aria-label={t('protocol')} disabled={!snapshot.writable || busy} value={selectedProvider.protocol} onChange={(event) => { updateProvider(selectedIndex, 'protocol', event.target.value as 'openai' | 'anthropic') }}><option value="openai">OpenAI Chat Completions</option><option value="anthropic">Anthropic Messages</option></select></Field>
1627
1668
  {selectedProvider.protocol === 'anthropic' ? <Field label={t('anthropicThinking')} hint={t('anthropicThinkingHint')}><select aria-label={t('anthropicThinking')} value={selectedProvider.anthropicThinking} onChange={(event) => { updateProvider(selectedIndex, 'anthropicThinking', event.target.value as 'omit' | 'disabled' | 'adaptive') }}><option value="omit">omit (widest compatibility)</option><option value="disabled">disabled (model support required)</option><option value="adaptive">adaptive (model support required)</option></select></Field> : null}
1628
1669
  <Field label={t('userAgent')}><Input aria-label={t('userAgent')} value={selectedProvider.userAgent} onChange={(event) => { updateProvider(selectedIndex, 'userAgent', event.target.value) }} /></Field>
1629
- <Field label={t('timeout')}><Input aria-label={t('timeout')} inputMode="numeric" value={selectedProvider.timeoutMs} onChange={(event) => { updateProvider(selectedIndex, 'timeoutMs', event.target.value) }} /></Field>
1670
+ <Field label={t('t1')} hint={t('t1Hint')}><Input aria-label={t('t1')} inputMode="numeric" value={selectedProvider.t1Seconds} onChange={(event) => { updateProvider(selectedIndex, 't1Seconds', event.target.value) }} /></Field>
1671
+ <Field label={t('t2')} hint={t('t2Hint')}><Input aria-label={t('t2')} inputMode="numeric" value={selectedProvider.t2Seconds} onChange={(event) => { updateProvider(selectedIndex, 't2Seconds', event.target.value) }} /></Field>
1630
1672
  <Field label={t('maxBytes')}><Input aria-label={t('maxBytes')} inputMode="numeric" value={selectedProvider.maxImageBytes} onChange={(event) => { updateProvider(selectedIndex, 'maxImageBytes', event.target.value) }} /></Field>
1631
1673
  <Field label={t('maxPixels')}><Input aria-label={t('maxPixels')} inputMode="numeric" value={selectedProvider.maxImagePixels} onChange={(event) => { updateProvider(selectedIndex, 'maxImagePixels', event.target.value) }} /></Field>
1632
1674
  <Field label={t('concurrency')}><Input aria-label={t('concurrency')} inputMode="numeric" value={selectedProvider.concurrency} onChange={(event) => { updateProvider(selectedIndex, 'concurrency', event.target.value) }} /></Field>
@@ -1703,13 +1745,23 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1703
1745
  <Field label={t('language')}><select value={draft.language} onChange={(event) => { update('language', event.target.value as 'zh' | 'en') }}><option value="zh">中文</option><option value="en">English</option></select></Field>
1704
1746
  </div></section>
1705
1747
 
1748
+ <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('limits')}</h3></div><div className="dvt-form-grid">
1749
+ <Field label={t('hardTimeout')} hint={t('hardTimeoutHint')}><Input aria-label={t('hardTimeout')} inputMode="numeric" value={draft.hardTimeoutSeconds} onChange={(event) => { update('hardTimeoutSeconds', event.target.value) }} /></Field>
1750
+ <Field label={t('sessionConcurrency')} hint={t('sessionConcurrencyHint')}><Input aria-label={t('sessionConcurrency')} inputMode="numeric" value={draft.sessionMaxConcurrency} onChange={(event) => { update('sessionMaxConcurrency', event.target.value) }} /></Field>
1751
+ <Field label={t('minAvailable')} hint={t('minAvailableHint')}><Input aria-label={t('minAvailable')} inputMode="numeric" value={draft.minAvailableSeconds} onChange={(event) => { update('minAvailableSeconds', event.target.value) }} /></Field>
1752
+ </div></section>
1753
+
1706
1754
  <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('imageInput')}</h3></div><label className="dvt-check"><input type="checkbox" checked={draft.hiddenVariants} disabled={!snapshot.writable || busy} onChange={(event) => { update('hiddenVariants', event.target.checked) }} /><span>{t('hiddenVariantsLabel')}</span><small>{t('hiddenVariantsHint')}</small></label></section>
1707
1755
 
1756
+ <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('storage')}</h3></div><div className="dvt-form-grid">
1757
+ <Field label={t('storageDir')} hint={t('storageDirHint')}><Input aria-label={t('storageDir')} placeholder="/tmp/dsh-vision-toolkit" value={draft.storageDir} onChange={(event) => { update('storageDir', event.target.value) }} /></Field>
1758
+ <Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
1759
+ </div></section>
1760
+
1708
1761
  <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('runtime')}</h3><span className={`dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`}>{snapshot.runtime.ready ? snapshot.runtime.upstream?.source === 'managed' ? t('runtimeManaged') : snapshot.runtime.upstream?.source === 'external' ? t('runtimeExternal') : t('runtimeReady') : t('runtimeUnavailable')}</span></div><div className="dvt-form-grid">
1709
1762
  <Field label={t('runtimeMode')}><select value={draft.runtimeMode} onChange={(event) => { update('runtimeMode', event.target.value as 'managed' | 'external') }}><option value="managed">{t('runtimeManaged')}</option><option value="external">{t('runtimeExternal')}</option></select></Field>
1710
1763
  {draft.runtimeMode === 'external' ? <Field label={t('toolkitPath')}><Input value={draft.toolkitPath} onChange={(event) => { update('toolkitPath', event.target.value) }} /></Field> : null}
1711
1764
  <Field label={t('python')}><Input placeholder="python3" value={draft.python} onChange={(event) => { update('python', event.target.value) }} /></Field>
1712
- <Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
1713
1765
  </div>
1714
1766
  {snapshot.runtime.upstream === undefined ? null : <div className="dvt-runtime-facts"><code>{snapshot.runtime.upstream.path}</code><code>{snapshot.runtime.upstream.python} · {snapshot.runtime.upstream.pythonVersion}</code><code>{snapshot.runtime.upstream.runtimeHome}</code></div>}
1715
1767
  </section>
@@ -58,6 +58,8 @@ interface PasteOccurrence {
58
58
  source: string
59
59
  ref: string
60
60
  offset: number
61
+ /** DSH rc.8+ stores the full @label text; older releases used one placeholder. */
62
+ length?: number
61
63
  label: string
62
64
  }
63
65
 
@@ -166,6 +168,10 @@ function pasteLabel(file: File, index: number): string {
166
168
  return file.name.trim() || `clipboard-image-${index + 1}`
167
169
  }
168
170
 
171
+ function occurrenceEnd(occurrence: PasteOccurrence): number {
172
+ return occurrence.offset + (occurrence.length ?? 1)
173
+ }
174
+
169
175
  /** Owns browser File objects until DSH serializes the corresponding text references. */
170
176
  export class PasteImageController {
171
177
  private readonly records = new Map<string, PasteRecord>()
@@ -241,6 +247,7 @@ export class PasteImageController {
241
247
  if (before !== '' && !/\s$/u.test(before)) cursor = this.insertText(input, ' ', cursor)
242
248
  for (const [index, file] of files.entries()) {
243
249
  const ref = id()
250
+ const label = pasteLabel(file, index)
244
251
  const record: PasteRecord = { ref, file, batch, status: 'ready' }
245
252
  batch.records.push(record)
246
253
  this.records.set(ref, record)
@@ -248,11 +255,14 @@ export class PasteImageController {
248
255
  const accepted = input.insertReference({
249
256
  source: SOURCE,
250
257
  ref,
251
- label: pasteLabel(file, index),
252
- clipboardText: `[pasted image: ${pasteLabel(file, index)}]`,
258
+ label,
259
+ clipboardText: `[pasted image: ${label}]`,
253
260
  }, { start: cursor, end: cursor, draftRev: snapshot.draftRev })
254
261
  if (!accepted) throw new Error('The composer changed before pasted images could be inserted')
255
- cursor += 1
262
+ const inserted = input.state.getSnapshot().occurrences.find(occurrence =>
263
+ occurrence.source === SOURCE && occurrence.ref === ref)
264
+ if (inserted === undefined) throw new Error('The pasted image reference was not present after insertion')
265
+ cursor = occurrenceEnd(inserted)
256
266
  const hasNext = index + 1 < files.length
257
267
  const suffix = input.state.getSnapshot().draft.slice(cursor)
258
268
  if (hasNext || (suffix !== '' && !/^\s/u.test(suffix))) cursor = this.insertText(input, ' ', cursor)
@@ -594,7 +604,7 @@ export class PasteImageController {
594
604
  insertText: (text: string, span: { start: number; end: number; draftRev: number }) => boolean
595
605
  }).insertText('', {
596
606
  start: current.offset,
597
- end: current.offset + 1,
607
+ end: occurrenceEnd(current),
598
608
  draftRev: snapshot.draftRev,
599
609
  })
600
610
  if (!accepted) return
package/src/config.ts CHANGED
@@ -77,8 +77,10 @@ export interface VisionProviderConfig {
77
77
  anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
78
78
  /** Outbound User-Agent for provider requests and connection tests. */
79
79
  userAgent?: string
80
- /** Per-provider single request timeout in milliseconds. */
81
- timeoutMs?: number
80
+ /** t1: per-request hedge threshold in seconds. A single request exceeding t1 keeps running while the next provider starts in parallel. */
81
+ t1Seconds?: number
82
+ /** t2: per-provider cumulative cutoff in seconds. Total accumulated request time reaching t2 terminates the provider. */
83
+ t2Seconds?: number
82
84
  /** Per-provider maximum input image bytes; larger images skip to a later provider or are compressed. */
83
85
  maxImageBytes?: number
84
86
  /** Per-provider maximum decoded pixel count per input image. */
@@ -109,13 +111,17 @@ export interface VisionToolkitConfig {
109
111
  providers?: VisionProviderConfig[]
110
112
  /** Vision output language (`zh` or `en`). */
111
113
  language?: 'zh' | 'en'
112
- /** Single remote/upstream call budget in milliseconds. */
113
- timeoutMs?: number
114
+ /** Global hard timeout in seconds for one tool invocation; the call never exceeds it. */
115
+ hardTimeoutSeconds?: number
116
+ /** Per-session cap on concurrent tool operations (default 6). */
117
+ sessionMaxConcurrency?: number
118
+ /** Minimum remaining budget in seconds required before issuing a new request (default 20). */
119
+ minAvailableSeconds?: number
114
120
  /** Maximum input image size in bytes; larger images are auto-compressed (lossless first). */
115
121
  maxImageBytes?: number
116
122
  /** Maximum decoded pixel count per input image; larger images are auto-downscaled to fit. */
117
123
  maxImagePixels?: number
118
- /** In-flight tool execution cap per session. */
124
+ /** Default per-model in-flight request cap inherited by providers that do not set their own. */
119
125
  concurrency?: number
120
126
  runtime?: {
121
127
  /** `managed` uses the packaged snapshot and isolated venv; `external` uses a clean pinned checkout. */
@@ -125,6 +131,14 @@ export interface VisionToolkitConfig {
125
131
  /** Optional Python 3.11+ bootstrap/interpreter override. */
126
132
  python?: string
127
133
  }
134
+ /**
135
+ * Optional shared storage root. When set, every workspace gets an isolated,
136
+ * automatically generated child directory below this root instead of writing
137
+ * `.dsh-vision-toolkit` into the workspace.
138
+ */
139
+ storageDir?: string
140
+ /** Internal read-only history used to keep persisted paths valid after storage moves. */
141
+ storageHistory?: string[]
128
142
  /** Extra directories (besides the workspace) inputs may come from. */
129
143
  allowedDirs?: string[]
130
144
  /**
@@ -179,14 +193,17 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
179
193
  protocol: z.union(['openai', 'anthropic'] as const).default('openai'),
180
194
  anthropicThinking: z.union(['omit', 'disabled', 'adaptive'] as const).default('omit'),
181
195
  userAgent: z.string(),
182
- timeoutMs: z.number(),
196
+ t1Seconds: z.number(),
197
+ t2Seconds: z.number(),
183
198
  maxImageBytes: z.number(),
184
199
  maxImagePixels: z.number(),
185
200
  concurrency: z.number(),
186
201
  attempts: z.number(),
187
202
  })).default([]),
188
203
  language: z.union(['zh', 'en'] as const).default('zh'),
189
- timeoutMs: z.number().default(30000),
204
+ hardTimeoutSeconds: z.number().default(180),
205
+ sessionMaxConcurrency: z.number().default(6),
206
+ minAvailableSeconds: z.number().default(20),
190
207
  maxImageBytes: z.number().default(4194304),
191
208
  maxImagePixels: z.number().default(20000000),
192
209
  concurrency: z.number().default(4),
@@ -195,6 +212,8 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
195
212
  agentVisionToolkitPath: z.string(),
196
213
  python: z.string(),
197
214
  }),
215
+ storageDir: z.string(),
216
+ storageHistory: z.array(z.string()).default([]),
198
217
  allowedDirs: z.array(z.string()).default([]),
199
218
  imageInputVariants: z.object({
200
219
  enabled: z.boolean().default(true),
@@ -216,7 +235,8 @@ export interface ResolvedProvider {
216
235
  protocol: 'openai' | 'anthropic'
217
236
  anthropicThinking: 'omit' | 'disabled' | 'adaptive'
218
237
  userAgent: string
219
- timeoutMs: number
238
+ t1Seconds: number
239
+ t2Seconds: number
220
240
  maxImageBytes: number
221
241
  maxImagePixels: number
222
242
  concurrency: number
@@ -236,7 +256,9 @@ export interface ResolvedVisionToolkitConfig {
236
256
  /** Ordered failover pool; array order is the priority, highest first. */
237
257
  providers: ResolvedProvider[]
238
258
  language: 'zh' | 'en'
239
- timeoutMs: number
259
+ hardTimeoutSeconds: number
260
+ sessionMaxConcurrency: number
261
+ minAvailableSeconds: number
240
262
  maxImageBytes: number
241
263
  maxImagePixels: number
242
264
  concurrency: number
@@ -245,6 +267,8 @@ export interface ResolvedVisionToolkitConfig {
245
267
  agentVisionToolkitPath?: string
246
268
  python?: string
247
269
  }
270
+ storageDir?: string
271
+ storageHistory: string[]
248
272
  allowedDirs: string[]
249
273
  imageInputVariants: {
250
274
  enabled: boolean
@@ -254,7 +278,7 @@ export interface ResolvedVisionToolkitConfig {
254
278
  }
255
279
  }
256
280
 
257
- const MAX_TIMEOUT_MS = 600000
281
+ const MAX_TIMEOUT_SECONDS = 600
258
282
  const MAX_IMAGE_BYTES = 268435456
259
283
  const MAX_IMAGE_PIXELS = 268435456
260
284
  const MAX_CONCURRENCY = 16
@@ -264,7 +288,6 @@ const DEFAULT_PROVIDER_ATTEMPTS = 3
264
288
 
265
289
  /** Global limits a provider inherits when it does not set its own. */
266
290
  interface ProviderDefaults {
267
- timeoutMs: number
268
291
  maxImageBytes: number
269
292
  maxImagePixels: number
270
293
  concurrency: number
@@ -343,9 +366,13 @@ function resolveProvider(
343
366
  if (userAgent.length === 0) {
344
367
  throw new VisionToolkitError('config', `${label}.userAgent must not be empty`)
345
368
  }
346
- const timeoutMs = input.timeoutMs ?? defaults.timeoutMs
347
- if (!Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > MAX_TIMEOUT_MS) {
348
- throw new VisionToolkitError('config', `${label}.timeoutMs must be an integer between 1000 and ${MAX_TIMEOUT_MS}`)
369
+ const t1Seconds = input.t1Seconds ?? 90
370
+ if (!Number.isInteger(t1Seconds) || t1Seconds < 1 || t1Seconds > MAX_TIMEOUT_SECONDS) {
371
+ throw new VisionToolkitError('config', `${label}.t1Seconds must be an integer between 1 and ${MAX_TIMEOUT_SECONDS}`)
372
+ }
373
+ const t2Seconds = input.t2Seconds ?? 90
374
+ if (!Number.isInteger(t2Seconds) || t2Seconds < 1 || t2Seconds > MAX_TIMEOUT_SECONDS) {
375
+ throw new VisionToolkitError('config', `${label}.t2Seconds must be an integer between 1 and ${MAX_TIMEOUT_SECONDS}`)
349
376
  }
350
377
  const maxImageBytes = input.maxImageBytes ?? defaults.maxImageBytes
351
378
  if (!Number.isInteger(maxImageBytes) || maxImageBytes < 1024 || maxImageBytes > MAX_IMAGE_BYTES) {
@@ -375,7 +402,8 @@ function resolveProvider(
375
402
  protocol,
376
403
  anthropicThinking,
377
404
  userAgent,
378
- timeoutMs,
405
+ t1Seconds,
406
+ t2Seconds,
379
407
  maxImageBytes,
380
408
  maxImagePixels,
381
409
  concurrency,
@@ -397,9 +425,17 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
397
425
  if (language !== 'zh' && language !== 'en') {
398
426
  throw new VisionToolkitError('config', 'language must be "zh" or "en"')
399
427
  }
400
- const timeoutMs = config.timeoutMs ?? 30000
401
- if (!Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > MAX_TIMEOUT_MS) {
402
- throw new VisionToolkitError('config', `timeoutMs must be an integer between 1000 and ${MAX_TIMEOUT_MS}`)
428
+ const hardTimeoutSeconds = config.hardTimeoutSeconds ?? 180
429
+ if (!Number.isInteger(hardTimeoutSeconds) || hardTimeoutSeconds < 1 || hardTimeoutSeconds > MAX_TIMEOUT_SECONDS) {
430
+ throw new VisionToolkitError('config', `hardTimeoutSeconds must be an integer between 1 and ${MAX_TIMEOUT_SECONDS}`)
431
+ }
432
+ const sessionMaxConcurrency = config.sessionMaxConcurrency ?? 6
433
+ if (!Number.isInteger(sessionMaxConcurrency) || sessionMaxConcurrency < 1 || sessionMaxConcurrency > MAX_CONCURRENCY) {
434
+ throw new VisionToolkitError('config', `sessionMaxConcurrency must be an integer between 1 and ${MAX_CONCURRENCY}`)
435
+ }
436
+ const minAvailableSeconds = config.minAvailableSeconds ?? 20
437
+ if (!Number.isInteger(minAvailableSeconds) || minAvailableSeconds < 1 || minAvailableSeconds > MAX_TIMEOUT_SECONDS) {
438
+ throw new VisionToolkitError('config', `minAvailableSeconds must be an integer between 1 and ${MAX_TIMEOUT_SECONDS}`)
403
439
  }
404
440
  const maxImageBytes = config.maxImageBytes ?? 4194304
405
441
  if (!Number.isInteger(maxImageBytes) || maxImageBytes < 1024 || maxImageBytes > MAX_IMAGE_BYTES) {
@@ -431,12 +467,16 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
431
467
  if (python !== undefined && python.length === 0) {
432
468
  throw new VisionToolkitError('config', 'runtime.python must not be empty')
433
469
  }
470
+ const storageDir = config.storageDir?.trim()
471
+ const storageHistory = [...new Set((config.storageHistory ?? [])
472
+ .map(dir => dir.trim())
473
+ .filter(dir => dir.length > 0 && dir !== storageDir))]
434
474
  const allowedDirs = (config.allowedDirs ?? []).map(dir => dir.trim()).filter(dir => dir.length > 0)
435
475
  const imageInputVariants = config.imageInputVariants ?? {}
436
476
  const variantProviders = (imageInputVariants.providers ?? [])
437
477
  .map(provider => provider.trim())
438
478
  .filter(provider => provider.length > 0)
439
- const providerDefaults: ProviderDefaults = { timeoutMs, maxImageBytes, maxImagePixels, concurrency }
479
+ const providerDefaults: ProviderDefaults = { maxImageBytes, maxImagePixels, concurrency }
440
480
  const configuredProviders = config.providers ?? []
441
481
  if (configuredProviders.length > MAX_PROVIDERS) {
442
482
  throw new VisionToolkitError('config', `providers must have at most ${MAX_PROVIDERS} entries`)
@@ -457,7 +497,9 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
457
497
  },
458
498
  providers,
459
499
  language,
460
- timeoutMs,
500
+ hardTimeoutSeconds,
501
+ sessionMaxConcurrency,
502
+ minAvailableSeconds,
461
503
  maxImageBytes,
462
504
  maxImagePixels,
463
505
  concurrency,
@@ -466,6 +508,8 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
466
508
  ...(toolkitPath !== undefined ? { agentVisionToolkitPath: toolkitPath } : {}),
467
509
  ...(python !== undefined ? { python } : {}),
468
510
  },
511
+ ...(storageDir === undefined || storageDir.length === 0 ? {} : { storageDir }),
512
+ storageHistory,
469
513
  allowedDirs,
470
514
  imageInputVariants: {
471
515
  enabled: imageInputVariants.enabled ?? true,
@@ -476,6 +520,49 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
476
520
  }
477
521
  }
478
522
 
523
+ /** Merge prior storage roots into the next resolved generation's read-only history. */
524
+ export function retainedStorageHistory(
525
+ next: VisionToolkitConfig,
526
+ previous: VisionToolkitConfig,
527
+ ): string[] {
528
+ const resolvedNext = resolveConfig(next)
529
+ const resolvedPrevious = resolveConfig(previous)
530
+ return [...new Set([
531
+ ...resolvedPrevious.storageHistory,
532
+ ...resolvedNext.storageHistory,
533
+ ...(resolvedPrevious.storageDir === undefined ? [] : [resolvedPrevious.storageDir]),
534
+ ])].filter(storageDir => storageDir !== resolvedNext.storageDir)
535
+ }
536
+
537
+ export interface WatchedSettingsGeneration {
538
+ /** Configuration to activate now; omitted after a successful history writeback. */
539
+ config?: VisionToolkitConfig
540
+ /** Whether the derived history still needs plugin-owned durable persistence. */
541
+ requiresDurableStorageHistory?: boolean
542
+ /** Non-fatal internal-history persistence error. */
543
+ persistenceError?: unknown
544
+ }
545
+
546
+ /** Prepare one live Settings generation without letting internal history writeback block activation. */
547
+ export async function prepareWatchedSettingsGeneration(
548
+ next: VisionToolkitConfig,
549
+ previous: VisionToolkitConfig,
550
+ writable: boolean,
551
+ persistStorageHistory: (storageHistory: string[]) => Promise<void>,
552
+ ): Promise<WatchedSettingsGeneration> {
553
+ const storageHistory = retainedStorageHistory(next, previous)
554
+ if (JSON.stringify(storageHistory) === JSON.stringify(resolveConfig(next).storageHistory)) return { config: next }
555
+
556
+ const config = { ...next, storageHistory }
557
+ if (!writable) return { config, requiresDurableStorageHistory: true }
558
+ try {
559
+ await persistStorageHistory(storageHistory)
560
+ return {}
561
+ } catch (persistenceError) {
562
+ return { config, requiresDurableStorageHistory: true, persistenceError }
563
+ }
564
+ }
565
+
479
566
  /** Whether a resolved provider should use the bundled public key instead of DSH credentials. */
480
567
  export function isBuiltInFreeVisionProvider(provider: ResolvedVisionToolkitConfig['provider']): boolean {
481
568
  return String(provider.credential) === BUILT_IN_FREE_VISION_CREDENTIAL
package/src/errors.ts CHANGED
@@ -5,11 +5,35 @@
5
5
  * @module dsh-vision-toolkit/errors
6
6
  */
7
7
 
8
- /** Discriminant tag for every Vision Toolkit failure. */
8
+ /**
9
+ * Discriminant tag for every Vision Toolkit failure.
10
+ *
11
+ * The remote vision-provider failures are split by a machine-routable taxonomy
12
+ * so the failover loop can decide, per error, whether to retry the SAME
13
+ * provider or advance to the next one:
14
+ *
15
+ * - `auth` 401/403 — credential is wrong or unauthorized. Never retry.
16
+ * - `quota` 402 — account out of quota/unpaid. Never retry.
17
+ * - `rate_limit` 429 — throttled. Park and revisit, never retry in place.
18
+ * - `server` 5xx — transient provider fault. Worth a same-provider retry.
19
+ * - `network` connection refused / DNS / socket — transient. Worth a retry.
20
+ * - `region` provider unavailable in this region. Never retry.
21
+ * - `tos` rejected by content/safety policy. Never retry.
22
+ * - `invalid_request` 400/404/422 — bad request or unknown model. Never retry.
23
+ * - `service` fallback for unclassifiable remote failures. Never retry.
24
+ */
9
25
  export const VISION_TOOLKIT_ERROR_CODES = [
10
26
  'config',
11
27
  'input',
12
28
  'capacity',
29
+ 'auth',
30
+ 'quota',
31
+ 'rate_limit',
32
+ 'server',
33
+ 'network',
34
+ 'region',
35
+ 'tos',
36
+ 'invalid_request',
13
37
  'service',
14
38
  'runtime',
15
39
  'output',
@@ -73,7 +73,7 @@ function hash(value: string): string {
73
73
  return createHash('sha256').update(value).digest('hex')
74
74
  }
75
75
 
76
- /** Fingerprint every runtime setting that can change the generated description. */
76
+ /** Fingerprint every runtime setting that can change generated evidence or its embedded path. */
77
77
  export function evidenceRuntimeFingerprint(
78
78
  config: ResolvedVisionToolkitConfig,
79
79
  credentialSha256?: string,
@@ -108,11 +108,14 @@ export function evidenceRuntimeFingerprint(
108
108
  attempts: provider.attempts,
109
109
  })),
110
110
  language: config.language,
111
- timeoutMs: config.timeoutMs,
111
+ hardTimeoutSeconds: config.hardTimeoutSeconds,
112
+ sessionMaxConcurrency: config.sessionMaxConcurrency,
113
+ minAvailableSeconds: config.minAvailableSeconds,
112
114
  concurrency: config.concurrency,
113
115
  maxImageBytes: config.maxImageBytes,
114
116
  maxImagePixels: config.maxImagePixels,
115
117
  runtime: config.runtime,
118
+ storageDir: config.storageDir ?? null,
116
119
  }))
117
120
  }
118
121