Upgrade DeepSeek client and improve API reliability

#1
Files changed (3) hide show
  1. app.py +14 -10
  2. config.py +27 -1
  3. core/pipeline_agent.py +498 -124
app.py CHANGED
@@ -94,15 +94,19 @@ def clean_trailing_references(text, keywords=("reference", "references", "refere
94
  def clean_markdown_headings(text):
95
  return re.sub(r'(?m)^#+\s+(.*)$', r'**\1**', text)
96
 
97
- global_ds_key = os.environ.get("DEEPSEEK_API_KEY", "")
98
- if not global_ds_key:
99
- try: global_ds_key = st.secrets.get("DEEPSEEK_API_KEY", "")
100
- except: pass
 
 
 
 
 
101
 
102
- global_s2_key = os.environ.get("S2_API_KEY", "")
103
- if not global_s2_key:
104
- try: global_s2_key = st.secrets.get("S2_API_KEY", "")
105
- except: pass
106
 
107
  WECHAT_BLOCKER_JS = """<script>try{var ua=navigator.userAgent.toLowerCase();if(ua.indexOf('micromessenger')!==-1){var parentDoc=window.parent.document;if(!parentDoc.getElementById('wx-blocker')){var overlay=parentDoc.createElement('div');overlay.id='wx-blocker';overlay.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;background:#1a365d;z-index:9999999;color:white;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-family:sans-serif;padding:30px;box-sizing:border-box;';overlay.innerHTML='<h2 style="font-size:26px;margin-bottom:15px;font-weight:bold;">环境受限 / Restricted</h2><p style="font-size:18px;line-height:1.6;color:#e2e8f0;max-width:100%;">为了保障最终双语 ZIP 压缩包正常下载,<b>本系统禁止在微信内直接运行</b>。<br><br>👉 <b>请点击右上角 [···] 菜单,选择“在浏览器打开” (Open in Browser)</b></p>';parentDoc.body.appendChild(overlay);}}}catch(e){}</script>"""
108
  components.html(WECHAT_BLOCKER_JS, height=0, width=0)
@@ -788,11 +792,11 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
788
  update_heartbeat()
789
  st.write(f"📡 已发起 DeepSeek API 请求...")
790
 
791
- target_model = "deepseek-chat"
792
  st.write(f"✍️ 核心推导由 R1 完成,当前节点使用 {target_model} 高速执笔...")
793
 
794
  en_draft_clean = re.sub(r'<think>.*?</think>', '', en_ph.write_stream(
795
- stream_with_heartbeat(agent.stream_deepseek(sys_prompt_m3, base_user, max_tokens=6000, model_name=target_model))
796
  ), flags=re.DOTALL).strip()
797
 
798
  status.update(label=f"✅ 【{display_name}】撰写完毕!", state="complete", expanded=False)
 
94
  def clean_markdown_headings(text):
95
  return re.sub(r'(?m)^#+\s+(.*)$', r'**\1**', text)
96
 
97
+ def _read_runtime_secret(name):
98
+ # Read a Hugging Face/Streamlit secret and remove copied whitespace.
99
+ value = os.environ.get(name, "")
100
+ if not value:
101
+ try:
102
+ value = st.secrets.get(name, "")
103
+ except Exception:
104
+ value = ""
105
+ return str(value or "").strip()
106
 
107
+
108
+ global_ds_key = _read_runtime_secret("DEEPSEEK_API_KEY")
109
+ global_s2_key = _read_runtime_secret("S2_API_KEY")
 
110
 
111
  WECHAT_BLOCKER_JS = """<script>try{var ua=navigator.userAgent.toLowerCase();if(ua.indexOf('micromessenger')!==-1){var parentDoc=window.parent.document;if(!parentDoc.getElementById('wx-blocker')){var overlay=parentDoc.createElement('div');overlay.id='wx-blocker';overlay.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;background:#1a365d;z-index:9999999;color:white;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-family:sans-serif;padding:30px;box-sizing:border-box;';overlay.innerHTML='<h2 style="font-size:26px;margin-bottom:15px;font-weight:bold;">环境受限 / Restricted</h2><p style="font-size:18px;line-height:1.6;color:#e2e8f0;max-width:100%;">为了保障最终双语 ZIP 压缩包正常下载,<b>本系统禁止在微信内直接运行</b>。<br><br>👉 <b>请点击右上角 [···] 菜单,选择“在浏览器打开” (Open in Browser)</b></p>';parentDoc.body.appendChild(overlay);}}}catch(e){}</script>"""
112
  components.html(WECHAT_BLOCKER_JS, height=0, width=0)
 
792
  update_heartbeat()
793
  st.write(f"📡 已发起 DeepSeek API 请求...")
794
 
795
+ target_model = "deepseek-v4-flash"
796
  st.write(f"✍️ 核心推导由 R1 完成,当前节点使用 {target_model} 高速执笔...")
797
 
798
  en_draft_clean = re.sub(r'<think>.*?</think>', '', en_ph.write_stream(
799
+ stream_with_heartbeat(agent.stream_deepseek(sys_prompt_m3, base_user, max_tokens=6000, model_name=target_model, thinking=False))
800
  ), flags=re.DOTALL).strip()
801
 
802
  status.update(label=f"✅ 【{display_name}】撰写完毕!", state="complete", expanded=False)
config.py CHANGED
@@ -22,7 +22,33 @@ def load_config():
22
  },
23
 
24
  # --- API 节点配置 ---
25
- "deepseek_api_url": os.environ.get("DEEPSEEK_API_URL", "https://api.deepseek.com/chat/completions"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  "openalex_api_url": os.environ.get("OPENALEX_API_URL", "https://api.openalex.org/works"),
27
  "s2_api_url": os.environ.get("S2_API_URL", "https://api.semanticscholar.org/graph/v1"),
28
 
 
22
  },
23
 
24
  # --- API 节点配置 ---
25
+ "deepseek_api_url": os.environ.get(
26
+ "DEEPSEEK_API_URL",
27
+ "https://api.deepseek.com/chat/completions",
28
+ ),
29
+ "deepseek_default_model": os.environ.get(
30
+ "DEEPSEEK_DEFAULT_MODEL",
31
+ "deepseek-v4-flash",
32
+ ),
33
+ "deepseek_fallback_model": os.environ.get(
34
+ "DEEPSEEK_FALLBACK_MODEL",
35
+ "deepseek-v4-flash",
36
+ ),
37
+ "deepseek_default_thinking": os.environ.get(
38
+ "DEEPSEEK_DEFAULT_THINKING",
39
+ "true",
40
+ ).strip().lower() in {"1", "true", "yes", "on"},
41
+ "deepseek_reasoning_effort": os.environ.get(
42
+ "DEEPSEEK_REASONING_EFFORT",
43
+ "high",
44
+ ),
45
+ "deepseek_max_retries": int(
46
+ os.environ.get("DEEPSEEK_MAX_RETRIES", "3")
47
+ ),
48
+ "deepseek_show_reasoning": os.environ.get(
49
+ "DEEPSEEK_SHOW_REASONING",
50
+ "false",
51
+ ).strip().lower() in {"1", "true", "yes", "on"},
52
  "openalex_api_url": os.environ.get("OPENALEX_API_URL", "https://api.openalex.org/works"),
53
  "s2_api_url": os.environ.get("S2_API_URL", "https://api.semanticscholar.org/graph/v1"),
54
 
core/pipeline_agent.py CHANGED
@@ -25,9 +25,31 @@ except ImportError:
25
 
26
  from config import APP_CONFIG
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  class SSciPipelineAgent:
29
  def __init__(self, ds_key, s2_key=""):
30
- self.ds_key = ds_key
 
 
 
 
 
31
  try:
32
  self.s2_key = st.secrets.get("S2_API_KEY", s2_key)
33
  except:
@@ -55,6 +77,24 @@ class SSciPipelineAgent:
55
  self.chunk_words_cyrillic = APP_CONFIG["chunk_words_cyrillic"]
56
 
57
  self.deepseek_api_url = APP_CONFIG["deepseek_api_url"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  self.openalex_api_url = APP_CONFIG["openalex_api_url"]
59
  self.s2_api_url = APP_CONFIG["s2_api_url"]
60
  self.timeouts = APP_CONFIG["api_timeouts"]
@@ -255,138 +295,472 @@ class SSciPipelineAgent:
255
  return self._truncate_context(full, max_tokens)
256
  return full
257
 
258
- def call_deepseek(self, system_prompt, user_prompt, max_tokens=8000, temp=0.3, model_name="deepseek-reasoner"):
259
- tokens_est = self._count_tokens(user_prompt)
260
- logging.info(f"🚀 [DeepSeek 静态请求] 模型: {model_name} | MaxTokens: {max_tokens} | 输入估算: {tokens_est} tokens")
261
-
262
- for attempt in range(3):
263
- with self.lock:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  self.ds_calls += 1
265
- current_model = model_name
266
- if attempt == 2 and model_name == "deepseek-reasoner":
267
- current_model = "deepseek-chat"
268
- temp = 0.3
269
- logging.warning("⚠️ 强制降级为 deepseek-chat 执行本节点!")
270
-
271
- payload = {
272
- "model": current_model,
273
- "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
274
- "max_tokens": max_tokens,
275
- "stream": True
276
- }
277
- if current_model != "deepseek-reasoner":
278
- payload["temperature"] = temp
279
-
280
- full_content = ""
 
 
 
 
 
281
  start_time = time.time()
 
282
  try:
283
- with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["standard"]) as response:
284
- if response.status_code != 200:
285
- logging.error(f"❌ 尝试 {attempt+1}/3 失败: 状态码 {response.status_code} - {response.text[:200]}")
286
- if attempt == 2:
287
- raise requests.exceptions.HTTPError("API 熔断")
288
- time.sleep(3 ** attempt)
289
- continue
290
-
291
- for line in response.iter_lines():
292
- if line and line.decode('utf-8').startswith('data: ') and line.decode('utf-8') != 'data: [DONE]':
293
- try:
294
- delta = json.loads(line.decode('utf-8')[6:])['choices'][0].get('delta', {})
295
- content = delta.get('content', '')
296
- if content:
297
- full_content += content
298
- except Exception:
299
- pass
300
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  elapsed = time.time() - start_time
302
- logging.info(f"✅ [静态请求返回] 耗时: {elapsed:.2f}s | 输出长度: {len(full_content)} 字符")
303
- return full_content.strip()
304
- except Exception as e:
305
- logging.error(f"❌ [网络异常] 尝试 {attempt+1}: {e}")
306
- if attempt == 2:
307
- raise e
308
- time.sleep(3 ** attempt)
309
-
310
- def stream_deepseek(self, system_prompt, user_prompt, max_tokens=8000, model_name="deepseek-reasoner"):
311
- tokens_est = self._count_tokens(user_prompt)
312
- logging.info(f"🌊 [DeepSeek 流式请求] 模型: {model_name} | 输入估算: {tokens_est} tokens")
313
-
314
- temp = 0.3 if model_name == "deepseek-chat" else 0.2
315
- for attempt in range(3):
316
- with self.lock:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  self.ds_calls += 1
318
- current_model = model_name
319
- if attempt >= 1 and model_name == "deepseek-reasoner":
320
- current_model = "deepseek-chat"
321
- temp = 0.3
322
- logging.warning("⚠️ 推理节点异常,强制降级至 deepseek-chat 并重试。")
323
-
324
- payload = {
325
- "model": current_model,
326
- "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
327
- "max_tokens": max_tokens,
328
- "stream": True
329
- }
330
- if current_model != "deepseek-reasoner":
331
- payload["temperature"] = temp
332
-
333
- think_heartbeat = 0
334
- has_started_reasoning = False
 
 
 
 
 
 
 
 
335
  start_time = time.time()
336
-
337
  try:
338
- with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["stream"]) as response:
 
 
 
 
 
 
339
  if response.status_code != 200:
