From b3c753fac233180d25434e7f3c5fa77cba2484b5 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Sat, 15 Aug 2026 22:38:48 +0800 Subject: [PATCH] fix: label token usage by runtime and model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Token usage grouped and displayed provider identifiers, which exposed the internal goodbuddy Harness provider and made it appear comparable to OpenAI. Usage rows now follow the product's Runtime and model identity, including Direct model, OpenCode, Continue, and DeepSeek Harness, while provider data remains available only for cache accounting. Release note: 修复 Token 用量中显示 goodbuddy 等内部标识的问题;统计现在按 Runtime 与模型名称清晰归类。 --- FEATURES.md | 2 +- src/renderer/src/ActivityPanel.test.tsx | 35 +++++++++++++++-- src/renderer/src/ActivityPanel.tsx | 30 ++++++++------- .../src/i18n/locales/en-US/activity.ts | 9 ++++- .../src/i18n/locales/zh-CN/activity.ts | 9 ++++- src/renderer/src/token-usage.test.ts | 38 +++++++++++++++++-- src/renderer/src/token-usage.ts | 32 ++++++++-------- 7 files changed, 116 insertions(+), 39 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 24583ce..628ca43 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -46,7 +46,7 @@ ### 工作管理、长期协作与工作流 -- [x] **任务、活动与成果**:集中管理任务状态、审计活动和成果文件;Token 用量按 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。 +- [x] **任务、活动与成果**:集中管理任务状态、审计活动和成果文件;Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。 - [x] **记忆与智能心跳**:提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。 - [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。 - [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。 diff --git a/src/renderer/src/ActivityPanel.test.tsx b/src/renderer/src/ActivityPanel.test.tsx index eacae6f..dac866b 100644 --- a/src/renderer/src/ActivityPanel.test.tsx +++ b/src/renderer/src/ActivityPanel.test.tsx @@ -524,7 +524,7 @@ describe('ActivityPanel', () => { expect(within(stats).getByText('32%')).toBeInTheDocument() const projectRow = screen.getByRole('row', { - name: '项目甲gpt-5 · openai 100 20 10 40 40% 120' + name: '项目甲直连模型 · gpt-5 100 20 10 40 40% 120' }) expect(projectRow).toBeInTheDocument() expect( @@ -551,7 +551,36 @@ describe('ActivityPanel', () => { expect(screen.getByText('已删除会话')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: '按模型' })) - expect(screen.getByText('gpt-5')).toBeInTheDocument() - expect(screen.getByText('未知模型')).toBeInTheDocument() + expect(screen.getByText('直连模型 · gpt-5')).toBeInTheDocument() + expect( + screen.getByText('直连模型 · 未知模型') + ).toBeInTheDocument() + }) + + it('shows Runtime names instead of internal provider identifiers', () => { + const usage = makeTokenUsage() + usage.records.push({ + ...usage.records[0]!, + requestId: 'request-harness', + runtime: 'deepseek-harness', + provider: 'goodbuddy', + model: 'deepseek-v4-flash' + }) + + render( + + ) + fireEvent.click(screen.getByRole('tab', { name: '用量统计' })) + fireEvent.click(screen.getByRole('button', { name: '按模型' })) + + expect( + screen.getByText('DeepSeek Harness · deepseek-v4-flash') + ).toBeInTheDocument() + expect(screen.queryByText('goodbuddy')).not.toBeInTheDocument() }) }) diff --git a/src/renderer/src/ActivityPanel.tsx b/src/renderer/src/ActivityPanel.tsx index aaad481..56d159b 100644 --- a/src/renderer/src/ActivityPanel.tsx +++ b/src/renderer/src/ActivityPanel.tsx @@ -452,9 +452,18 @@ export function ActivityPanel({ const localizeTokenRow = ( row: (typeof tokenRows)[number] ): { label: string; detail?: string } => { - const missingModel = row.key.endsWith(':') - const modelProvider = - row.key.match(/model:([^:]*):$/u)?.[1] ?? '' + const modelLabel = + row.model || t('tokenUsage.fallbacks.unknownModel') + const runtimeLabel = + row.runtime === 'model' + ? t('tokenUsage.runtimes.model') + : row.runtime === 'opencode' + ? t('tokenUsage.runtimes.opencode') + : row.runtime === 'continue' + ? t('tokenUsage.runtimes.continue') + : row.runtime === 'deepseek-harness' + ? t('tokenUsage.runtimes.deepseekHarness') + : row.runtime || t('tokenUsage.fallbacks.unknownRuntime') const label = tokenGroup === 'project' && row.key.startsWith('project:unassigned:') @@ -462,18 +471,13 @@ export function ActivityPanel({ : tokenGroup === 'conversation' && row.key.startsWith('conversation:deleted:') ? t('tokenUsage.fallbacks.deletedConversation') - : tokenGroup === 'model' && missingModel - ? t('tokenUsage.fallbacks.unknownModel') + : tokenGroup === 'model' + ? `${runtimeLabel} · ${modelLabel}` : row.label const detail = - missingModel && tokenGroup !== 'model' - ? [ - t('tokenUsage.fallbacks.unknownModel'), - modelProvider - ] - .filter(Boolean) - .join(' · ') - : row.detail + tokenGroup === 'model' + ? undefined + : `${runtimeLabel} · ${modelLabel}` return { label, detail } } const renderRecordCard = ( diff --git a/src/renderer/src/i18n/locales/en-US/activity.ts b/src/renderer/src/i18n/locales/en-US/activity.ts index 1f1fc6c..cc7bd1a 100644 --- a/src/renderer/src/i18n/locales/en-US/activity.ts +++ b/src/renderer/src/i18n/locales/en-US/activity.ts @@ -53,6 +53,12 @@ export const activity = { conversation: 'By conversation', model: 'By model' }, + runtimes: { + model: 'Direct model', + opencode: 'OpenCode', + continue: 'Continue', + deepseekHarness: 'DeepSeek Harness' + }, columns: { project: 'Project', conversation: 'Conversation', @@ -69,7 +75,8 @@ export const activity = { fallbacks: { unassignedProject: 'Unassigned project', deletedConversation: 'Deleted conversation', - unknownModel: 'Unknown model' + unknownModel: 'Unknown model', + unknownRuntime: 'Unknown Runtime' } }, stats: { diff --git a/src/renderer/src/i18n/locales/zh-CN/activity.ts b/src/renderer/src/i18n/locales/zh-CN/activity.ts index a6fbbd5..d9a92e8 100644 --- a/src/renderer/src/i18n/locales/zh-CN/activity.ts +++ b/src/renderer/src/i18n/locales/zh-CN/activity.ts @@ -50,6 +50,12 @@ export const activity = { conversation: '按会话', model: '按模型' }, + runtimes: { + model: '直连模型', + opencode: 'OpenCode', + continue: 'Continue', + deepseekHarness: 'DeepSeek Harness' + }, columns: { project: '项目', conversation: '会话', @@ -66,7 +72,8 @@ export const activity = { fallbacks: { unassignedProject: '未归属项目', deletedConversation: '已删除会话', - unknownModel: '未知模型' + unknownModel: '未知模型', + unknownRuntime: '未知 Runtime' } }, stats: { diff --git a/src/renderer/src/token-usage.test.ts b/src/renderer/src/token-usage.test.ts index 04e703f..a92af1b 100644 --- a/src/renderer/src/token-usage.test.ts +++ b/src/renderer/src/token-usage.test.ts @@ -67,9 +67,10 @@ describe('token usage aggregation', () => { }) expect(groupTokenUsage(usage, 'project')).toEqual([ { - key: 'project:project-1:model:openai:gpt-5', + key: 'project:project-1:runtime:model:model:gpt-5', label: '项目一', - detail: 'gpt-5 · openai', + model: 'gpt-5', + runtime: 'model', inputTokens: 112, outputTokens: 23, cacheReadTokens: 40, @@ -83,7 +84,7 @@ describe('token usage aggregation', () => { expect(groupTokenUsage(usage, 'model')).toEqual([ expect.objectContaining({ label: 'gpt-5', - detail: 'openai', + runtime: 'model', totalTokens: 135 }) ]) @@ -156,4 +157,35 @@ describe('token usage aggregation', () => { expect(groupTokenUsage(usage, 'project')).toHaveLength(2) expect(groupTokenUsage(usage, 'conversation')).toHaveLength(3) }) + + it('groups identical model names by Runtime instead of provider', () => { + const usage = makeTokenUsage() + usage.records = [ + { + ...usage.records[0]!, + provider: 'openai', + runtime: 'model', + model: 'deepseek-v4-flash' + }, + { + ...usage.records[1]!, + provider: 'goodbuddy', + runtime: 'deepseek-harness', + model: 'deepseek-v4-flash' + } + ] + + expect(groupTokenUsage(usage, 'model')).toEqual([ + expect.objectContaining({ + key: 'runtime:model:model:deepseek-v4-flash', + label: 'deepseek-v4-flash', + runtime: 'model' + }), + expect.objectContaining({ + key: 'runtime:deepseek-harness:model:deepseek-v4-flash', + label: 'deepseek-v4-flash', + runtime: 'deepseek-harness' + }) + ]) + }) }) diff --git a/src/renderer/src/token-usage.ts b/src/renderer/src/token-usage.ts index 74d7cc4..706ea77 100644 --- a/src/renderer/src/token-usage.ts +++ b/src/renderer/src/token-usage.ts @@ -15,7 +15,8 @@ export type TokenUsageTotals = { export type TokenUsageGroupRow = TokenUsageTotals & { key: string label: string - detail?: string + model: string + runtime: string } type TokenUsageRecord = TokenUsageSummary['records'][number] @@ -69,25 +70,20 @@ function usageNumbers( function groupIdentity( record: TokenUsageRecord, group: TokenUsageGroup -): Pick { +): Pick { const model = record.model.trim() - const provider = record.provider.trim() - const modelKey = `${provider}:${model}` - const modelLabel = model - const modelDetail = provider - ? modelLabel - ? `${modelLabel} · ${provider}` - : provider - : modelLabel + const runtime = record.runtime.trim() + const runtimeModelKey = `runtime:${runtime}:model:${model}` if (group === 'project') { const projectKey = record.projectId ? `project:${record.projectId}` : 'project:unassigned' return { - key: `${projectKey}:model:${modelKey}`, + key: `${projectKey}:${runtimeModelKey}`, label: record.projectName?.trim() || '', - detail: modelDetail + model, + runtime } } @@ -96,16 +92,18 @@ function groupIdentity( ? `conversation:${record.conversationId}` : 'conversation:deleted' return { - key: `${conversationKey}:model:${modelKey}`, + key: `${conversationKey}:${runtimeModelKey}`, label: record.conversationTitle?.trim() || '', - detail: modelDetail + model, + runtime } } return { - key: `model:${modelKey}`, - label: modelLabel, - detail: provider || undefined + key: runtimeModelKey, + label: model, + model, + runtime } }