Skip to content

Commit 58be0c0

Browse files
committed
wip
1 parent 89cb808 commit 58be0c0

File tree

3 files changed

+138
-21
lines changed

3 files changed

+138
-21
lines changed

lib/pricing.test.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,20 @@ describe("extractPricingFromGatewayModel", () => {
6464
expect(pricing).toBeNull();
6565
});
6666

67-
it("should return null for model with empty pricing object", () => {
67+
it("should throw error for model with empty pricing object", () => {
6868
const model: GatewayModel = {
6969
id: "local/model",
7070
name: "Local Model",
7171
pricing: {},
7272
modelType: "language",
7373
};
7474

75-
const pricing = extractPricingFromGatewayModel(model);
76-
expect(pricing).toBeNull();
75+
expect(() => extractPricingFromGatewayModel(model)).toThrowError(
76+
/Invalid pricing/,
77+
);
7778
});
7879

79-
it("should handle invalid pricing values gracefully", () => {
80+
it("should throw error for invalid pricing values", () => {
8081
const model: GatewayModel = {
8182
id: "test/model",
8283
name: "Test Model",
@@ -87,11 +88,9 @@ describe("extractPricingFromGatewayModel", () => {
8788
modelType: "language",
8889
};
8990

90-
const pricing = extractPricingFromGatewayModel(model);
91-
92-
expect(pricing).not.toBeNull();
93-
expect(pricing!.inputCostPerToken).toBe(0);
94-
expect(pricing!.outputCostPerToken).toBe(0.000015);
91+
expect(() => extractPricingFromGatewayModel(model)).toThrowError(
92+
/Invalid pricing/,
93+
);
9594
});
9695
});
9796

lib/pricing.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,18 @@ export function extractPricingFromGatewayModel(
5555

5656
const { pricing } = model;
5757

58-
const inputCost = pricing.input ? parseFloat(pricing.input) : 0;
59-
const outputCost = pricing.output ? parseFloat(pricing.output) : 0;
58+
const inputCost = pricing.input ? parseFloat(pricing.input) : NaN;
59+
const outputCost = pricing.output ? parseFloat(pricing.output) : NaN;
6060

61-
if ((inputCost === 0 || isNaN(inputCost)) && (outputCost === 0 || isNaN(outputCost))) {
62-
return null;
61+
if (isNaN(inputCost) || isNaN(outputCost)) {
62+
throw new Error(
63+
`Invalid pricing for model ${model.id}: input and output pricing must be valid numbers.`,
64+
);
6365
}
6466

6567
const result: ModelPricing = {
66-
inputCostPerToken: isNaN(inputCost) ? 0 : inputCost,
67-
outputCostPerToken: isNaN(outputCost) ? 0 : outputCost,
68+
inputCostPerToken: inputCost,
69+
outputCostPerToken: outputCost,
6870
};
6971

7072
if (pricing.cachedInputTokens) {

lib/utils.test.ts

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { describe, it, expect } from "vitest";
2-
import { sanitizeModelName, getTimestampedFilename } from "./utils.ts";
2+
import {
3+
sanitizeModelName,
4+
getTimestampedFilename,
5+
calculateTotalCost,
6+
} from "./utils.ts";
7+
import type { ModelPricing } from "./pricing.ts";
8+
import type { SingleTestResult } from "./report.ts";
39

410
describe("sanitizeModelName", () => {
511
it("replaces slashes with dashes", () => {
@@ -34,12 +40,22 @@ describe("getTimestampedFilename", () => {
3440
const fixedDate = new Date("2025-12-12T14:30:45Z");
3541

3642
it("generates filename without model name", () => {
37-
const result = getTimestampedFilename("result", "json", undefined, fixedDate);
43+
const result = getTimestampedFilename(
44+
"result",
45+
"json",
46+
undefined,
47+
fixedDate,
48+
);
3849
expect(result).toBe("result-2025-12-12-14-30-45.json");
3950
});
4051

4152
it("generates filename with simple model name", () => {
42-
const result = getTimestampedFilename("result", "json", "gpt-4o", fixedDate);
53+
const result = getTimestampedFilename(
54+
"result",
55+
"json",
56+
"gpt-4o",
57+
fixedDate,
58+
);
4359
expect(result).toBe("result-2025-12-12-14-30-45-gpt-4o.json");
4460
});
4561

@@ -50,7 +66,9 @@ describe("getTimestampedFilename", () => {
5066
"anthropic/claude-sonnet-4",
5167
fixedDate,
5268
);
53-
expect(result).toBe("result-2025-12-12-14-30-45-anthropic-claude-sonnet-4.json");
69+
expect(result).toBe(
70+
"result-2025-12-12-14-30-45-anthropic-claude-sonnet-4.json",
71+
);
5472
});
5573

5674
it("generates filename with model name containing special characters", () => {
@@ -64,13 +82,111 @@ describe("getTimestampedFilename", () => {
6482
});
6583

6684
it("handles different file extensions", () => {
67-
const result = getTimestampedFilename("output", "txt", "test-model", fixedDate);
85+
const result = getTimestampedFilename(
86+
"output",
87+
"txt",
88+
"test-model",
89+
fixedDate,
90+
);
6891
expect(result).toBe("output-2025-12-12-14-30-45-test-model.txt");
6992
});
7093

7194
it("pads single-digit months and days", () => {
7295
const earlyDate = new Date("2025-01-05T08:09:07Z");
73-
const result = getTimestampedFilename("result", "json", undefined, earlyDate);
96+
const result = getTimestampedFilename(
97+
"result",
98+
"json",
99+
undefined,
100+
earlyDate,
101+
);
74102
expect(result).toBe("result-2025-01-05-08-09-07.json");
75103
});
76104
});
105+
106+
describe("calculateTotalCost", () => {
107+
const pricing: ModelPricing = {
108+
inputCostPerToken: 1.0 / 1_000_000,
109+
outputCostPerToken: 2.0 / 1_000_000,
110+
cacheReadInputTokenCost: 0.1 / 1_000_000,
111+
};
112+
113+
it("calculates zero cost for empty results", () => {
114+
const tests: SingleTestResult[] = [];
115+
const result = calculateTotalCost(tests, pricing);
116+
117+
expect(result).toEqual({
118+
inputCost: 0,
119+
outputCost: 0,
120+
cacheReadCost: 0,
121+
totalCost: 0,
122+
inputTokens: 0,
123+
outputTokens: 0,
124+
cachedInputTokens: 0,
125+
});
126+
});
127+
128+
it("aggregates usage from multiple steps and tests", () => {
129+
const tests: SingleTestResult[] = [
130+
{
131+
testName: "test1",
132+
prompt: "p1",
133+
resultWriteContent: null,
134+
verification: {} as any,
135+
steps: [
136+
{
137+
usage: {
138+
inputTokens: 100,
139+
outputTokens: 50,
140+
cachedInputTokens: 10,
141+
},
142+
} as any,
143+
{
144+
usage: {
145+
inputTokens: 200,
146+
outputTokens: 100,
147+
cachedInputTokens: 0,
148+
},
149+
} as any,
150+
],
151+
},
152+
{
153+
testName: "test2",
154+
prompt: "p2",
155+
resultWriteContent: null,
156+
verification: {} as any,
157+
steps: [
158+
{
159+
usage: {
160+
inputTokens: 300,
161+
outputTokens: 150,
162+
cachedInputTokens: 20,
163+
},
164+
} as any,
165+
],
166+
},
167+
];
168+
169+
// Total Input: 100 + 200 + 300 = 600
170+
// Total Output: 50 + 100 + 150 = 300
171+
// Total Cached: 10 + 0 + 20 = 30
172+
// Uncached Input: 600 - 30 = 570
173+
174+
// Costs (per Token):
175+
// Input: 570 * (1.0 / 1e6) = 0.00057
176+
// Output: 300 * (2.0 / 1e6) = 0.0006
177+
// Cache: 30 * (0.1 / 1e6) = 0.000003
178+
// Total: 0.00057 + 0.0006 + 0.000003 = 0.001173
179+
180+
const result = calculateTotalCost(tests, pricing);
181+
182+
expect(result).toEqual({
183+
inputCost: 0.00057,
184+
outputCost: 0.0006,
185+
cacheReadCost: 0.000003,
186+
totalCost: 0.001173,
187+
inputTokens: 600,
188+
outputTokens: 300,
189+
cachedInputTokens: 30,
190+
});
191+
});
192+
});

0 commit comments

Comments
 (0)