mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-08 18:57:55 +02:00
Merge pull request #2866 from actus7/consolidate/cost-tracker-v3
fix(hooks): consolidate cost-tracker and pricing fixes (2 PRs)
This commit is contained in:
@@ -70,19 +70,37 @@ function readHarnessCost(sessionId, maxAgeSeconds) {
|
||||
|
||||
// Approximate per-1M-token billing rates (USD).
|
||||
// Cache creation: 1.25x input rate. Cache read: 0.1x input rate.
|
||||
// Source: https://platform.claude.com/docs/en/about-claude/pricing
|
||||
// Current-generation list prices: Fable/Mythos 5 $10/$50, Opus 5 and
|
||||
// Opus 4.5-4.8 $5/$25, Sonnet 5 $2/$10, Sonnet 4.6 $3/$15, and Haiku 4.5
|
||||
// $1/$5. Opus 4.0/4.1 and Opus 3 stay on the legacy $15/$75 tier.
|
||||
const RATE_TABLE = {
|
||||
haiku: { in: 0.80, out: 4.0, cacheWrite: 1.00, cacheRead: 0.08 },
|
||||
sonnet: { in: 3.00, out: 15.0, cacheWrite: 3.75, cacheRead: 0.30 },
|
||||
opus: { in: 15.00, out: 75.0, cacheWrite: 18.75, cacheRead: 1.50 }
|
||||
haiku: { in: 1.00, out: 5.0, cacheWrite: 1.25, cacheRead: 0.10 },
|
||||
sonnet: { in: 3.00, out: 15.0, cacheWrite: 3.75, cacheRead: 0.30 },
|
||||
sonnet5: { in: 2.00, out: 10.0, cacheWrite: 2.50, cacheRead: 0.20 },
|
||||
opus: { in: 5.00, out: 25.0, cacheWrite: 6.25, cacheRead: 0.50 },
|
||||
opusLegacy: { in: 15.00, out: 75.0, cacheWrite: 18.75, cacheRead: 1.50 },
|
||||
fable: { in: 10.00, out: 50.0, cacheWrite: 12.50, cacheRead: 1.00 }
|
||||
};
|
||||
|
||||
// Opus 4.0's dated snapshot omits the minor segment, so an `opus-4-0`
|
||||
// substring check alone misses `claude-opus-4-20250514`.
|
||||
const LEGACY_OPUS_RE = /3-opus|opus-4-0(?!\d)|opus-4-1(?!\d)|opus-4[-@]\d{8}/;
|
||||
|
||||
function getRates(model) {
|
||||
const m = String(model || '').toLowerCase();
|
||||
if (m.includes('fable') || m.includes('mythos')) return RATE_TABLE.fable;
|
||||
if (m.includes('haiku')) return RATE_TABLE.haiku;
|
||||
if (isSonnet5(m)) return RATE_TABLE.sonnet5;
|
||||
if (LEGACY_OPUS_RE.test(m)) return RATE_TABLE.opusLegacy;
|
||||
if (m.includes('opus')) return RATE_TABLE.opus;
|
||||
return RATE_TABLE.sonnet;
|
||||
}
|
||||
|
||||
function isSonnet5(model) {
|
||||
return /(?:^|[^a-z0-9])sonnet-5(?:[^a-z0-9]|$)/.test(model);
|
||||
}
|
||||
|
||||
function toNumber(v) {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
|
||||
@@ -43,9 +43,16 @@ function extractSessionSummary(transcriptPath) {
|
||||
if (entry.type === 'user' || entry.role === 'user' || entry.message?.role === 'user') {
|
||||
// Support both direct content and nested message.content (Claude Code JSONL format)
|
||||
const rawContent = entry.message?.content ?? entry.content;
|
||||
// Skip tool_result carrier turns — they are not user asks.
|
||||
const isToolResult = Array.isArray(rawContent) && rawContent.some(c => c && c.type === 'tool_result');
|
||||
const text = typeof rawContent === 'string' ? rawContent : Array.isArray(rawContent) ? rawContent.map(c => (c && c.text) || '').join(' ') : '';
|
||||
const cleaned = stripAnsi(text).trim();
|
||||
if (cleaned) {
|
||||
// Skip harness noise: local command echoes, caveats, system reminders.
|
||||
const isNoise = /^<(local-command-caveat|local-command-stdout|command-name|command-message|command-args|system-reminder|task-notification)/i.test(cleaned);
|
||||
// `isMeta` is also used for genuine channel- and plugin-originated
|
||||
// human prompts. Exclude known structured noise above instead of
|
||||
// discarding every metadata-marked user turn.
|
||||
if (cleaned && !isToolResult && !isNoise) {
|
||||
userMessages.push(cleaned.slice(0, 200));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Shared cost estimation for ECC hooks.
|
||||
*
|
||||
* Approximate per-1M-token blended rates (conservative defaults).
|
||||
*/
|
||||
|
||||
const RATE_TABLE = {
|
||||
haiku: { in: 0.8, out: 4.0 },
|
||||
sonnet: { in: 3.0, out: 15.0 },
|
||||
opus: { in: 15.0, out: 75.0 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Estimate USD cost from token counts.
|
||||
* @param {string} model - Model name (may contain "haiku", "sonnet", or "opus")
|
||||
* @param {number} inputTokens
|
||||
* @param {number} outputTokens
|
||||
* @returns {number} Estimated cost in USD (rounded to 6 decimal places)
|
||||
*/
|
||||
function estimateCost(model, inputTokens, outputTokens) {
|
||||
const normalized = String(model || '').toLowerCase();
|
||||
let rates = RATE_TABLE.sonnet;
|
||||
if (normalized.includes('haiku')) rates = RATE_TABLE.haiku;
|
||||
if (normalized.includes('opus')) rates = 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 };
|
||||
@@ -54,6 +54,49 @@ function runScript(input, envOverrides = {}) {
|
||||
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
|
||||
}
|
||||
|
||||
function removeHarnessCostCache(sessionId) {
|
||||
const cachePath = path.join(os.tmpdir(), `harness-cost-${sessionId}.json`);
|
||||
try {
|
||||
fs.unlinkSync(cachePath);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function assertSonnet5CacheCost(cacheUsage, expectedCost, description) {
|
||||
const tmpHome = makeTempDir();
|
||||
const sessionId = `sonnet5-${description}-${process.pid}-${Date.now()}`;
|
||||
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||
writeTranscript(transcriptPath, [{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
id: `msg_sonnet5_${description}`,
|
||||
model: 'claude-sonnet-5',
|
||||
usage: {
|
||||
input_tokens: 1_000_000,
|
||||
output_tokens: 1_000_000,
|
||||
...cacheUsage,
|
||||
},
|
||||
},
|
||||
}]);
|
||||
|
||||
try {
|
||||
removeHarnessCostCache(sessionId);
|
||||
const result = runScript(
|
||||
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||
withTempHome(tmpHome)
|
||||
);
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
|
||||
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||
assert.strictEqual(row.estimated_cost_usd, expectedCost, description);
|
||||
} finally {
|
||||
removeHarnessCostCache(sessionId);
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing cost-tracker.js ===\n');
|
||||
|
||||
@@ -297,7 +340,245 @@ function runTests() {
|
||||
}
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 9. Ignores stale harness-cost cache and falls back to transcript estimate
|
||||
// 9. Prices Sonnet 5 at the documented $2/$10 rate.
|
||||
(test('prices Sonnet 5 at $12 per 1M input + 1M output tokens', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const sessionId = `sonnet5-${process.pid}-${Date.now()}`;
|
||||
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||
writeTranscript(transcriptPath, [
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
id: 'msg_sonnet5',
|
||||
model: 'claude-sonnet-5',
|
||||
usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(os.tmpdir(), `harness-cost-${sessionId}.json`),
|
||||
JSON.stringify({ ts: Math.floor(Date.now() / 1000), cost_usd: 999 }),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
try {
|
||||
removeHarnessCostCache(sessionId);
|
||||
const result = runScript(
|
||||
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||
withTempHome(tmpHome)
|
||||
);
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
|
||||
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||
assert.strictEqual(row.estimated_cost_usd, 12, 'Expected Sonnet 5 1M/1M to cost $12.00');
|
||||
} finally {
|
||||
removeHarnessCostCache(sessionId);
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 9b. Sonnet 5 cache write/read tokens use the correct rates.
|
||||
(test('prices Sonnet 5 cache tokens at the documented rates', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const sessionId = `sonnet5-cache-${process.pid}-${Date.now()}`;
|
||||
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||
writeTranscript(transcriptPath, [
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
id: 'msg_sonnet5_cache',
|
||||
model: 'claude-sonnet-5',
|
||||
usage: {
|
||||
input_tokens: 1_000_000,
|
||||
output_tokens: 1_000_000,
|
||||
cache_creation_input_tokens: 1_000_000,
|
||||
cache_read_input_tokens: 1_000_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
removeHarnessCostCache(sessionId);
|
||||
const result = runScript(
|
||||
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||
withTempHome(tmpHome)
|
||||
);
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
|
||||
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||
assert.strictEqual(row.estimated_cost_usd, 14.7, 'Expected Sonnet 5 1M input + 1M output + 1M cache write + 1M cache read to cost $14.70');
|
||||
} finally {
|
||||
removeHarnessCostCache(sessionId);
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 9c. Cache write/read rates are independently covered.
|
||||
(test('prices Sonnet 5 cache writes at $2.50 per 1M tokens', () => {
|
||||
assertSonnet5CacheCost(
|
||||
{ cache_creation_input_tokens: 1_000_000 },
|
||||
14.5,
|
||||
'cache-write'
|
||||
);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('prices Sonnet 5 cache reads at $0.20 per 1M tokens', () => {
|
||||
assertSonnet5CacheCost(
|
||||
{ cache_read_input_tokens: 1_000_000 },
|
||||
12.2,
|
||||
'cache-read'
|
||||
);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 10. Sonnet 4.6 keeps the existing $3/$15 rate and is not mistaken for Sonnet 5.
|
||||
(test('prices Sonnet 4.6 at $18 per 1M input + 1M output tokens', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const sessionId = `sonnet46-${process.pid}-${Date.now()}`;
|
||||
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||
writeTranscript(transcriptPath, [
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
id: 'msg_sonnet46',
|
||||
model: 'claude-sonnet-4-6',
|
||||
usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
removeHarnessCostCache(sessionId);
|
||||
const result = runScript(
|
||||
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||
withTempHome(tmpHome)
|
||||
);
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
|
||||
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||
assert.strictEqual(row.estimated_cost_usd, 18, 'Expected Sonnet 4.6 1M/1M to remain $18.00');
|
||||
} finally {
|
||||
removeHarnessCostCache(sessionId);
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 10b. Dated Sonnet 5 IDs are matched correctly.
|
||||
(test('prices dated Sonnet 5 IDs at $12', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const sessionId = `sonnet5-dated-${process.pid}-${Date.now()}`;
|
||||
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||
writeTranscript(transcriptPath, [
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
id: 'msg_sonnet5_dated',
|
||||
model: 'claude-sonnet-5-20261001',
|
||||
usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
removeHarnessCostCache(sessionId);
|
||||
const result = runScript(
|
||||
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||
withTempHome(tmpHome)
|
||||
);
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
|
||||
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||
assert.strictEqual(row.estimated_cost_usd, 12, 'Expected dated Sonnet 5 1M/1M to cost $12.00');
|
||||
} finally {
|
||||
removeHarnessCostCache(sessionId);
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 10c. Near-miss Sonnet 5 IDs fall back to standard Sonnet rates.
|
||||
(test('rejects claude-sonnet-50 as a Sonnet 5 near-miss', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const sessionId = `sonnet50-near-miss-${process.pid}-${Date.now()}`;
|
||||
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||
writeTranscript(transcriptPath, [
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
id: 'msg_sonnet50',
|
||||
model: 'claude-sonnet-50',
|
||||
usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
removeHarnessCostCache(sessionId);
|
||||
const result = runScript(
|
||||
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||
withTempHome(tmpHome)
|
||||
);
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
|
||||
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||
assert.strictEqual(row.estimated_cost_usd, 18, 'Expected claude-sonnet-50 near-miss to fall back to $18.00 Sonnet rate');
|
||||
} finally {
|
||||
removeHarnessCostCache(sessionId);
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 10d. Opus 4.0's dated ID has no explicit minor segment. It must retain
|
||||
// the legacy $15/$75 rate while Opus 4.5 uses the current $5/$25 rate.
|
||||
(test('distinguishes the dated Opus 4.0 snapshot from current Opus 4.x', () => {
|
||||
const priceModel = model => {
|
||||
const tmpHome = makeTempDir();
|
||||
const sessionId = `opus-rate-${process.pid}-${Date.now()}-${model}`;
|
||||
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||
writeTranscript(transcriptPath, [
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
id: `msg_${model}`,
|
||||
model,
|
||||
usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
removeHarnessCostCache(sessionId);
|
||||
const result = runScript(
|
||||
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||
withTempHome(tmpHome)
|
||||
);
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||
return JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()).estimated_cost_usd;
|
||||
} finally {
|
||||
removeHarnessCostCache(sessionId);
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
assert.strictEqual(
|
||||
priceModel('claude-opus-4-20250514'),
|
||||
90,
|
||||
'Expected dated Opus 4.0 to retain the legacy $15/$75 rate'
|
||||
);
|
||||
assert.strictEqual(
|
||||
priceModel('claude-opus-4-5-20251101'),
|
||||
30,
|
||||
'Expected Opus 4.5 to use the current $5/$25 rate'
|
||||
);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 11. Ignores stale harness-cost cache and falls back to transcript estimate
|
||||
(test('ignores stale harness-cost cache (>300s) and uses transcript estimate', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const sessionId = 'harness-stale-' + Date.now();
|
||||
|
||||
@@ -2118,6 +2118,41 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await asyncTest('keeps isMeta human prompts while filtering structured transcript noise', async () => {
|
||||
const testDir = createTestDir();
|
||||
const transcriptPath = path.join(testDir, 'transcript.jsonl');
|
||||
const lines = [
|
||||
JSON.stringify({ type: 'user', isMeta: true, content: 'Prompt delivered by a channel plugin' }),
|
||||
JSON.stringify({ type: 'user', isMeta: true, content: '<system-reminder>internal harness context</system-reminder>' }),
|
||||
JSON.stringify({
|
||||
type: 'user',
|
||||
message: { role: 'user', content: [{ type: 'tool_result', content: 'tool output' }] },
|
||||
}),
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, lines.join('\n'));
|
||||
|
||||
const result = await runScript(
|
||||
path.join(scriptsDir, 'session-end.js'),
|
||||
JSON.stringify({ transcript_path: transcriptPath }),
|
||||
{ HOME: testDir, USERPROFILE: testDir }
|
||||
);
|
||||
assert.strictEqual(result.code, 0);
|
||||
|
||||
const sessionsDir = getCanonicalSessionsDir(testDir);
|
||||
const sessionFiles = fs.readdirSync(sessionsDir).filter(file => file.endsWith('.tmp'));
|
||||
assert.strictEqual(sessionFiles.length, 1, 'Should create one session file');
|
||||
const content = fs.readFileSync(path.join(sessionsDir, sessionFiles[0]), 'utf8');
|
||||
assert.ok(content.includes('Prompt delivered by a channel plugin'));
|
||||
assert.ok(!content.includes('internal harness context'));
|
||||
assert.ok(!content.includes('tool output'));
|
||||
assert.ok(content.includes('Total user messages: 1'));
|
||||
cleanupTestDir(testDir);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await asyncTest('extracts tool names and file paths from transcript', async () => {
|
||||
const testDir = createTestDir();
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* 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 haiku, sonnet, opus keys', () => {
|
||||
assert.ok(RATE_TABLE.haiku, 'Missing haiku');
|
||||
assert.ok(RATE_TABLE.sonnet, 'Missing sonnet');
|
||||
assert.ok(RATE_TABLE.opus, 'Missing opus');
|
||||
assert.strictEqual(typeof RATE_TABLE.haiku.in, 'number');
|
||||
assert.strictEqual(typeof RATE_TABLE.haiku.out, 'number');
|
||||
assert.strictEqual(typeof RATE_TABLE.sonnet.in, 'number');
|
||||
assert.strictEqual(typeof RATE_TABLE.sonnet.out, 'number');
|
||||
assert.strictEqual(typeof RATE_TABLE.opus.in, 'number');
|
||||
assert.strictEqual(typeof RATE_TABLE.opus.out, 'number');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
// estimateCost tests
|
||||
console.log('\nestimateCost:');
|
||||
|
||||
if (
|
||||
test('opus 1M/1M tokens returns 90', () => {
|
||||
const cost = estimateCost('opus', 1_000_000, 1_000_000);
|
||||
assert.strictEqual(cost, 90);
|
||||
})
|
||||
)
|
||||
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 4.8', () => {
|
||||
const cost = estimateCost('haiku', 1_000_000, 1_000_000);
|
||||
assert.strictEqual(cost, 4.8);
|
||||
})
|
||||
)
|
||||
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 opus rates', () => {
|
||||
const cost = estimateCost('claude-opus-4-6', 500, 200);
|
||||
// (500 / 1_000_000) * 15 + (200 / 1_000_000) * 75 = 0.0075 + 0.015 = 0.0225
|
||||
const expected = Math.round(0.0225 * 1e6) / 1e6;
|
||||
assert.strictEqual(cost, expected);
|
||||
})
|
||||
)
|
||||
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);
|
||||
Reference in New Issue
Block a user