mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-29 11:19:39 +02:00
`estimateCost` used `inputTokens` and `outputTokens` without validation. A negative count yielded a negative cost, and a non-finite one yielded NaN. That matters here specifically because this module backs the cost-aware-llm-pipeline budget skill: `NaN > budget` is false, so a corrupt token count silently passes the budget check it exists to enforce. It now throws a RangeError naming the offending field. The unknown-model fallback to sonnet rates deliberately stays fail-open: that degrades an estimate, whereas these inputs corrupt one. Separately, the estimator prices Fable and Mythos at $10/$50 per million tokens, but all three pricing tables (the skill and its ja-JP and zh-CN translations) listed only Haiku, Sonnet, and Opus, so a reader could not budget for two supported model families. Added the missing row to each. 16 of the new assertions fail against the previous behaviour (23 passed, 16 failed) and all pass with the guard (39 passed). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KV43bDJpgyMPovT2CHAoba
84 lines
3.9 KiB
JavaScript
84 lines
3.9 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Shared cost estimation for ECC hooks.
|
|
*
|
|
* Published per-1M-token rates. Input and output only: callers pass two token
|
|
* counts, so cache tiers are out of scope here.
|
|
*/
|
|
|
|
// The previous table priced every `opus` model at $15/$75, which are Claude 3
|
|
// Opus era rates. Opus 4.5 and later bill at $5/$25, so every current-
|
|
// generation Opus estimate was exactly 3x real spend. `haiku` was $0.80/$4.00,
|
|
// which is Claude 3.5 Haiku: Haiku 4.5 bills at $1/$5, so Haiku was understated
|
|
// 1.25x. Fable and Mythos had no bucket at all and fell through to `sonnet`,
|
|
// understating them 3.3x.
|
|
//
|
|
// The legacy rows exist so that correcting the current generation does not
|
|
// reprice the old one. Claude 3 Haiku ($0.25/$1.25) is deliberately not
|
|
// modelled: Claude Code never ran it.
|
|
const RATE_TABLE = {
|
|
haiku: { in: 1.0, out: 5.0 },
|
|
haikuLegacy: { in: 0.8, out: 4.0 },
|
|
sonnet: { in: 3.0, out: 15.0 },
|
|
opus: { in: 5.0, out: 25.0 },
|
|
opusLegacy: { in: 15.0, out: 75.0 },
|
|
fable: { in: 10.0, out: 50.0 }
|
|
};
|
|
|
|
// The only Opus models that really billed at $15/$75: Claude 3 Opus, Opus 4.0
|
|
// and Opus 4.1. Every spelling of each has to match, alias and dated snapshot
|
|
// alike, which is why the bare `opus-4-<date>` form is listed on its own:
|
|
// Opus 4.0's snapshot is `claude-opus-4-20250514`, with no minor segment, so
|
|
// an `opus-4-0` substring alone misses it and reprices a legacy estimate at a
|
|
// third of its real cost. The `[-@]` covers Vertex AI, which joins the date
|
|
// with `@` (`claude-opus-4@20250514`); Bedrock's
|
|
// `anthropic.claude-3-opus-20240229-v1:0` is caught by the first alternative.
|
|
//
|
|
// Opus 4.5 through Opus 5 are $5/$25 and take the default bucket, which also
|
|
// means a future Opus is assumed to be $5/$25. That assumption is the same
|
|
// shape of silent staleness this change fixes, so if Opus is ever repriced
|
|
// again, a new row belongs here rather than a rediscovery of this comment.
|
|
const LEGACY_OPUS_RE = /claude-3-opus|opus-4-0(?!\d)|opus-4-1(?!\d)|opus-4[-@]\d{8}/;
|
|
|
|
// Claude 3.5 Haiku, whose $0.80/$4.00 this table used to apply to all Haiku.
|
|
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 - 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')) {
|
|
rates = LEGACY_HAIKU_RE.test(normalized) ? RATE_TABLE.haikuLegacy : RATE_TABLE.haiku;
|
|
} else if (normalized.includes('fable') || normalized.includes('mythos')) {
|
|
rates = RATE_TABLE.fable;
|
|
} else if (normalized.includes('opus')) {
|
|
rates = LEGACY_OPUS_RE.test(normalized) ? RATE_TABLE.opusLegacy : RATE_TABLE.opus;
|
|
}
|
|
|
|
const cost = (inputTokens / 1_000_000) * rates.in + (outputTokens / 1_000_000) * rates.out;
|
|
return Math.round(cost * 1e6) / 1e6;
|
|
}
|
|
|
|
module.exports = { estimateCost, RATE_TABLE };
|