340
- logging.error(f"❌ 流式尝试 {attempt+1}/3 失败: 状态码 {response.status_code}")
341
- if attempt == 2:
342
- response.raise_for_status()
343
- time.sleep(2)
344
- continue
345
-
346
- for line in response.iter_lines():
347
- if line and line.decode('utf-8').startswith('data: ') and line.decode('utf-8') != 'data: [DONE]':
348
- try:
349
- delta = json.loads(line.decode('utf-8')[6:])['choices'][0].get('delta', {})
350
-
351
- reasoning_content = delta.get('reasoning_content', '')
352
- content = delta.get('content', '')
353
-
354
- if reasoning_content:
355
- if not has_started_reasoning:
356
- yield "\n<think>\n"
357
- has_started_reasoning = True
358
- logging.info(f"🤔 [R1 推理启动] 开始深度思考...")
359
-
360
- think_heartbeat += 1
361
-
362
- if think_heartbeat % 25 == 0:
363
- elapsed = time.time() - start_time
364
- logging.info(f"⏳ [DeepSeek 心跳] {current_model} 思考中... ({elapsed:.1f}秒, 接收 {think_heartbeat} 块)")
365
-
366
- yield reasoning_content
367
- continue
368
-
369
- if has_started_reasoning and not reasoning_content and content:
370
- yield "\n</think>\n\n"
371
- has_started_reasoning = False
372
- elapsed = time.time() - start_time
373
- logging.info(f"💡 [R1 推理结束] 耗时 {elapsed:.1f}秒。开始流式输出正文...")
374
-
375
- if content:
376
- yield content
377
-
378
- except Exception:
379
- pass
380
-
381
- logging.info(f"🏁 [流式生成结束] 总耗时 {(time.time() - start_time):.1f}秒。")
382
- return
383
- except GeneratorExit:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  raise
385
- except Exception as e:
386
- logging.error(f"❌ [流式中断]: {e}")
387
- if attempt == 2:
388
- raise
389
- time.sleep(2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
  def parse_user_core_literature(self, user_lit_text):
392
  if not user_lit_text.strip():
 
25
 
26
  from config import APP_CONFIG
27
 
28
+
29
+ class DeepSeekAPIError(RuntimeError):
30
+ pass
31
+
32
+
33
+ class DeepSeekAuthenticationError(DeepSeekAPIError):
34
+ pass
35
+
36
+
37
+ class DeepSeekBalanceError(DeepSeekAPIError):
38
+ pass
39
+
40
+
41
+ class DeepSeekRequestError(DeepSeekAPIError):
42
+ pass
43
+
44
+
45
  class SSciPipelineAgent:
46
  def __init__(self, ds_key, s2_key=""):
47
+ self.ds_key = str(ds_key or "").strip()
48
+ if not self.ds_key:
49
+ raise DeepSeekAuthenticationError(
50
+ "DEEPSEEK_API_KEY is empty. Configure it in Hugging Face "
51
+ "Space Settings > Variables and secrets."
52
+ )
53
  try:
54
  self.s2_key = st.secrets.get("S2_API_KEY", s2_key)
55
  except:
 
77
  self.chunk_words_cyrillic = APP_CONFIG["chunk_words_cyrillic"]
78
 
79
  self.deepseek_api_url = APP_CONFIG["deepseek_api_url"]
80
+ self.deepseek_default_model = str(
81
+ APP_CONFIG.get("deepseek_default_model", "deepseek-v4-flash")
82
+ ).strip()
83
+ self.deepseek_fallback_model = str(
84
+ APP_CONFIG.get("deepseek_fallback_model", "deepseek-v4-flash")
85
+ ).strip()
86
+ self.deepseek_default_thinking = bool(
87
+ APP_CONFIG.get("deepseek_default_thinking", True)
88
+ )
89
+ self.deepseek_reasoning_effort = str(
90
+ APP_CONFIG.get("deepseek_reasoning_effort", "high")
91
+ ).strip().lower()
92
+ self.deepseek_max_retries = max(
93
+ 1, int(APP_CONFIG.get("deepseek_max_retries", 3))
94
+ )
95
+ self.deepseek_show_reasoning = bool(
96
+ APP_CONFIG.get("deepseek_show_reasoning", False)
97
+ )
98
  self.openalex_api_url = APP_CONFIG["openalex_api_url"]
99
  self.s2_api_url = APP_CONFIG["s2_api_url"]
100
  self.timeouts = APP_CONFIG["api_timeouts"]
 
295
  return self._truncate_context(full, max_tokens)
296
  return full
297
 
298
+ def _resolve_deepseek_model(self, model_name=None, thinking=None):
299
+ # Legacy names are accepted only as internal aliases.
300
+ requested = str(
301
+ model_name or self.deepseek_default_model
302
+ ).strip().lower()
303
+
304
+ legacy_aliases = {
305
+ "deepseek-chat": ("deepseek-v4-flash", False),
306
+ "deepseek-reasoner": ("deepseek-v4-flash", True),
307
+ }
308
+ if requested in legacy_aliases:
309
+ public_model, alias_thinking = legacy_aliases[requested]
310
+ if thinking is None:
311
+ thinking = alias_thinking
312
+ return public_model, bool(thinking)
313
+
314
+ allowed_models = {"deepseek-v4-flash", "deepseek-v4-pro"}
315
+ if requested not in allowed_models:
316
+ raise DeepSeekRequestError(
317
+ f"Unsupported DeepSeek model: {requested!r}. "
318
+ "Use 'deepseek-v4-flash' or 'deepseek-v4-pro'."
319
+ )
320
+
321
+ if thinking is None:
322
+ thinking = self.deepseek_default_thinking
323
+
324
+ return requested, bool(thinking)
325
+
326
+ def _build_deepseek_payload(
327
+ self,
328
+ system_prompt,
329
+ user_prompt,
330
+ *,
331
+ model_name,
332
+ thinking,
333
+ max_tokens,
334
+ temp,
335
+ stream,
336
+ reasoning_effort,
337
+ ):
338
+ effort = str(
339
+ reasoning_effort or self.deepseek_reasoning_effort
340
+ ).strip().lower()
341
+ if effort not in {"high", "max"}:
342
+ effort = "high"
343
+
344
+ payload = {
345
+ "model": model_name,
346
+ "messages": [
347
+ {"role": "system", "content": str(system_prompt)},
348
+ {"role": "user", "content": str(user_prompt)},
349
+ ],
350
+ "max_tokens": int(max_tokens),
351
+ "stream": bool(stream),
352
+ "thinking": {
353
+ "type": "enabled" if thinking else "disabled"
354
+ },
355
+ }
356
+
357
+ if thinking:
358
+ payload["reasoning_effort"] = effort
359
+ else:
360
+ payload["temperature"] = max(
361
+ 0.0, min(float(temp), 2.0)
362
+ )
363
+
364
+ return payload
365
+
366
+ @staticmethod
367
+ def _deepseek_headers(api_key):
368
+ return {
369
+ "Authorization": f"Bearer {api_key}",
370
+ "Content-Type": "application/json",
371
+ "Accept": "application/json",
372
+ "User-Agent": "SmartPaper/DeepSeekV4",
373
+ }
374
+
375
+ @staticmethod
376
+ def _safe_response_text(response, limit=600):
377
+ try:
378
+ return response.text[:limit]
379
+ except Exception:
380
+ return ""
381
+
382
+ def _raise_for_deepseek_status(self, response):
383
+ status = int(response.status_code)
384
+ detail = self._safe_response_text(response)
385
+
386
+ if status == 401:
387
+ raise DeepSeekAuthenticationError(
388
+ "DeepSeek authentication failed (401). "
389
+ "Check DEEPSEEK_API_KEY in Hugging Face Space Secrets."
390
+ )
391
+ if status == 402:
392
+ raise DeepSeekBalanceError(
393
+ "DeepSeek account balance is insufficient (402)."
394
+ )
395
+ if status == 400:
396
+ raise DeepSeekRequestError(
397
+ f"DeepSeek rejected the request format (400): {detail}"
398
+ )
399
+ if status == 404:
400
+ raise DeepSeekRequestError(
401
+ f"DeepSeek model or endpoint was not found (404): {detail}"
402
+ )
403
+ if status == 422:
404
+ raise DeepSeekRequestError(
405
+ f"DeepSeek request parameters are invalid (422): {detail}"
406
+ )
407
+
408
+ if status in {408, 409, 425, 429, 500, 502, 503, 504}:
409
+ raise requests.exceptions.HTTPError(
410
+ f"Transient DeepSeek API failure ({status}): {detail}",
411
+ response=response,
412
+ )
413
+
414
+ raise DeepSeekRequestError(
415
+ f"Unexpected DeepSeek API status {status}: {detail}"
416
+ )
417
+
418
+ @staticmethod
419
+ def _retry_delay(attempt_index):
420
+ base = min(2 ** attempt_index, 8)
421
+ return base + random.uniform(0.25, 1.25)
422
+
423
+ @staticmethod
424
+ def _parse_sse_line(raw_line):
425
+ if raw_line is None:
426
+ return None
427
+ if isinstance(raw_line, bytes):
428
+ line = raw_line.decode("utf-8", errors="replace").strip()
429
+ else:
430
+ line = str(raw_line).strip()
431
+
432
+ if not line or not line.startswith("data:"):
433
+ return None
434
+
435
+ data = line[5:].strip()
436
+ if not data:
437
+ return None
438
+ if data == "[DONE]":
439
+ return {"done": True}
440
+
441
+ try:
442
+ return json.loads(data)
443
+ except json.JSONDecodeError:
444
+ logging.warning(
445
+ "⚠️ Ignored malformed DeepSeek SSE chunk: %s",
446
+ data[:160],
447
+ )
448
+ return None
449
+
450
+ def _attempt_plan(self, requested_model, requested_thinking):
451
+ total = self.deepseek_max_retries
452
+ for attempt in range(total):
453
+ model = requested_model
454
+ thinking = requested_thinking
455
+
456
+ if attempt == total - 1:
457
+ if model != self.deepseek_fallback_model:
458
+ model = self.deepseek_fallback_model
459
+ if thinking:
460
+ thinking = False
461
+
462
+ yield attempt, model, thinking
463
+
464
+ def call_deepseek(
465
+ self,
466
+ system_prompt,
467
+ user_prompt,
468
+ max_tokens=8000,
469
+ temp=0.3,
470
+ model_name="deepseek-reasoner",
471
+ thinking=None,
472
+ reasoning_effort=None,
473
+ ):
474
+ requested_model, requested_thinking = (
475
+ self._resolve_deepseek_model(model_name, thinking)
476
+ )
477
+ tokens_est = self._count_tokens(str(user_prompt))
478
+
479
+ logging.info(
480
+ "🚀 [DeepSeek 静态请求] 模型=%s | Thinking=%s | "
481
+ "MaxTokens=%s | 输入估算=%s tokens",
482
+ requested_model,
483
+ requested_thinking,
484
+ max_tokens,
485
+ tokens_est,
486
+ )
487
+
488
+ last_error = None
489
+
490
+ for attempt, current_model, current_thinking in self._attempt_plan(
491
+ requested_model, requested_thinking
492
+ ):
493
+ with self.lock:
494
  self.ds_calls += 1
495
+
496
+ if (
497
+ current_model != requested_model
498
+ or current_thinking != requested_thinking
499
+ ):
500
+ logging.warning(
501
+ "⚠️ 最后一次尝试降级:模型=%s | Thinking=%s",
502
+ current_model,
503
+ current_thinking,
504
+ )
505
+
506
+ payload = self._build_deepseek_payload(
507
+ system_prompt,
508
+ user_prompt,
509
+ model_name=current_model,
510
+ thinking=current_thinking,
511
+ max_tokens=max_tokens,
512
+ temp=temp,
513
+ stream=False,
514
+ reasoning_effort=reasoning_effort,
515
+ )
516
  start_time = time.time()
517
+
518
  try:
519
+ response = requests.post(
520
+ self.deepseek_api_url,
521
+ headers=self._deepseek_headers(self.ds_key),
522
+ json=payload,
523
+ timeout=self.timeouts["standard"],
524
+ )
525
+
526
+ if response.status_code != 200:
527
+ self._raise_for_deepseek_status(response)
528
+
529
+ try:
530
+ result = response.json()
531
+ except ValueError as exc:
532
+ raise DeepSeekAPIError(
533
+ "DeepSeek returned a non-JSON success response."
534
+ ) from exc
535
+
536
+ choices = result.get("choices") or []
537
+ if not choices:
538
+ raise DeepSeekAPIError(
539
+ "DeepSeek returned no choices."
540
+ )
541
+
542
+ message = choices[0].get("message") or {}
543
+ content = str(message.get("content") or "").strip()
544
+
545
+ if not content:
546
+ finish_reason = choices[0].get("finish_reason")
547
+ raise DeepSeekAPIError(
548
+ "DeepSeek returned empty final content. "
549
+ f"finish_reason={finish_reason!r}"
550
+ )
551
+
552
  elapsed = time.time() - start_time
553
+ logging.info(
554
+ "✅ [静态请求返回] 耗时=%.2fs | 输出长度=%s 字符",
555
+ elapsed,
556
+ len(content),
557
+ )
558
+ return content
559
+
560
+ except (
561
+ DeepSeekAuthenticationError,
562
+ DeepSeekBalanceError,
563
+ DeepSeekRequestError,
564
+ ):
565
+ raise
566
+ except (
567
+ requests.exceptions.Timeout,
568
+ requests.exceptions.ConnectionError,
569
+ requests.exceptions.HTTPError,
570
+ DeepSeekAPIError,
571
+ ) as exc:
572
+ last_error = exc
573
+ is_last = attempt == self.deepseek_max_retries - 1
574
+ logging.error(
575
+ "❌ DeepSeek 尝试 %s/%s 失败: %s",
576
+ attempt + 1,
577
+ self.deepseek_max_retries,
578
+ exc,
579
+ )
580
+ if is_last:
581
+ break
582
+ time.sleep(self._retry_delay(attempt))
583
+
584
+ raise DeepSeekAPIError(
585
+ f"DeepSeek API failed after "
586
+ f"{self.deepseek_max_retries} attempts: {last_error}"
587
+ )
588
+
589
+ def stream_deepseek(
590
+ self,
591
+ system_prompt,
592
+ user_prompt,
593
+ max_tokens=8000,
594
+ model_name="deepseek-reasoner",
595
+ thinking=None,
596
+ reasoning_effort=None,
597
+ temp=0.3,
598
+ ):
599
+ requested_model, requested_thinking = (
600
+ self._resolve_deepseek_model(model_name, thinking)
601
+ )
602
+ tokens_est = self._count_tokens(str(user_prompt))
603
+
604
+ logging.info(
605
+ "🚀 [DeepSeek 流式请求] 模型=%s | Thinking=%s | "
606
+ "MaxTokens=%s | 输入估算=%s tokens",
607
+ requested_model,
608
+ requested_thinking,
609
+ max_tokens,
610
+ tokens_est,
611
+ )
612
+
613
+ last_error = None
614
+
615
+ for attempt, current_model, current_thinking in self._attempt_plan(
616
+ requested_model, requested_thinking
617
+ ):
618
+ with self.lock:
619
  self.ds_calls += 1
620
+
621
+ if (
622
+ current_model != requested_model
623
+ or current_thinking != requested_thinking
624
+ ):
625
+ logging.warning(
626
+ "⚠️ 最后一次尝试降级:模型=%s | Thinking=%s",
627
+ current_model,
628
+ current_thinking,
629
+ )
630
+
631
+ payload = self._build_deepseek_payload(
632
+ system_prompt,
633
+ user_prompt,
634
+ model_name=current_model,
635
+ thinking=current_thinking,
636
+ max_tokens=max_tokens,
637
+ temp=temp,
638
+ stream=True,
639
+ reasoning_effort=reasoning_effort,
640
+ )
641
+
642
+ emitted_content = False
643
+ reasoning_open = False
644
+ reasoning_chunks = 0
645
  start_time = time.time()
646
+
647
  try:
648
+ with requests.post(
649
+ self.deepseek_api_url,
650
+ headers=self._deepseek_headers(self.ds_key),
651
+ json=payload,
652
+ stream=True,
653
+ timeout=self.timeouts["stream"],
654
+ ) as response:
655
  if response.status_code != 200:
656
+ self._raise_for_deepseek_status(response)
657
+
658
+ for raw_line in response.iter_lines(
659
+ decode_unicode=True
660
+ ):
661
+ event = self._parse_sse_line(raw_line)
662
+ if not event:
663
+ continue
664
+ if event.get("done"):
665
+ break
666
+
667
+ choices = event.get("choices") or []
668
+ if not choices:
669
+ continue
670
+
671
+ delta = choices[0].get("delta") or {}
672
+ reasoning_content = (
673
+ delta.get("reasoning_content") or ""
674
+ )
675
+ content = delta.get("content") or ""
676
+
677
+ if reasoning_content:
678
+ reasoning_chunks += 1
679
+ if not reasoning_open:
680
+ reasoning_open = True
681
+ logging.info(
682
+ "🧠 DeepSeek thinking started."
683
+ )
684
+ if self.deepseek_show_reasoning:
685
+ yield "\n<think>\n"
686
+
687
+ if self.deepseek_show_reasoning:
688
+ yield str(reasoning_content)
689
+ else:
690
+ yield ""
691
+
692
+ if reasoning_chunks % 25 == 0:
693
+ logging.info(
694
+ "⏳ DeepSeek thinking heartbeat: "
695
+ "%.1fs, %s chunks",
696
+ time.time() - start_time,
697
+ reasoning_chunks,
698
+ )
699
+
700
+ if content:
701
+ if (
702
+ reasoning_open
703
+ and self.deepseek_show_reasoning
704
+ ):
705
+ yield "\n</think>\n\n"
706
+ reasoning_open = False
707
+ emitted_content = True
708
+ yield str(content)
709
+
710
+ if (
711
+ reasoning_open
712
+ and self.deepseek_show_reasoning
713
+ ):
714
+ yield "\n</think>\n"
715
+
716
+ if not emitted_content:
717
+ raise DeepSeekAPIError(
718
+ "DeepSeek stream ended without final content."
719
+ )
720
+
721
+ logging.info(
722
+ "✅ [流式生成结束] 总耗时=%.1fs",
723
+ time.time() - start_time,
724
+ )
725
+ return
726
+
727
+ except GeneratorExit:
728
  raise
729
+ except (
730
+ DeepSeekAuthenticationError,
731
+ DeepSeekBalanceError,
732
+ DeepSeekRequestError,
733
+ ):
734
+ raise
735
+ except (
736
+ requests.exceptions.Timeout,
737
+ requests.exceptions.ConnectionError,
738
+ requests.exceptions.HTTPError,
739
+ DeepSeekAPIError,
740
+ ) as exc:
741
+ last_error = exc
742
+ logging.error(
743
+ "❌ DeepSeek 流式尝试 %s/%s 失败: %s",
744
+ attempt + 1,
745
+ self.deepseek_max_retries,
746
+ exc,
747
+ )
748
+
749
+ if emitted_content:
750
+ raise DeepSeekAPIError(
751
+ "DeepSeek stream was interrupted after partial "
752
+ "content had already been emitted."
753
+ ) from exc
754
+
755
+ is_last = attempt == self.deepseek_max_retries - 1
756
+ if is_last:
757
+ break
758
+ time.sleep(self._retry_delay(attempt))
759
+
760
+ raise DeepSeekAPIError(
761
+ f"DeepSeek streaming failed after "
762
+ f"{self.deepseek_max_retries} attempts: {last_error}"
763
+ )
764
 
765
  def parse_user_core_literature(self, user_lit_text):
766
  if not user_lit_text.strip():