@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.
Files changed (101) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +15 -70
  3. package/README.zh.md +15 -69
  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 +128 -12
  10. package/lib/client.js.map +1 -1
  11. package/lib/config.js +69 -0
  12. package/lib/config.js.map +1 -1
  13. package/lib/errors.js +25 -2
  14. package/lib/errors.js.map +1 -1
  15. package/lib/evidence-cache.js +2 -1
  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/object-storage.js +141 -0
  24. package/lib/object-storage.js.map +1 -0
  25. package/lib/paste-images.js +67 -19
  26. package/lib/paste-images.js.map +1 -1
  27. package/lib/paths.js +214 -28
  28. package/lib/paths.js.map +1 -1
  29. package/lib/runtime-manager.js +76 -10
  30. package/lib/runtime-manager.js.map +1 -1
  31. package/lib/runtime.js +219 -38
  32. package/lib/runtime.js.map +1 -1
  33. package/lib/storage-history.js +154 -0
  34. package/lib/storage-history.js.map +1 -0
  35. package/lib/types/artifact-access.d.ts.map +1 -1
  36. package/lib/types/client/index.d.ts +45 -2
  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 +55 -0
  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/object-storage.d.ts +54 -0
  51. package/lib/types/object-storage.d.ts.map +1 -0
  52. package/lib/types/paste-images.d.ts +12 -4
  53. package/lib/types/paste-images.d.ts.map +1 -1
  54. package/lib/types/paths.d.ts +31 -5
  55. package/lib/types/paths.d.ts.map +1 -1
  56. package/lib/types/runtime-manager.d.ts +28 -4
  57. package/lib/types/runtime-manager.d.ts.map +1 -1
  58. package/lib/types/runtime.d.ts +31 -1
  59. package/lib/types/runtime.d.ts.map +1 -1
  60. package/lib/types/storage-history.d.ts +63 -0
  61. package/lib/types/storage-history.d.ts.map +1 -0
  62. package/lib/types/upstream.d.ts +1 -0
  63. package/lib/types/upstream.d.ts.map +1 -1
  64. package/lib/types/web.d.ts +7 -0
  65. package/lib/types/web.d.ts.map +1 -1
  66. package/lib/upstream.js +34 -8
  67. package/lib/upstream.js.map +1 -1
  68. package/lib/web.js +44 -9
  69. package/lib/web.js.map +1 -1
  70. package/package.json +3 -1
  71. package/src/artifact-access.ts +22 -2
  72. package/src/client/index.tsx +169 -9
  73. package/src/client/paste-images.tsx +14 -4
  74. package/src/config.ts +128 -0
  75. package/src/errors.ts +25 -2
  76. package/src/evidence-cache.ts +2 -1
  77. package/src/exposure.ts +16 -2
  78. package/src/image-input-variants.ts +21 -6
  79. package/src/index.ts +65 -6
  80. package/src/object-storage.ts +174 -0
  81. package/src/paste-images.ts +81 -19
  82. package/src/paths.ts +249 -28
  83. package/src/runtime-manager.ts +93 -10
  84. package/src/runtime.ts +215 -36
  85. package/src/storage-history.ts +172 -0
  86. package/src/upstream.ts +36 -7
  87. package/src/web.ts +54 -9
  88. package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +11 -11
  89. package/vendor/agent-vision-toolkit/__pycache__/detect.cpython-314.pyc +0 -0
  90. package/vendor/agent-vision-toolkit/__pycache__/ground.cpython-314.pyc +0 -0
  91. package/vendor/agent-vision-toolkit/__pycache__/vision_client.cpython-314.pyc +0 -0
  92. package/vendor/agent-vision-toolkit/bin/__pycache__/glancecpython-314.pyc +0 -0
  93. package/vendor/agent-vision-toolkit/bin/glance +8 -1
  94. package/vendor/agent-vision-toolkit/detect.py +13 -7
  95. package/vendor/agent-vision-toolkit/ground.py +43 -18
  96. package/vendor/agent-vision-toolkit/tests/test_vision_client.py +88 -0
  97. package/vendor/agent-vision-toolkit/vision_client.py +84 -6
  98. package/assets/community-group-qr.png +0 -0
  99. package/assets/logo_aihubmix.png +0 -0
  100. package/assets/logo_eapi_dark.png +0 -0
  101. package/assets/wechat-reward.png +0 -0
@@ -408,6 +408,94 @@ def main():
408
408
  os.environ.pop("VISION_API_PROTOCOL", None)
409
409
  assert Handler.calls == 0
410
410
 
