moving
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { CostTracker, DEFAULT_MODEL_PRICING, type ModelPricing } from '../src/cost-tracker.js';
|
||||
import {
|
||||
BudgetExceededError,
|
||||
BudgetPricingUnavailableError,
|
||||
CostTracker,
|
||||
DEFAULT_MODEL_PRICING,
|
||||
type ModelPricing,
|
||||
} from '../src/cost-tracker.js';
|
||||
|
||||
describe('CostTracker', () => {
|
||||
const pricing: Record<string, ModelPricing> = {
|
||||
@@ -58,6 +64,18 @@ describe('CostTracker', () => {
|
||||
expect(DEFAULT_MODEL_PRICING['claude-haiku-4-5']).toBeDefined();
|
||||
});
|
||||
|
||||
it('uses provider rates for the live Gemini and Codex acceptance models', () => {
|
||||
expect(DEFAULT_MODEL_PRICING['google/gemini-2.5-flash'])
|
||||
.toEqual({ inputPer1k: 0.0003, outputPer1k: 0.0025 });
|
||||
expect(DEFAULT_MODEL_PRICING['openrouter/openai/gpt-5.3-codex'])
|
||||
.toEqual({ inputPer1k: 0.00175, outputPer1k: 0.014 });
|
||||
|
||||
const tracker = new CostTracker();
|
||||
tracker.addUsage('google/gemini-2.5-flash', 1000, 1000);
|
||||
tracker.addUsage('openrouter/openai/gpt-5.3-codex', 1000, 1000);
|
||||
expect(tracker.getStats().estimatedCost).toBeCloseTo(0.01855, 6);
|
||||
});
|
||||
|
||||
it('warns once and uses family-aware fallback for an unknown Opus id', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const tracker = new CostTracker();
|
||||
@@ -83,13 +101,217 @@ describe('CostTracker', () => {
|
||||
});
|
||||
|
||||
describe('getDailyTotal', () => {
|
||||
it('returns total cost across all models for current session', () => {
|
||||
it('adds persisted carryover to in-process usage without double-seeding', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-07-21T12:00:00.000Z'));
|
||||
const tracker = new CostTracker();
|
||||
tracker.addUsage('claude-sonnet-4-6', 1000, 500);
|
||||
tracker.addUsage('claude-sonnet-4-6', 2000, 1000);
|
||||
const total = tracker.getDailyTotal();
|
||||
expect(total).toBeGreaterThan(0);
|
||||
expect(total).toBe(tracker.getStats().estimatedCost);
|
||||
try {
|
||||
tracker.initializeDailyCarryover('2026-07-21', 6);
|
||||
tracker.initializeDailyCarryover('2026-07-21', 8);
|
||||
tracker.addUsage('claude-sonnet-4-6', 1000, 1000);
|
||||
|
||||
expect(tracker.hasDailyCarryover('2026-07-21')).toBe(true);
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(6.018, 6);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('resets carryover and in-process usage at the next UTC day', () => {
|
||||
vi.useFakeTimers();
|
||||
const tracker = new CostTracker();
|
||||
try {
|
||||
vi.setSystemTime(new Date('2026-07-20T23:59:00.000Z'));
|
||||
tracker.initializeDailyCarryover('2026-07-20', 6);
|
||||
tracker.addUsage('claude-sonnet-4-6', 1000, 1000);
|
||||
|
||||
vi.setSystemTime(new Date('2026-07-21T00:01:00.000Z'));
|
||||
expect(tracker.getDailyTotal()).toBe(0);
|
||||
expect(tracker.hasDailyCarryover('2026-07-21')).toBe(false);
|
||||
|
||||
tracker.initializeDailyCarryover('2026-07-21', 2);
|
||||
tracker.addUsage('claude-sonnet-4-6', 2000, 1000);
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(2.021, 6);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hard daily spend reservations', () => {
|
||||
const pricing: Record<string, ModelPricing> = {
|
||||
paid: { inputPer1k: 1, outputPer1k: 1 },
|
||||
};
|
||||
|
||||
it('atomically prevents concurrent reservations from sharing the same capacity', async () => {
|
||||
const tracker = new CostTracker(pricing);
|
||||
tracker.setBudget(1, 'hard');
|
||||
|
||||
const attempts = await Promise.allSettled([
|
||||
Promise.resolve().then(() => tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 400, maxOutputTokens: 200,
|
||||
})),
|
||||
Promise.resolve().then(() => tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 400, maxOutputTokens: 200,
|
||||
})),
|
||||
]);
|
||||
|
||||
expect(attempts.filter(result => result.status === 'fulfilled')).toHaveLength(1);
|
||||
expect(attempts.find(result => result.status === 'rejected')).toMatchObject({
|
||||
reason: expect.any(BudgetExceededError),
|
||||
});
|
||||
expect(tracker.getReservedDailyTotal()).toBeCloseTo(0.6, 6);
|
||||
});
|
||||
|
||||
it('reconciles a conservative reservation to actual usage exactly once', () => {
|
||||
const tracker = new CostTracker(pricing);
|
||||
tracker.setBudget(1, 'hard');
|
||||
const reservation = tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 400, maxOutputTokens: 400, workspaceId: 'workspace-a',
|
||||
});
|
||||
|
||||
tracker.reconcileModelSpend(reservation, { inputTokens: 100, outputTokens: 100 });
|
||||
tracker.reconcileModelSpend(reservation, { inputTokens: 900, outputTokens: 900 });
|
||||
|
||||
expect(tracker.getReservedDailyTotal()).toBe(0);
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(0.2, 6);
|
||||
expect(tracker.getWorkspaceCost('workspace-a')).toBeCloseTo(0.2, 6);
|
||||
expect(() => tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 400, maxOutputTokens: 400,
|
||||
})).not.toThrow();
|
||||
});
|
||||
|
||||
it('retains the conservative reservation after ambiguous provider failure', () => {
|
||||
const tracker = new CostTracker(pricing);
|
||||
tracker.setBudget(1, 'hard');
|
||||
const reservation = tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 500, maxOutputTokens: 500,
|
||||
});
|
||||
|
||||
tracker.commitReservedModelSpend(reservation);
|
||||
|
||||
expect(tracker.getReservedDailyTotal()).toBe(0);
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(1, 6);
|
||||
expect(() => tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 1, maxOutputTokens: 1,
|
||||
})).toThrow(BudgetExceededError);
|
||||
});
|
||||
|
||||
it('allows explicitly verified free execution after the paid cap is exhausted', () => {
|
||||
const tracker = new CostTracker(pricing);
|
||||
tracker.setBudget(1, 'hard');
|
||||
const paid = tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 500, maxOutputTokens: 500,
|
||||
});
|
||||
tracker.commitReservedModelSpend(paid);
|
||||
|
||||
const local = tracker.reserveModelSpend({
|
||||
model: 'unpriced-local-model',
|
||||
inputTokens: 10_000,
|
||||
maxOutputTokens: 10_000,
|
||||
billingClass: 'free',
|
||||
});
|
||||
tracker.reconcileModelSpend(local, { inputTokens: 10_000, outputTokens: 10_000 });
|
||||
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(1, 6);
|
||||
});
|
||||
|
||||
it('normalizes zero to disabled and rejects negative budgets', () => {
|
||||
const tracker = new CostTracker(pricing);
|
||||
|
||||
tracker.setBudget(0, 'hard');
|
||||
expect(tracker.getBudget()).toEqual({ dailyBudgetUsd: null, mode: 'hard' });
|
||||
expect(() => tracker.setBudget(-1, 'hard')).toThrow(/non-negative finite/i);
|
||||
});
|
||||
|
||||
it('preserves an explicit priced classification for ollama-prefixed routes', () => {
|
||||
const tracker = new CostTracker({
|
||||
'ollama/remote-paid': { inputPer1k: 1, outputPer1k: 1 },
|
||||
});
|
||||
tracker.setBudget(1, 'hard');
|
||||
const reservation = tracker.reserveModelSpend({
|
||||
model: 'ollama/remote-paid', inputTokens: 300, maxOutputTokens: 300,
|
||||
billingClass: 'priced',
|
||||
});
|
||||
|
||||
tracker.reconcileModelSpend(reservation, { inputTokens: 100, outputTokens: 100 });
|
||||
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(0.2, 6);
|
||||
});
|
||||
|
||||
it('commits the reservation when provider usage is non-finite', () => {
|
||||
const tracker = new CostTracker(pricing);
|
||||
tracker.setBudget(1, 'hard');
|
||||
const reservation = tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 400, maxOutputTokens: 400,
|
||||
});
|
||||
|
||||
tracker.reconcileModelSpend(reservation, { inputTokens: Number.NaN, outputTokens: 0 });
|
||||
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(0.8, 6);
|
||||
expect(() => tracker.reserveModelSpend({
|
||||
model: 'paid', inputTokens: 200, maxOutputTokens: 1,
|
||||
})).toThrow(BudgetExceededError);
|
||||
});
|
||||
|
||||
it('rejects non-finite direct usage before it can poison the ledger', () => {
|
||||
const tracker = new CostTracker(pricing);
|
||||
|
||||
expect(() => tracker.addUsage('paid', Number.NaN, 0)).toThrow(/finite/i);
|
||||
expect(tracker.getDailyTotal()).toBe(0);
|
||||
});
|
||||
|
||||
it('fails closed when hard mode lacks trusted pricing for a paid route', () => {
|
||||
const tracker = new CostTracker();
|
||||
tracker.setBudget(1, 'hard');
|
||||
|
||||
expect(() => tracker.reserveModelSpend({
|
||||
model: 'openrouter/auto', inputTokens: 1, maxOutputTokens: 1,
|
||||
billingClass: 'priced',
|
||||
})).toThrow(BudgetPricingUnavailableError);
|
||||
});
|
||||
|
||||
it('prices provider-wrapped model IDs only through a trusted catalog suffix', () => {
|
||||
const tracker = new CostTracker();
|
||||
tracker.setBudget(1, 'hard');
|
||||
|
||||
const reservation = tracker.reserveModelSpend({
|
||||
model: 'anthropic/claude-sonnet-4-6',
|
||||
inputTokens: 1_000,
|
||||
maxOutputTokens: 1_000,
|
||||
billingClass: 'priced',
|
||||
});
|
||||
tracker.reconcileModelSpend(reservation, { inputTokens: 1_000, outputTokens: 1_000 });
|
||||
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(0.018, 6);
|
||||
expect(() => tracker.reserveModelSpend({
|
||||
model: 'anthropic/untrusted-custom-model',
|
||||
inputTokens: 1,
|
||||
maxOutputTokens: 1,
|
||||
billingClass: 'priced',
|
||||
})).toThrow(BudgetPricingUnavailableError);
|
||||
});
|
||||
|
||||
it('honors explicit paid versus free billing for Ollama-routed reservations', () => {
|
||||
const tracker = new CostTracker();
|
||||
|
||||
const paid = tracker.reserveModelSpend({
|
||||
model: 'ollama/minimax-m2.7:cloud',
|
||||
inputTokens: 1_000,
|
||||
maxOutputTokens: 1_000,
|
||||
billingClass: 'priced',
|
||||
});
|
||||
tracker.reconcileModelSpend(paid, { inputTokens: 1_000, outputTokens: 1_000 });
|
||||
const local = tracker.reserveModelSpend({
|
||||
model: 'ollama/qwen2.5:1.5b',
|
||||
inputTokens: 1_000,
|
||||
maxOutputTokens: 1_000,
|
||||
billingClass: 'free',
|
||||
});
|
||||
tracker.reconcileModelSpend(local, { inputTokens: 1_000, outputTokens: 1_000 });
|
||||
|
||||
expect(tracker.getDailyTotal()).toBeCloseTo(0.018, 6);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user