fix(llm): _row_to_dict根因修复——sqlor行是DictObject(dict子类),数据在dict本体,vars()取到空实例__dict__致治理链模型字段全空(策略首次激活时暴露);init.py用量查询同修

This commit is contained in:
yumoqing 2026-09-04 17:25:29 +08:00
parent 611008cb38
commit e0c1dc751d
4 changed files with 21 additions and 90 deletions

View File

@ -124,7 +124,11 @@ async def _run_one_pass():
async def llm_usage_accounting_loop():
"""后台循环:抢分布式锁 → 出账一批 → 睡。永不抛出。"""
"""后台循环:抢分布式锁 → 出账一批 → 睡。永不抛出。
只由独立记账进程app/pipeline_accounting_worker.py运行
不挂进 web 服务器的事件循环同步记账在 web异步记账在独立进程
"""
logger.info("[pipeline_llm] 异步出账循环已启动(间隔 %ds", _SCAN_INTERVAL)
while True:
try:
@ -143,19 +147,3 @@ async def llm_usage_accounting_loop():
except Exception as e:
logger.error("pipeline_llm.accounting: 循环异常(继续): %s", e)
await asyncio.sleep(_SCAN_INTERVAL)
def start_accounting_loop():
"""注册到当前事件循环load 时调用)。已有则跳过。"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = None
if loop is None or not loop.is_running():
logger.info("[pipeline_llm] 无运行中的事件循环,跳过出账循环注册")
return
for t in asyncio.all_tasks(loop):
coro = getattr(t, 'get_coro', lambda: None)()
if coro is not None and getattr(coro, '__name__', '') == 'llm_usage_accounting_loop':
return
loop.create_task(llm_usage_accounting_loop())

View File

@ -29,7 +29,7 @@ logger = logging.getLogger("pipeline_llm.gateway")
DBNAME = "pipeline"
GOVERN_REDIS_DB = 4 # 治理计数器命名空间(会话用 db3互不干扰
_COOLDOWN_SEC = 60 # 429 冷却窗口
_EST_TOKENS_PER_CALL = 800 # 无法估算时的单次调用预估(预授权用)
_EST_TOKENS_PER_CALL = 800 # 历史常量(预授权已废弃,保留防外部引用)
# 治理开关缓存:机构是否启用治理(有策略记录或有 llm_model 记录)
_govern_cache: dict = {}
@ -97,10 +97,12 @@ def _redis():
def _row_to_dict(r):
"""行对象转 dict。sqlor 行是 DictObjectdict 子类),数据存在 dict 本体——
vars() 取的是空的实例 __dict__2026-09-04 实测根因必须 dict() 拷贝"""
try:
return {k: v for k, v in vars(r).items() if not callable(v)}
except TypeError:
return dict(r)
except (TypeError, ValueError):
return {k: v for k, v in vars(r).items() if not callable(v)}
def _parse_json(s, default):
@ -143,71 +145,9 @@ async def _rate_check(scope: str, scope_id: str, limit: int):
return True, ''
# ────────────────────────── 门禁 ③④:额度检查 + 预授权 ──────────────────────────
async def _reserve_quota(sor, org_id: str, user_id: str, amount: float):
"""预授权:组织池 + 个人额度扣减。返回 (ok, msg, 实际冻结金额)。
条件 UPDATE 防超扣个人不足先回滚组织侧0 元调用未定价模型不冻结
"""
if amount <= 0:
return True, '', 0.0
# 组织池
recs = await sor.sqlExe(
"SELECT id, balance FROM llm_org_quota WHERE org_id=${o}$ AND status='active' LIMIT 1",
{"o": org_id})
await sor.sqlExe("COMMIT", {})
org_quota_id = ''
if recs:
if _fnum(getattr(recs[0], 'balance', 0)) < amount:
return False, '组织余额不足:当前余额 %.4f,本次预扣 %.4f。请充值组织池(模型治理→组织限额限流)' % (
_fnum(getattr(recs[0], 'balance', 0)), amount), 0.0
org_quota_id = getattr(recs[0], 'id', '')
await sor.sqlExe(
"UPDATE llm_org_quota SET balance=balance-${a}$ WHERE id=${i}$ AND balance>=${a}$",
{"a": amount, "i": org_quota_id})
await sor.sqlExe("COMMIT", {})
# 个人额度(有记录才校验;共享池+个人上限模式)
if user_id:
recs = await sor.sqlExe(
"SELECT id, quota_limit, quota_used FROM llm_user_quota "
"WHERE user_id=${u}$ AND org_id=${o}$ AND status='active' LIMIT 1",
{"u": user_id, "o": org_id})
await sor.sqlExe("COMMIT", {})
if recs:
lim = _fnum(getattr(recs[0], 'quota_limit', 0))
used = _fnum(getattr(recs[0], 'quota_used', 0))
if lim > 0 and used + amount > lim:
if org_quota_id: # 回滚组织侧
await sor.sqlExe(
"UPDATE llm_org_quota SET balance=balance+${a}$ WHERE id=${i}$",
{"a": amount, "i": org_quota_id})
await sor.sqlExe("COMMIT", {})
return False, '个人额度不足:已用 %.4f / 上限 %.4f,本次需 %.4f。请联系管理员调高个人额度' % (
used, lim, amount), 0.0
await sor.sqlExe(
"UPDATE llm_user_quota SET quota_used=quota_used+${a}$ WHERE id=${i}$",
{"a": amount, "i": getattr(recs[0], 'id', '')})
await sor.sqlExe("COMMIT", {})
return True, '', amount
async def _release_quota(sor, org_id: str, user_id: str, amount: float):
"""释放预授权(调用失败时)。"""
if amount <= 0:
return
await sor.sqlExe(
"UPDATE llm_org_quota SET balance=balance+${a}$ WHERE org_id=${o}$ AND status='active'",
{"a": amount, "o": org_id})
if user_id:
await sor.sqlExe(
"UPDATE llm_user_quota SET quota_used=GREATEST(quota_used-${a}$,0) "
"WHERE user_id=${u}$ AND org_id=${o}$",
{"a": amount, "u": user_id, "o": org_id})
await sor.sqlExe("COMMIT", {})
# ────────────────────────── 门禁 ⑤⑥:策略选模型 + 候选轮转 ──────────────────────────
# 注2026-09 记账重构废弃了组织池/个人额度预授权(原 _reserve_quota/_release_quota 已删除)——
# 本机构模型只记成本侧owner 模型走异步三方账,防滥用由限流(①②)承担。
async def _org_policy(sor, org_id: str):
"""组织容错策略。无记录返回 (主模型空, 辅助模型空, 备链空, 端点偏好 any)。"""

View File

@ -76,13 +76,14 @@ async def _resolve_call(sor, org_id, user_id, model_name, capability,
est_tokens=int(est_tokens or 0), task_ref=task_ref or '')
if not ok:
if res == '__LEGACY__':
# 未配策略/指定模型不在新表:不再有旧表兜底,报可行动错误
# 全平台无策略/指定模型不在新表:不再有旧表兜底,报可行动错误
if model_name:
raise GovernError(
'模型「%s」未注册到模型治理(模型注册表无此名或已停用)。'
'请在模型治理→模型注册中添加,或改用已注册模型' % model_name)
raise GovernError(
'机构 %s 未配置模型容错策略:请在模型治理→组织容错策略配置主/备模型' % org_id)
'无可用模型策略:机构 %s 未配置且平台也未配置组织容错策略。'
'请在模型治理→组织容错策略配置主/备模型' % org_id)
raise GovernError(res)
if not isinstance(res, dict):
raise GovernError('治理引擎返回异常结果: %s' % str(res)[:200])

View File

@ -444,10 +444,12 @@ async def llm_usage_query(params_kw):
await sor.sqlExe("COMMIT", {})
rows = []
for r in (recs or []):
d = {}
for k, v in vars(r).items():
if not callable(v):
d[k] = v
# sqlor 行是 DictObjectdict 子类),必须 dict() 拷贝——
# vars() 取到的是空实例 __dict__同 gateway._row_to_dict 根因)
try:
d = dict(r)
except (TypeError, ValueError):
d = {k: v for k, v in vars(r).items() if not callable(v)}
rows.append(d)
s = sums[0] if sums else None
result['success'] = True