diff --git a/docs/ja-JP/skills/cost-aware-llm-pipeline/SKILL.md b/docs/ja-JP/skills/cost-aware-llm-pipeline/SKILL.md index adb4b532c..196361e1a 100644 --- a/docs/ja-JP/skills/cost-aware-llm-pipeline/SKILL.md +++ b/docs/ja-JP/skills/cost-aware-llm-pipeline/SKILL.md @@ -158,6 +158,7 @@ def process(text: str, config: Config, tracker: CostTracker) -> tuple[Result, Co | Haiku 4.5 | $1.00 | $5.00 | 1x | | Sonnet 4.6 | $3.00 | $15.00 | 3x | | Opus 4.5 | $5.00 | $25.00 | 5x | +| Fable 5 / Mythos 5 | $10.00 | $50.00 | 10x | ## ベストプラクティス diff --git a/docs/zh-CN/skills/cost-aware-llm-pipeline/SKILL.md b/docs/zh-CN/skills/cost-aware-llm-pipeline/SKILL.md index 396171570..f54f31297 100644 --- a/docs/zh-CN/skills/cost-aware-llm-pipeline/SKILL.md +++ b/docs/zh-CN/skills/cost-aware-llm-pipeline/SKILL.md @@ -158,6 +158,7 @@ def process(text: str, config: Config, tracker: CostTracker) -> tuple[Result, Co | Haiku 4.5 | $1.00 | $5.00 | 1x | | Sonnet 4.6 | $3.00 | $15.00 | 3x | | Opus 4.5 | $5.00 | $25.00 | 5x | +| Fable 5 / Mythos 5 | $10.00 | $50.00 | 10x | ## 最佳实践 diff --git a/scripts/lib/cost-estimate.js b/scripts/lib/cost-estimate.js index 0dfa9c834..cc7d3fdd7 100644 --- a/scripts/lib/cost-estimate.js +++ b/scripts/lib/cost-estimate.js @@ -48,11 +48,24 @@ const LEGACY_HAIKU_RE = /3-5-haiku|haiku-3-5/; * Estimate USD cost from token counts. * @param {string} model - Model name (may contain "haiku", "sonnet", "opus", * "fable" or "mythos"); anything else is priced at sonnet rates. - * @param {number} inputTokens - * @param {number} outputTokens + * @param {number} inputTokens - Finite, non-negative. + * @param {number} outputTokens - Finite, non-negative. * @returns {number} Estimated cost in USD (rounded to 6 decimal places) + * @throws {RangeError} If either token count is negative or non-finite. */ function estimateCost(model, inputTokens, outputTokens) { + // Callers use this to decide whether a call fits a budget, and both bad + // inputs defeat that check silently rather than loudly: a negative count + // yields a negative cost, and a non-finite one yields NaN, for which every + // `cost > budget` comparison is false. An unpriceable model still falls + // back to sonnet rates on purpose — that degrades an estimate; this + // corrupts one. + for (const [name, value] of [['inputTokens', inputTokens], ['outputTokens', outputTokens]]) { + if (!Number.isFinite(value) || value < 0) { + throw new RangeError(`${name} must be a finite non-negative number, got ${value}`); + } + } + const normalized = String(model || '').toLowerCase(); let rates = RATE_TABLE.sonnet; if (normalized.includes('haiku')) { diff --git a/skills/cost-aware-llm-pipeline/SKILL.md b/skills/cost-aware-llm-pipeline/SKILL.md index 63b10f6b3..34945844c 100644 --- a/skills/cost-aware-llm-pipeline/SKILL.md +++ b/skills/cost-aware-llm-pipeline/SKILL.md @@ -159,6 +159,7 @@ def process(text: str, config: Config, tracker: CostTracker) -> tuple[Result, Co | Haiku 4.5 | $1.00 | $5.00 | 1x | | Sonnet 4.6 | $3.00 | $15.00 | 3x | | Opus 4.5 | $5.00 | $25.00 | 5x | +| Fable 5 / Mythos 5 | $10.00 | $50.00 | 10x | ## Best Practices diff --git a/tests/lib/cost-estimate.test.js b/tests/lib/cost-estimate.test.js index 2aa60770f..ce5a08193 100644 --- a/tests/lib/cost-estimate.test.js +++ b/tests/lib/cost-estimate.test.js @@ -172,6 +172,36 @@ function runTests() { passed++; else failed++; + // A negative count yields a negative cost and a non-finite one yields NaN, + // and `NaN > budget` is false — so bad input defeats a budget check silently + // rather than loudly. Pin the throw so a regression fails here. + for (const bad of [-1, -0.5, NaN, Infinity, -Infinity, undefined, null, '100']) { + if ( + test(`rejects ${String(bad)} as an input token count`, () => { + assert.throws(() => estimateCost('sonnet', bad, 100), RangeError); + }) + ) + passed++; + else failed++; + + if ( + test(`rejects ${String(bad)} as an output token count`, () => { + assert.throws(() => estimateCost('sonnet', 100, bad), RangeError); + }) + ) + passed++; + else failed++; + } + + if ( + test('accepts zero and fractional token counts', () => { + assert.strictEqual(estimateCost('sonnet', 0, 0), 0); + assert.strictEqual(estimateCost('sonnet', 500_000, 0), 1.5); + }) + ) + passed++; + else failed++; + // Summary console.log(`\nResults: ${passed} passed, ${failed} failed\n`); return { passed, failed };