mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(ai): display cache hit/miss token usage in chat status bar
Extend TokenUsage types across the full data pipeline to include cacheReadTokens and cacheWriteTokens. The ChatStatusBar tooltip now shows cache hit information when available. This is a generic implementation that works with any API provider reporting cache metrics, not limited to DeepSeek. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2b6aed52c9
commit
39700b95b7
17 files changed
+104
-20
No files matched your search
@@ -156,7 +156,7 @@ describe('runChatTurn', () => {
|
||||
onEvent({
|
||||
type: 'done',
|
||||
isFinished: true,
|
||||
usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
|
||||
usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -174,7 +174,7 @@ describe('runChatTurn', () => {
|
||||
aiChatId: 'ai_chat_1',
|
||||
role: 'assistant',
|
||||
content: 'hi',
|
||||
tokenUsage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
|
||||
tokenUsage: { promptTokens: 1, completionTokens: 1, totalTokens: 2, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -222,7 +222,7 @@ describe('runChatTurn', () => {
|
||||
onEvent({
|
||||
type: 'done',
|
||||
isFinished: true,
|
||||
usage: { promptTokens: 3, completionTokens: 5, totalTokens: 8 },
|
||||
usage: { promptTokens: 3, completionTokens: 5, totalTokens: 8, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -248,7 +248,7 @@ describe('runChatTurn', () => {
|
||||
role: 'assistant',
|
||||
content: '年度趋势如下。',
|
||||
contentBlocks: result.contentBlocks,
|
||||
tokenUsage: { promptTokens: 3, completionTokens: 5, totalTokens: 8 },
|
||||
tokenUsage: { promptTokens: 3, completionTokens: 5, totalTokens: 8, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -316,7 +316,7 @@ describe('runChatTurn', () => {
|
||||
onEvent({
|
||||
type: 'done',
|
||||
isFinished: true,
|
||||
usage: { promptTokens: 3, completionTokens: 5, totalTokens: 8 },
|
||||
usage: { promptTokens: 3, completionTokens: 5, totalTokens: 8, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -347,7 +347,7 @@ describe('runChatTurn', () => {
|
||||
role: 'assistant',
|
||||
content: '年度趋势如下。',
|
||||
contentBlocks: result.contentBlocks,
|
||||
tokenUsage: { promptTokens: 3, completionTokens: 5, totalTokens: 8 },
|
||||
tokenUsage: { promptTokens: 3, completionTokens: 5, totalTokens: 8, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -411,7 +411,7 @@ describe('runChatTurn', () => {
|
||||
onEvent({
|
||||
type: 'done',
|
||||
isFinished: true,
|
||||
usage: { promptTokens: 3, completionTokens: 5, totalTokens: 8 },
|
||||
usage: { promptTokens: 3, completionTokens: 5, totalTokens: 8, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -429,7 +429,7 @@ describe('runChatTurn', () => {
|
||||
role: 'assistant',
|
||||
content: '2026-Q1 是峰值。',
|
||||
contentBlocks: result.contentBlocks,
|
||||
tokenUsage: { promptTokens: 3, completionTokens: 5, totalTokens: 8 },
|
||||
tokenUsage: { promptTokens: 3, completionTokens: 5, totalTokens: 8, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ export function createElectronRunAgentStream(): (
|
||||
round: 0,
|
||||
toolsUsed: 0,
|
||||
contextTokens: 0,
|
||||
totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
||||
totalUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
||||
updatedAt: Date.now(),
|
||||
} satisfies AgentRuntimeStatus,
|
||||
})
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface TokenUsage {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheWriteTokens: number
|
||||
}
|
||||
|
||||
export interface AgentRuntimeStatus {
|
||||
|
||||
@@ -50,7 +50,13 @@ export function registerAiChatRoutes(server: FastifyInstance, ctx: HttpRouteCont
|
||||
dataKeywords?: string[]
|
||||
dataMessageCount?: number
|
||||
contentBlocks?: unknown[]
|
||||
tokenUsage?: { promptTokens: number; completionTokens: number; totalTokens: number }
|
||||
tokenUsage?: {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
cacheReadTokens?: number
|
||||
cacheWriteTokens?: number
|
||||
}
|
||||
}
|
||||
}>('/_web/ai/chats/:id/messages', async (request) => {
|
||||
const { role, content, dataKeywords, dataMessageCount, contentBlocks, tokenUsage } = request.body
|
||||
@@ -121,7 +127,13 @@ export function registerAiChatRoutes(server: FastifyInstance, ctx: HttpRouteCont
|
||||
role: 'user' | 'assistant' | 'summary'
|
||||
content: string
|
||||
contentBlocks?: unknown[]
|
||||
tokenUsage?: { promptTokens: number; completionTokens: number; totalTokens: number }
|
||||
tokenUsage?: {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
cacheReadTokens?: number
|
||||
cacheWriteTokens?: number
|
||||
}
|
||||
}
|
||||
}>('/_web/ai/chats/:id/messages/insert-after', async (request, reply) => {
|
||||
const { afterMessageId, role, content, contentBlocks, tokenUsage } = request.body
|
||||
|
||||
@@ -64,15 +64,29 @@ describe('AgentEventHandler', () => {
|
||||
})
|
||||
|
||||
handler.handleCoreEvent(
|
||||
{ type: 'usage_update', usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150 } },
|
||||
{
|
||||
type: 'usage_update',
|
||||
usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150, cacheReadTokens: 30, cacheWriteTokens: 10 },
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const usage = handler.cloneUsage()
|
||||
assert.equal(usage.totalTokens, 150)
|
||||
assert.equal(usage.cacheReadTokens, 30)
|
||||
assert.equal(usage.cacheWriteTokens, 10)
|
||||
|
||||
handler.handleCoreEvent(
|
||||
{ type: 'usage_update', usage: { promptTokens: 200, completionTokens: 100, totalTokens: 300 } },
|
||||
{
|
||||
type: 'usage_update',
|
||||
usage: {
|
||||
promptTokens: 200,
|
||||
completionTokens: 100,
|
||||
totalTokens: 300,
|
||||
cacheReadTokens: 60,
|
||||
cacheWriteTokens: 20,
|
||||
},
|
||||
},
|
||||
[]
|
||||
)
|
||||
assert.equal(usage.totalTokens, 150, 'clone should be independent')
|
||||
|
||||
@@ -45,7 +45,13 @@ export async function runAgentCore(options: AgentCoreOptions): Promise<AgentCore
|
||||
|
||||
const resolvedStreamFn = (options.streamFn ?? defaultStreamSimple) as typeof defaultStreamSimple
|
||||
|
||||
const totalUsage: AgentTokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
||||
const totalUsage: AgentTokenUsage = {
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
}
|
||||
const toolsUsed: string[] = []
|
||||
let toolRounds = 0
|
||||
|
||||
@@ -54,6 +60,8 @@ export async function runAgentCore(options: AgentCoreOptions): Promise<AgentCore
|
||||
totalUsage.promptTokens += usage.input || 0
|
||||
totalUsage.completionTokens += usage.output || 0
|
||||
totalUsage.totalTokens += usage.totalTokens || usage.input + usage.output || 0
|
||||
totalUsage.cacheReadTokens += usage.cacheRead || 0
|
||||
totalUsage.cacheWriteTokens += usage.cacheWrite || 0
|
||||
}
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface TokenUsage {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheWriteTokens: number
|
||||
}
|
||||
|
||||
export interface AgentRuntimeStatus {
|
||||
@@ -143,7 +145,13 @@ export class AgentEventHandler {
|
||||
readonly toolsUsed: string[] = []
|
||||
toolRounds: number = 0
|
||||
|
||||
private totalUsage: TokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
||||
private totalUsage: TokenUsage = {
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
}
|
||||
private lastStatusAt = 0
|
||||
private readonly onChunk: (chunk: AgentStreamChunk) => void
|
||||
private readonly context: EventHandlerContext
|
||||
@@ -205,6 +213,8 @@ export class AgentEventHandler {
|
||||
promptTokens: this.totalUsage.promptTokens,
|
||||
completionTokens: this.totalUsage.completionTokens,
|
||||
totalTokens: this.totalUsage.totalTokens,
|
||||
cacheReadTokens: this.totalUsage.cacheReadTokens,
|
||||
cacheWriteTokens: this.totalUsage.cacheWriteTokens,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface AgentTokenUsage {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheWriteTokens: number
|
||||
}
|
||||
|
||||
export interface SimpleHistoryMessage {
|
||||
|
||||
@@ -59,6 +59,8 @@ export interface TokenUsageData {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
cacheReadTokens?: number
|
||||
cacheWriteTokens?: number
|
||||
}
|
||||
|
||||
export interface AIMessage {
|
||||
@@ -831,13 +833,21 @@ export class AIChatManager {
|
||||
|
||||
getAIChatTokenUsage(aiChatId: string): TokenUsageData {
|
||||
const rows = this.getActivePathRows(aiChatId)
|
||||
const result: TokenUsageData = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
||||
const result: TokenUsageData = {
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
totalTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
}
|
||||
for (const row of rows) {
|
||||
if (row.tokenUsage) {
|
||||
const usage = JSON.parse(row.tokenUsage) as TokenUsageData
|
||||
result.promptTokens += usage.promptTokens
|
||||
result.completionTokens += usage.completionTokens
|
||||
result.totalTokens += usage.totalTokens
|
||||
result.cacheReadTokens! += usage.cacheReadTokens || 0
|
||||
result.cacheWriteTokens! += usage.cacheWriteTokens || 0
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -18,7 +18,7 @@ const layoutStore = useLayoutStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
sessionTokenUsage: { totalTokens: number }
|
||||
sessionTokenUsage: { totalTokens: number; cacheReadTokens: number; cacheWriteTokens: number }
|
||||
agentStatus?: AgentRuntimeStatus | null
|
||||
currentAIChatId?: string | null
|
||||
estimatedContextTokens?: number
|
||||
@@ -80,6 +80,9 @@ function formatCompactNumber(value: number): string {
|
||||
const totalTokenUsageText = computed(() => formatNumber(props.sessionTokenUsage.totalTokens))
|
||||
const totalTokenUsageCompactText = computed(() => formatCompactNumber(props.sessionTokenUsage.totalTokens))
|
||||
|
||||
const hasCacheData = computed(() => props.sessionTokenUsage.cacheReadTokens > 0)
|
||||
const cacheReadText = computed(() => formatCompactNumber(props.sessionTokenUsage.cacheReadTokens))
|
||||
|
||||
const contextTokens = computed(() => {
|
||||
if (props.agentStatus?.contextTokens) return props.agentStatus.contextTokens
|
||||
if (props.estimatedContextTokens && props.estimatedContextTokens > 0) return props.estimatedContextTokens
|
||||
@@ -338,6 +341,9 @@ const thinkingLevelLabel = computed(() => {
|
||||
{{ formatCompactNumber(modelContextWindow) }}
|
||||
</div>
|
||||
<div>{{ t('ai.chat.statusBar.tokenUsageTitle') }}: {{ totalTokenUsageCompactText }}</div>
|
||||
<div v-if="hasCacheData">
|
||||
{{ t('ai.chat.statusBar.cacheHit') }}: {{ cacheReadText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UTooltip>
|
||||
|
||||
@@ -215,6 +215,7 @@
|
||||
}
|
||||
},
|
||||
"tokenUsageTitle": "Total token usage",
|
||||
"cacheHit": "Cache hit",
|
||||
"export": {
|
||||
"label": "Export",
|
||||
"title": "Export current conversation"
|
||||
|
||||
@@ -215,6 +215,7 @@
|
||||
}
|
||||
},
|
||||
"tokenUsageTitle": "累計 Token 使用量",
|
||||
"cacheHit": "キャッシュヒット",
|
||||
"export": {
|
||||
"label": "エクスポート",
|
||||
"title": "現在のチャットをエクスポート"
|
||||
|
||||
@@ -215,6 +215,7 @@
|
||||
}
|
||||
},
|
||||
"tokenUsageTitle": "累计 Token 使用量",
|
||||
"cacheHit": "缓存命中",
|
||||
"export": {
|
||||
"label": "导出",
|
||||
"title": "导出当前对话"
|
||||
|
||||
@@ -215,6 +215,7 @@
|
||||
}
|
||||
},
|
||||
"tokenUsageTitle": "累計 Token 用量",
|
||||
"cacheHit": "快取命中",
|
||||
"export": {
|
||||
"label": "匯出",
|
||||
"title": "匯出當前對話"
|
||||
|
||||
@@ -66,6 +66,8 @@ export interface TokenUsage {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheWriteTokens: number
|
||||
}
|
||||
|
||||
export interface AgentRuntimeStatus {
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface TokenUsageData {
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
totalTokens: number
|
||||
cacheReadTokens?: number
|
||||
cacheWriteTokens?: number
|
||||
}
|
||||
|
||||
export interface AIMessage {
|
||||
|
||||
+15
-3
@@ -211,7 +211,17 @@ export function buildAIChatKey(params: {
|
||||
}
|
||||
|
||||
function createEmptyTokenUsage(): TokenUsage {
|
||||
return { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
||||
return { promptTokens: 0, completionTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
|
||||
}
|
||||
|
||||
function toTokenUsage(data: { [K in keyof TokenUsage]?: number }): TokenUsage {
|
||||
return {
|
||||
promptTokens: data.promptTokens ?? 0,
|
||||
completionTokens: data.completionTokens ?? 0,
|
||||
totalTokens: data.totalTokens ?? 0,
|
||||
cacheReadTokens: data.cacheReadTokens ?? 0,
|
||||
cacheWriteTokens: data.cacheWriteTokens ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSerializedError(error: unknown): SerializedErrorInfo {
|
||||
@@ -523,7 +533,7 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
buffer.messages.splice(0, buffer.messages.length, ...history.map((msg) => toRuntimeMessage(msg)))
|
||||
buffer.sourceMessages.splice(0, buffer.sourceMessages.length)
|
||||
buffer.currentKeywords.splice(0, buffer.currentKeywords.length)
|
||||
buffer.sessionTokenUsage = tokenUsage
|
||||
buffer.sessionTokenUsage = toTokenUsage(tokenUsage)
|
||||
buffer.loaded = true
|
||||
}
|
||||
|
||||
@@ -1083,6 +1093,8 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
promptTokens: state.sessionTokenUsage.promptTokens + chunk.usage.promptTokens,
|
||||
completionTokens: state.sessionTokenUsage.completionTokens + chunk.usage.completionTokens,
|
||||
totalTokens: state.sessionTokenUsage.totalTokens + chunk.usage.totalTokens,
|
||||
cacheReadTokens: state.sessionTokenUsage.cacheReadTokens + chunk.usage.cacheReadTokens,
|
||||
cacheWriteTokens: state.sessionTokenUsage.cacheWriteTokens + chunk.usage.cacheWriteTokens,
|
||||
}
|
||||
}
|
||||
setAgentPhase(state, 'completed', chunk.usage ? { totalUsage: chunk.usage } : undefined)
|
||||
@@ -1708,7 +1720,7 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
}
|
||||
}
|
||||
|
||||
targetBuffer.sessionTokenUsage = await useAIService().getAIChatTokenUsage(state.currentAIChatId!)
|
||||
targetBuffer.sessionTokenUsage = toTokenUsage(await useAIService().getAIChatTokenUsage(state.currentAIChatId!))
|
||||
state.sessionTokenUsage = { ...targetBuffer.sessionTokenUsage }
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
|
||||
Reference in new issue
Block a user