pipeline-llm/pipeline_llm/product_interface.py

193 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""pipeline_llm.product_interface — 产线模型的产品层接口(照 llmage 样板)。
产品模块product_management通过 env.product_interface 调度本模块完成
展示/预检/执行/计费。资源层只算「真实定价(原价)」,完全不碰折扣——
客户折扣→售价、供应商折扣→成本,由产品层 calculate_product_cost 接
(见 resource-productization 技能的折扣分工铁律)。
resource_ref_id = llm_model.id。
"""
import logging
from ahserver.serverenv import ServerEnv
from sqlor.dbpools import get_sor_context
from .gateway import _row_to_dict
logger = logging.getLogger("pipeline_llm.product_interface")
_MODULE_NAME = 'pipeline_llm'
async def _resolve_model(resource_ref_id):
"""resource_ref_id → llm_model 行。返回 (model_dict, err)。"""
env = ServerEnv()
async with get_sor_context(env, _MODULE_NAME) as sor:
recs = await sor.sqlExe(
"SELECT id, name, vendor_model_id, capability, status, ppid, org_id, "
"description "
"FROM llm_model WHERE id=${i}$ LIMIT 1", {"i": resource_ref_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return None, '模型不存在llm_model.id=%s' % resource_ref_id
return _row_to_dict(recs[0]), None
async def get_product_display(resource_ref_id):
"""展示定价信息。2026-09-04 起模型表不存单价,定价统一走 ppid 定价引擎。"""
m, err = await _resolve_model(resource_ref_id)
if err:
return {'success': False, 'message': err}
ppid = m.get('ppid') or ''
pricing_text = '按定价方案计费ppid=%s' % ppid if ppid else '未挂定价方案'
return {
'success': True,
'pricing_text': pricing_text,
'pricing_detail': {
'ppid': ppid,
'capability': m.get('capability') or 't2t',
'model_id': m.get('vendor_model_id') or m.get('name', ''),
},
'extra_info': m.get('description') or '',
}
async def check_product_availability(resource_ref_id, user_org_id=None):
"""模型状态 + 定价方案是否就绪。"""
m, err = await _resolve_model(resource_ref_id)
if err:
return {'available': False, 'reason': err}
if (m.get('status') or '') != 'active':
return {'available': False, 'reason': '模型已停用(%s' % m.get('name')}
if not (m.get('ppid') or ''):
return {'available': False, 'reason': '模型未挂定价方案ppid 为空),请先配置定价'}
return {'available': True, 'reason': ''}
async def check_product_consumable(resource_ref_id, user_id, user_org_id):
"""消费前综合预检:可用性 + 客户资金账户余额。"""
avail = await check_product_availability(resource_ref_id, user_org_id)
if not avail.get('available'):
return {'consumable': False, 'reason': avail.get('reason', ''),
'pricing_available': False}
env = ServerEnv()
balance = None
if user_org_id and hasattr(env, 'getCustomerBalance'):
try:
acc_dbname = env.get_module_dbname('accounting')
from sqlor.dbpools import DBPools
async with DBPools().sqlorContext(acc_dbname) as sor:
balance = await env.getCustomerBalance(sor, user_org_id)
except Exception:
balance = None # accounting 未接入 → 不拦
if balance is None:
return {'consumable': True, 'reason': '', 'pricing_available': True}
if float(balance) <= 0:
return {'consumable': False, 'reason': '客户资金账户余额不足(%.2f),请先充值' % balance,
'pricing_available': True}
return {'consumable': True, 'reason': '', 'pricing_available': True}
async def execute_product_service(resource_ref_id, user_id, user_org_id, request_data):
"""产品消费引擎入口不承接模型调用(防双重计费)。
模型的真实消费路径是产线会话/推理入口llm_bridge → inference
该路径已完成成本侧结算并写 pending 用量流水,由异步三方账出客户账单。
"""
return {'success': False, 'status': 'unsupported',
'message': '产线模型消费走推理入口(会话/llm_v1不经产品消费引擎'}
async def execute_product_service_stream(resource_ref_id, user_id, user_org_id, request_data):
"""同 execute_product_service不承接防双重计费。"""
yield {'success': False, 'status': 'unsupported',
'message': '产线模型消费走推理入口(会话/llm_v1不经产品消费引擎',
'done': True}
async def calculate_product_cost(resource_ref_id, usage_data, user_org_id=None):
"""资源层真实定价(原价)。不碰折扣(折扣由产品层按产品精确计算)。
usage_data: {'prompt_tokens': N, 'completion_tokens': N}(或按量因子 dict
键名唯一契约 = 上游 usage 原文结构2026-09-10 起 req_tokens/resp_tokens 别名
随冗余列一并废除)
"""
m, err = await _resolve_model(resource_ref_id)
if err:
return {'success': False, 'message': err}
ppid = m.get('ppid') or ''
if not ppid:
return {'success': False, 'message': '模型未挂定价方案ppid 为空)'}
env = ServerEnv()
if not hasattr(env, 'buffered_charging'):
return {'success': False, 'message': '定价引擎pricing 模块)未加载'}
usage = dict(usage_data or {})
try:
prices = await env.buffered_charging(ppid, usage)
except Exception as e:
logger.warning("pipeline_llm: 定价计算失败 ppid=%s: %s", ppid, e)
return {'success': False, 'message': '定价计算失败: %s' % e}
if prices is None:
return {'success': False, 'message': '定价计算返回空ppid=%s' % ppid}
amount = sum(float(getattr(p, 'amount', 0) or 0) for p in prices)
cost = sum(float(getattr(p, 'cost', 0) or 0) for p in prices)
return {
'success': True,
'original_amount': round(amount, 6),
'amount': round(amount, 6), # 资源层不碰折扣,产品层再乘客户折扣
'cost': round(cost, 6),
'discount': 1.0,
'pricing_program_id': ppid,
}
async def load_product_category_product(parent_category_id):
"""产品导入:把模型注册表包装成产品(标准化格式)。
类别 = 能力类型t2t/t2i/...,取 appcodes llm_capability 的中文名);
产品 = 所有 active 的模型。只读源数据返回,写入由产品模块负责。
"""
env = ServerEnv()
async with get_sor_context(env, _MODULE_NAME) as sor:
models = await sor.sqlExe(
"SELECT id, name, vendor_model_id, capability, status, org_id, description, "
"ppid FROM llm_model "
"WHERE status='active' ORDER BY capability, name", {})
await sor.sqlExe("COMMIT", {})
caps = await sor.sqlExe(
"SELECT k, v FROM appcodes_kv WHERE parentid='llm_capability'", {})
await sor.sqlExe("COMMIT", {})
if not models:
return {'success': False, 'error': '模型注册表没有可导入的启用模型'}
models = [_row_to_dict(m) for m in models]
cap_names = {getattr(c, 'k', ''): getattr(c, 'v', '') for c in (caps or [])}
seen_caps = []
for m in models:
cap = m.get('capability') or 't2t'
if cap not in seen_caps:
seen_caps.append(cap)
categories = [{
'source_id': cap,
'name': cap_names.get(cap, cap),
'description': '产线平台模型(%s' % cap_names.get(cap, cap),
'product_type': 'pipeline_llm_model',
'product_type_title': '产线模型按量',
'sort_order': 0,
} for cap in seen_caps]
products = []
for m in models:
cap = m.get('capability') or 't2t'
name = m.get('name') or ''
products.append({
'source_category_id': cap,
'resource_ref_id': m.get('id', ''),
'product_code': m.get('vendor_model_id', '') or name,
'product_name': name,
'product_type': 'pipeline_llm_model',
'brief_intro': m.get('description', '') or '',
'sort_order': 0,
'providerid': '', # 模型供应关系暂无对应供应商机构,成本侧用定价引擎
})
return {'success': True, 'categories': categories, 'products': products}