|
|
@@ -79,6 +79,18 @@ def _http_bytes(url, timeout):
|
|
|
return body
|
|
|
|
|
|
|
|
|
+def _url_image_to_data_url(url, timeout, max_bytes=8 * 1024 * 1024):
|
|
|
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
|
+ with urllib.request.urlopen(req, timeout=timeout, context=_SSL) as res:
|
|
|
+ ctype = (res.headers.get("content-type") or "image/png").split(";")[0].strip()
|
|
|
+ raw = res.read(max_bytes + 1)
|
|
|
+ if len(raw) > max_bytes:
|
|
|
+ raise RuntimeError("image is larger than 8MB")
|
|
|
+ if not ctype.startswith("image/"):
|
|
|
+ ctype = "image/png"
|
|
|
+ return f"data:{ctype};base64," + base64.b64encode(raw).decode("ascii")
|
|
|
+
|
|
|
+
|
|
|
def _short_json(data, limit=600):
|
|
|
try:
|
|
|
return json.dumps(data, ensure_ascii=False)[:limit]
|
|
|
@@ -233,10 +245,10 @@ def generate(provider, prompt, api_key, base_url, model, size):
|
|
|
return fn(prompt, api_key=api_key, base_url=base_url, model=model, size=size)
|
|
|
|
|
|
|
|
|
-def chat_json_openai(messages, api_key, base_url="https://api.openai.com/v1",
|
|
|
- model="gpt-4o-mini", timeout=120):
|
|
|
- url = base_url.rstrip("/") + "/chat/completions"
|
|
|
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
|
+TEXT_MODEL_FALLBACKS = ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5"]
|
|
|
+
|
|
|
+
|
|
|
+def _chat_json_once(url, headers, messages, model, timeout):
|
|
|
payload = {
|
|
|
"model": model,
|
|
|
"messages": messages,
|
|
|
@@ -246,28 +258,46 @@ def chat_json_openai(messages, api_key, base_url="https://api.openai.com/v1",
|
|
|
try:
|
|
|
data = _http_json(url, payload, headers, timeout)
|
|
|
except urllib.error.HTTPError as e:
|
|
|
- body = _read_http_error(e)
|
|
|
fallback = dict(payload)
|
|
|
fallback.pop("response_format", None)
|
|
|
try:
|
|
|
data = _http_json(url, fallback, headers, timeout)
|
|
|
except urllib.error.HTTPError as e2:
|
|
|
- body2 = _read_http_error(e2)
|
|
|
- raise RuntimeError(f"文字模型 HTTP {e2.code}:{body2[:600]}\n"
|
|
|
- f"(model={model}, url={url};已移除 response_format 重试)")
|
|
|
+ raise e2
|
|
|
content = (((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "").strip()
|
|
|
if content.startswith("```"):
|
|
|
content = content.strip("`")
|
|
|
if content.lower().startswith("json"):
|
|
|
content = content[4:].strip()
|
|
|
try:
|
|
|
- return json.loads(content)
|
|
|
+ obj = json.loads(content)
|
|
|
except Exception as e:
|
|
|
raise RuntimeError(f"文字模型没有返回合法 JSON:{e};内容={content[:800]}")
|
|
|
+ if isinstance(obj, dict):
|
|
|
+ obj.setdefault("_model_used", model)
|
|
|
+ return obj
|
|
|
+
|
|
|
+
|
|
|
+def chat_json_openai(messages, api_key, base_url="https://api.openai.com/v1",
|
|
|
+ model="gpt-5.4-mini", timeout=120):
|
|
|
+ url = base_url.rstrip("/") + "/chat/completions"
|
|
|
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
|
+ candidates = [model] + [x for x in TEXT_MODEL_FALLBACKS if x != model]
|
|
|
+ errors = []
|
|
|
+ for candidate in candidates:
|
|
|
+ try:
|
|
|
+ return _chat_json_once(url, headers, messages, candidate, timeout)
|
|
|
+ except urllib.error.HTTPError as e:
|
|
|
+ body = _read_http_error(e)
|
|
|
+ errors.append(f"{candidate}: HTTP {e.code} {body[:180]}")
|
|
|
+ if e.code not in (429, 500, 502, 503, 504):
|
|
|
+ break
|
|
|
+ raise RuntimeError(f"文字模型不可用,已尝试 {', '.join(candidates)}:"
|
|
|
+ f"{' | '.join(errors)}\n(url={url};已移除 response_format 重试)")
|
|
|
|
|
|
|
|
|
def analyze_reference_images(reference_urls=None, image_data_urls=None, api_key="",
|
|
|
- base_url="https://api.openai.com/v1", model="gpt-4o-mini",
|
|
|
+ base_url="https://api.openai.com/v1", model="gpt-5.4-mini",
|
|
|
timeout=120):
|
|
|
"""Use a vision-capable OpenAI-compatible chat model to summarize style refs.
|
|
|
|
|
|
@@ -288,7 +318,13 @@ def analyze_reference_images(reference_urls=None, image_data_urls=None, api_key=
|
|
|
),
|
|
|
}]
|
|
|
for url in reference_urls:
|
|
|
- content.append({"type": "image_url", "image_url": {"url": url}})
|
|
|
+ try:
|
|
|
+ content.append({"type": "image_url", "image_url": {"url": _url_image_to_data_url(url, timeout=30)}})
|
|
|
+ except Exception as e:
|
|
|
+ content.append({
|
|
|
+ "type": "text",
|
|
|
+ "text": f"参考图 URL 本地下载失败,不能作为视觉输入,只能作为文字线索:{url};错误:{str(e)[:160]}",
|
|
|
+ })
|
|
|
for data_url in image_data_urls:
|
|
|
content.append({"type": "image_url", "image_url": {"url": data_url}})
|
|
|
return chat_json_openai([
|