scan, split, OCR, prepare for LLM
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

qwen_client.py 3.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. from __future__ import annotations
  2. import base64
  3. import logging
  4. import os
  5. import time as _time
  6. import cv2
  7. import requests
  8. from src.env import load_env
  9. logger = logging.getLogger(__name__)
  10. API_URL: str = "https://opencode.ai/zen/go/v1/messages"
  11. DEFAULT_MODEL: str = "qwen3.8-max"
  12. IMAGE_QUALITY: int = 90
  13. def _api_key() -> str:
  14. load_env()
  15. return os.environ.get("OPENCODE_API_KEY", "")
  16. def image_to_base64(image) -> str:
  17. _, buf = cv2.imencode(".jpg", image, [cv2.IMWRITE_JPEG_QUALITY, IMAGE_QUALITY])
  18. return base64.b64encode(buf).decode("utf-8")
  19. def call_qwen_vlm(
  20. image_b64: str,
  21. prompt: str,
  22. model: str = DEFAULT_MODEL,
  23. max_tokens: int = 300,
  24. ) -> tuple[str | None, dict]:
  25. """Отправляет изображение + prompt в Qwen3.8 Max API.
  26. Возвращает (текст, usage_dict)."""
  27. empty_usage: dict = {}
  28. if not _api_key():
  29. logger.warning("OPENCODE_API_KEY не задан")
  30. return None, empty_usage
  31. messages = [{
  32. "role": "user",
  33. "content": [
  34. {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
  35. {"type": "text", "text": prompt},
  36. ],
  37. }]
  38. for attempt in (1, 2, 3):
  39. try:
  40. r = requests.post(
  41. API_URL,
  42. headers={"x-api-key": _api_key(), "anthropic-version": "2023-06-01", "Content-Type": "application/json"},
  43. json={"model": model, "max_tokens": max_tokens, "messages": messages},
  44. timeout=300,
  45. )
  46. r.raise_for_status()
  47. data = r.json()
  48. text = ""
  49. for item in data.get("content", []):
  50. if item.get("type") == "text":
  51. text = item.get("text", "").strip()
  52. stop = data.get("stop_reason", "")
  53. usage = data.get("usage", {})
  54. if text and stop == "max_tokens":
  55. logger.warning("Qwen API: ответ обрезан (max_tokens=%d), stop_reason=%s", max_tokens, stop)
  56. return text or None, usage
  57. except requests.HTTPError as e:
  58. status = e.response.status_code if e.response is not None else 0
  59. if status in (500, 503) and attempt < 3:
  60. delay = 5 * attempt
  61. logger.info("Qwen API: HTTP %d, ожидание %dс и повтор...", status, delay)
  62. _time.sleep(delay)
  63. continue
  64. logger.warning("Qwen API: ошибка — %s", e)
  65. return None, {}
  66. except (requests.Timeout, requests.ConnectionError) as e:
  67. if attempt < 3:
  68. delay = 5 * attempt
  69. logger.info("Qwen API: таймаут/соединение, ожидание %dс и повтор...", delay)
  70. _time.sleep(delay)
  71. continue
  72. logger.warning("Qwen API: ошибка — %s", e)
  73. return None, {}
  74. except (requests.RequestException, KeyError, IndexError) as e:
  75. logger.warning("Qwen API: ошибка — %s", e)
  76. return None, {}
  77. return None, {}