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:
+1
-1
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
### 工作管理、长期协作与工作流
|
### 工作管理、长期协作与工作流
|
||||||
|
|
||||||
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和成果文件;Token 用量按 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
|
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和成果文件;Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
|
||||||
- [x] **记忆与智能心跳**:提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
|
- [x] **记忆与智能心跳**:提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
|
||||||
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
|
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
|
||||||
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
|
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
|
||||||
|
|||||||
@@ -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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user