mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-13 21:27:54 +02:00
`scripts/lib/cost-estimate.js` carries a second, independent copy of the rate table that `scripts/hooks/cost-tracker.js` had, with the same defect: every `opus` model priced 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. Two more errors in the same table: `haiku` was $0.80/$4.00, which is Claude 3.5 Haiku rather than Haiku 4.5's $1/$5; and Fable and Mythos had no bucket at all, so they fell through to `sonnet` and were understated 3.3x. Legacy buckets are added rather than overwriting, so correcting the current generation does not reprice the old one. `opusLegacy` keeps $15/$75 for the three models that really billed it (Claude 3 Opus, Opus 4.0, Opus 4.1) and `haikuLegacy` keeps $0.80/$4.00 for Claude 3.5 Haiku. The matching regexes are the ones already used by the cost tracker, so the two tables now agree on which model is legacy. Opus 4.0's snapshot is `claude-opus-4-20250514` with no minor segment, which is why the bare `opus-4-<date>` form is matched separately: an `opus-4-0` substring alone misses it. `skills/cost-aware-llm-pipeline/SKILL.md` and its zh-CN and ja-JP translations published the same stale numbers as prose. The Relative Cost column is derived from the rates, so it is corrected with them: against a corrected Haiku 4.5 baseline the multiples are now exact, which is why the approximation markers are dropped. `RATE_TABLE` keeps its existing keys and shape, so the export stays backward compatible. Tests: the existing test pinned the stale values and was updated to pin the correct ones. Coverage is added for each legacy spelling (alias, dated snapshot, Vertex `@` form, Bedrock prefix), for the current Opus line, and for the Fable and Mythos bucket.
182 lines
4.8 KiB
JavaScript
182 lines
4.8 KiB
JavaScript
/**
|
|
* Tests for scripts/lib/cost-estimate.js
|
|
*
|
|
* Run with: node tests/lib/cost-estimate.test.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
|
|
const { estimateCost, RATE_TABLE } = require('../../scripts/lib/cost-estimate');
|
|
|
|
// Test helper
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` \u2713 ${name}`);
|
|
return true;
|
|
} catch (err) {
|
|
console.log(` \u2717 ${name}`);
|
|
console.log(` Error: ${err.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing cost-estimate.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
// RATE_TABLE structure
|
|
console.log('RATE_TABLE:');
|
|
|
|
if (
|
|
test('RATE_TABLE has a bucket per billing tier', () => {
|
|
for (const key of ['haiku', 'haikuLegacy', 'sonnet', 'opus', 'opusLegacy', 'fable']) {
|
|
assert.ok(RATE_TABLE[key], `Missing ${key}`);
|
|
assert.strictEqual(typeof RATE_TABLE[key].in, 'number', `${key}.in not a number`);
|
|
assert.strictEqual(typeof RATE_TABLE[key].out, 'number', `${key}.out not a number`);
|
|
}
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
if (
|
|
test('RATE_TABLE carries current published rates, not Claude 3 era rates', () => {
|
|
assert.deepStrictEqual(RATE_TABLE.opus, { in: 5.0, out: 25.0 });
|
|
assert.deepStrictEqual(RATE_TABLE.opusLegacy, { in: 15.0, out: 75.0 });
|
|
assert.deepStrictEqual(RATE_TABLE.haiku, { in: 1.0, out: 5.0 });
|
|
assert.deepStrictEqual(RATE_TABLE.haikuLegacy, { in: 0.8, out: 4.0 });
|
|
assert.deepStrictEqual(RATE_TABLE.sonnet, { in: 3.0, out: 15.0 });
|
|
assert.deepStrictEqual(RATE_TABLE.fable, { in: 10.0, out: 50.0 });
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
// estimateCost tests
|
|
console.log('\nestimateCost:');
|
|
|
|
if (
|
|
test('opus 1M/1M tokens returns 30', () => {
|
|
const cost = estimateCost('opus', 1_000_000, 1_000_000);
|
|
assert.strictEqual(cost, 30);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
if (
|
|
test('sonnet 1M/1M tokens returns 18', () => {
|
|
const cost = estimateCost('sonnet', 1_000_000, 1_000_000);
|
|
assert.strictEqual(cost, 18);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
if (
|
|
test('haiku 1M/1M tokens returns 6', () => {
|
|
const cost = estimateCost('haiku', 1_000_000, 1_000_000);
|
|
assert.strictEqual(cost, 6);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
if (
|
|
test('null model with 0 tokens returns 0', () => {
|
|
const cost = estimateCost(null, 0, 0);
|
|
assert.strictEqual(cost, 0);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
if (
|
|
test('full model name claude-opus-4-6 uses current opus rates', () => {
|
|
const cost = estimateCost('claude-opus-4-6', 500, 200);
|
|
// (500 / 1_000_000) * 5 + (200 / 1_000_000) * 25 = 0.0025 + 0.005 = 0.0075
|
|
const expected = Math.round(0.0075 * 1e6) / 1e6;
|
|
assert.strictEqual(cost, expected);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
// Every spelling of the three Opus models that really billed at $15/$75 has
|
|
// to keep doing so. Opus 4.0's snapshot is `claude-opus-4-20250514`, with no
|
|
// minor segment, so a bare `opus-4-0` substring misses it.
|
|
for (const legacy of [
|
|
'claude-3-opus-20240229',
|
|
'anthropic.claude-3-opus-20240229-v1:0',
|
|
'claude-opus-4-20250514',
|
|
'claude-opus-4@20250514',
|
|
'claude-opus-4-0',
|
|
'claude-opus-4-1'
|
|
]) {
|
|
if (
|
|
test(`${legacy} keeps legacy opus rates`, () => {
|
|
assert.strictEqual(estimateCost(legacy, 1_000_000, 1_000_000), 90);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
}
|
|
|
|
for (const current of ['claude-opus-4-5', 'claude-opus-4-7', 'claude-opus-4-8', 'claude-opus-5']) {
|
|
if (
|
|
test(`${current} uses current opus rates`, () => {
|
|
assert.strictEqual(estimateCost(current, 1_000_000, 1_000_000), 30);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
}
|
|
|
|
if (
|
|
test('claude-3-5-haiku keeps legacy haiku rates', () => {
|
|
assert.strictEqual(estimateCost('claude-3-5-haiku-20241022', 1_000_000, 1_000_000), 4.8);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
if (
|
|
test('claude-haiku-4-5 uses current haiku rates', () => {
|
|
assert.strictEqual(estimateCost('claude-haiku-4-5-20251001', 1_000_000, 1_000_000), 6);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
// Fable and Mythos had no bucket at all and fell through to sonnet, which
|
|
// understated them 3.3x.
|
|
for (const model of ['claude-fable-5', 'claude-mythos-5']) {
|
|
if (
|
|
test(`${model} uses fable rates`, () => {
|
|
assert.strictEqual(estimateCost(model, 1_000_000, 1_000_000), 60);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
}
|
|
|
|
if (
|
|
test('unknown model falls back to sonnet rates', () => {
|
|
const cost = estimateCost('unknown-model', 1_000_000, 1_000_000);
|
|
assert.strictEqual(cost, 18);
|
|
})
|
|
)
|
|
passed++;
|
|
else failed++;
|
|
|
|
// Summary
|
|
console.log(`\nResults: ${passed} passed, ${failed} failed\n`);
|
|
return { passed, failed };
|
|
}
|
|
|
|
const { failed } = runTests();
|
|
process.exit(failed > 0 ? 1 : 0);
|