增加成本统计信息
This commit is contained in:
parent
4e8caf4c53
commit
3de7ef68ba
@ -35,6 +35,7 @@
|
||||
## TODO
|
||||
|
||||
- [x] 完成最基本的各类基础设施
|
||||
- [ ] chat 模式下支持进行成本分析
|
||||
- [ ] 支持同时调试多个 MCP Server
|
||||
- [ ] 支持通过大模型进行在线验证
|
||||
- [ ] 支持 completion/complete 协议字段
|
||||
|
@ -29,7 +29,7 @@ bridge.addCommandListener('hello', data => {
|
||||
|
||||
|
||||
function initDebug() {
|
||||
connectionArgs.commandString = 'mcp run ../servers/main.py';
|
||||
connectionArgs.commandString = 'uv run mcp run ../servers/main.py';
|
||||
connectionMethods.current = 'STDIO';
|
||||
|
||||
setTimeout(async () => {
|
||||
|
@ -1,12 +1,23 @@
|
||||
import { ToolItem } from "@/hook/type";
|
||||
import { ref } from "vue";
|
||||
|
||||
import type { OpenAI } from 'openai';
|
||||
type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
||||
|
||||
export interface IExtraInfo {
|
||||
created: number,
|
||||
serverName: string,
|
||||
usage?: ChatCompletionChunk['usage'];
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system' | 'tool';
|
||||
content: string;
|
||||
tool_call_id?: string
|
||||
name?: string // 工具名称,当 role 为 tool
|
||||
tool_calls?: ToolCall[]
|
||||
tool_calls?: ToolCall[],
|
||||
extraInfo: IExtraInfo
|
||||
}
|
||||
|
||||
// 新增状态和工具数据
|
||||
|
@ -22,6 +22,7 @@
|
||||
<div class="message-text">
|
||||
<div v-if="message.content" v-html="markdownToHtml(message.content)"></div>
|
||||
</div>
|
||||
<MessageMeta :message="message" />
|
||||
</div>
|
||||
|
||||
<!-- 助手调用的工具部分 -->
|
||||
@ -81,6 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MessageMeta :message="message" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -129,12 +131,16 @@ import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, ne
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage, ScrollbarInstance } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { ChatMessage, ChatStorage, getToolSchema, ToolCall } from './chat';
|
||||
import { ChatMessage, ChatStorage, getToolSchema, IExtraInfo, ToolCall } from './chat';
|
||||
|
||||
|
||||
import Setting from './setting.vue';
|
||||
import MessageMeta from './message-meta.vue';
|
||||
|
||||
// 引入 markdown.ts 中的函数
|
||||
import { markdownToHtml, copyToClipboard } from './markdown';
|
||||
import { TaskLoop } from './task-loop';
|
||||
import { ChatCompletionChunk, TaskLoop } from './task-loop';
|
||||
import { llmManager, llms } from '@/views/setting/llm';
|
||||
|
||||
defineComponent({ name: 'chat' });
|
||||
|
||||
@ -174,6 +180,7 @@ interface IRenderMessage {
|
||||
toolResult?: string;
|
||||
tool_calls?: ToolCall[];
|
||||
showJson?: Ref<boolean>;
|
||||
extraInfo: IExtraInfo;
|
||||
}
|
||||
|
||||
const renderMessages = computed(() => {
|
||||
@ -182,7 +189,8 @@ const renderMessages = computed(() => {
|
||||
if (message.role === 'user') {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: message.content
|
||||
content: message.content,
|
||||
extraInfo: message.extraInfo
|
||||
});
|
||||
} else if (message.role === 'assistant') {
|
||||
if (message.tool_calls) {
|
||||
@ -190,12 +198,14 @@ const renderMessages = computed(() => {
|
||||
role: 'assistant/tool_calls',
|
||||
content: message.content,
|
||||
tool_calls: message.tool_calls,
|
||||
showJson: ref(false)
|
||||
showJson: ref(false),
|
||||
extraInfo: message.extraInfo
|
||||
});
|
||||
} else {
|
||||
messages.push({
|
||||
role: 'assistant/content',
|
||||
content: message.content
|
||||
content: message.content,
|
||||
extraInfo: message.extraInfo
|
||||
});
|
||||
}
|
||||
|
||||
@ -302,7 +312,11 @@ const handleSend = () => {
|
||||
|
||||
tabStorage.messages.push({
|
||||
role: 'assistant',
|
||||
content: `错误: ${msg}`
|
||||
content: `错误: ${msg}`,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
||||
}
|
||||
});
|
||||
|
||||
isLoading.value = false;
|
||||
@ -361,7 +375,6 @@ const jsonResultToHtml = (jsonString: string) => {
|
||||
return html;
|
||||
};
|
||||
|
||||
// 新增格式化工具参数的方法
|
||||
const formatToolArguments = (args: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(args);
|
||||
@ -510,9 +523,7 @@ const formatToolArguments = (args: string) => {
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
/* 原有样式保持不变 */
|
||||
|
||||
/* 新增工具调用样式 */
|
||||
.tool-calls {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
76
renderer/src/components/main-panel/chat/message-meta.vue
Normal file
76
renderer/src/components/main-panel/chat/message-meta.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="message-meta" @mouseenter="showTime = true" @mouseleave="showTime = false">
|
||||
<span v-if="usageStatistic" class="message-usage">
|
||||
<span>
|
||||
输入 {{ usageStatistic.input }}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
输出 {{ usageStatistic.output }}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
消耗的总 token {{ usageStatistic.total }}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
缓存命中率 {{ usageStatistic.cacheHitRatio }}%
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span v-else class="message-usage">
|
||||
<span>你使用的供应商暂时不支持统计信息</span>
|
||||
</span>
|
||||
|
||||
<span v-show="showTime" class="message-time">
|
||||
{{ props.message.extraInfo.serverName }} 作答于
|
||||
{{ new Date(message.extraInfo.created).toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, defineProps, ref } from 'vue';
|
||||
import { makeUsageStatistic } from './usage';
|
||||
|
||||
defineComponent({ name: 'message-meta' });
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const usageStatistic = makeUsageStatistic(props.message.extraInfo);
|
||||
const showTime = ref(false);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.message-meta {
|
||||
margin-top: 8px;
|
||||
font-size: 0.8em;
|
||||
color: var(--el-text-color-secondary);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
opacity: 0.7;
|
||||
padding: 2px 6px 2px 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.message-usage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-usage > span {
|
||||
background-color: var(--el-fill-color-light);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
</style>
|
@ -6,8 +6,8 @@ import type { OpenAI } from 'openai';
|
||||
import { callTool } from "../tool/tools";
|
||||
import { llmManager, llms } from "@/views/setting/llm";
|
||||
|
||||
type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
||||
type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
||||
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
||||
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
||||
interface TaskLoopOptions {
|
||||
maxEpochs: number;
|
||||
}
|
||||
@ -18,6 +18,7 @@ interface TaskLoopOptions {
|
||||
export class TaskLoop {
|
||||
private bridge = useMessageBridge();
|
||||
private currentChatId = '';
|
||||
private completionUsage: ChatCompletionChunk['usage'] | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly streamingContent: Ref<string>,
|
||||
@ -27,7 +28,9 @@ export class TaskLoop {
|
||||
private onDone: () => void = () => {},
|
||||
private onEpoch: () => void = () => {},
|
||||
private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20 },
|
||||
) {}
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
private async handleToolCalls(toolCalls: ToolCall[]) {
|
||||
// TODO: 调用多个工具并返回调用结果?
|
||||
@ -89,6 +92,13 @@ export class TaskLoop {
|
||||
}
|
||||
}
|
||||
|
||||
private handleChunkUsage(chunk: ChatCompletionChunk) {
|
||||
const usage = chunk.usage;
|
||||
if (usage) {
|
||||
this.completionUsage = usage;
|
||||
}
|
||||
}
|
||||
|
||||
private doConversation(chatData: ChatCompletionCreateParamsBase) {
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
@ -103,6 +113,7 @@ export class TaskLoop {
|
||||
// 处理增量的 content 和 tool_calls
|
||||
this.handleChunkDeltaContent(chunk);
|
||||
this.handleChunkDeltaToolCalls(chunk);
|
||||
this.handleChunkUsage(chunk);
|
||||
|
||||
this.onChunk(chunk);
|
||||
}, { once: false });
|
||||
@ -140,6 +151,7 @@ export class TaskLoop {
|
||||
// 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息
|
||||
const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength);
|
||||
userMessages.push(...loadMessages);
|
||||
|
||||
// 增加一个id用于锁定状态
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
@ -188,7 +200,14 @@ export class TaskLoop {
|
||||
*/
|
||||
public async start(tabStorage: ChatStorage, userMessage: string) {
|
||||
// 添加目前的消息
|
||||
tabStorage.messages.push({ role: 'user', content: userMessage });
|
||||
tabStorage.messages.push({
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < this.taskOptions.maxEpochs; ++ i) {
|
||||
|
||||
@ -197,6 +216,7 @@ export class TaskLoop {
|
||||
// 初始累计清空
|
||||
this.streamingContent.value = '';
|
||||
this.streamingToolCalls.value = [];
|
||||
this.completionUsage = undefined;
|
||||
|
||||
// 构造 chatData
|
||||
const chatData = this.makeChatData(tabStorage);
|
||||
@ -212,7 +232,11 @@ export class TaskLoop {
|
||||
tabStorage.messages.push({
|
||||
role: 'assistant',
|
||||
content: this.streamingContent.value || '',
|
||||
tool_calls: this.streamingToolCalls.value
|
||||
tool_calls: this.streamingToolCalls.value,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
||||
}
|
||||
});
|
||||
|
||||
const toolCallResult = await this.handleToolCalls(this.streamingToolCalls.value);
|
||||
@ -222,14 +246,24 @@ export class TaskLoop {
|
||||
tabStorage.messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id || toolCall.function.name,
|
||||
content: toolCallResult
|
||||
content: toolCallResult,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown',
|
||||
usage: this.completionUsage
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} else if (this.streamingContent.value) {
|
||||
tabStorage.messages.push({
|
||||
role: 'assistant',
|
||||
content: this.streamingContent.value
|
||||
content: this.streamingContent.value,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown',
|
||||
usage: this.completionUsage
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
|
37
renderer/src/components/main-panel/chat/usage.ts
Normal file
37
renderer/src/components/main-panel/chat/usage.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { IExtraInfo } from "./chat";
|
||||
|
||||
export interface UsageStatistic {
|
||||
input: number;
|
||||
output: number;
|
||||
total: number;
|
||||
cacheHitRatio: number;
|
||||
}
|
||||
|
||||
export function makeUsageStatistic(extraInfo: IExtraInfo): UsageStatistic | undefined {
|
||||
if (extraInfo.serverName === 'unknown' || extraInfo.usage === undefined || extraInfo.usage === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const usage = extraInfo.usage;
|
||||
|
||||
switch (extraInfo.serverName) {
|
||||
case 'deepseek':
|
||||
return {
|
||||
input: usage.prompt_tokens,
|
||||
output: usage.completion_tokens,
|
||||
total: usage.prompt_tokens + usage.completion_tokens,
|
||||
cacheHitRatio: Math.ceil(usage.prompt_tokens_details?.cached_tokens || 0 / usage.prompt_tokens * 1000) / 10,
|
||||
}
|
||||
|
||||
case 'openai':
|
||||
return {
|
||||
// TODO: 完成其他的数值统计
|
||||
input: usage?.prompt_tokens,
|
||||
output: usage?.completion_tokens,
|
||||
total: usage.prompt_tokens + usage.completion_tokens,
|
||||
cacheHitRatio: Math.ceil(usage.prompt_tokens_details?.cached_tokens || 0 / usage.prompt_tokens * 1000) / 10,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
@ -27,6 +27,8 @@ async function connectHandler(option: MCPOptions, webview: PostMessageble) {
|
||||
// 比如 error: Failed to spawn: `server.py`
|
||||
// Caused by: No such file or directory (os error 2)
|
||||
|
||||
console.log('error', error);
|
||||
|
||||
const connectResult = {
|
||||
code: 500,
|
||||
msg: (error as any).toString()
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"tabs": [],
|
||||
"currentIndex": -1
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"currentIndex": 1,
|
||||
"currentIndex": 2,
|
||||
"tabs": [
|
||||
{
|
||||
"name": "资源",
|
||||
@ -18,6 +18,87 @@
|
||||
"storage": {
|
||||
"currentPromptName": "translate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "交互测试",
|
||||
"icon": "icon-robot",
|
||||
"type": "blank",
|
||||
"componentIndex": 3,
|
||||
"storage": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,请问什么是",
|
||||
"extraInfo": {
|
||||
"created": 1744876735890,
|
||||
"serverName": "deepseek"
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "你好!请问你是想问什么呢?可以具体一点吗?比如:\n\n- 什么是人工智能?\n- 什么是区块链?\n- 什么是量子计算?\n- 或者其他任何你想了解的概念或问题?\n\n告诉我你的具体需求,我会尽力解答!",
|
||||
"extraInfo": {
|
||||
"created": 1744876742266,
|
||||
"serverName": "deepseek",
|
||||
"usage": {
|
||||
"prompt_tokens": 453,
|
||||
"completion_tokens": 49,
|
||||
"total_tokens": 502,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 448
|
||||
},
|
||||
"prompt_cache_hit_tokens": 448,
|
||||
"prompt_cache_miss_tokens": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你的名字是",
|
||||
"extraInfo": {
|
||||
"created": 1744878380791,
|
||||
"serverName": "openai"
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "错误: OpenAI API error: 404 404 page not found",
|
||||
"extraInfo": {
|
||||
"created": 1744878381940,
|
||||
"serverName": "openai"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"modelIndex": 0,
|
||||
"enableTools": [
|
||||
{
|
||||
"name": "add",
|
||||
"description": "对两个数字进行实数域的加法",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "multiply",
|
||||
"description": "对两个数字进行实数域的乘法运算",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "is_even",
|
||||
"description": "判断一个整数是否为偶数",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "capitalize",
|
||||
"description": "将字符串首字母大写",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"enableWebSearch": false,
|
||||
"temperature": 0.7,
|
||||
"contextLength": 10,
|
||||
"systemPrompt": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user