fix: user message usage (#5657)

* fix: user message usage estimation

* fix: estimate usage on resending edited user message

* refactor: renaming

* refactor: renaming
This commit is contained in:
one 2025-05-09 14:17:28 +08:00 committed by GitHub
parent 759fa78a30
commit a2d8beafcf
4 changed files with 70 additions and 23 deletions

View File

@ -1,5 +1,6 @@
import { createSelector } from '@reduxjs/toolkit' import { createSelector } from '@reduxjs/toolkit'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { estimateUserPromptUsage } from '@renderer/services/TokenService'
import store, { type RootState, useAppDispatch, useAppSelector } from '@renderer/store' import store, { type RootState, useAppDispatch, useAppSelector } from '@renderer/store'
import { messageBlocksSelectors, updateOneBlock } from '@renderer/store/messageBlock' import { messageBlocksSelectors, updateOneBlock } from '@renderer/store/messageBlock'
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage' import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
@ -20,6 +21,7 @@ import type { Assistant, Model, Topic } from '@renderer/types'
import type { Message, MessageBlock } from '@renderer/types/newMessage' import type { Message, MessageBlock } from '@renderer/types/newMessage'
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { abortCompletion } from '@renderer/utils/abortController' import { abortCompletion } from '@renderer/utils/abortController'
import { findFileBlocks } from '@renderer/utils/messageUtils/find'
import { useCallback } from 'react' import { useCallback } from 'react'
const findMainTextBlockId = (message: Message): string | undefined => { const findMainTextBlockId = (message: Message): string | undefined => {
@ -128,6 +130,12 @@ export function useMessageOperations(topic: Topic) {
return return
} }
const files = findFileBlocks(message).map((block) => block.file)
const usage = await estimateUserPromptUsage({ content: editedContent, files })
await dispatch(updateMessageAndBlocksThunk(topic.id, { id: message.id, usage }, []))
await dispatch(resendUserMessageWithEditThunk(topic.id, message, mainTextBlockId, editedContent, assistant)) await dispatch(resendUserMessageWithEditThunk(topic.id, message, mainTextBlockId, editedContent, assistant))
}, },
[dispatch, topic.id] [dispatch, topic.id]

View File

@ -22,7 +22,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService' import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
import { estimateMessageUsage, estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService' import { estimateTextTokens as estimateTxtTokens, estimateUserPromptUsage } from '@renderer/services/TokenService'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
import WebSearchService from '@renderer/services/WebSearchService' import WebSearchService from '@renderer/services/WebSearchService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
@ -215,7 +215,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
) )
} }
baseUserMessage.usage = await estimateMessageUsage(baseUserMessage) baseUserMessage.usage = await estimateUserPromptUsage(baseUserMessage)
const { message, blocks } = getUserMessage(baseUserMessage) const { message, blocks } = getUserMessage(baseUserMessage)

View File

@ -6,7 +6,7 @@ import { fetchMessagesSummary } from '@renderer/services/ApiService'
import store from '@renderer/store' import store from '@renderer/store'
import { messageBlocksSelectors, removeManyBlocks } from '@renderer/store/messageBlock' import { messageBlocksSelectors, removeManyBlocks } from '@renderer/store/messageBlock'
import { selectMessagesForTopic } from '@renderer/store/newMessage' import { selectMessagesForTopic } from '@renderer/store/newMessage'
import type { Assistant, FileType, MCPServer, Model, Topic } from '@renderer/types' import type { Assistant, FileType, MCPServer, Model, Topic, Usage } from '@renderer/types'
import { FileTypes } from '@renderer/types' import { FileTypes } from '@renderer/types'
import type { Message, MessageBlock } from '@renderer/types/newMessage' import type { Message, MessageBlock } from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
@ -110,7 +110,8 @@ export function getUserMessage({
// Keep other potential params if needed by createMessage // Keep other potential params if needed by createMessage
knowledgeBaseIds, knowledgeBaseIds,
mentions, mentions,
enabledMCPs enabledMCPs,
usage
}: { }: {
assistant: Assistant assistant: Assistant
topic: Topic topic: Topic
@ -120,6 +121,7 @@ export function getUserMessage({
knowledgeBaseIds?: string[] knowledgeBaseIds?: string[]
mentions?: Model[] mentions?: Model[]
enabledMCPs?: MCPServer[] enabledMCPs?: MCPServer[]
usage?: Usage
}): { message: Message; blocks: MessageBlock[] } { }): { message: Message; blocks: MessageBlock[] } {
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
@ -163,7 +165,8 @@ export function getUserMessage({
// 移除knowledgeBaseIds // 移除knowledgeBaseIds
mentions, mentions,
enabledMCPs, enabledMCPs,
type type,
usage
} }
) )

