diff --git a/wwwroot/api/llm_launch_check_api.dspy b/wwwroot/api/llm_launch_check_api.dspy index e30e61c..612db26 100644 --- a/wwwroot/api/llm_launch_check_api.dspy +++ b/wwwroot/api/llm_launch_check_api.dspy @@ -108,8 +108,7 @@ if action == 'check': return json.dumps(result, ensure_ascii=False) elif action == 'inference': - # Perform test inference - # Get llm info + # Perform test inference - direct uapi call to get usage data async with get_sor_context(request._run_ns, 'llmage') as sor: recs = await sor.sqlExe( "select * from llm where id=${llmid}$", {'llmid': llmid}) @@ -126,28 +125,37 @@ elif action == 'inference': api_map = maps[0] - # Call llminference logic + # Direct uapi call try: - from llmage.llmclient import llm_inference + env = request._run_ns + uapi = env.UpAppApi(request) userid = await get_user() userorgid = await get_userorgid() - # Simple test message - test_input = {'messages': [{'role': 'user', 'content': '你好,这是一条测试消息'}]} + # Get caller userid for the upapp + caller_userid = await env.uapi_data.get_calluserid(llm.upappid, orgid=llm.ownerid) - result = await llm_inference( - llmid=llmid, - apiname=api_map.apiname, - user_input=test_input, - userid=userid, - userorgid=userorgid, - stream=False - ) + # Simple test message + test_params = { + 'messages': [{'role': 'user', 'content': '你好,这是一条测试消息'}], + 'model': llm.model + } + + # Call uapi + response = await uapi.call(llm.upappid, api_map.apiname, caller_userid, params=test_params) + if isinstance(response, bytes): + response = response.decode('utf-8') + + data = json.loads(response) + usage = data.get('usage', {}) + content = data.get('content', '') + if not content and 'choices' in data: + content = data['choices'][0].get('message', {}).get('content', '') return json.dumps({ 'success': True, - 'response': result.get('response', ''), - 'usage': result.get('usage', {}) + 'response': content or '(空响应)', + 'usage': usage }, ensure_ascii=False) except Exception as e: return json.dumps({ @@ -155,4 +163,84 @@ elif action == 'inference': 'error': str(e) }, ensure_ascii=False) +elif action == 'check_charging': + # Check if pricing can calculate costs from usage data + usages_str = params_kw.get('usages', '{}') + llmusage_id = params_kw.get('llmusage_id', '') + + try: + usages = json.loads(usages_str) if isinstance(usages_str, str) else usages_str + except Exception as e: + return json.dumps({'error': f'invalid usages format: {e}'}, ensure_ascii=False) + + if not usages: + return json.dumps({'error': 'usages is empty'}, ensure_ascii=False) + + env = request._run_ns + + # Get ppid from llm_api_map + async with get_sor_context(env, 'llmage') as sor: + maps = await sor.sqlExe( + "select * from llm_api_map where llmid=${llmid}$ and isdefaultcatelog='1'", + {'llmid': llmid}) + if not maps: + return json.dumps({'error': 'no default api map'}, ensure_ascii=False) + + api_map = maps[0] + ppid = api_map.ppid + + if not ppid: + return json.dumps({ + 'success': False, + 'error': 'llm_api_map 中没有定价项目(ppid)' + }, ensure_ascii=False) + + # Get pricing program name + async with get_sor_context(env, 'pricing') as psor: + pregs = await psor.sqlExe( + "select name from pricing_program where id=${ppid}$", {'ppid': ppid}) + pp_name = pregs[0].name if pregs else '未知' + + # Test charging calculation + try: + prices = await env.buffered_charging(ppid, usages) + if prices is None or len(prices) == 0: + return json.dumps({ + 'success': False, + 'error': f'buffered_charging 返回空,定价项目 {pp_name}(ppid={ppid}) 无法计算费用', + 'usages': usages + }, ensure_ascii=False) + + # Calculate totals + total_amount = 0 + total_cost = 0 + breakdown = [] + for p in prices: + total_amount += p.amount + if p.cost: + total_cost += p.cost + breakdown.append({ + 'factor': getattr(p, 'factor', ''), + 'amount': p.amount, + 'cost': p.cost + }) + + return json.dumps({ + 'success': True, + 'ppid': ppid, + 'pp_name': pp_name, + 'usages': usages, + 'total_amount': total_amount, + 'total_cost': total_cost, + 'breakdown': breakdown + }, ensure_ascii=False) + except Exception as e: + return json.dumps({ + 'success': False, + 'error': f'计费计算失败: {e}', + 'ppid': ppid, + 'pp_name': pp_name, + 'usages': usages + }, ensure_ascii=False) + return json.dumps({'error': 'invalid action'}, ensure_ascii=False) diff --git a/wwwroot/llm_launch_check.ui b/wwwroot/llm_launch_check.ui index ed08708..31dd1bd 100644 --- a/wwwroot/llm_launch_check.ui +++ b/wwwroot/llm_launch_check.ui @@ -45,9 +45,26 @@ "text": "", "i18n": false } + }, + { + "widgettype": "Button", + "options": { + "name": "check_charging_btn", + "text": "检查计费", + "i18n": false, + "disabled": true + } + }, + { + "widgettype": "Text", + "options": { + "name": "charging_result", + "text": "", + "i18n": false + } } ], - "init": "async function(self) {\n const llmid = '{{llmid}}';\n const statusEl = self.children.check_status;\n const listEl = self.children.checks_list;\n const testBtn = self.children.test_btn;\n const testResult = self.children.test_result;\n \n async function runCheck() {\n statusEl.setText('检查中...');\n listEl.clear();\n testBtn.setDisabled(true);\n \n try {\n const url = bricks.absurl('../api/llm_launch_check_api.dspy?llmid=' + llmid, self);\n const resp = await fetch(url);\n const data = await resp.json();\n \n if (data.error) {\n statusEl.setText('错误: ' + data.error);\n return;\n }\n \n statusEl.setText(data.all_passed ? '✓ 所有检查通过' : '✗ 存在未通过项');\n statusEl.setStyle({color: data.all_passed ? '#4CAF50' : '#F44336', fontSize: '16px', fontWeight: 'bold'});\n \n data.checks.forEach(check => {\n const row = bricks.createWidget('HBox', {\n spacing: 10,\n children: [\n {widgettype: 'Text', options: {text: check.passed ? '✓' : '✗', style: {color: check.passed ? '#4CAF50' : '#F44336', fontSize: '18px', width: '30px'}}},\n {widgettype: 'Text', options: {text: check.name, style: {fontWeight: 'bold', width: '200px'}}},\n {widgettype: 'Text', options: {text: check.detail, style: {color: '#666'}}}\n ]\n });\n listEl.addChild(row);\n });\n \n if (data.all_passed) {\n testBtn.setDisabled(false);\n }\n } catch (e) {\n statusEl.setText('请求失败: ' + e.message);\n }\n }\n \n testBtn.on('click', async () => {\n testResult.setText('体验中...');\n try {\n const url = bricks.absurl('../api/llm_launch_check_api.dspy?llmid=' + llmid + '&action=inference', self);\n const resp = await fetch(url);\n const data = await resp.json();\n \n if (data.success) {\n testResult.setText('响应: ' + (data.response || JSON.stringify(data.usage)));\n testResult.setStyle({color: '#4CAF50'});\n } else {\n testResult.setText('失败: ' + data.error);\n testResult.setStyle({color: '#F44336'});\n }\n } catch (e) {\n testResult.setText('请求失败: ' + e.message);\n testResult.setStyle({color: '#F44336'});\n }\n });\n \n await runCheck();\n}" + "init": "async function(self) {\n const llmid = '{{llmid}}';\n const statusEl = self.children.check_status;\n const listEl = self.children.checks_list;\n const testBtn = self.children.test_btn;\n const testResult = self.children.test_result;\n const checkChargingBtn = self.children.check_charging_btn;\n const chargingResult = self.children.charging_result;\n let lastUsage = null;\n \n async function runCheck() {\n statusEl.setText('检查中...');\n listEl.clear();\n testBtn.setDisabled(true);\n checkChargingBtn.setDisabled(true);\n \n try {\n const url = bricks.absurl('../api/llm_launch_check_api.dspy?llmid=' + llmid, self);\n const resp = await fetch(url);\n const data = await resp.json();\n \n if (data.error) {\n statusEl.setText('错误: ' + data.error);\n return;\n }\n \n statusEl.setText(data.all_passed ? '✓ 所有检查通过' : '✗ 存在未通过项');\n statusEl.setStyle({color: data.all_passed ? '#4CAF50' : '#F44336', fontSize: '16px', fontWeight: 'bold'});\n \n data.checks.forEach(check => {\n const row = bricks.createWidget('HBox', {\n spacing: 10,\n children: [\n {widgettype: 'Text', options: {text: check.passed ? '✓' : '✗', style: {color: check.passed ? '#4CAF50' : '#F44336', fontSize: '18px', width: '30px'}}},\n {widgettype: 'Text', options: {text: check.name, style: {fontWeight: 'bold', width: '200px'}}},\n {widgettype: 'Text', options: {text: check.detail, style: {color: '#666'}}}\n ]\n });\n listEl.addChild(row);\n });\n \n if (data.all_passed) {\n testBtn.setDisabled(false);\n }\n } catch (e) {\n statusEl.setText('请求失败: ' + e.message);\n }\n }\n \n testBtn.on('click', async () => {\n testResult.setText('体验中...');\n testResult.setStyle({color: '#666'});\n checkChargingBtn.setDisabled(true);\n lastUsage = null;\n \n try {\n const url = bricks.absurl('../api/llm_launch_check_api.dspy?llmid=' + llmid + '&action=inference', self);\n const resp = await fetch(url);\n const data = await resp.json();\n \n if (data.success) {\n const usageStr = data.usage ? JSON.stringify(data.usage, null, 2) : '无';\n testResult.setText('✓ 响应: ' + (data.response || '(空)') + '\\n用量: ' + usageStr);\n testResult.setStyle({color: '#4CAF50'});\n lastUsage = data.usage;\n if (lastUsage) {\n checkChargingBtn.setDisabled(false);\n }\n } else {\n testResult.setText('✗ 失败: ' + data.error);\n testResult.setStyle({color: '#F44336'});\n }\n } catch (e) {\n testResult.setText('请求失败: ' + e.message);\n testResult.setStyle({color: '#F44336'});\n }\n });\n \n checkChargingBtn.on('click', async () => {\n if (!lastUsage) {\n chargingResult.setText('无用量数据');\n return;\n }\n \n chargingResult.setText('检查计费中...');\n chargingResult.setStyle({color: '#666'});\n \n try {\n const usagesJson = encodeURIComponent(JSON.stringify(lastUsage));\n const url = bricks.absurl('../api/llm_launch_check_api.dspy?llmid=' + llmid + '&action=check_charging&usages=' + usagesJson, self);\n const resp = await fetch(url);\n const data = await resp.json();\n \n if (data.error) {\n chargingResult.setText('✗ 错误: ' + data.error);\n chargingResult.setStyle({color: '#F44336'});\n return;\n }\n \n if (data.success) {\n let text = '✓ 计费检查通过\\n';\n text += '定价项目: ' + data.pp_name + ' (ppid=' + data.ppid + ')\\n';\n text += '用量: ' + JSON.stringify(data.usages) + '\\n';\n text += '总金额: ¥' + data.total_amount.toFixed(4) + '\\n';\n text += '总成本: ¥' + data.total_cost.toFixed(4) + '\\n';\n if (data.breakdown && data.breakdown.length > 0) {\n text += '明细:\\n';\n data.breakdown.forEach(b => {\n text += ' ' + (b.factor || '项目') + ': ¥' + b.amount.toFixed(4) + ' (成本¥' + (b.cost || 0).toFixed(4) + ')\\n';\n });\n }\n chargingResult.setText(text);\n chargingResult.setStyle({color: '#4CAF50'});\n } else {\n chargingResult.setText('✗ ' + data.error);\n chargingResult.setStyle({color: '#F44336'});\n }\n } catch (e) {\n chargingResult.setText('请求失败: ' + e.message);\n chargingResult.setStyle({color: '#F44336'});\n }\n });\n \n await runCheck();\n}" } } {% else %}