wulf-pulse/lib/services/llm/pricing.test.ts
lorentz 8f8b5ab7be feat: AI ticket analyzer (phases 1-6)
Multi-stage LLM pipeline that produces structured analyses of Autotask
tickets from local Postgres. Migration 069 + Zod schemas, Stage 0
preprocessor, IT Glue redaction + search, Anthropic SDK wrapper, Stages
1/3/4 (Haiku/Sonnet/Opus), pipeline + cost circuit breaker, job worker
(opt-in autostart), 6 API routes, 3 frontend pages, share-row
persistence (email send deferred to phase 7). 128 vitest tests, tsc
clean. Build journal in docs/wulf-pulse-ticket-analyzer-build-notes.md.

Sync: adds syncTicketNotes() + ticket_notes to ordered/date-filtered
entities so the analyzer's local mirror stays current via scheduler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:59:40 -04:00

85 lines
2.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { estimateCostUsd, PRICING } from './pricing';
import { HAIKU, SONNET, OPUS } from './models';
describe('PRICING', () => {
it('has rows for all three analyzer models', () => {
expect(PRICING[HAIKU]).toBeDefined();
expect(PRICING[SONNET]).toBeDefined();
expect(PRICING[OPUS]).toBeDefined();
});
it('output is more expensive than input on every model', () => {
for (const m of [HAIKU, SONNET, OPUS] as const) {
expect(PRICING[m].output).toBeGreaterThan(PRICING[m].input);
}
});
it('cache_read is much cheaper than uncached input', () => {
for (const m of [HAIKU, SONNET, OPUS] as const) {
expect(PRICING[m].cacheRead).toBeLessThan(PRICING[m].input);
}
});
});
describe('estimateCostUsd', () => {
it('charges Haiku $1/1M input + $5/1M output', () => {
// 1,000,000 input + 1,000,000 output = $1 + $5 = $6
expect(
estimateCostUsd(HAIKU, { input_tokens: 1_000_000, output_tokens: 1_000_000 })
).toBe(6);
});
it('charges Sonnet $3/1M input + $15/1M output', () => {
expect(
estimateCostUsd(SONNET, { input_tokens: 1_000_000, output_tokens: 1_000_000 })
).toBe(18);
});
it('charges Opus $5/1M input + $25/1M output', () => {
expect(
estimateCostUsd(OPUS, { input_tokens: 1_000_000, output_tokens: 1_000_000 })
).toBe(30);
});
it('charges cache_read at the discounted rate', () => {
// 1M cache_read at Haiku's $0.10 = $0.10
const cost = estimateCostUsd(HAIKU, {
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 1_000_000,
});
expect(cost).toBeCloseTo(0.1, 4);
});
it('charges cache_creation at the 1.25x premium', () => {
// 1M cache_write at Haiku's $1.25 = $1.25
const cost = estimateCostUsd(HAIKU, {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 1_000_000,
});
expect(cost).toBeCloseTo(1.25, 4);
});
it('rounds to 4 decimals (matches DB column)', () => {
const cost = estimateCostUsd(HAIKU, { input_tokens: 1, output_tokens: 1 });
// 1 input @ $1/M = $0.000001 ≈ rounds to 0.0000
expect(cost.toString().split('.')[1]?.length ?? 0).toBeLessThanOrEqual(4);
});
it('returns 0 for zero usage', () => {
expect(
estimateCostUsd(OPUS, { input_tokens: 0, output_tokens: 0 })
).toBe(0);
});
it('produces a realistic ticket-analysis cost for a typical Stage 1 call', () => {
// ~10K input tokens, ~500 output tokens on Haiku ≈ $0.013
const cost = estimateCostUsd(HAIKU, {
input_tokens: 10_000,
output_tokens: 500,
});
expect(cost).toBeCloseTo(0.0125, 4);
});
});