411
+ # VISION_STREAM truthiness mirrors VISION_SSL_VERIFY: off by default.
412
+ for enabled_value in ("1", "true", "yes", "on", " TRUE "):
413
+ os.environ["VISION_STREAM"] = enabled_value
414
+ assert vision_client._stream_enabled() is True
415
+ for disabled_value in ("", "0", "false", "no", "off", "disabled"):
416
+ os.environ["VISION_STREAM"] = disabled_value
417
+ assert vision_client._stream_enabled() is False
418
+ os.environ.pop("VISION_STREAM", None)
419
+
420
+ # Streamed chat_completions: `stream: true` is sent and deltas accumulate.
421
+ Handler.calls, Handler.statuses, Handler.bodies, Handler.response_headers = 0, [200], [
422
+ (
423
+ 'data: {"choices":[{"delta":{"content":"Hello "}}]}\n\n'
424
+ 'data: {"choices":[{"delta":{"content":"streamed"}}]}\n\n'
425
+ 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n'
426
+ 'data: [DONE]\n\n'
427
+ ).encode(),
428
+ ], []
429
+ os.environ["VISION_STREAM"] = "1"
430
+ try:
431
+ assert vision_client.describe_image("data:image/png;base64,AAAA") == "Hello streamed"
432
+ finally:
433
+ os.environ.pop("VISION_STREAM", None)
434
+ assert json.loads(Handler.last_body)["stream"] is True
435
+ assert Handler.calls == 1
436
+
437
+ # Default is non-streaming: no `stream` key is sent.
438
+ Handler.calls, Handler.statuses, Handler.bodies = 0, [200], []
439
+ vision_client.describe_image("data:image/png;base64,AAAA")
440
+ assert "stream" not in json.loads(Handler.last_body)
441
+ assert Handler.calls == 1
442
+
443
+ # Anthropic streaming: text_delta deltas accumulate across blocks.
444
+ Handler.calls, Handler.statuses, Handler.bodies, Handler.response_headers = 0, [200], [
445
+ (
446
+ 'data: {"type":"message_start","message":{"id":"m1"}}\n\n'
447
+ 'data: {"type":"content_block_start","index":0,"content_block":{"type":"text"}}\n\n'
448
+ 'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"anthropic "}}\n\n'
449
+ 'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"streamed"}}\n\n'
450
+ 'data: {"type":"content_block_stop","index":0}\n\n'
451
+ 'data: {"type":"message_stop"}\n\n'
452
+ ).encode(),
453
+ ], []
454
+ os.environ["VISION_API_PROTOCOL"] = "anthropic"
455
+ os.environ["VISION_STREAM"] = "1"
456
+ try:
457
+ assert vision_client.describe_image("data:image/png;base64,AAAA") == "anthropic streamed"
458
+ finally:
459
+ os.environ.pop("VISION_API_PROTOCOL", None)
460
+ os.environ.pop("VISION_STREAM", None)
461
+ assert json.loads(Handler.last_body)["stream"] is True
462
+ assert Handler.calls == 1
463
+
464
+ # Responses streaming: output_text deltas accumulate.
465
+ Handler.calls, Handler.statuses, Handler.bodies, Handler.response_headers = 0, [200], [
466
+ (
467
+ 'data: {"type":"response.output_text.delta","delta":"responses "}\n\n'
468
+ 'data: {"type":"response.output_text.delta","delta":"streamed"}\n\n'
469
+ 'data: {"type":"response.completed"}\n\n'
470
+ ).encode(),
471
+ ], []
472
+ os.environ["VISION_API_PROTOCOL"] = "responses"
473
+ os.environ["VISION_STREAM"] = "1"
474
+ try:
475
+ assert vision_client.describe_image("data:image/png;base64,AAAA") == "responses streamed"
476
+ finally:
477
+ os.environ.pop("VISION_API_PROTOCOL", None)
478
+ os.environ.pop("VISION_STREAM", None)
479
+ assert json.loads(Handler.last_body)["stream"] is True
480
+ assert Handler.calls == 1
481
+
482
+ # A streamed error event surfaces as a redacted VisionError.
483
+ Handler.calls, Handler.statuses, Handler.bodies = 0, [200], [
484
+ b'data: {"error":{"message":"overloaded for test-key","code":"overloaded"}}\n\n'
485
+ ]
486
+ os.environ["VISION_STREAM"] = "1"
487
+ try:
488
+ try:
489
+ vision_client.describe_image("data:image/png;base64,AAAA")
490
+ except vision_client.VisionError as exc:
491
+ assert "test-key" not in str(exc)
492
+ assert "<redacted>" in str(exc)
493
+ else:
494
+ raise AssertionError("streamed errors must fail cleanly")
495
+ finally:
496
+ os.environ.pop("VISION_STREAM", None)
497
+ assert Handler.calls == 1
498
+
411
499
  Handler.calls, Handler.statuses, Handler.bodies = 0, [200], []
412
500
  with tempfile.TemporaryDirectory() as raw:
413
501
  image = Path(raw) / "fixture.png"
@@ -196,6 +196,77 @@ def _retryable_http_error(status: int, body: bytes) -> bool:
196
196
  }
197
197
 
198
198
 
