60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
import { expect, test } from 'bun:test';
|
|||
|
|
import { costOf, formatUsd, rateFor, usageLine } from '../src/pricing';
|
||
|
|
|
||
|
|
test('a known model resolves to a rate', () => {
|
||
|
|
expect(rateFor('gpt-5')).toEqual({ inputPerMTok: 1.25, outputPerMTok: 10 });
|
||
|
|
expect(rateFor('claude-sonnet-4-5')).toEqual({ inputPerMTok: 3, outputPerMTok: 15 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a dated model id resolves through its prefix', () => {
|
||
|
|
expect(rateFor('claude-sonnet-4-5-20250929')).toEqual(rateFor('claude-sonnet-4'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the longest matching prefix wins so mini does not resolve to the base model', () => {
|
||
|
|
expect(rateFor('gpt-5-mini')?.inputPerMTok).toBe(0.25);
|
||
|
|
expect(rateFor('gpt-5')?.inputPerMTok).toBe(1.25);
|
||
|
|
expect(rateFor('gpt-5-nano')?.inputPerMTok).toBe(0.05);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('an OpenRouter-style prefixed id is stripped before matching', () => {
|
||
|
|
expect(rateFor('anthropic/claude-sonnet-4-5')).toEqual(rateFor('claude-sonnet-4-5'));
|
||
|
|
expect(rateFor('openai/gpt-5-mini')).toEqual(rateFor('gpt-5-mini'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('matching is case-insensitive', () => {
|
||
|
|
expect(rateFor('GPT-5')).toEqual(rateFor('gpt-5'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('an unknown model has no rate and no cost', () => {
|
||
|
|
expect(rateFor('some-local-llama')).toBeUndefined();
|
||
|
|
expect(costOf('some-local-llama', 1000, 500)).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cost is computed per million tokens', () => {
|
||
|
|
// 1M in at $1.25 plus 1M out at $10.
|
||
|
|
expect(costOf('gpt-5', 1_000_000, 1_000_000)).toBeCloseTo(11.25, 6);
|
||
|
|
expect(costOf('gpt-5', 500_000, 0)).toBeCloseTo(0.625, 6);
|
||
|
|
expect(costOf('gpt-5', 0, 0)).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('formatUsd keeps sub-cent amounts visible', () => {
|
||
|
|
expect(formatUsd(0)).toBe('$0.00');
|
||
|
|
expect(formatUsd(0.0001234)).toBe('$0.0001');
|
||
|
|
expect(formatUsd(0.005)).toBe('$0.0050');
|
||
|
|
expect(formatUsd(1.239)).toBe('$1.24');
|
||
|
|
expect(formatUsd(12)).toBe('$12.00');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('usageLine reports tokens plus dollars for a priced model', () => {
|
||
|
|
const line = usageLine('gpt-5', 10_000, 2_000);
|
||
|
|
expect(line).toContain('10000 in / 2000 out tokens');
|
||
|
|
expect(line).toContain('$0.03');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('usageLine says so plainly when the model is unpriced', () => {
|
||
|
|
const line = usageLine('qwen-local', 10, 5);
|
||
|
|
expect(line).toContain('10 in / 5 out tokens');
|
||
|
|
expect(line).toContain('unpriced');
|
||
|
|
expect(line).not.toContain('$');
|
||
|
|
});
|