diff --git a/scripts/hooks/cost-tracker.js b/scripts/hooks/cost-tracker.js index 3de1eaaec..fa3677e4f 100755 --- a/scripts/hooks/cost-tracker.js +++ b/scripts/hooks/cost-tracker.js @@ -70,15 +70,22 @@ function readHarnessCost(sessionId, maxAgeSeconds) { // Approximate per-1M-token billing rates (USD). // Cache creation: 1.25x input rate. Cache read: 0.1x input rate. +// Current-generation list prices: Fable/Mythos 5 $10/$50, Opus 5 and +// Opus 4.5-4.8 $5/$25, Sonnet 5/4.6 $3/$15, 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 }, + 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 } }; 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 (m.includes('opus-4-1') || m.includes('opus-4-0') || m.includes('3-opus')) return RATE_TABLE.opusLegacy; if (m.includes('opus')) return RATE_TABLE.opus; return RATE_TABLE.sonnet; } diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index c224371aa..60ba74720 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -43,9 +43,13 @@ 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); + if (cleaned && !isToolResult && !isNoise && !entry.isMeta) { userMessages.push(cleaned.slice(0, 200)); } }