fix(llm): call_llm 429 自动 fallback + 移除 compliance 硬编码 provider

- nvidia_client.py 新增 _get_provider_fallback_list() 从 DB 读提供商顺序
- call_llm 循环尝试提供商列表,HTTP 429 自动跳过到下一个
- 移除 compliance_optimizer.py 中硬编码 provider="opencode-go"
- 新增 logging 模块引用
This commit is contained in:
Yuzhiran Dev
2026-06-01 13:23:35 +08:00
parent 09c467e56d
commit 31d8434010
2 changed files with 77 additions and 60 deletions
+76 -59
View File
@@ -6,9 +6,12 @@ Unified LLM Client
import os import os
import requests import requests
import json import json
import logging
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__)
from dotenv import load_dotenv from dotenv import load_dotenv
env_path = Path(__file__).resolve().parents[2] / ".env" env_path = Path(__file__).resolve().parents[2] / ".env"
@@ -88,6 +91,20 @@ def _get_db_defaults(provider: Optional[str] = None) -> dict:
pass pass
return {"temperature": 0.20, "max_tokens": 2048, "system_prompt": "你是一个专业的内容创作助手。"} return {"temperature": 0.20, "max_tokens": 2048, "system_prompt": "你是一个专业的内容创作助手。"}
def _get_provider_fallback_list() -> List[str]:
"""返回提供商尝试顺序列表(活跃优先,其余按配置顺序)"""
try:
from ..database import SessionLocal
from ..models import LLMConfig
db = SessionLocal()
configs = db.query(LLMConfig).order_by(LLMConfig.is_active.desc(), LLMConfig.id).all()
db.close()
providers = [c.provider for c in configs if c.provider]
return providers
except Exception:
pass
return ["opencode-go", "nvidia"]
def call_llm( def call_llm(
prompt: str, prompt: str,
model: Optional[str] = None, model: Optional[str] = None,
@@ -105,66 +122,66 @@ def call_llm(
temperature = temperature if temperature is not None else defaults["temperature"] temperature = temperature if temperature is not None else defaults["temperature"]
max_tokens = max_tokens if max_tokens is not None else defaults["max_tokens"] max_tokens = max_tokens if max_tokens is not None else defaults["max_tokens"]
system_prompt = system_prompt if system_prompt is not None else defaults["system_prompt"] system_prompt = system_prompt if system_prompt is not None else defaults["system_prompt"]
cfg = _get_provider_config(provider)
endpoint = f"{cfg['base_url'].rstrip('/')}/chat/completions"
headers = {
"Authorization": f"Bearer {cfg['api_key']}",
"Content-Type": "application/json"
}
payload = {
"model": model or cfg["model"],
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"temperature": temperature,
"max_tokens": max_tokens,
"top_p": top_p,
"frequency_penalty": frequency_penalty,
"presence_penalty": presence_penalty,
"stream": stream,
}
if additional_params:
payload.update(additional_params)
try: providers_to_try = [provider] if provider else _get_provider_fallback_list()
resp = requests.post(endpoint, json=payload, headers=headers, timeout=120, stream=stream) last_error = None
if resp.status_code != 200: for p in providers_to_try:
raise LLMError(f"HTTP {resp.status_code}: {resp.text[:200]}") try:
if stream: cfg = _get_provider_config(p)
content_parts = [] endpoint = f"{cfg['base_url'].rstrip('/')}/chat/completions"
reasoning_parts = [] headers = {"Authorization": f"Bearer {cfg['api_key']}", "Content-Type": "application/json"}
for line in resp.iter_lines(): payload = {
if not line: continue "model": model or cfg["model"],
if line.startswith(b'data: '): "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}],
data = line[6:] "temperature": temperature, "max_tokens": max_tokens, "top_p": top_p,
if data == b'[DONE]': break "frequency_penalty": frequency_penalty, "presence_penalty": presence_penalty,
try: "stream": stream,
chunk = json.loads(data) }
delta = chunk['choices'][0]['delta'] if additional_params:
if delta.get('content'): payload.update(additional_params)
content_parts.append(delta['content'])
if delta.get('reasoning_content'): resp = requests.post(endpoint, json=payload, headers=headers, timeout=120, stream=stream)
reasoning_parts.append(delta['reasoning_content']) if resp.status_code == 429:
except Exception: continue logger.warning(f"[LLM] {p} 429 配额超限 → 尝试下一个")
if content_parts: last_error = LLMError(f"HTTP {resp.status_code}: {resp.text[:200]}")
return "".join(content_parts) continue
return "".join(reasoning_parts) if resp.status_code != 200:
else: raise LLMError(f"HTTP {resp.status_code}: {resp.text[:200]}")
data = resp.json()
msg = data["choices"][0]["message"] if stream:
# 优先取 content(推理模型如 deepseek 的最终答案在此字段) content_parts, reasoning_parts = [], []
# 如果 content 为空但 reasoning_content 有值(说明 max_tokens 不够没输出完),取其末尾作为近似答案 for line in resp.iter_lines():
content = msg.get('content') or '' if not line: continue
if not content.strip(): if line.startswith(b'data: '):
rc = msg.get('reasoning_content', '') data = line[6:]
if rc: if data == b'[DONE]': break
# 取 reasoning 末尾最可能包含答案的句子 try:
parts = [p.strip() for p in rc.replace('\n', '').split('') if p.strip()] chunk = json.loads(data)
content = parts[-1] if parts else rc delta = chunk['choices'][0]['delta']
return content.strip() if content else '' if delta.get('content'): content_parts.append(delta['content'])
except requests.RequestException as e: if delta.get('reasoning_content'): reasoning_parts.append(delta['reasoning_content'])
raise LLMError(f"Request failed: {e}") except Exception: continue
return "".join(content_parts) or "".join(reasoning_parts)
else:
data = resp.json()
msg = data["choices"][0]["message"]
content = msg.get('content') or ''
if not content.strip():
rc = msg.get('reasoning_content', '')
if rc:
parts = [p.strip() for p in rc.replace('\n', '').split('') if p.strip()]
content = parts[-1] if parts else rc
return content.strip() or ''
except LLMError as e:
if '429' in str(e):
logger.warning(f"[LLM] {p} 429 配额超限 → 尝试下一个")
last_error = e; continue
raise
except Exception as e:
logger.warning(f"[LLM] {p} 调用异常: {e} → 尝试下一个")
last_error = e; continue
raise last_error or LLMError("所有 LLM 提供商均不可用")
def expand_content_with_llm( def expand_content_with_llm(
topic: dict, topic: dict,
+1 -1
View File
@@ -182,7 +182,7 @@ def polish_with_llm(html: str, platform: str, remaining_issues: Optional[List[Di
prompt = get_prompt("compliance_fix", issues_desc=issues_desc, html=html) prompt = get_prompt("compliance_fix", issues_desc=issues_desc, html=html)
else: else:
prompt = get_prompt("compliance_polish", html=html) prompt = get_prompt("compliance_polish", html=html)
polished = call_llm(prompt, provider="opencode-go", temperature=temperature, max_tokens=max_tokens, system_prompt=system_prompt) polished = call_llm(prompt, provider=None, temperature=temperature, max_tokens=max_tokens, system_prompt=system_prompt)
polished = clean_html_content(polished) polished = clean_html_content(polished)
polished = strip_ai_preface(polished) polished = strip_ai_preface(polished)
polished = strip_thinking_html(polished) polished = strip_thinking_html(polished)