Every chat session now runs through the Hermes bridge, so the three chat chains become feature-equivalent instead of three partially-maintained dispatch paths. Keeping the coding-agent and in-house agent runtimes alive meant every bridge change had to be mirrored into two more runtimes that had already drifted. Historical rows are read, not migrated: sessions stored with the retired source normalize to cli on load, workflow graphs naming the retired agents keep loading and run on the bridge, and the visible-source check plus SOURCE_LABELS still recognize the old value so those sessions stay listed. The public surface follows the code: the OpenAPI generator and the MCP chat-run tool schema drop the six retired entry fields so external callers are not told about runtimes that no longer exist. harness-check gains a RETIRED_RUNTIME scan so the residual check is a mechanical gate instead of a manual grep, with the two surviving read-side compat sites recorded with reasons. Refs: docs/phases/phase_003.md
125 lines
3.2 KiB
TypeScript
125 lines
3.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const updateUsageMock = vi.hoisted(() => vi.fn())
|
|
|
|
vi.mock('../../packages/server/src/db/hermes/usage-store', () => ({
|
|
updateUsage: updateUsageMock,
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/logger', () => ({
|
|
logger: { warn: vi.fn() },
|
|
}))
|
|
|
|
import { normalizeTokenUsage, recordSessionUsage } from '../../packages/server/src/services/usage-recorder'
|
|
|
|
describe('usage recorder', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('normalizes OpenAI and Anthropic token detail fields', () => {
|
|
expect(normalizeTokenUsage({
|
|
input_tokens: 120,
|
|
output_tokens: 45,
|
|
input_tokens_details: { cached_tokens: 30 },
|
|
output_tokens_details: { reasoning_tokens: 12 },
|
|
})).toEqual({
|
|
inputTokens: 120,
|
|
outputTokens: 45,
|
|
cacheReadTokens: 30,
|
|
cacheWriteTokens: 0,
|
|
reasoningTokens: 12,
|
|
isEstimated: false,
|
|
})
|
|
|
|
expect(normalizeTokenUsage({
|
|
usage: {
|
|
input_tokens: 80,
|
|
output_tokens: 20,
|
|
cache_read_input_tokens: 15,
|
|
cache_creation_input_tokens: 4,
|
|
},
|
|
})).toEqual({
|
|
inputTokens: 80,
|
|
outputTokens: 20,
|
|
cacheReadTokens: 15,
|
|
cacheWriteTokens: 4,
|
|
reasoningTokens: 0,
|
|
isEstimated: false,
|
|
})
|
|
|
|
expect(normalizeTokenUsage({
|
|
prompt_tokens: 90,
|
|
completion_tokens: 18,
|
|
prompt_tokens_details: { cached_tokens: 40 },
|
|
completion_tokens_details: { reasoning_tokens: 6 },
|
|
})).toEqual({
|
|
inputTokens: 90,
|
|
outputTokens: 18,
|
|
cacheReadTokens: 40,
|
|
cacheWriteTokens: 0,
|
|
reasoningTokens: 6,
|
|
isEstimated: false,
|
|
})
|
|
})
|
|
|
|
it('fills missing provider fields from the per-run estimate', () => {
|
|
expect(normalizeTokenUsage(
|
|
{ input_tokens: null, output_tokens: 9 },
|
|
{ inputTokens: 25, outputTokens: 8 },
|
|
)).toEqual({
|
|
inputTokens: 25,
|
|
outputTokens: 9,
|
|
cacheReadTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
reasoningTokens: 0,
|
|
isEstimated: true,
|
|
})
|
|
})
|
|
|
|
it('splits cached tokens from provider input totals when requested', () => {
|
|
expect(normalizeTokenUsage({
|
|
input_tokens: 120,
|
|
output_tokens: 7,
|
|
input_tokens_details: { cached_tokens: 30 },
|
|
}, {}, { inputIncludesCache: true })).toEqual({
|
|
inputTokens: 90,
|
|
outputTokens: 7,
|
|
cacheReadTokens: 30,
|
|
cacheWriteTokens: 0,
|
|
reasoningTokens: 0,
|
|
isEstimated: false,
|
|
})
|
|
})
|
|
|
|
it('persists normalized metadata through the shared store entrypoint', () => {
|
|
recordSessionUsage({
|
|
sessionId: 'session-1',
|
|
runId: 'run-1',
|
|
source: 'hermes',
|
|
agent: 'hermes',
|
|
profile: 'work',
|
|
model: 'gpt-5',
|
|
provider: 'openai',
|
|
usage: { prompt_tokens: 10, completion_tokens: 4 },
|
|
})
|
|
|
|
expect(updateUsageMock).toHaveBeenCalledWith('session-1', {
|
|
runId: 'run-1',
|
|
source: 'hermes',
|
|
agent: 'hermes',
|
|
usageScope: undefined,
|
|
apiCalls: undefined,
|
|
inputTokens: 10,
|
|
outputTokens: 4,
|
|
cacheReadTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
reasoningTokens: 0,
|
|
model: 'gpt-5',
|
|
provider: 'openai',
|
|
profile: 'work',
|
|
isEstimated: false,
|
|
})
|
|
})
|
|
})
|