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
+32 -3
View File
@@ -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(
<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 = (
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 = (
@@ -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: {
@@ -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: {
+35 -3
View File
@@ -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'
})
])
})
})
+15 -17
View File
@@ -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<TokenUsageGroupRow, 'key' | 'label' | 'detail'> {
): Pick<TokenUsageGroupRow, 'key' | 'label' | 'model' | 'runtime'> {
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
}
}