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:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user