View File

@ -1,5 +1,5 @@
import { Assistant, FileType, FileTypes, Usage } from '@renderer/types' import { Assistant, FileType, FileTypes, Usage } from '@renderer/types'
import type { Message, MessageInputBaseParams } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { findFileBlocks, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find' import { findFileBlocks, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find'
import { flatten, takeRight } from 'lodash' import { flatten, takeRight } from 'lodash'
import { approximateTokenSize } from 'tokenx' import { approximateTokenSize } from 'tokenx'
@ -56,16 +56,59 @@ export function estimateImageTokens(file: FileType) {
return Math.floor(file.size / 100) return Math.floor(file.size / 100)
} }
export async function estimateMessageUsage(message: Partial<Message>, params?: MessageInputBaseParams): Promise<Usage> { /**
* token
*
* content files
* Message
*
* @param {Object} params -
* @param {string} [params.content] -
* @param {FileType[]} [params.files] -
* @returns {Promise<Usage>} Usage prompt_tokenscompletion_tokenstotal_tokens
*/
export async function estimateUserPromptUsage({
content,
files
}: {
content?: string
files?: FileType[]
}): Promise<Usage> {
let imageTokens = 0 let imageTokens = 0
let files: FileType[] = []
if (params?.files) { if (files && files.length > 0) {
files = params.files const images = files.filter((f) => f.type === FileTypes.IMAGE)
} else { if (images.length > 0) {
const fileBlocks = findFileBlocks(message as Message) for (const image of images) {
files = fileBlocks.map((f) => f.file) imageTokens = estimateImageTokens(image) + imageTokens
}
}
} }
const tokens = estimateTextTokens(content || '')
return {
prompt_tokens: tokens,
completion_tokens: tokens,
total_tokens: tokens + (imageTokens ? imageTokens - 7 : 0)
}
}
/**
* Message token
*
* message reasoningContent
* token usage
*
* @param {Partial<Message>} message - Message
* @returns {Promise<Usage>} Usage prompt_tokenscompletion_tokenstotal_tokens
*/
export async function estimateMessageUsage(message: Partial<Message>): Promise<Usage> {
const fileBlocks = findFileBlocks(message as Message)
const files = fileBlocks.map((f) => f.file)
let imageTokens = 0
if (files.length > 0) { if (files.length > 0) {
const images = files.filter((f) => f.type === FileTypes.IMAGE) const images = files.filter((f) => f.type === FileTypes.IMAGE)
if (images.length > 0) { if (images.length > 0) {
@ -74,16 +117,9 @@ export async function estimateMessageUsage(message: Partial<Message>, params?: M
} }
} }
} }
let content = ''
if (params?.content) { const content = getMainTextContent(message as Message)
content = params.content const reasoningContent = getThinkingContent(message as Message)
} else {
content = getMainTextContent(message as Message)
}
let reasoningContent = ''
if (!params) {
reasoningContent = getThinkingContent(message as Message)
}
const combinedContent = [content, reasoningContent].filter((s) => s !== undefined).join(' ') const combinedContent = [content, reasoningContent].filter((s) => s !== undefined).join(' ')
const tokens = estimateTextTokens(combinedContent) const tokens = estimateTextTokens(combinedContent)