131 lines
3.4 KiB
TypeScript
131 lines
3.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { LanguageModelUsage } from 'ai';
|
|
import { normalizeUsage, hasBillableTokens } from '@/lib/usage/normalize';
|
|
|
|
/**
|
|
* Builds a minimal AI SDK v6 LanguageModelUsage object. Any field left
|
|
* undefined exercises the "missing → 0" normalization path.
|
|
*/
|
|
function makeUsage(partial: {
|
|
inputTokens?: number;
|
|
outputTokens?: number;
|
|
totalTokens?: number;
|
|
cacheReadTokens?: number;
|
|
cacheWriteTokens?: number;
|
|
reasoningTokens?: number;
|
|
}): LanguageModelUsage {
|
|
return {
|
|
inputTokens: partial.inputTokens,
|
|
outputTokens: partial.outputTokens,
|
|
totalTokens: partial.totalTokens,
|
|
inputTokenDetails: {
|
|
noCacheTokens: undefined,
|
|
cacheReadTokens: partial.cacheReadTokens,
|
|
cacheWriteTokens: partial.cacheWriteTokens,
|
|
},
|
|
outputTokenDetails: {
|
|
textTokens: undefined,
|
|
reasoningTokens: partial.reasoningTokens,
|
|
},
|
|
} as LanguageModelUsage;
|
|
}
|
|
|
|
describe('normalizeUsage', () => {
|
|
it('extracts the v6 four-class token shape', () => {
|
|
const result = normalizeUsage(
|
|
makeUsage({
|
|
inputTokens: 100,
|
|
outputTokens: 50,
|
|
cacheReadTokens: 20,
|
|
cacheWriteTokens: 10,
|
|
reasoningTokens: 5,
|
|
}),
|
|
);
|
|
expect(result).toEqual({
|
|
inputTokens: 100,
|
|
outputTokens: 50,
|
|
cacheReadTokens: 20,
|
|
cacheCreationTokens: 10,
|
|
reasoningTokens: 5,
|
|
});
|
|
});
|
|
|
|
it('fills missing fields with 0', () => {
|
|
const result = normalizeUsage(makeUsage({ inputTokens: 9 }));
|
|
expect(result).toEqual({
|
|
inputTokens: 9,
|
|
outputTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
reasoningTokens: 0,
|
|
});
|
|
});
|
|
|
|
it('handles a fully empty usage object', () => {
|
|
const result = normalizeUsage(makeUsage({}));
|
|
expect(result).toEqual({
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
reasoningTokens: 0,
|
|
});
|
|
});
|
|
|
|
it('falls back to deprecated cachedInputTokens / reasoningTokens flat fields', () => {
|
|
// Some providers populate the deprecated flat fields rather than the nested details.
|
|
const usage = {
|
|
inputTokens: 30,
|
|
outputTokens: 12,
|
|
totalTokens: 42,
|
|
inputTokenDetails: {
|
|
noCacheTokens: undefined,
|
|
cacheReadTokens: undefined,
|
|
cacheWriteTokens: undefined,
|
|
},
|
|
outputTokenDetails: { textTokens: undefined, reasoningTokens: undefined },
|
|
cachedInputTokens: 7,
|
|
reasoningTokens: 3,
|
|
} as LanguageModelUsage;
|
|
const result = normalizeUsage(usage);
|
|
expect(result.cacheReadTokens).toBe(7);
|
|
expect(result.reasoningTokens).toBe(3);
|
|
});
|
|
|
|
it('tolerates a null/undefined usage object', () => {
|
|
expect(normalizeUsage(undefined)).toEqual({
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
reasoningTokens: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('hasBillableTokens', () => {
|
|
it('returns false when every class is 0', () => {
|
|
expect(
|
|
hasBillableTokens({
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
reasoningTokens: 0,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('returns true when any billable class is non-zero', () => {
|
|
expect(
|
|
hasBillableTokens({
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheReadTokens: 5,
|
|
cacheCreationTokens: 0,
|
|
reasoningTokens: 0,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
});
|