fix(models): price claude-haiku-4.5 copilot session-store rows (#1093)

The Copilot session-store.db writes the model as claude-haiku-4.5
(tier-first, dot), but no pricing alias existed for that raw id, so
calculateCost returned $0 while GitHub billed the real Anthropic rate
(kelchm's #946 validation: $0.0063536 for a sampled request). Aliases
it to the existing, correctly-priced claude-haiku-4-5 snapshot row --
no new rate invented. Unlike #1090's gpt-5.6-codex case, this id has
no prefix-fallback self-heal, so the daily cache bumps v26 -> v27 to
force re-derivation of already-finalized days.

Drafted with cline-pass/deepseek-v4-pro via local gateway.
This commit is contained in:
iamtoruk 2026-08-22 11:41:08 -07:00
parent 9cfa957e3e
commit fa89015947
5 changed files with 24 additions and 3 deletions

View file

@ -81,6 +81,7 @@
- `codeburn sync push --attribution` (opt-in): sends git attribution spans — the session→commit correlation from `codeburn yield` (`codeburn.session.attribution` and `codeburn.commit` span types with normalized repo remote, commit SHAs, merged/reverted state, and PR links). Nothing new is sent without the flag; local-only repos and Windows filesystem paths are never emitted as repo identities, and sessions whose project path no longer resolves never inherit the push-time working directory's repo. See docs/sync/README.md "Git attribution".
### Fixed (CLI)
- **Copilot claude-haiku-4.5 store rows now price correctly instead of $0.** The Copilot session-store.db writes the model as `claude-haiku-4.5` (tier-first, dot), but no pricing alias existed for that raw id, so `calculateCost` returned $0 while GitHub billed the real Anthropic rate (e.g., $0.0063536 for a sampled request). Added an alias to the existing correctly-priced `claude-haiku-4-5` row (no new rate invented). The daily cache bumps from v26 to v27 to force re-derivation of already-finalized days, since this id has no self-heal via prefix fallback. (#1093)
- **A pull request no longer swallows a whole repo's spend.** The working-directory correlation rule attributed every session sharing a checkout with a PR-linked session, with no time bound — a repo whose only captured PR link was pasted once attributed a month of unrelated work (129 of 131 sessions on real data) to that PR. Checkout evidence now only attributes sessions overlapping the linked sessions' own activity window. (#961)
- **Shell reads finally count as reads.** `rg`, `grep`, `cat`, `git log` and friends were invisible to the read-edit-ratio detector (90%+ of real reads uncounted on bash-first workflows) while every Bash call counted as a verification step, so `edit → grep → edit` scored as rework. One shared read-shaped-command classifier fixes both detectors; unknown or mutating commands keep the old behavior. (#941, thanks @laulpogan)
- **Phantom corrections from injected skill prose.** The user-correction detector matched "the wrong answer" inside a templated skill prompt, counting the same non-correction four times on real data; "answer" left the wrong-<noun> pattern list, concrete artifacts (wrong file, wrong approach) still count. (#952)

View file

@ -33,7 +33,7 @@ const WORK = process.env['UPGRADE_PATH_WORK'] || join(tmpdir(), 'codeburn upgrad
const OLD_SESSION_CACHE = 'session-cache.v7.json'
const OLD_DAILY_CACHE = 'daily-cache.v17.json'
const NEW_SESSION_CACHE_DIR = 'session-cache.v9'
const NEW_DAILY_CACHE = 'daily-cache.v26.json'
const NEW_DAILY_CACHE = 'daily-cache.v27.json'
const HOME = join(WORK, 'user home')
const PAYLOADS = join(WORK, 'payloads')

View file

@ -6,6 +6,10 @@ import { join } from 'path'
import { getCodeburnCacheDir } from './cache-dir.js'
import type { DateRange, ProjectSummary } from './types.js'
// Bumped to 27: claude-haiku-4.5 copilot store rows now price correctly (alias added) — #1093.
// Previously the raw id 'claude-haiku-4.5' (tier-first, dot) from session-store.db had no
// pricing alias, so days finalized with this model were stuck at $0. No self-heal via prefix
// fallback exists for this id, so a version bump is required to force re-derivation.
// Bumped to 26: copilot input/cache tokens for sessions covered by the CLI's
// session-store.db move from one shutdown-rollup lump (stamped at session end)
// to per-request DB rows with real timestamps, supplementary accounting calls
@ -163,8 +167,9 @@ import type { DateRange, ProjectSummary } from './types.js'
// everyone, which is a lossless no-op for days already correct.
// v25: #1047 activity-id pricing. v24 on main already shipped #1090.
// v26: #946 copilot session-store accounting (see the top of this ladder).
export const DAILY_CACHE_VERSION = 26
const MIN_SUPPORTED_VERSION = 26
// v27: #1093 claude-haiku-4.5 alias (see top).
export const DAILY_CACHE_VERSION = 27
const MIN_SUPPORTED_VERSION = 27
/// Providers whose per-day CALL COUNT means something different at
/// DAILY_CACHE_VERSION 26 than it did before it. Copilot's supplementary

View file

@ -298,6 +298,8 @@ const BUILTIN_ALIASES: Record<string, string> = {
'anthropic--claude-4.5-opus': 'claude-opus-4-5',
'anthropic--claude-4.5-sonnet': 'claude-sonnet-4-5',
'anthropic--claude-4.5-haiku': 'claude-haiku-4-5',
// #1093: copilot session-store.db writes 'claude-haiku-4.5' (tier-first, dot)
'claude-haiku-4.5': 'claude-haiku-4-5',
'claude-sonnet-4.6': 'claude-sonnet-4-6',
'claude-sonnet-4.5': 'claude-sonnet-4-5',
'claude-opus-4.7': 'claude-opus-4-7',

View file

@ -117,6 +117,19 @@ describe('getModelCosts', () => {
expect(calculateCost('gpt-5.6-codex-max', 1_000_000, 1_000_000, 0, 0, 0)).toBeGreaterThan(0)
})
it('prices claude-haiku-4.5 (copilot session-store raw id), aliased to the existing claude-haiku-4-5 row (#1093)', () => {
const haiku45 = getModelCosts('claude-haiku-4.5')
const haiku45Dash = getModelCosts('claude-haiku-4-5')
expect(haiku45).not.toBeNull()
expect(haiku45).toEqual(haiku45Dash)
expect(haiku45!.inputCostPerToken).toBe(1e-6)
expect(haiku45!.outputCostPerToken).toBe(5e-6)
expect(haiku45!.cacheWriteCostPerToken).toBe(1.25e-6)
expect(haiku45!.cacheReadCostPerToken).toBe(1e-7)
expect(haiku45!.cacheWriteCostIsExplicit).toBe(true)
expect(calculateCost('claude-haiku-4.5', 1_000_000, 1_000_000, 0, 0, 0)).toBe(6)
})
// A price override on a synthetic bare id can only be reached if the leading
// segment was stripped, so these assert the namespace allowlist itself without
// pinning to any real model's presence in (or absence from) the snapshot.