@mengruo/dsh-vision-toolkit 0.0.1
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +356 -0
- package/README.zh.md +354 -0
- package/assets/community-group-qr.png +0 -0
- package/assets/dsh-conversation-artifact.png +0 -0
- package/assets/dsh-conversation-image-qa-top.png +0 -0
- package/assets/dsh-conversation-image-qa.png +0 -0
- package/assets/dsh-conversation-pixel-diff.png +0 -0
- package/assets/dsh-conversation-screenshot-debugging-top.png +0 -0
- package/assets/dsh-conversation-screenshot-debugging.png +0 -0
- package/assets/dsh-conversation-tool-call.png +0 -0
- package/assets/dsh-conversation-vision-trace.png +0 -0
- package/assets/dsh-view-example.png +0 -0
- package/assets/hero-v2.png +0 -0
- package/assets/logo_aihubmix.png +0 -0
- package/assets/python-bootstrap.json +48 -0
- package/assets/skill/SKILL.md +329 -0
- package/assets/skill/UPSTREAM.json +71 -0
- package/assets/skill/references/gui.md +88 -0
- package/assets/skill/references/long-screenshot-ocr.md +77 -0
- package/assets/skill/references/restore-graphic.md +84 -0
- package/assets/skill/references/restore-structure.md +45 -0
- package/assets/skill/references/restore-ui.md +202 -0
- package/assets/social-preview.png +0 -0
- package/assets/upstream/README.md +18 -0
- package/assets/upstream/focus-hint-comparison-1.webp +0 -0
- package/assets/upstream/focus-hint-comparison-2.webp +0 -0
- package/assets/upstream/infographic-reference.webp +0 -0
- package/assets/upstream/infographic-result.webp +0 -0
- package/assets/upstream/ui-fast-restore-reference.webp +0 -0
- package/assets/upstream/ui-fast-restore-result.webp +0 -0
- package/assets/upstream/ui-result.webp +0 -0
- package/assets/upstream/ui-sketch.webp +0 -0
- package/assets/vision-model-test.png +0 -0
- package/assets/vision-settings.png +0 -0
- package/assets/wechat-reward.png +0 -0
- package/cordis.patch.yml +6 -0
- package/docs/aihubmix-gemini-vision.i18n.yaml +6 -0
- package/docs/aihubmix-gemini-vision.md +138 -0
- package/docs/aihubmix-gemini-vision.zh.md +138 -0
- package/docs/assets/aihubmix-api-keys.png +0 -0
- package/docs/assets/aihubmix-create-key.png +0 -0
- package/docs/assets/aihubmix-free-vision-model.png +0 -0
- package/docs/assets/aihubmix-home.png +0 -0
- package/docs/assets/aihubmix-sign-up.png +0 -0
- package/docs/assets/groq-console-home.png +0 -0
- package/docs/assets/groq-console-keys-login.png +0 -0
- package/docs/assets/groq-docs-quickstart.png +0 -0
- package/docs/assets/groq-docs-qwen3.6.png +0 -0
- package/docs/assets/groq-docs-vision.png +0 -0
- package/docs/assets/vision-settings.png +0 -0
- package/docs/dsh-desktop-install.md +67 -0
- package/docs/dsh-desktop-install.zh.md +67 -0
- package/docs/groq-qwen3.6-vision.i18n.yaml +6 -0
- package/docs/groq-qwen3.6-vision.md +236 -0
- package/docs/groq-qwen3.6-vision.zh.md +236 -0
- package/docs/python-runtime.i18n.yaml +6 -0
- package/docs/python-runtime.md +89 -0
- package/docs/python-runtime.zh.md +89 -0
- package/docs/requirements-traceability/README.i18n.yaml +6 -0
- package/docs/requirements-traceability/README.md +75 -0
- package/docs/requirements-traceability/README.zh.md +75 -0
- package/examples/ui-restoration/README.i18n.yaml +6 -0
- package/examples/ui-restoration/README.md +70 -0
- package/examples/ui-restoration/README.zh.md +70 -0
- package/examples/ui-restoration/assets/final-heatmap.png +0 -0
- package/examples/ui-restoration/assets/final-report.json +83 -0
- package/examples/ui-restoration/assets/implementation.png +0 -0
- package/examples/ui-restoration/assets/initial-heatmap.png +0 -0
- package/examples/ui-restoration/assets/initial-report.json +83 -0
- package/examples/ui-restoration/assets/initial.png +0 -0
- package/examples/ui-restoration/assets/metrics.json +12 -0
- package/examples/ui-restoration/assets/reference.png +0 -0
- package/examples/ui-restoration/implementation.html +94 -0
- package/examples/ui-restoration/initial.html +57 -0
- package/lib/artifact-access.js +369 -0
- package/lib/artifact-access.js.map +1 -0
- package/lib/artifacts.js +56 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/client.js +2099 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +172 -0
- package/lib/config.js.map +1 -0
- package/lib/defaults.js +6 -0
- package/lib/defaults.js.map +1 -0
- package/lib/errors.js +56 -0
- package/lib/errors.js.map +1 -0
- package/lib/evidence-cache.js +350 -0
- package/lib/evidence-cache.js.map +1 -0
- package/lib/exposure.js +247 -0
- package/lib/exposure.js.map +1 -0
- package/lib/image-input-variants.js +882 -0
- package/lib/image-input-variants.js.map +1 -0
- package/lib/index.js +105 -0
- package/lib/index.js.map +1 -0
- package/lib/paste-images.js +217 -0
- package/lib/paste-images.js.map +1 -0
- package/lib/paths.js +354 -0
- package/lib/paths.js.map +1 -0
- package/lib/plugin-update.js +1003 -0
- package/lib/plugin-update.js.map +1 -0
- package/lib/runtime-install.js +964 -0
- package/lib/runtime-install.js.map +1 -0
- package/lib/runtime-manager.js +133 -0
- package/lib/runtime-manager.js.map +1 -0
- package/lib/runtime.js +1678 -0
- package/lib/runtime.js.map +1 -0
- package/lib/skill.js +25 -0
- package/lib/skill.js.map +1 -0
- package/lib/tools.js +549 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/artifact-access.d.ts +61 -0
- package/lib/types/artifact-access.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +42 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/client/display-config.d.ts +24 -0
- package/lib/types/client/display-config.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +350 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/model-variants-hider.d.ts +40 -0
- package/lib/types/client/model-variants-hider.d.ts.map +1 -0
- package/lib/types/client/paste-images.d.ts +126 -0
- package/lib/types/client/paste-images.d.ts.map +1 -0
- package/lib/types/config.d.ts +123 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/defaults.d.ts +6 -0
- package/lib/types/defaults.d.ts.map +1 -0
- package/lib/types/errors.d.ts +35 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/evidence-cache.d.ts +91 -0
- package/lib/types/evidence-cache.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +50 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/image-input-variants.d.ts +144 -0
- package/lib/types/image-input-variants.d.ts.map +1 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/paste-images.d.ts +73 -0
- package/lib/types/paste-images.d.ts.map +1 -0
- package/lib/types/paths.d.ts +117 -0
- package/lib/types/paths.d.ts.map +1 -0
- package/lib/types/plugin-update.d.ts +119 -0
- package/lib/types/plugin-update.d.ts.map +1 -0
- package/lib/types/runtime-install.d.ts +92 -0
- package/lib/types/runtime-install.d.ts.map +1 -0
- package/lib/types/runtime-manager.d.ts +60 -0
- package/lib/types/runtime-manager.d.ts.map +1 -0
- package/lib/types/runtime.d.ts +412 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/skill.d.ts +14 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/tools.d.ts +35 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/upstream.d.ts +233 -0
- package/lib/types/upstream.d.ts.map +1 -0
- package/lib/types/version.d.ts +15 -0
- package/lib/types/version.d.ts.map +1 -0
- package/lib/types/web-request.d.ts +11 -0
- package/lib/types/web-request.d.ts.map +1 -0
- package/lib/types/web.d.ts +118 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/upstream.js +860 -0
- package/lib/upstream.js.map +1 -0
- package/lib/version.js +18 -0
- package/lib/version.js.map +1 -0
- package/lib/web-request.js +29 -0
- package/lib/web-request.js.map +1 -0
- package/lib/web.js +462 -0
- package/lib/web.js.map +1 -0
- package/package.json +149 -0
- package/patches/vision-tools-dsh.patch +970 -0
- package/runtime/requirements.lock +3 -0
- package/src/artifact-access.ts +386 -0
- package/src/artifacts.ts +85 -0
- package/src/client/display-config.ts +62 -0
- package/src/client/index.tsx +1580 -0
- package/src/client/model-variants-hider.ts +159 -0
- package/src/client/paste-images.tsx +747 -0
- package/src/config.ts +290 -0
- package/src/defaults.ts +5 -0
- package/src/errors.ts +62 -0
- package/src/evidence-cache.ts +418 -0
- package/src/exposure.ts +265 -0
- package/src/image-input-variants.ts +996 -0
- package/src/index.ts +141 -0
- package/src/paste-images.ts +286 -0
- package/src/paths.ts +383 -0
- package/src/plugin-update.ts +1143 -0
- package/src/runtime-install.ts +1161 -0
- package/src/runtime-manager.ts +174 -0
- package/src/runtime.ts +2189 -0
- package/src/skill.ts +35 -0
- package/src/tools.ts +691 -0
- package/src/upstream.ts +1078 -0
- package/src/version.ts +37 -0
- package/src/web-request.ts +27 -0
- package/src/web.ts +594 -0
- package/vendor/agent-vision-toolkit/CHANGELOG.md +30 -0
- package/vendor/agent-vision-toolkit/LICENSE +21 -0
- package/vendor/agent-vision-toolkit/README.md +405 -0
- package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +94 -0
- package/vendor/agent-vision-toolkit/bin/crop +90 -0
- package/vendor/agent-vision-toolkit/bin/detect +13 -0
- package/vendor/agent-vision-toolkit/bin/glance +93 -0
- package/vendor/agent-vision-toolkit/bin/ground +13 -0
- package/vendor/agent-vision-toolkit/bin/trace +129 -0
- package/vendor/agent-vision-toolkit/detect.py +60 -0
- package/vendor/agent-vision-toolkit/ground.py +267 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/dominant_colors.py +224 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/extract_fg.py +278 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/html_shot.py +420 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/long_screenshot_ocr.py +1245 -0
- package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/pixel_diff.py +88 -0
- package/vendor/agent-vision-toolkit/tests/test_vision_client.py +450 -0
- package/vendor/agent-vision-toolkit/vision_client.py +310 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared multi-provider vision client used by the proxy and glance CLI."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
from email.utils import parsedate_to_datetime
|
|
8
|
+
import http.client
|
|
9
|
+
import json
|
|
10
|
+
import mimetypes
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import ssl
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import urllib.error
|
|
17
|
+
import urllib.request
|
|
18
|
+
|
|
19
|
+
DEFAULT_PROMPT = "Please describe the contents of this image in detail."
|
|
20
|
+
DEFAULT_USER_AGENT = (
|
|
21
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
22
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
23
|
+
"Chrome/126.0.0.0 Safari/537.36"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
LANG_INSTRUCTIONS = {
|
|
27
|
+
"zh": "请使用简体中文回答。",
|
|
28
|
+
"en": "Please respond in English.",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class VisionError(RuntimeError):
|
|
33
|
+
"""A safe, user-facing vision request failure."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_env_file(path: str | os.PathLike[str] | None) -> None:
|
|
37
|
+
if not path:
|
|
38
|
+
return
|
|
39
|
+
env_path = Path(path).expanduser()
|
|
40
|
+
if not env_path.is_file():
|
|
41
|
+
return
|
|
42
|
+
for raw_line in env_path.read_text().splitlines():
|
|
43
|
+
line = raw_line.strip()
|
|
44
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
45
|
+
continue
|
|
46
|
+
key, _, value = line.partition("=")
|
|
47
|
+
key = key.strip()
|
|
48
|
+
value = value.strip().strip('"').strip("'")
|
|
49
|
+
# The env file is the user's explicit configuration: whatever it sets wins,
|
|
50
|
+
# even when the same variable already exists in the system environment.
|
|
51
|
+
if key:
|
|
52
|
+
os.environ[key] = value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_default_env() -> None:
|
|
56
|
+
explicit = os.environ.get("VISION_ENV_FILE")
|
|
57
|
+
if explicit:
|
|
58
|
+
load_env_file(Path(explicit).expanduser())
|
|
59
|
+
return
|
|
60
|
+
candidates = []
|
|
61
|
+
local_appdata = os.environ.get("LOCALAPPDATA")
|
|
62
|
+
if local_appdata:
|
|
63
|
+
candidates.append(Path(local_appdata) / "agent-vision-toolkit" / "env")
|
|
64
|
+
candidates.extend([
|
|
65
|
+
Path.home() / ".config" / "agent-vision-toolkit" / "env",
|
|
66
|
+
Path(__file__).resolve().parent / ".env",
|
|
67
|
+
Path.cwd() / ".env",
|
|
68
|
+
])
|
|
69
|
+
for path in candidates:
|
|
70
|
+
load_env_file(path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _required(name: str) -> str:
|
|
74
|
+
value = os.environ.get(name, "").strip()
|
|
75
|
+
if not value:
|
|
76
|
+
raise VisionError(f"Missing config {name}; fill it in the .env file")
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def validate_vision_config() -> None:
|
|
81
|
+
for name in ("VISION_API_KEY", "VISION_BASE_URL", "VISION_MODEL"):
|
|
82
|
+
_required(name)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def image_path_to_data_url(path: str | os.PathLike[str]) -> str:
|
|
86
|
+
image_path = Path(path).expanduser()
|
|
87
|
+
if not image_path.is_file():
|
|
88
|
+
raise VisionError(f"Image not found: {image_path}")
|
|
89
|
+
mime, _ = mimetypes.guess_type(image_path.name)
|
|
90
|
+
if mime not in {"image/png", "image/jpeg", "image/gif", "image/webp"}:
|
|
91
|
+
raise VisionError("Only PNG, JPEG, GIF, and WebP images are supported")
|
|
92
|
+
return f"data:{mime};base64,{base64.b64encode(image_path.read_bytes()).decode()}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _message_text(message: object) -> str:
|
|
96
|
+
if isinstance(message, str):
|
|
97
|
+
return message.strip()
|
|
98
|
+
if isinstance(message, list):
|
|
99
|
+
return "\n".join(
|
|
100
|
+
part["text"] for part in message
|
|
101
|
+
if isinstance(part, dict) and isinstance(part.get("text"), str)
|
|
102
|
+
).strip()
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _responses_text(response: object) -> str:
|
|
107
|
+
if not isinstance(response, dict) or not isinstance(response.get("output"), list):
|
|
108
|
+
return ""
|
|
109
|
+
return "\n".join(
|
|
110
|
+
part["text"]
|
|
111
|
+
for item in response["output"]
|
|
112
|
+
if isinstance(item, dict) and item.get("type") == "message"
|
|
113
|
+
and isinstance(item.get("content"), list)
|
|
114
|
+
for part in item["content"]
|
|
115
|
+
if isinstance(part, dict) and part.get("type") == "output_text"
|
|
116
|
+
and isinstance(part.get("text"), str)
|
|
117
|
+
).strip()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _anthropic_image_source(url: str) -> dict[str, str]:
|
|
121
|
+
if not url.startswith("data:"):
|
|
122
|
+
return {"type": "url", "url": url}
|
|
123
|
+
header, separator, data = url.partition(",")
|
|
124
|
+
if separator == "" or ";base64" not in header:
|
|
125
|
+
raise VisionError("Anthropic image data URLs must use base64 encoding")
|
|
126
|
+
media_type = header[5:].split(";", 1)[0]
|
|
127
|
+
if not media_type:
|
|
128
|
+
raise VisionError("Anthropic image data URLs must include a media type")
|
|
129
|
+
return {"type": "base64", "media_type": media_type, "data": data}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _anthropic_text(response: object) -> str:
|
|
133
|
+
if not isinstance(response, dict) or not isinstance(response.get("content"), list):
|
|
134
|
+
return ""
|
|
135
|
+
return "\n".join(
|
|
136
|
+
block["text"]
|
|
137
|
+
for block in response["content"]
|
|
138
|
+
if isinstance(block, dict) and block.get("type") == "text"
|
|
139
|
+
and isinstance(block.get("text"), str)
|
|
140
|
+
).strip()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _redact(text: str, *secrets: str) -> str:
|
|
144
|
+
for secret in secrets:
|
|
145
|
+
if secret:
|
|
146
|
+
text = text.replace(secret, "<redacted>")
|
|
147
|
+
return text
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _ssl_context() -> ssl.SSLContext | None:
|
|
151
|
+
"""Return an unverified context when VISION_SSL_VERIFY disables certificate checks."""
|
|
152
|
+
verify = os.environ.get("VISION_SSL_VERIFY", "").strip().lower()
|
|
153
|
+
if verify in {"0", "false", "off", "no", "none", "disabled"}:
|
|
154
|
+
context = ssl.create_default_context()
|
|
155
|
+
context.check_hostname = False
|
|
156
|
+
context.verify_mode = ssl.CERT_NONE
|
|
157
|
+
return context
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _retry_delay(error: urllib.error.HTTPError, attempt: int) -> float:
|
|
162
|
+
value = error.headers.get("Retry-After")
|
|
163
|
+
if value:
|
|
164
|
+
try:
|
|
165
|
+
return max(0.0, min(float(value), 60.0))
|
|
166
|
+
except ValueError:
|
|
167
|
+
try:
|
|
168
|
+
retry_at = parsedate_to_datetime(value)
|
|
169
|
+
return max(0.0, min(retry_at.timestamp() - time.time(), 60.0))
|
|
170
|
+
except (TypeError, ValueError, OverflowError):
|
|
171
|
+
pass
|
|
172
|
+
return min(2 ** attempt, 4)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _api_error_code(body: bytes) -> str:
|
|
176
|
+
try:
|
|
177
|
+
payload = json.loads(body.decode(errors="replace"))
|
|
178
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
179
|
+
return ""
|
|
180
|
+
if not isinstance(payload, dict):
|
|
181
|
+
return ""
|
|
182
|
+
error = payload.get("error")
|
|
183
|
+
if not isinstance(error, dict):
|
|
184
|
+
return ""
|
|
185
|
+
code = error.get("code")
|
|
186
|
+
return code if isinstance(code, str) else ""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _retryable_http_error(status: int, body: bytes) -> bool:
|
|
190
|
+
if status not in {429, 500, 502, 503, 504, 529}:
|
|
191
|
+
return False
|
|
192
|
+
return _api_error_code(body) not in {
|
|
193
|
+
"daily_rate_limit_exceeded",
|
|
194
|
+
"global_daily_limit_exceeded",
|
|
195
|
+
"rate_limit_exceeded",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def describe_image(image_url: str | list[str], prompt: str | None = None, max_tokens: int = 4096,
|
|
200
|
+
apply_lang: bool = True) -> str:
|
|
201
|
+
"""Describe one data/http image URL (str) or several (list) in a single call."""
|
|
202
|
+
validate_vision_config()
|
|
203
|
+
urls = [image_url] if isinstance(image_url, str) else list(image_url)
|
|
204
|
+
if not urls:
|
|
205
|
+
raise VisionError("No image was provided")
|
|
206
|
+
for url in urls:
|
|
207
|
+
if not url.startswith(("data:", "http://", "https://")):
|
|
208
|
+
raise VisionError("Only data URLs or http(s) image URLs are supported")
|
|
209
|
+
base_url = _required("VISION_BASE_URL").rstrip("/")
|
|
210
|
+
api_key = _required("VISION_API_KEY")
|
|
211
|
+
user_agent = os.environ.get("VISION_USER_AGENT", "").strip() or DEFAULT_USER_AGENT
|
|
212
|
+
text = prompt or DEFAULT_PROMPT
|
|
213
|
+
if apply_lang:
|
|
214
|
+
instruction = LANG_INSTRUCTIONS.get(os.environ.get("LANG", "").strip().lower())
|
|
215
|
+
if instruction:
|
|
216
|
+
text = f"{instruction}\n\n{text}"
|
|
217
|
+
model = _required("VISION_MODEL")
|
|
218
|
+
protocol = os.environ.get("VISION_API_PROTOCOL", "").strip().lower() or "chat_completions"
|
|
219
|
+
if protocol == "responses":
|
|
220
|
+
payload = {
|
|
221
|
+
"model": model,
|
|
222
|
+
"store": False,
|
|
223
|
+
"input": [{"role": "user", "content": [
|
|
224
|
+
{"type": "input_image", "image_url": url} for url in urls
|
|
225
|
+
] + [{"type": "input_text", "text": text}]}],
|
|
226
|
+
}
|
|
227
|
+
if max_tokens is not None:
|
|
228
|
+
payload["max_output_tokens"] = max_tokens
|
|
229
|
+
reasoning_effort = os.environ.get("VISION_REASONING_EFFORT", "").strip()
|
|
230
|
+
if reasoning_effort:
|
|
231
|
+
payload["reasoning"] = {"effort": reasoning_effort}
|
|
232
|
+
endpoint = "/responses"
|
|
233
|
+
extract_text = _responses_text
|
|
234
|
+
elif protocol == "chat_completions":
|
|
235
|
+
payload = {
|
|
236
|
+
"model": model,
|
|
237
|
+
"messages": [{"role": "user", "content": [
|
|
238
|
+
{"type": "image_url", "image_url": {"url": url}} for url in urls
|
|
239
|
+
] + [{"type": "text", "text": text}]}],
|
|
240
|
+
}
|
|
241
|
+
if max_tokens is not None:
|
|
242
|
+
payload["max_tokens"] = max_tokens
|
|
243
|
+
endpoint = "/chat/completions"
|
|
244
|
+
extract_text = lambda data: _message_text(data["choices"][0]["message"]["content"])
|
|
245
|
+
elif protocol == "anthropic":
|
|
246
|
+
payload = {
|
|
247
|
+
"model": model,
|
|
248
|
+
"max_tokens": max_tokens if max_tokens is not None else 4096,
|
|
249
|
+
"messages": [{"role": "user", "content": [
|
|
250
|
+
{"type": "image", "source": _anthropic_image_source(url)} for url in urls
|
|
251
|
+
] + [{"type": "text", "text": text}]}],
|
|
252
|
+
}
|
|
253
|
+
thinking = os.environ.get("VISION_ANTHROPIC_THINKING", "").strip().lower() or "omit"
|
|
254
|
+
if thinking != "omit":
|
|
255
|
+
if thinking not in {"disabled", "adaptive"}:
|
|
256
|
+
raise VisionError(
|
|
257
|
+
"Unsupported VISION_ANTHROPIC_THINKING; use omit, disabled, or adaptive"
|
|
258
|
+
)
|
|
259
|
+
payload["thinking"] = {"type": thinking}
|
|
260
|
+
endpoint = "/messages"
|
|
261
|
+
extract_text = _anthropic_text
|
|
262
|
+
else:
|
|
263
|
+
raise VisionError(
|
|
264
|
+
"Unsupported VISION_API_PROTOCOL; use chat_completions, responses, or anthropic"
|
|
265
|
+
)
|
|
266
|
+
headers = {
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
"User-Agent": user_agent,
|
|
269
|
+
}
|
|
270
|
+
if protocol == "anthropic":
|
|
271
|
+
headers.update({
|
|
272
|
+
"x-api-key": api_key,
|
|
273
|
+
"anthropic-version": "2023-06-01",
|
|
274
|
+
})
|
|
275
|
+
else:
|
|
276
|
+
headers["Authorization"] = "Bearer " + api_key
|
|
277
|
+
request = urllib.request.Request(base_url + endpoint, data=json.dumps(payload).encode(), headers=headers)
|
|
278
|
+
context = _ssl_context()
|
|
279
|
+
retries = 2
|
|
280
|
+
timeout = 180
|
|
281
|
+
for attempt in range(retries + 1):
|
|
282
|
+
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
|
|
289
|
+
if not text:
|
|
290
|
+
raise VisionError("Vision API returned an empty description")
|
|
291
|
+
return text
|
|
292
|
+
except urllib.error.HTTPError as exc:
|
|
293
|
+
raw_body = exc.read()
|
|
294
|
+
body = _redact(raw_body.decode(errors="replace")[:400], api_key)
|
|
295
|
+
body = body.replace("\r", " ").replace("\n", " ")
|
|
296
|
+
if _retryable_http_error(exc.code, raw_body) and attempt < retries:
|
|
297
|
+
print(f"vision: HTTP {exc.code}, retrying ({attempt + 1}/{retries})", file=sys.stderr)
|
|
298
|
+
time.sleep(_retry_delay(exc, attempt))
|
|
299
|
+
continue
|
|
300
|
+
raise VisionError(f"Vision API HTTP {exc.code}: {body}") from exc
|
|
301
|
+
except (urllib.error.URLError, TimeoutError, ConnectionError, http.client.IncompleteRead) as exc:
|
|
302
|
+
if attempt < retries:
|
|
303
|
+
print(f"vision: {type(exc).__name__}, retrying ({attempt + 1}/{retries})", file=sys.stderr)
|
|
304
|
+
time.sleep(min(2 ** attempt, 4))
|
|
305
|
+
continue
|
|
306
|
+
reason = _redact(str(getattr(exc, "reason", str(exc))), api_key)
|
|
307
|
+
raise VisionError(f"Vision API network error: {reason}") from exc
|
|
308
|
+
except json.JSONDecodeError as exc:
|
|
309
|
+
raise VisionError("Vision API returned invalid JSON") from exc
|
|
310
|
+
raise VisionError("Vision API request failed")
|