fix(记账): 同步调用usage原文落usages+出账补prompt_tokens_details/hour维度——根治derived缓存变量求值失败(uncache_tokens=0输入不计费)与忙闲时分档计价缺失(2026-09-07实测qwen3.8-max记账failed根因)

This commit is contained in:
yumoqing 2026-09-07 16:57:43 +08:00
parent 08cf9272cd
commit e9e57cf825
2 changed files with 33 additions and 3 deletions

View File

@ -33,7 +33,8 @@ _LOCK_TTL = 90 # 锁 TTL(略大于单批预算耗时)
async def _get_pending(sor):
recs = await sor.sqlExe(
"SELECT id, org_id, user_id, model_id, req_tokens, resp_tokens, cost, ppid, usages "
"SELECT id, org_id, user_id, model_id, req_tokens, resp_tokens, cost, ppid, usages, "
"created_at "
"FROM llm_usage WHERE accounting_status='created' AND status='SUCCEEDED' "
"ORDER BY created_at LIMIT %d" % _BATCH, {})
await sor.sqlExe("COMMIT", {})
@ -63,7 +64,7 @@ async def _settle_one(sor, row):
logger.warning("pipeline_llm.accounting: model %s 无产品映射,流水 %s 置 failed",
model_id, row.id)
return False
usage_data = {
usage_data: dict = {
'prompt_tokens': int(getattr(row, 'req_tokens', 0) or 0),
'completion_tokens': int(getattr(row, 'resp_tokens', 0) or 0),
}
@ -77,6 +78,26 @@ async def _settle_one(sor, row):
usage_data.update(extra)
except Exception:
pass
# derived 依赖归一(2026-09-07 根治):token 定价的 cached_tokens/uncache_tokens
# 靠 YAML derived 变量引用 prompt_tokens_details.cached_tokens 计算——该键缺失时
# DictObject 属性链抛 AttributeError,引擎兜底成 0,输入 token 永不计费
# (实测 qwen3.8-max uncache_tokens=0 记账失败根因之一)。缺失补 0 结构,
# derived 正常求值:无缓存时 uncache=prompt_tokens、cached=0。
ptd = usage_data.get('prompt_tokens_details')
if not isinstance(ptd, dict):
usage_data['prompt_tokens_details'] = {'cached_tokens': 0}
elif ptd.get('cached_tokens') is None:
ptd['cached_tokens'] = 0
# 忙闲时计价维度(2026-09-07):价目表存在忙时(8点-22点)/闲时(22点-次日8点)
# 两档定价(deepseek-v4-pro-0813),按调用时刻 llm_usage.created_at 的小时数注入
# hour——YAML 用 hour 区间 filters 选档。多余键对不用它的定价方案无影响
# (引擎只严格检查定价项里出现的维度键)。
created_at = getattr(row, 'created_at', None)
if created_at is not None:
try:
usage_data['hour'] = int(created_at.hour)
except Exception:
pass
fn = getattr(env, 'product_accounting_generic', None)
if fn is None:
# 产品模块未加载 → 保留 created 下轮再试

View File

@ -742,7 +742,16 @@ async def chat_inference(org_id, user_id, payload, model_name='', task_ref=''):
usage = data.get('usage') or {}
rt = int(_fnum(usage.get('prompt_tokens')))
ct = int(_fnum(usage.get('completion_tokens')))
await govern_settle(ctx, True, rt, ct, '' if usage else 'est')
# usage 原文落 llm_usage.usages(2026-09-07 根治,对齐异步路径):
# 定价引擎的 derived 变量引用(cached_tokens=prompt_tokens_details.cached_tokens、
# uncache_tokens=prompt_tokens - prompt_tokens_details.cached_tokens)依赖原始 usage
# 结构——此前同步路径只存 req/resp 两列、usages 为空,导致 prompt_tokens_details
# 缺失 → derived 求值失败被兜底成 0 → 输入 token 永不计费(实测 qwen3.8-max
# uncache_tokens=0 记账失败根因)。sage/llmage 同步范式即 usages=json.dumps(usage)。
usages = dict(usage) if isinstance(usage, dict) else {}
if usages:
usages['model'] = ctx.get('model_id') or ''
await govern_settle(ctx, True, rt, ct, '' if usage else 'est', usages)
return data