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 requests
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_path = Path(__file__).resolve().parents[2] / ".env"
|
||||
@@ -88,6 +91,20 @@ def _get_db_defaults(provider: Optional[str] = None) -> dict:
|
||||
pass
|
||||
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(
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
@@ -105,35 +122,34 @@ def call_llm(
|
||||
temperature = temperature if temperature is not None else defaults["temperature"]
|
||||
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"]
|
||||
cfg = _get_provider_config(provider)
|
||||
|
||||
providers_to_try = [provider] if provider else _get_provider_fallback_list()
|
||||
last_error = None
|
||||
for p in providers_to_try:
|
||||
try:
|
||||
cfg = _get_provider_config(p)
|
||||
endpoint = f"{cfg['base_url'].rstrip('/')}/chat/completions"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {cfg['api_key']}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
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,
|
||||
"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:
|
||||
resp = requests.post(endpoint, json=payload, headers=headers, timeout=120, stream=stream)
|
||||
if resp.status_code == 429:
|
||||
logger.warning(f"[LLM] {p} 429 配额超限 → 尝试下一个")
|
||||
last_error = LLMError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
||||
continue
|
||||
if resp.status_code != 200:
|
||||
raise LLMError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
||||
|
||||
if stream:
|
||||
content_parts = []
|
||||
reasoning_parts = []
|
||||
content_parts, reasoning_parts = [], []
|
||||
for line in resp.iter_lines():
|
||||
if not line: continue
|
||||
if line.startswith(b'data: '):
|
||||
@@ -142,29 +158,30 @@ def call_llm(
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
delta = chunk['choices'][0]['delta']
|
||||
if delta.get('content'):
|
||||
content_parts.append(delta['content'])
|
||||
if delta.get('reasoning_content'):
|
||||
reasoning_parts.append(delta['reasoning_content'])
|
||||
if delta.get('content'): content_parts.append(delta['content'])
|
||||
if delta.get('reasoning_content'): reasoning_parts.append(delta['reasoning_content'])
|
||||
except Exception: continue
|
||||
if content_parts:
|
||||
return "".join(content_parts)
|
||||
return "".join(reasoning_parts)
|
||||
return "".join(content_parts) or "".join(reasoning_parts)
|
||||
else:
|
||||
data = resp.json()
|
||||
msg = data["choices"][0]["message"]
|
||||
# 优先取 content(推理模型如 deepseek 的最终答案在此字段)
|
||||
# 如果 content 为空但 reasoning_content 有值(说明 max_tokens 不够没输出完),取其末尾作为近似答案
|
||||
content = msg.get('content') or ''
|
||||
if not content.strip():
|
||||
rc = msg.get('reasoning_content', '')
|
||||
if rc:
|
||||
# 取 reasoning 末尾最可能包含答案的句子
|
||||
parts = [p.strip() for p in rc.replace('\n', '。').split('。') if p.strip()]
|
||||
content = parts[-1] if parts else rc
|
||||
return content.strip() if content else ''
|
||||
except requests.RequestException as e:
|
||||
raise LLMError(f"Request failed: {e}")
|
||||
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(
|
||||
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)
|
||||
else:
|
||||
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 = strip_ai_preface(polished)
|
||||
polished = strip_thinking_html(polished)
|
||||
|
||||
Reference in New Issue
Block a user