fix: label token usage by runtime and model

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 与模型名称清晰归类。
This commit is contained in:
mesalogo
2026-08-15 22:38:48 +08:00
parent 36b38ce2a8
commit b3c753fac2
7 changed files with 116 additions and 39 deletions
+1 -1
View File
@@ -46,7 +46,7 @@
### 工作管理、长期协作与工作流 ### 工作管理、长期协作与工作流
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和成果文件;Token 用量按 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。 - [x] **任务、活动与成果**:集中管理任务状态、审计活动和成果文件;Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
- [x] **记忆与智能心跳**:提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。 - [x] **记忆与智能心跳**:提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。 - [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。 - [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
+32 -3
View File
@@ -524,7 +524,7 @@ describe('ActivityPanel', () => {
expect(within(stats).getByText('32%')).toBeInTheDocument() expect(within(stats).getByText('32%')).toBeInTheDocument()
const projectRow = screen.getByRole('row', { 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(projectRow).toBeInTheDocument()
expect( expect(
@@ -551,7 +551,36 @@ describe('ActivityPanel', () => {
expect(screen.getByText('已删除会话')).toBeInTheDocument() expect(screen.getByText('已删除会话')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '按模型' })) fireEvent.click(screen.getByRole('button', { name: '按模型' }))
expect(screen.getByText('gpt-5')).toBeInTheDocument() expect(screen.getByText('直连模型 · gpt-5')).toBeInTheDocument()
expect(screen.getByText('未知模型')).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(
<ActivityPanel
onClear={vi.fn()}
onOpenConversation={vi.fn()}
records={[]}
tokenUsage={usage}
/>
)
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()
}) })
}) })
+17 -13
View File
@@ -452,9 +452,18 @@ export function ActivityPanel({
const localizeTokenRow = ( const localizeTokenRow = (
row: (typeof tokenRows)[number] row: (typeof tokenRows)[number]
): { label: string; detail?: string } => { ): { label: string; detail?: string } => {
const missingModel = row.key.endsWith(':') const modelLabel =
const modelProvider = row.model || t('tokenUsage.fallbacks.unknownModel')
row.key.match(/model:([^:]*):$/u)?.[1] ?? '' 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 = const label =
tokenGroup === 'project' && tokenGroup === 'project' &&
row.key.startsWith('project:unassigned:') row.key.startsWith('project:unassigned:')
@@ -462,18 +471,13 @@ export function ActivityPanel({
: tokenGroup === 'conversation' && : tokenGroup === 'conversation' &&
row.key.startsWith('conversation:deleted:') row.key.startsWith('conversation:deleted:')
? t('tokenUsage.fallbacks.deletedConversation') ? t('tokenUsage.fallbacks.deletedConversation')
: tokenGroup === 'model' && missingModel : tokenGroup === 'model'
? t('tokenUsage.fallbacks.unknownModel') ? `${runtimeLabel} · ${modelLabel}`
: row.label : row.label
const detail = const detail =
missingModel && tokenGroup !== 'model' tokenGroup === 'model'
? [ ? undefined
t('tokenUsage.fallbacks.unknownModel'), : `${runtimeLabel} · ${modelLabel}`
modelProvider
]
.filter(Boolean)
.join(' · ')
: row.detail
return { label, detail } return { label, detail }
} }
const renderRecordCard = ( const renderRecordCard = (
@@ -53,6 +53,12 @@ export const activity = {
conversation: 'By conversation', conversation: 'By conversation',
model: 'By model' model: 'By model'
}, },
runtimes: {
model: 'Direct model',
opencode: 'OpenCode',
continue: 'Continue',
deepseekHarness: 'DeepSeek Harness'
},
columns: { columns: {
project: 'Project', project: 'Project',
conversation: 'Conversation', conversation: 'Conversation',
@@ -69,7 +75,8 @@ export const activity = {
fallbacks: { fallbacks: {
unassignedProject: 'Unassigned project', unassignedProject: 'Unassigned project',
deletedConversation: 'Deleted conversation', deletedConversation: 'Deleted conversation',
unknownModel: 'Unknown model' unknownModel: 'Unknown model',
unknownRuntime: 'Unknown Runtime'
} }
}, },
stats: { stats: {
@@ -50,6 +50,12 @@ export const activity = {
conversation: '按会话', conversation: '按会话',
model: '按模型' model: '按模型'
}, },
runtimes: {
model: '直连模型',
opencode: 'OpenCode',
continue: 'Continue',
deepseekHarness: 'DeepSeek Harness'
},
columns: { columns: {
project: '项目', project: '项目',
conversation: '会话', conversation: '会话',
@@ -66,7 +72,8 @@ export const activity = {
fallbacks: { fallbacks: {
unassignedProject: '未归属项目', unassignedProject: '未归属项目',
deletedConversation: '已删除会话', deletedConversation: '已删除会话',
unknownModel: '未知模型' unknownModel: '未知模型',
unknownRuntime: '未知 Runtime'
} }
}, },
stats: { stats: {
+35 -3
View File
@@ -67,9 +67,10 @@ describe('token usage aggregation', () => {
}) })
expect(groupTokenUsage(usage, 'project')).toEqual([ expect(groupTokenUsage(usage, 'project')).toEqual([
{ {
key: 'project:project-1:model:openai:gpt-5', key: 'project:project-1:runtime:model:model:gpt-5',
label: '项目一', label: '项目一',
detail: 'gpt-5 · openai', model: 'gpt-5',
runtime: 'model',
inputTokens: 112, inputTokens: 112,
outputTokens: 23, outputTokens: 23,
cacheReadTokens: 40, cacheReadTokens: 40,
@@ -83,7 +84,7 @@ describe('token usage aggregation', () => {
expect(groupTokenUsage(usage, 'model')).toEqual([ expect(groupTokenUsage(usage, 'model')).toEqual([
expect.objectContaining({ expect.objectContaining({
label: 'gpt-5', label: 'gpt-5',
detail: 'openai', runtime: 'model',
totalTokens: 135 totalTokens: 135
}) })
]) ])
@@ -156,4 +157,35 @@ describe('token usage aggregation', () => {
expect(groupTokenUsage(usage, 'project')).toHaveLength(2) expect(groupTokenUsage(usage, 'project')).toHaveLength(2)
expect(groupTokenUsage(usage, 'conversation')).toHaveLength(3) 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'
})
])
})
}) })
+15 -17
View File
@@ -15,7 +15,8 @@ export type TokenUsageTotals = {
export type TokenUsageGroupRow = TokenUsageTotals & { export type TokenUsageGroupRow = TokenUsageTotals & {
key: string key: string
label: string label: string
detail?: string model: string
runtime: string
} }
type TokenUsageRecord = TokenUsageSummary['records'][number] type TokenUsageRecord = TokenUsageSummary['records'][number]
@@ -69,25 +70,20 @@ function usageNumbers(
function groupIdentity( function groupIdentity(
record: TokenUsageRecord, record: TokenUsageRecord,
group: TokenUsageGroup group: TokenUsageGroup
): Pick<TokenUsageGroupRow, 'key' | 'label' | 'detail'> { ): Pick<TokenUsageGroupRow, 'key' | 'label' | 'model' | 'runtime'> {
const model = record.model.trim() const model = record.model.trim()
const provider = record.provider.trim() const runtime = record.runtime.trim()
const modelKey = `${provider}:${model}` const runtimeModelKey = `runtime:${runtime}:model:${model}`
const modelLabel = model
const modelDetail = provider
? modelLabel
? `${modelLabel} · ${provider}`
: provider
: modelLabel
if (group === 'project') { if (group === 'project') {
const projectKey = record.projectId const projectKey = record.projectId
? `project:${record.projectId}` ? `project:${record.projectId}`
: 'project:unassigned' : 'project:unassigned'
return { return {
key: `${projectKey}:model:${modelKey}`, key: `${projectKey}:${runtimeModelKey}`,
label: record.projectName?.trim() || '', label: record.projectName?.trim() || '',
detail: modelDetail model,
runtime
} }
} }
@@ -96,16 +92,18 @@ function groupIdentity(
? `conversation:${record.conversationId}` ? `conversation:${record.conversationId}`
: 'conversation:deleted' : 'conversation:deleted'
return { return {
key: `${conversationKey}:model:${modelKey}`, key: `${conversationKey}:${runtimeModelKey}`,
label: record.conversationTitle?.trim() || '', label: record.conversationTitle?.trim() || '',
detail: modelDetail model,
runtime
} }
} }
return { return {
key: `model:${modelKey}`, key: runtimeModelKey,
label: modelLabel, label: model,
detail: provider || undefined model,
runtime
} }
} }