199
+ def _stream_enabled() -> bool:
200
+ """Whether VISION_STREAM asks for a streamed completion (default off)."""
201
+ value = os.environ.get("VISION_STREAM", "").strip().lower()
202
+ return value in {"1", "true", "yes", "on"}
203
+
204
+
205
+ def _stream_error(payload: dict) -> str | None:
206
+ """Return a human-readable error detail from one SSE event, or None."""
207
+ error = payload.get("error")
208
+ if isinstance(error, dict):
209
+ code = error.get("code")
210
+ message = error.get("message")
211
+ if code is not None or message is not None:
212
+ return str(message) if message is not None else str(code)
213
+ return json.dumps(error)
214
+ if payload.get("type") == "error":
215
+ error = payload.get("error")
216
+ if isinstance(error, dict) and error.get("message") is not None:
217
+ return str(error["message"])
218
+ return None
219
+
220
+
221
+ def _stream_text(stream, protocol: str, api_key: str) -> str:
222
+ """Accumulate the text deltas of a server-sent-events completion stream."""
223
+ parts: list[str] = []
224
+ error_detail: str | None = None
225
+ for raw_line in stream:
226
+ if isinstance(raw_line, (bytes, bytearray)):
227
+ line = raw_line.decode("utf-8", errors="replace")
228
+ else:
229
+ line = str(raw_line)
230
+ line = line.strip()
231
+ if not line or line.startswith(":"):
232
+ continue
233
+ data = line[5:].strip() if line.startswith("data:") else line
234
+ if data == "[DONE]":
235
+ break
236
+ try:
237
+ payload = json.loads(data)
238
+ except (json.JSONDecodeError, ValueError):
239
+ continue
240
+ if not isinstance(payload, dict):
241
+ continue
242
+ error_detail = _stream_error(payload)
243
+ if error_detail is not None:
244
+ break
245
+ if protocol == "anthropic":
246
+ if payload.get("type") == "content_block_delta":
247
+ delta = payload.get("delta")
248
+ if isinstance(delta, dict) and delta.get("type") == "text_delta":
249
+ text = delta.get("text")
250
+ if isinstance(text, str):
251
+ parts.append(text)
252
+ elif protocol == "responses":
253
+ if payload.get("type") == "response.output_text.delta":
254
+ delta = payload.get("delta")
255
+ if isinstance(delta, str):
256
+ parts.append(delta)
257
+ else: # chat_completions
258
+ choices = payload.get("choices")
259
+ if isinstance(choices, list) and choices:
260
+ choice = choices[0]
261
+ delta = choice.get("delta") if isinstance(choice, dict) else None
262
+ content = delta.get("content") if isinstance(delta, dict) else None
263
+ if isinstance(content, str):
264
+ parts.append(content)
265
+ if error_detail is not None:
266
+ raise VisionError(f"Vision API stream error: {_redact(error_detail, api_key)}")
267
+ return "".join(parts)
268
+
269
+
199
270
  def describe_image(image_url: str | list[str], prompt: str | None = None, max_tokens: int = 4096,
200
271
  apply_lang: bool = True) -> str:
201
272
  """Describe one data/http image URL (str) or several (list) in a single call."""
@@ -216,6 +287,7 @@ def describe_image(image_url: str | list[str], prompt: str | None = None, max_to
216
287
  text = f"{instruction}\n\n{text}"
217
288
  model = _required("VISION_MODEL")
218
289
  protocol = os.environ.get("VISION_API_PROTOCOL", "").strip().lower() or "chat_completions"
290
+ stream = _stream_enabled()
219
291
  if protocol == "responses":
220
292
  payload = {
221
293
  "model": model,
@@ -263,6 +335,8 @@ def describe_image(image_url: str | list[str], prompt: str | None = None, max_to
263
335
  raise VisionError(
264
336
  "Unsupported VISION_API_PROTOCOL; use chat_completions, responses, or anthropic"
265
337
  )
338
+ if stream:
339
+ payload["stream"] = True
266
340
  headers = {
267
341
  "Content-Type": "application/json",
268
342
  "User-Agent": user_agent,
@@ -280,12 +354,16 @@ def describe_image(image_url: str | list[str], prompt: str | None = None, max_to
280
354
  timeout = 180
281
355
  for attempt in range(retries + 1):
282
356
  try:
283
- with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
284
- data = json.load(response)
285
- try:
286
- text = extract_text(data)
287
- except (KeyError, IndexError, TypeError) as exc:
288
- raise VisionError("Vision API returned an incompatible response structure") from exc
357
+ if stream:
358
+ with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
359
+ text = _stream_text(response, protocol, api_key)
360
+ else:
361
+ with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
362
+ data = json.load(response)
363
+ try:
364
+ text = extract_text(data)
365
+ except (KeyError, IndexError, TypeError) as exc:
366
+ raise VisionError("Vision API returned an incompatible response structure") from exc
289
367
  if not text:
290
368
  raise VisionError("Vision API returned an empty description")
291
369
  return text
Binary file
Binary file
Binary file
Binary file