feat(llm): status统一枚举SUCCEEDED/FAILED/PENDING/RUNNING+非SUCCEEDED记账状态NULL;llm_api_profile加method独立列(2026-09-06用户定夺)

- llm_usage.status 各家归一枚举(其他=recharge对账行);accounting_status仅SUCCEEDED行写created,其余NULL不冒充待记账
- govern_settle失败行不再挂created;充值行不写accounting_status
- 出账循环拾取条件 status='SUCCEEDED';面板/概览统计同步改
- llm_api_profile.method GET/POST独立列(默认POST),运行时读列决定动词;查询步骤存量兼容回退解析request_template.$.method
- 字典llm_usage_status更新五枚举;README同步
This commit is contained in:
yumoqing 2026-09-06 17:15:40 +08:00
parent 825dd2e130
commit 671d320d59
10 changed files with 76 additions and 46 deletions

View File

@ -19,11 +19,11 @@
| `llm_vendor` | 供应商(端点目录 JSON;2026-09-05 起不存协议字段,请求形态归模型挂的适配模板) | | `llm_vendor` | 供应商(端点目录 JSON;2026-09-05 起不存协议字段,请求形态归模型挂的适配模板) |
| `llm_account` | 供应商账号(钱包:余额/充值/选端点,api_key AES 加密;模型可绑定账号 account_id 收窄候选池) | | `llm_account` | 供应商账号(钱包:余额/充值/选端点,api_key AES 加密;模型可绑定账号 account_id 收窄候选池) |
| `llm_model` | 模型注册(能力分类/挂定价 ppid/sync_mode/query_profile_ids,name 唯一;2026-09 起不存单价,定价走 ppid) | | `llm_model` | 模型注册(能力分类/挂定价 ppid/sync_mode/query_profile_ids,name 唯一;2026-09 起不存单价,定价走 ppid) |
| `llm_api_profile` | 适配模板(协议×能力形态去重;data/response 模板全 Jinja2,含媒体转换函数) | | `llm_api_profile` | 适配模板(协议×能力形态去重;data/response 模板全 Jinja2,含媒体转换函数;method 独立列,2026-09-06 起不塞 request_template) |
| `llm_org_policy` | 组织容错策略(主备模型 + 辅助模型 utility_model_id + 端点偏好) | | `llm_org_policy` | 组织容错策略(主备模型 + 辅助模型 utility_model_id + 端点偏好) |
| `llm_org_quota` | 组织限额限流(池余额 + QPM) | | `llm_org_quota` | 组织限额限流(池余额 + QPM) |
| `llm_user_quota` | 个人限额限流(额度上限 + QPM) | | `llm_user_quota` | 个人限额限流(额度上限 + QPM) |
| `llm_usage` | 用量流水(双维度记账事实表;usages 字段存非 token 计价因子 JSON) | | `llm_usage` | 用量流水(双维度记账事实表;status 统一枚举 SUCCEEDED/FAILED/PENDING/RUNNING/recharge;accounting_status 仅 SUCCEEDED 行有意义否则 NULL;usages 字段存非 token 计价因子 JSON) |
## 调用门禁链 ## 调用门禁链
@ -60,17 +60,23 @@
- **限流用 Redis 分钟窗口原子计数**(db4,与会话 db3 隔离);Redis 故障时限流放行(fail-open), - **限流用 Redis 分钟窗口原子计数**(db4,与会话 db3 隔离);Redis 故障时限流放行(fail-open),
余额仍由 DB 条件 UPDATE 守住。 余额仍由 DB 条件 UPDATE 守住。
- **429 → (账号,端点)对冷却 60s**;账号级冷却用 `*` 通配键。 - **429 → (账号,端点)对冷却 60s**;账号级冷却用 `*` 通配键。
- **记账三态(2026-09-05 用户定夺)**:`accounting_status` 只有三个值—— - **status 统一枚举 + 记账三态(2026-09-05 定夺;2026-09-06 补 NULL 语义,m0012)**:
`created`(已创建待记账)→ `accounted`(记账成功)/ `failed`(记账失败)。 `llm_usage.status` 各家模型归一为 SUCCEEDED/FAILED/PENDING/RUNNING(其他=recharge 充值对账行)。
所有成功调用统一 created 进出账队列;自用 vs 跨机构分流由产品层 `accounting_status` 只有三个值——`created`(待记账)→ `accounted`(记账成功)/ `failed`(记账失败);
`product_accounting_generic` 的 is_self_use 决定(自用只记 PAY* 采购成本, **只有 status='SUCCEEDED' 才有记账意义**:仅成功行写 created,非 SUCCEEDED 行保持 **NULL**
(不冒充待记账)。出账循环扫 `accounting_status='created' AND status='SUCCEEDED'`。
自用 vs 跨机构分流由产品层 `product_accounting_generic` 的 is_self_use 决定(自用只记 PAY* 采购成本,
跨机构另记 PAY 客户应付)。流水 `usages` 存非 token 计价因子(视频时长/ 跨机构另记 PAY 客户应付)。流水 `usages` 存非 token 计价因子(视频时长/
分辨率等),异步出账循环(独立进程,需 load_appbase 提供 get_business_date) 分辨率等),异步出账循环(独立进程,需 load_appbase 提供 get_business_date)
读入定价引擎。未挂 ppid 的模型出账置 failed 并写明原因(不静默)。 读入定价引擎。未挂 ppid 的模型出账置 failed 并写明原因(不静默)。
- **媒体转换铁律(2026-09-05)**:生成类模型适配模板 data 用 - **method 独立列(2026-09-06 用户定夺,m0012)**:`llm_api_profile.method`(GET/POST,
`b64media2url(request, xxx_file)`(上传转公网 URL)、response 用 默认 POST)不塞 request_template JSON;运行时读该列决定 HTTP 动词,存量行由迁移
从 request_template 的 $.method 回填,查询步骤兼容回退解析。
- **媒体转换铁律(2026-09-06 三数组契约,对齐 sage/llmage)**:生成类模型适配模板
data 用 `b64media2url(request, _f)` 逐元素转公网 URL、response 用
`downloadfile2url(request, url)`(生成物落地本地,上游 URL 仅 24 小时); `downloadfile2url(request, url)`(生成物落地本地,上游 URL 仅 24 小时);
同类能力对外契约统一(i2v:image_file 进 → video 出)。 上行媒体统一三数组参数 `image_files`/`audio_files`/`video_files`(字符串或数组
均可,Jinja `is string` 动态判断),变长输入用 `{% for %}` 按文档示例组装。
- **异步模型执行器**:sync_mode=async 时走提交→按 query_profile_ids 轮询→ - **异步模型执行器**:sync_mode=async 时走提交→按 query_profile_ids 轮询→
取结果;等待预算默认 600 秒,`_timeout` 可覆盖(上限 900)。 取结果;等待预算默认 600 秒,`_timeout` 可覆盖(上限 900)。

