diff --git a/apps/cli/src/ai/chat-command.test.ts b/apps/cli/src/ai/chat-command.test.ts index e7610025..965b8ad2 100644 --- a/apps/cli/src/ai/chat-command.test.ts +++ b/apps/cli/src/ai/chat-command.test.ts @@ -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 }, }) }) diff --git a/apps/desktop/main/ai/agent-stream-runner.ts b/apps/desktop/main/ai/agent-stream-runner.ts index 69d834ea..2c4202d1 100644 --- a/apps/desktop/main/ai/agent-stream-runner.ts +++ b/apps/desktop/main/ai/agent-stream-runner.ts @@ -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, }) diff --git a/apps/desktop/shared/types.ts b/apps/desktop/shared/types.ts index b48676c1..a4e38e70 100644 --- a/apps/desktop/shared/types.ts +++ b/apps/desktop/shared/types.ts @@ -28,6 +28,8 @@ export interface TokenUsage { promptTokens: number completionTokens: number totalTokens: number + cacheReadTokens: number + cacheWriteTokens: number } export interface AgentRuntimeStatus { diff --git a/packages/http-routes/src/routes/web/ai-chats.ts b/packages/http-routes/src/routes/web/ai-chats.ts index ed75bcb4..4c8ffcce 100644 --- a/packages/http-routes/src/routes/web/ai-chats.ts +++ b/packages/http-routes/src/routes/web/ai-chats.ts @@ -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 diff --git a/packages/node-runtime/src/ai/agent/__tests__/event-handler.test.ts b/packages/node-runtime/src/ai/agent/__tests__/event-handler.test.ts index b27c4bba..05b81bc5 100644 --- a/packages/node-runtime/src/ai/agent/__tests__/event-handler.test.ts +++ b/packages/node-runtime/src/ai/agent/__tests__/event-handler.test.ts @@ -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') diff --git a/packages/node-runtime/src/ai/agent/core.ts b/packages/node-runtime/src/ai/agent/core.ts index b5610212..087815f6 100644 --- a/packages/node-runtime/src/ai/agent/core.ts +++ b/packages/node-runtime/src/ai/agent/core.ts @@ -45,7 +45,13 @@ export async function runAgentCore(options: AgentCoreOptions): Promise 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, } } diff --git a/packages/node-runtime/src/ai/agent/types.ts b/packages/node-runtime/src/ai/agent/types.ts index 81963d7e..a4aeae35 100644 --- a/packages/node-runtime/src/ai/agent/types.ts +++ b/packages/node-runtime/src/ai/agent/types.ts @@ -14,6 +14,8 @@ export interface AgentTokenUsage { promptTokens: number completionTokens: number totalTokens: number + cacheReadTokens: number + cacheWriteTokens: number } export interface SimpleHistoryMessage { diff --git a/packages/node-runtime/src/ai/chats.ts b/packages/node-runtime/src/ai/chats.ts index ca8b0993..4520514e 100644 --- a/packages/node-runtime/src/ai/chats.ts +++ b/packages/node-runtime/src/ai/chats.ts @@ -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 diff --git a/src/components/AIChat/chat/ChatStatusBar.vue b/src/components/AIChat/chat/ChatStatusBar.vue index 078b4e6e..cce0e5f8 100644 --- a/src/components/AIChat/chat/ChatStatusBar.vue +++ b/src/components/AIChat/chat/ChatStatusBar.vue @@ -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) }}
{{ t('ai.chat.statusBar.tokenUsageTitle') }}: {{ totalTokenUsageCompactText }}
+
+ {{ t('ai.chat.statusBar.cacheHit') }}: {{ cacheReadText }} +
diff --git a/src/i18n/locales/en-US/ai.json b/src/i18n/locales/en-US/ai.json index e08b7fdd..4614587e 100644 --- a/src/i18n/locales/en-US/ai.json +++ b/src/i18n/locales/en-US/ai.json @@ -215,6 +215,7 @@ } }, "tokenUsageTitle": "Total token usage", + "cacheHit": "Cache hit", "export": { "label": "Export", "title": "Export current conversation" diff --git a/src/i18n/locales/ja-JP/ai.json b/src/i18n/locales/ja-JP/ai.json index 6f6ae543..a794f33c 100644 --- a/src/i18n/locales/ja-JP/ai.json +++ b/src/i18n/locales/ja-JP/ai.json @@ -215,6 +215,7 @@ } }, "tokenUsageTitle": "累計 Token 使用量", + "cacheHit": "キャッシュヒット", "export": { "label": "エクスポート", "title": "現在のチャットをエクスポート" diff --git a/src/i18n/locales/zh-CN/ai.json b/src/i18n/locales/zh-CN/ai.json index d203f23f..0ad13cfe 100644 --- a/src/i18n/locales/zh-CN/ai.json +++ b/src/i18n/locales/zh-CN/ai.json @@ -215,6 +215,7 @@ } }, "tokenUsageTitle": "累计 Token 使用量", + "cacheHit": "缓存命中", "export": { "label": "导出", "title": "导出当前对话" diff --git a/src/i18n/locales/zh-TW/ai.json b/src/i18n/locales/zh-TW/ai.json index 2cf0a76f..df8eee9e 100644 --- a/src/i18n/locales/zh-TW/ai.json +++ b/src/i18n/locales/zh-TW/ai.json @@ -215,6 +215,7 @@ } }, "tokenUsageTitle": "累計 Token 用量", + "cacheHit": "快取命中", "export": { "label": "匯出", "title": "匯出當前對話" diff --git a/src/services/ai-stream/types.ts b/src/services/ai-stream/types.ts index ab2fa8c1..c2fe2fe5 100644 --- a/src/services/ai-stream/types.ts +++ b/src/services/ai-stream/types.ts @@ -66,6 +66,8 @@ export interface TokenUsage { promptTokens: number completionTokens: number totalTokens: number + cacheReadTokens: number + cacheWriteTokens: number } export interface AgentRuntimeStatus { diff --git a/src/services/ai/types.ts b/src/services/ai/types.ts index 67810998..9b792a93 100644 --- a/src/services/ai/types.ts +++ b/src/services/ai/types.ts @@ -36,6 +36,8 @@ export interface TokenUsageData { promptTokens: number completionTokens: number totalTokens: number + cacheReadTokens?: number + cacheWriteTokens?: number } export interface AIMessage { diff --git a/src/stores/aiChat.ts b/src/stores/aiChat.ts index 568b86c3..b7d708f9 100644 --- a/src/stores/aiChat.ts +++ b/src/stores/aiChat.ts @@ -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) {