| 1 | import os |
| 2 | import sys |
| 3 | |
| 4 | models_dir = os.path.dirname(os.path.abspath(__file__)) |
| 5 | backend_dir = os.path.dirname(models_dir) |
| 6 | if backend_dir not in sys.path: |
| 7 | sys.path.insert(0, backend_dir) |
| 8 | |
| 9 | import time |
| 10 | import logging |
| 11 | from openai import OpenAI |
| 12 | from config import Config |
| 13 | |
| 14 | logger = logging.getLogger(__name__) |
| 15 | |
| 16 | DEFAULT_DEEPSEEK_TIMEOUT = 300 |
| 17 | DEFAULT_DEEPSEEK_MAX_ATTEMPTS = 5 |
| 18 | |
| 19 | |
| 20 | class DeepSeek: |
| 21 | """ |
| 22 | deepseek-chat: DeepSeek-V3.2 非思考模式 |
| 23 | deepseek-reasoner: DeepSeek-V3.2 思考模式 |
| 24 | deepseek-v4-flash: DeepSeek-V4 Flash |
| 25 | deepseek-v4-pro: DeepSeek-V4 Pro |
| 26 | """ |
| 27 | def __init__(self, base_url="", api_key="", timeout=None, max_attempts=None): |
| 28 | import httpx |
| 29 | self.base_url = base_url or Config.DEEPSEEK_BASE_URL or "https://api.deepseek.com/v1" |
| 30 | self.api_key = api_key or Config.DEEPSEEK_API_KEY |
| 31 | self.timeout = self._as_int(timeout, DEFAULT_DEEPSEEK_TIMEOUT) |
| 32 | self.max_attempts = self._as_int(max_attempts, DEFAULT_DEEPSEEK_MAX_ATTEMPTS) |
| 33 | |
| 34 | if not self.api_key: |
| 35 | logger.warning("DEEPSEEK_API_KEY is not set") |
| 36 | |
| 37 | kwargs = {"api_key": self.api_key, "base_url": self.base_url, "timeout": self.timeout} |
| 38 | proxy = Config.provider_proxy("deepseek") |
| 39 | if proxy: |
| 40 | kwargs["http_client"] = httpx.Client(proxy=proxy, timeout=self.timeout) |
| 41 | self.client = OpenAI(**kwargs) |
| 42 | |
| 43 | @staticmethod |
| 44 | def _as_int(value, default: int) -> int: |
| 45 | try: |
| 46 | return int(value) |
| 47 | except (TypeError, ValueError): |
| 48 | return default |
| 49 | |
| 50 | @staticmethod |
| 51 | def _usage_value(usage, key: str, default=0): |
| 52 | if usage is None: |
| 53 | return default |
| 54 | value = getattr(usage, key, None) |
| 55 | if value is None and isinstance(usage, dict): |
| 56 | value = usage.get(key) |
| 57 | return value if value is not None else default |
| 58 | |
| 59 | @classmethod |
| 60 | def _completion_detail_value(cls, usage, key: str, default=0): |
| 61 | details = cls._usage_value(usage, "completion_tokens_details", None) |
| 62 | if details is None: |
| 63 | return default |
| 64 | value = getattr(details, key, None) |
| 65 | if value is None and isinstance(details, dict): |
| 66 | value = details.get(key) |
| 67 | return value if value is not None else default |
| 68 | |
| 69 | def _log_empty_response(self, model: str, attempt: int, response): |
| 70 | choice = response.choices[0] if response.choices else None |
| 71 | message = getattr(choice, "message", None) if choice else None |
| 72 | finish_reason = getattr(choice, "finish_reason", None) if choice else None |
| 73 | usage = getattr(response, "usage", None) |
| 74 | completion_tokens = self._usage_value(usage, "completion_tokens") |
| 75 | prompt_tokens = self._usage_value(usage, "prompt_tokens") |
| 76 | total_tokens = self._usage_value(usage, "total_tokens") |
| 77 | reasoning_tokens = self._completion_detail_value(usage, "reasoning_tokens") |
| 78 | |
| 79 | reasoning_content = getattr(message, "reasoning_content", "") if message else "" |
| 80 | reasoning_chars = len(reasoning_content or "") |
| 81 | token_budget_exhausted = finish_reason == "length" |
| 82 | reasoning_used_output = reasoning_tokens and completion_tokens and reasoning_tokens >= completion_tokens * 0.9 |
| 83 | |
| 84 | if token_budget_exhausted and reasoning_used_output: |
| 85 | reason = "output_token_budget_exhausted_by_reasoning" |
| 86 | suggestion = "reduce prompt/output length or use a non-reasoning/flash model" |
| 87 | elif token_budget_exhausted: |
| 88 | reason = "output_token_budget_exhausted" |
| 89 | suggestion = "reduce requested output length" |
| 90 | elif reasoning_content and not getattr(message, "content", None): |
| 91 | reason = "reasoning_only_no_final_content" |
| 92 | suggestion = "retry may succeed; consider reducing reasoning-heavy model usage for long generation" |
| 93 | else: |
| 94 | reason = "empty_final_content" |
| 95 | suggestion = "retrying" |
| 96 | |
| 97 | logger.warning( |
| 98 | "DeepSeek empty final content; retrying. reason=%s suggestion=%s model=%s attempt=%s/%s " |
| 99 | "finish_reason=%s prompt_tokens=%s completion_tokens=%s reasoning_tokens=%s " |
| 100 | "total_tokens=%s reasoning_chars=%s", |
| 101 | reason, |
| 102 | suggestion, |
| 103 | model, |
| 104 | attempt, |
| 105 | self.max_attempts, |
| 106 | finish_reason, |
| 107 | prompt_tokens, |
| 108 | completion_tokens, |
| 109 | reasoning_tokens, |
| 110 | total_tokens, |
| 111 | reasoning_chars, |
| 112 | ) |
| 113 | |
| 114 | def query(self, prompt, image_urls=[], model="deepseek-chat", web_search=False): |
| 115 | """ |
| 116 | Query DeepSeek model. |
| 117 | |
| 118 | :param web_search: If True, adds enable_web_search: True to API call |
| 119 | """ |
| 120 | if not model: |
| 121 | model = "deepseek-chat" |
| 122 | |
| 123 | messages = [{"role": "system", "content": "You are a helpful assistant."}] |
| 124 | messages.append({"role": "user", "content": prompt}) |
| 125 | |
| 126 | attempts = 0 |
| 127 | while attempts < self.max_attempts: |
| 128 | try: |
| 129 | # Build request parameters |
| 130 | request_params = { |
| 131 | "model": model, |
| 132 | "messages": messages, |
| 133 | "stream": False, |
| 134 | } |
| 135 | |
| 136 | response = self.client.chat.completions.create(**request_params) |
| 137 | |
| 138 | # DeepSeek might return reasoning_content for reasoner models, |
| 139 | # but standard content is what we return conform to other interfaces. |
| 140 | if response.choices and response.choices[0].message.content: |
| 141 | return response.choices[0].message.content |
| 142 | else: |
| 143 | self._log_empty_response(model, attempts + 1, response) |
| 144 | time.sleep(2) |
| 145 | except Exception as e: |
| 146 | logger.warning( |
| 147 | "DeepSeek request failed; retrying. model=%s attempt=%s/%s timeout=%ss error=%s", |
| 148 | model, |
| 149 | attempts + 1, |
| 150 | self.max_attempts, |
| 151 | self.timeout, |
| 152 | e, |
| 153 | ) |
| 154 | time.sleep(5) |
| 155 | attempts += 1 |
| 156 | |
| 157 | raise Exception("Max attempts reached, failed to get a response from DeepSeek.") |
| 158 | |
| 159 | |
| 160 | if __name__ == "__main__": |
| 161 | import sys |
| 162 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| 163 | from config import Config |
| 164 | |
| 165 | # 支持的模型列表 |
| 166 | MODELS = ["deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro"] |
| 167 | |
| 168 | print("=== DeepSeek 可用性测试 ===") |
| 169 | api_key = Config.DEEPSEEK_API_KEY |
| 170 | base_url = Config.DEEPSEEK_BASE_URL |
| 171 | if not api_key: |
| 172 | print("✗ DEEPSEEK_API_KEY 未设置,跳过") |
| 173 | sys.exit(1) |
| 174 | print(f" API Key: {api_key[:6]}***{api_key[-4:]}") |
| 175 | if base_url: |
| 176 | print(f" Base URL: {base_url}") |
| 177 | |
| 178 | client = DeepSeek(api_key=api_key, base_url=base_url) |
| 179 | prompt = "用一句话介绍你自己。" |
| 180 | print(f" Prompt: {prompt}") |
| 181 | |
| 182 | for model in MODELS: |
| 183 | print(f"\n--- 测试模型: {model} ---") |
| 184 | t0 = time.time() |
| 185 | try: |
| 186 | resp = client.query(prompt, model=model) |
| 187 | elapsed = time.time() - t0 |
| 188 | print(f"✓ 响应 ({elapsed:.1f}s): {resp.strip()[:200]}") |
| 189 | except Exception as e: |
| 190 | print(f"✗ 失败: {e}") |
| 191 |