View File

@ -34,8 +34,10 @@
{"k": "must_intl", "v": "仅国际(合规)"} {"k": "must_intl", "v": "仅国际(合规)"}
]}, ]},
{"parentid": "llm_usage_status", "parentname": "用量状态", "items": [ {"parentid": "llm_usage_status", "parentname": "用量状态", "items": [
{"k": "ok", "v": "成功"}, {"k": "SUCCEEDED", "v": "成功"},
{"k": "failed", "v": "失败"}, {"k": "FAILED", "v": "失败"},
{"k": "PENDING", "v": "等待"},
{"k": "RUNNING", "v": "运行中"},
{"k": "recharge", "v": "充值"} {"k": "recharge", "v": "充值"}
]} ]}
] ]

View File

@ -45,6 +45,14 @@
"type": "str", "type": "str",
"length": 200 "length": 200
}, },
{
"name": "method",
"title": "HTTP方法(GET/POST)",
"type": "str",
"length": 10,
"nullable": "no",
"default": "'POST'"
},
{ {
"name": "headers", "name": "headers",
"title": "请求头(JSON)", "title": "请求头(JSON)",

View File

@ -77,11 +77,10 @@
}, },
{ {
"name": "accounting_status", "name": "accounting_status",
"title": "记账状态(created已创建待记账/accounted记账成功/failed记账失败)", "title": "记账状态(created待记账/accounted记账成功/failed记账失败;仅status=SUCCEEDED时有意义,否则NULL)",
"type": "str", "type": "str",
"length": 20, "length": 20,
"nullable": "no", "nullable": "yes"
"default": "'created'"
}, },
{ {
"name": "ppid", "name": "ppid",
@ -97,11 +96,11 @@
}, },
{ {
"name": "status", "name": "status",
"title": "状态", "title": "调用状态(SUCCEEDED/FAILED/PENDING/RUNNING/其他如recharge)",
"type": "str", "type": "str",
"length": 20, "length": 20,
"nullable": "no", "nullable": "no",
"default": "'ok'" "default": "'SUCCEEDED'"
}, },
{ {
"name": "usages", "name": "usages",

View File

@ -59,6 +59,7 @@ CREATE TABLE IF NOT EXISTS llm_api_profile (
`protocol` varchar(20) NOT NULL comment '协议类型', `protocol` varchar(20) NOT NULL comment '协议类型',
`capability` varchar(20) NOT NULL DEFAULT 't2t' comment '能力类型', `capability` varchar(20) NOT NULL DEFAULT 't2t' comment '能力类型',
`path` varchar(200) comment '请求路径(相对端点base_url,如/chat/completions)', `path` varchar(200) comment '请求路径(相对端点base_url,如/chat/completions)',
`method` varchar(10) NOT NULL DEFAULT 'POST' comment 'HTTP方法(GET/POST,独立列不再塞request_template)',
`headers` text comment '请求头(JSON,含认证头模板)', `headers` text comment '请求头(JSON,含认证头模板)',
`request_template` text comment '请求模板(Jinja2)', `request_template` text comment '请求模板(Jinja2)',
`response_template` text comment '响应模板(Jinja2)', `response_template` text comment '响应模板(Jinja2)',
@ -121,10 +122,10 @@ CREATE TABLE IF NOT EXISTS llm_usage (
`resp_tokens` int NOT NULL DEFAULT 0 comment '响应token', `resp_tokens` int NOT NULL DEFAULT 0 comment '响应token',
`cost` decimal(12,6) NOT NULL DEFAULT 0 comment '成本金额(账号侧)', `cost` decimal(12,6) NOT NULL DEFAULT 0 comment '成本金额(账号侧)',
`charge` decimal(12,6) NOT NULL DEFAULT 0 comment '客户应付(异步三方账计算,本机构模型恒0)', `charge` decimal(12,6) NOT NULL DEFAULT 0 comment '客户应付(异步三方账计算,本机构模型恒0)',
`accounting_status` varchar(20) NOT NULL DEFAULT 'created' comment '记账状态(三态:created已创建待记账/accounted记账成功/failed记账失败)', `accounting_status` varchar(20) comment '记账状态(三态:created待记账/accounted记账成功/failed记账失败;仅status=SUCCEEDED的行有意义,其余NULL)',
`ppid` varchar(32) comment '定价项目ID', `ppid` varchar(32) comment '定价项目ID',
`task_ref` varchar(100) comment '调用来源', `task_ref` varchar(100) comment '调用来源',
`status` varchar(20) NOT NULL DEFAULT 'ok' comment '状态(ok/failed/recharge)', `status` varchar(20) NOT NULL DEFAULT 'SUCCEEDED' comment '调用状态(统一枚举:SUCCEEDED成功/FAILED失败/PENDING等待/RUNNING运行;其他如recharge)',
`usages` text comment '非token用量因子JSON(时长/分辨率等,异步出账读入定价引擎)', `usages` text comment '非token用量因子JSON(时长/分辨率等,异步出账读入定价引擎)',
`note` varchar(200) comment '备注/原因', `note` varchar(200) comment '备注/原因',
`created_at` datetime NOT NULL DEFAULT current_timestamp() comment '创建时间', `created_at` datetime NOT NULL DEFAULT current_timestamp() comment '创建时间',

View File

@ -1,9 +1,11 @@
"""pipeline_llm.accounting — 异步出账循环(owner 模型三方账)。 """pipeline_llm.accounting — 异步出账循环(owner 模型三方账)。
扫 llm_usage 中 accounting_status='created' 且 status='ok' 的用量流水, 扫 llm_usage 中 accounting_status='created' 且 status='SUCCEEDED' 的用量流水,
逐条映射模型→产品,调产品层 product_accounting_generic 记账 逐条映射模型→产品,调产品层 product_accounting_generic 记账
(产品层内部按 is_self_use 分流:自用只记 PAY* 采购成本;跨机构另记 PAY 客户应付, (产品层内部按 is_self_use 分流:自用只记 PAY* 采购成本;跨机构另记 PAY 客户应付,
折扣精确到产品),成功置 'accounted',失败置 'failed'(三态:created/accounted/failed)。 折扣精确到产品),成功置 'accounted',失败置 'failed'(三态:created/accounted/failed)。
2026-09-06 用户定夺:status 统一枚举 SUCCEEDED/FAILED/PENDING/RUNNING;
只有 SUCCEEDED 的行才写 accounting_status(其余 NULL),出账循环自然只拾取 SUCCEEDED。
与账号/存储的同步记账分工明确(2026-09 用户定夺): 与账号/存储的同步记账分工明确(2026-09 用户定夺):
- 账号/存储:purchase_realtime 同步记账 - 账号/存储:purchase_realtime 同步记账
@ -32,7 +34,7 @@ _LOCK_TTL = 90 # 锁 TTL(略大于单批预算耗时)
async def _get_pending(sor): async def _get_pending(sor):
recs = await sor.sqlExe( 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 "
"FROM llm_usage WHERE accounting_status='created' AND status='ok' " "FROM llm_usage WHERE accounting_status='created' AND status='SUCCEEDED' "
"ORDER BY created_at LIMIT %d" % _BATCH, {}) "ORDER BY created_at LIMIT %d" % _BATCH, {})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
return recs or [] return recs or []

View File

@ -539,15 +539,16 @@ async def govern_settle(ctx: dict, ok_call: bool, req_tokens: int = 0, resp_toke
图像张数等按量计费模型的计价因子,由异步出账读入定价引擎 图像张数等按量计费模型的计价因子,由异步出账读入定价引擎
(与 req_tokens/resp_tokens 并列,定价引擎按 pricing_data 选用)。 (与 req_tokens/resp_tokens 并列,定价引擎按 pricing_data 选用)。
accounting_status 三态(2026-09-05 用户定夺): accounting_status 三态(2026-09-05 用户定夺;2026-09-06 补 NULL 语义):
created —— 已创建、待记账(status='ok' 的行由异步出账循环拾取) created —— 已创建、待记账(仅 status='SUCCEEDED' 的行,出账循环拾取)
accounted —— 记账成功 accounted —— 记账成功
failed —— 记账失败 failed —— 记账失败
NULL —— 无记账意义:status≠SUCCEEDED 的行(FAILED/PENDING/RUNNING/recharge)
一律保持 NULL,不写 created(用户定夺:只有 SUCCEEDED 才有记账的意义)
status 统一枚举(2026-09-06 用户定夺):SUCCEEDED/FAILED/PENDING/RUNNING(其他=recharge)。
自用 vs 跨机构的分流不在这里——product_accounting_generic 内部按 自用 vs 跨机构的分流不在这里——product_accounting_generic 内部按
is_self_use 决定记账项(自用只记 PAY* 采购成本,跨机构另记 PAY 客户应付), is_self_use 决定记账项(自用只记 PAY* 采购成本,跨机构另记 PAY 客户应付),
所有成功调用统一 created 进出账队列,成本账不遗漏。 所有成功调用统一 created 进出账队列,成本账不遗漏。
status='failed'(调用失败)/'recharge'(充值)的行也置 created,但出账循环
只拾取 status='ok',故它们不会被记账(无可计费用量)。
""" """
if not ctx or not isinstance(ctx, dict): if not ctx or not isinstance(ctx, dict):
return return
@ -578,7 +579,10 @@ async def govern_settle(ctx: dict, ok_call: bool, req_tokens: int = 0, resp_toke
usages_str = json.dumps(usages, ensure_ascii=False, default=str)[:2000] usages_str = json.dumps(usages, ensure_ascii=False, default=str)[:2000]
except Exception: except Exception:
usages_str = '' usages_str = ''
await sor.C('llm_usage', { # status 统一枚举(2026-09-06 用户定夺):SUCCEEDED/FAILED;
# accounting_status 只在 SUCCEEDED 时写 created,否则缺省 NULL
# (只有 SUCCEEDED 才有记账意义——失败/充值行挂 created 曾被误报"待记账")
usage_row = {
'id': getID(), 'id': getID(),
'org_id': caller_org, 'org_id': caller_org,
'user_id': ctx.get('user_id', ''), 'user_id': ctx.get('user_id', ''),
@ -591,11 +595,13 @@ async def govern_settle(ctx: dict, ok_call: bool, req_tokens: int = 0, resp_toke
'charge': 0, # 客户应付由异步出账计算(自用为 0,跨机构按折扣) 'charge': 0, # 客户应付由异步出账计算(自用为 0,跨机构按折扣)
'ppid': model.get('ppid', '') or '', 'ppid': model.get('ppid', '') or '',
'task_ref': ctx.get('task_ref', ''), 'task_ref': ctx.get('task_ref', ''),
'status': 'ok' if ok_call else 'failed', 'status': 'SUCCEEDED' if ok_call else 'FAILED',
'accounting_status': 'created',
'usages': usages_str, 'usages': usages_str,
'note': (note or '')[:200], 'note': (note or '')[:200],
}) }
if ok_call:
usage_row['accounting_status'] = 'created'
await sor.C('llm_usage', usage_row)
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
except Exception as e: except Exception as e:
logger.warning("pipeline_llm: 结算失败(不阻断调用方): %s", e) logger.warning("pipeline_llm: 结算失败(不阻断调用方): %s", e)

View File

@ -182,18 +182,19 @@ async def _resolve_call(sor, org_id, user_id, model_name, capability,
raise GovernError( raise GovernError(
'模型「%s」能力为 %s,与请求能力 %s 不匹配。请按能力分类选择模型' % ( '模型「%s」能力为 %s,与请求能力 %s 不匹配。请按能力分类选择模型' % (
model_row.get('name', ''), cap, capability)) model_row.get('name', ''), cap, capability))
# 适配模板(组包:path/headers/request_template,全 Jinja2,借鉴 uapi) # 适配模板(组包:path/method/headers/request_template,全 Jinja2,借鉴 uapi)
profile = {} profile = {}
pid = model_row.get('profile_id') or '' pid = model_row.get('profile_id') or ''
if pid: if pid:
recs = await sor.sqlExe( recs = await sor.sqlExe(
"SELECT path, headers, request_template, response_template FROM llm_api_profile " "SELECT path, method, headers, request_template, response_template "
"WHERE id=${i}$ AND status='active' LIMIT 1", {"i": pid}) "FROM llm_api_profile WHERE id=${i}$ AND status='active' LIMIT 1", {"i": pid})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
if recs: if recs:
r = recs[0] r = recs[0]
profile = { profile = {
'path': getattr(r, 'path', '') or '', 'path': getattr(r, 'path', '') or '',
'method': (getattr(r, 'method', '') or 'POST').upper(),
'headers': getattr(r, 'headers', '') or '', 'headers': getattr(r, 'headers', '') or '',
'request_template': getattr(r, 'request_template', '') or '', 'request_template': getattr(r, 'request_template', '') or '',
'response_template': getattr(r, 'response_template', '') or '', 'response_template': getattr(r, 'response_template', '') or '',
@ -207,20 +208,22 @@ async def _resolve_call(sor, org_id, user_id, model_name, capability,
qps = [] qps = []
for qid in qpids or []: for qid in qpids or []:
recs = await sor.sqlExe( recs = await sor.sqlExe(
"SELECT path, headers, request_template, response_template FROM llm_api_profile " "SELECT path, method, headers, request_template, response_template "
"WHERE id=${i}$ AND status='active' LIMIT 1", {"i": qid}) "FROM llm_api_profile WHERE id=${i}$ AND status='active' LIMIT 1", {"i": qid})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
if not recs: if not recs:
continue continue
r = recs[0] r = recs[0]
# method 无独立列:从 request_template JSON 的 method 键取(缺省 GET) # method 独立列(2026-09-06 用户定夺:不塞 request_template);
qmethod = 'GET' # 存量未迁移行兼容:列空时回退解析 request_template JSON 的 method 键
try: qmethod = (getattr(r, 'method', '') or '').strip().upper() or 'GET'
rt = json.loads(getattr(r, 'request_template', '') or '{}') if qmethod == 'GET':
if isinstance(rt, dict) and rt.get('method'): try:
qmethod = str(rt.get('method')).upper() rt = json.loads(getattr(r, 'request_template', '') or '{}')
except Exception: if isinstance(rt, dict) and str(rt.get('method', '')).upper() in ('GET', 'POST'):
pass qmethod = str(rt['method']).upper()
except Exception:
pass
qps.append({ qps.append({
'path': getattr(r, 'path', '') or '', 'path': getattr(r, 'path', '') or '',
'headers': getattr(r, 'headers', '') or '', 'headers': getattr(r, 'headers', '') or '',
@ -395,6 +398,8 @@ async def _call_upstream_chat(ctx, payload):
url = _join_url(ctx.get('api_base'), path) url = _join_url(ctx.get('api_base'), path)
headers = await _render_headers(profile.get('headers', ''), ns) headers = await _render_headers(profile.get('headers', ''), ns)
body = await _build_upstream_body(ctx, payload) body = await _build_upstream_body(ctx, payload)
# method 独立列(2026-09-06 用户定夺):默认 POST,profile 声明优先
method = (profile.get('method') or 'POST').upper()
timeout = int(_fnum(ctx.get('timeout')) or _TOTAL_TIMEOUT) timeout = int(_fnum(ctx.get('timeout')) or _TOTAL_TIMEOUT)
# 可诊断性:只记长度不记值(真 key 不落日志)。print 确保进应用日志 # 可诊断性:只记长度不记值(真 key 不落日志)。print 确保进应用日志
# (模块 logging.getLogger 的 handler 未接应用日志管道,2026-09-04 实测) # (模块 logging.getLogger 的 handler 未接应用日志管道,2026-09-04 实测)
@ -406,8 +411,8 @@ async def _call_upstream_chat(ctx, payload):
for attempt in range(_MAX_ATTEMPTS): for attempt in range(_MAX_ATTEMPTS):
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post( async with session.request(
url, headers=headers, json=body, method, url, headers=headers, json=body,
timeout=aiohttp.ClientTimeout(total=timeout, connect=_CONNECT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=timeout, connect=_CONNECT_TIMEOUT),
) as resp: ) as resp:
if resp.status != 200: if resp.status != 200:
@ -587,8 +592,9 @@ async def _async_inference(ctx, payload, req_timeout):
else: else:
url = _join_url(ctx.get('api_base'), path) url = _join_url(ctx.get('api_base'), path)
headers = await _render_headers(profile.get('headers', ''), ns) headers = await _render_headers(profile.get('headers', ''), ns)
submit_method = (profile.get('method') or 'POST').upper()
try: try:
status, data = await _http_request('POST', url, headers, body, step_timeout) status, data = await _http_request(submit_method, url, headers, body, step_timeout)
except ValueError as e: except ValueError as e:
await _fail('异步任务提交失败:%s' % e) await _fail('异步任务提交失败:%s' % e)
return {} return {}

View File

@ -478,7 +478,7 @@ async def llm_dashboard(params_kw):
models = await sor.sqlExe("SELECT COUNT(*) AS c FROM llm_model WHERE status='active'", {}) models = await sor.sqlExe("SELECT COUNT(*) AS c FROM llm_model WHERE status='active'", {})
orgs = await sor.sqlExe("SELECT COUNT(*) AS c FROM llm_org_policy WHERE status='active'", {}) orgs = await sor.sqlExe("SELECT COUNT(*) AS c FROM llm_org_policy WHERE status='active'", {})
usage = await sor.sqlExe( usage = await sor.sqlExe(
"SELECT COUNT(*) AS c, COALESCE(SUM(charge),0) AS ch FROM llm_usage WHERE status='ok'", {}) "SELECT COUNT(*) AS c, COALESCE(SUM(charge),0) AS ch FROM llm_usage WHERE status='SUCCEEDED'", {})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
result['vendors'] = int(getattr(vendors[0], 'c', 0)) if vendors else 0 result['vendors'] = int(getattr(vendors[0], 'c', 0)) if vendors else 0
result['accounts'] = int(getattr(accounts[0], 'c', 0)) if accounts else 0 result['accounts'] = int(getattr(accounts[0], 'c', 0)) if accounts else 0

View File

@ -10,7 +10,7 @@ try:
models = await sor.sqlExe("SELECT COUNT(*) AS c FROM llm_model WHERE status='active'", {}) models = await sor.sqlExe("SELECT COUNT(*) AS c FROM llm_model WHERE status='active'", {})
orgs = await sor.sqlExe("SELECT COUNT(*) AS c FROM llm_org_policy WHERE status='active'", {}) orgs = await sor.sqlExe("SELECT COUNT(*) AS c FROM llm_org_policy WHERE status='active'", {})
usage = await sor.sqlExe( usage = await sor.sqlExe(
"SELECT COUNT(*) AS c, COALESCE(SUM(charge),0) AS ch FROM llm_usage WHERE status='ok'", {}) "SELECT COUNT(*) AS c, COALESCE(SUM(charge),0) AS ch FROM llm_usage WHERE status='SUCCEEDED'", {})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
data['vendors'] = int(getattr(vendors[0], 'c', 0)) if vendors else 0 data['vendors'] = int(getattr(vendors[0], 'c', 0)) if vendors else 0
data['accounts'] = int(getattr(accounts[0], 'c', 0)) if accounts else 0 data['accounts'] = int(getattr(accounts[0], 'c', 0)) if accounts else 0