Files
bigwo/api_client.py
2026-03-02 17:38:28 +08:00

123 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""DeepSeek API 客户端封装,含指数退避重试逻辑"""
import time
from typing import Callable
from openai import OpenAI
import openai
from exceptions import ApiError
# 文件扩展名 → MIME 类型映射
EXTENSION_MIME_MAP = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".bmp": "image/bmp",
".webp": "image/webp",
}
class ApiClient:
"""封装 DeepSeek API 调用,含重试和错误处理"""
RETRY_DELAYS = [1, 2, 4] # 指数退避延迟(秒)
MAX_RETRIES = 3
def __init__(
self,
api_key: str,
base_url: str = "https://api.deepseek.com",
_sleep: Callable[[float], None] = time.sleep,
_client: "OpenAI | None" = None,
):
self._client = _client or OpenAI(api_key=api_key, base_url=base_url)
self._sleep = _sleep
def chat(self, system_prompt: str, user_content: str, model: str = "deepseek-chat") -> str:
"""
调用 Chat Completion API。
速率限制和网络异常时自动指数退避重试,最多 3 次。
Raises:
ApiError: API 调用失败(非可重试错误或重试耗尽)
"""
def _call():
response = self._client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
],
)
return response.choices[0].message.content
return self._retry(_call)
def vision(
self,
system_prompt: str,
image_base64: str,
mime_type: str = "image/png",
model: str = "deepseek-chat",
) -> str:
"""
调用 Vision API 识别图片内容。
速率限制和网络异常时自动指数退避重试,最多 3 次。
Args:
system_prompt: 系统提示词
image_base64: 图片的 base64 编码
mime_type: 图片 MIME 类型(如 image/jpeg、image/png
model: 模型名称
Raises:
ApiError: API 调用失败
"""
def _call():
response = self._client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{image_base64}",
},
},
],
},
],
)
return response.choices[0].message.content
return self._retry(_call)
def _retry(self, call: Callable[[], str]) -> str:
"""执行带指数退避重试的 API 调用,对速率限制和网络异常重试"""
for attempt in range(self.MAX_RETRIES + 1):
try:
return call()
except openai.RateLimitError:
if attempt < self.MAX_RETRIES:
self._sleep(self.RETRY_DELAYS[attempt])
else:
raise ApiError("速率限制重试耗尽", status_code=429)
except openai.APIConnectionError:
if attempt < self.MAX_RETRIES:
self._sleep(self.RETRY_DELAYS[attempt])
else:
raise ApiError("网络连接失败,重试耗尽")
except openai.APITimeoutError:
if attempt < self.MAX_RETRIES:
self._sleep(self.RETRY_DELAYS[attempt])
else:
raise ApiError("API 请求超时,重试耗尽")
except openai.APIError as e:
raise ApiError(str(e), status_code=getattr(e, "status_code", None))