@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
|
@@ -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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
package/assets/logo_aihubmix.png
DELETED
|
Binary file
|
|
Binary file
|
package/assets/wechat-reward.png
DELETED
|
Binary file
|