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:
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user