feat: persist Record.speed to messages table (close fast-mode SQLite gap)#48
Merged
0bserver07 merged 1 commit intomainfrom May 1, 2026
Merged
feat: persist Record.speed to messages table (close fast-mode SQLite gap)#480bserver07 merged 1 commit intomainfrom
0bserver07 merged 1 commit intomainfrom
Conversation
…gap) PR #44 added Anthropic Opus priority/fast tier (service_tier="priority") detection in the in-process pipeline (Record.speed, compute_cost speed=..., aggregator collectors keyed by (model, speed)) but the SQLite messages table had no speed column, so every SQL-driven cost path silently re-billed fast records at the standard 1x rate. Verified gap with a synthetic Opus session containing one priority + one standard message of identical token counts: SQL-driven cost returned $0.1050 (both billed standard) when it should have returned $0.3675 (fast slice 6x multiplied) — a 3.5x understatement on the 50/50 split, 6x for pure-fast sessions. Changes: - New migration v003_messages_speed.sql adds messages.speed TEXT NOT NULL DEFAULT 'standard'. Existing rows backfill to 'standard' via the DEFAULT (the conservative direction — under-charging a priority record at standard is the bug we're fixing; over-charging would be worse). - schema.apply guards ALTER TABLE migrations with PRAGMA table_info so a partial-application state (column added by hand or previous run crashed before bumping user_version) recovers cleanly. CURRENT_VERSION = 3. - ingest/writer.py binds rec.speed into the new column. - store/queries.get_global_stats groups by (day, model, speed) and threads speed= into compute_cost. cross_project_daily_totals appends speed to the result tuple. - services/compare._fetch_messages, services/yield_tracker, reports/export (_load_messages_grouped + _models_from_messages), reports/aggregate, and routes/commands._interaction_to_command all bucket by (model, speed). - store/types.MessageRow gains speed: str = "standard". Tests: 12 new across test_migration_v003.py (column shape, backfill, idempotent re-apply, version bump), test_queries.py (speed-aware get_global_stats arithmetic, standard-only no-regression, Sonnet-fast no-multiplier, cross_project_daily_totals carries speed), and test_fast_mode_end_to_end.py (full adapter→writer→DB→query round-trip on a synthetic Claude JSONL with service_tier="priority"). 1127 passed, 2 skipped (1115 baseline + 12 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR #44 added Anthropic Opus priority/fast tier (
service_tier=\"priority\") detection in the in-process pipeline (Record.speed,compute_cost(..., speed=...), aggregator collectors keyed by(model, speed)) but the SQLitemessagestable had nospeedcolumn, so every SQL-driven cost path silently re-billed fast records at the standard 1× rate. This PR closes that gap.Verified gap
Synthetic Opus session with one priority + one standard message of identical token counts (1000 in, 500 out each):
A 3.5× understatement on the 50/50 split. Pure-fast sessions were under-reported by 6×.
Changes
v003_messages_speed.sqladdsmessages.speed TEXT NOT NULL DEFAULT 'standard'. Existing rows backfill to'standard'via the DEFAULT (the conservative direction — under-charging a priority record at standard is the bug we're fixing; over-charging would be worse).schema.applyis now reentrant for ALTER TABLE migrations — guards viaPRAGMA table_info(messages)so a partial-application state (column added by hand, or a previous run crashed before bumpinguser_version) recovers cleanly.CURRENT_VERSION = 3.stackunderflow/ingest/writer.py) bindsrec.speedinto the new column.(model, speed)and threadspeed=intocompute_cost:store/queries.get_global_statsandcross_project_daily_totalsservices/compare._fetch_messagesservices/yield_tracker._compute_cost_for_sessionreports/export._load_messages_grouped+_models_from_messagesreports/aggregate.build_reportroutes/commands._interaction_to_command(mixed-tier sessions)MessageRowtyped dataclass gainsspeed: str = \"standard\".Test plan
pytest tests/ -q→ 1127 passed, 2 skipped (1115 baseline + 12 new)ruff checkclean on all modified filesClaudeAdapteringests synthetic JSONL withservice_tier=\"priority\", the speed column round-trips,get_global_statsreports the 6× multiplied costschema.apply()on a DB where thespeedcolumn already exists is a no-op and still bumpsPRAGMA user_versionto 312 new tests across:
tests/stackunderflow/store/test_migration_v003.py(5: column shape, default backfill, idempotent re-apply, version bump, default-on-bare-INSERT)tests/stackunderflow/store/test_queries.py(4: fast-mode arithmetic, standard-only no-regression, Sonnet-no-multiplier,cross_project_daily_totalscarries speed)tests/stackunderflow/ingest/test_fast_mode_end_to_end.py(3: adapter→writer→DB→query round-trip, theget_global_statscost story,get_project_statsfull-pipeline cost story)Completes the fast-mode work end-to-end through the dashboard.