diff --git a/src/index.mjs b/src/index.mjs index a1bcb4a..c19c905 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -63,6 +63,8 @@ function units(payload) { // Report the token breakdown of a payload and where the tokens are going. export function analyzePayload(payload, { pricePerMTok = 3, counter = estimateTokens, top = 10 } = {}) { + requireNonNegativeNumber("pricePerMTok", pricePerMTok); + requireNonNegativeNumber("top", top, { integer: true }); const us = units(payload).map((u) => ({ ...u, tokens: u.fixedTokens ?? counter(u.text) })); const total = us.reduce((a, u) => a + u.tokens, 0); const byKind = {}, byRole = {}; diff --git a/test/basic.test.mjs b/test/basic.test.mjs index d55a228..487bd6e 100644 --- a/test/basic.test.mjs +++ b/test/basic.test.mjs @@ -174,6 +174,13 @@ test("units rejects non-array payload.messages with TypeError", () => { }); + +test("analyzePayload validates pricePerMTok and top", () => { + const payload = { messages: [{ role: "user", content: "hello" }] }; + assert.throws(() => analyzePayload(payload, { pricePerMTok: -5 }), RangeError); + assert.throws(() => analyzePayload(payload, { top: -1 }), RangeError); + assert.throws(() => analyzePayload(payload, { top: 1.5 }), RangeError); +}); test("analyzePayload charges image_url blocks a flat documented estimate", () => { const short = analyzePayload({ messages: [{ role: "user", content: [ { type: "image_url", image_url: { url: "https://example.com/a.png" } }, @@ -186,6 +193,7 @@ test("analyzePayload charges image_url blocks a flat documented estimate", () => assert.equal(long.byKind.image_url, 85); assert.equal(short.totalTokens, long.totalTokens); }); + test("estimateTokens keeps CJK text within the documented reference tolerance", () => { const text = "漢".repeat(200); const referenceTokens = 200;