nilometer 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/LICENSE +21 -0
- package/README.md +462 -2
- package/core/pricing/prices.json +348 -0
- package/core/schema/001_ingestion.sql +57 -0
- package/core/schema/002_line_count.sql +7 -0
- package/core/schema/003_derived.sql +70 -0
- package/core/schema/004_requests_dedup.sql +50 -0
- package/core/schema/005_repositories.sql +25 -0
- package/core/schema/006_status_readings.sql +33 -0
- package/core/schema/007_pricing.sql +95 -0
- package/core/schema/008_interruptions.sql +263 -0
- package/core/schema/009_headroom.sql +84 -0
- package/core/schema/010_attribution.sql +106 -0
- package/core/schema/011_projections.sql +83 -0
- package/core/schema/012_unsplit_cache_writes.sql +49 -0
- package/core/schema/013_repository_revalidation.sql +104 -0
- package/core/schema/014_observed_readings.sql +204 -0
- package/core/schema/015_response_observations.sql +127 -0
- package/core/schema/016_windows_repository_paths.sql +41 -0
- package/core/schema/017_unattributed_usage_once.sql +47 -0
- package/core/schema/018_request_time_index.sql +21 -0
- package/core/schema/019_dedup_earliest_of_equals.sql +61 -0
- package/core/schema/020_limit_quota_fields.sql +131 -0
- package/core/schema/021_plan_periods.sql +121 -0
- package/core/schema/022_attachment_walk.sql +223 -0
- package/core/schema/023_auto_continuations.sql +129 -0
- package/core/schema/024_counted_limit_groups.sql +29 -0
- package/core/schema/025_model_fallback.sql +77 -0
- package/core/schema/026_changed_readings.sql +106 -0
- package/core/schema/027_weekly_crossings.sql +32 -0
- package/core/schema/028_stale_readings.sql +196 -0
- package/core/schema/029_lockout_sessions.sql +68 -0
- package/core/schema/030_repository_aliases.sql +81 -0
- package/core/schema/031_cte_cursor_budget.sql +153 -0
- package/dist/cli/main.cli.js +39 -0
- package/dist/cli/program.js +801 -0
- package/dist/core/db/database.js +182 -0
- package/dist/core/db/fingerprint.js +71 -0
- package/dist/core/ingest/attribution.js +131 -0
- package/dist/core/ingest/classify.js +382 -0
- package/dist/core/ingest/command.js +95 -0
- package/dist/core/ingest/derive.js +212 -0
- package/dist/core/ingest/discover.js +126 -0
- package/dist/core/ingest/ingest.js +160 -0
- package/dist/core/ingest/reader.js +89 -0
- package/dist/core/ingest/reset-time.js +97 -0
- package/dist/core/ingest/results.js +201 -0
- package/dist/core/ingest/spool.js +165 -0
- package/dist/core/ingest/store.js +99 -0
- package/dist/core/install/install.js +185 -0
- package/dist/core/install/locations.js +89 -0
- package/dist/core/install/private-files.js +259 -0
- package/dist/core/install/record.js +103 -0
- package/dist/core/plans/plan-prices.js +128 -0
- package/dist/core/plans/repository-aliases.js +136 -0
- package/dist/core/pricing/prices.js +147 -0
- package/dist/core/settings/settings-file.js +215 -0
- package/dist/core/settings/statusline.js +240 -0
- package/dist/core/util/commands.js +91 -0
- package/dist/core/util/node-version.js +58 -0
- package/dist/core/util/open-page.js +69 -0
- package/dist/core/util/paths.js +36 -0
- package/dist/core/verify/compare.js +77 -0
- package/dist/core/verify/verify.js +220 -0
- package/dist/viewer/explain.js +574 -0
- package/dist/viewer/format.js +150 -0
- package/dist/viewer/html-charts.js +1292 -0
- package/dist/viewer/html-contract.js +2 -0
- package/dist/viewer/html-data.js +718 -0
- package/dist/viewer/html.js +1654 -0
- package/dist/viewer/queries.js +128 -0
- package/dist/viewer/render.js +602 -0
- package/dist/viewer/report.js +120 -0
- package/dist/viewer/save.js +110 -0
- package/docs/requirements.md +113 -0
- package/hooks/statusline.sh +104 -0
- package/package.json +61 -4
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- 007_pricing: dated API list prices and the per-request cost view (D-005, D-006, D-020).
|
|
2
|
+
--
|
|
3
|
+
-- prices is replaced from core/pricing/prices.json on every ingest. request_costs prices each
|
|
4
|
+
-- deduplicated request at the row in effect on its UTC day, in SQL, from unrounded values. The
|
|
5
|
+
-- displayed label for these numbers is "observed tokens at API list price" (README principle 3):
|
|
6
|
+
-- a projection, never "what you would have spent".
|
|
7
|
+
|
|
8
|
+
CREATE TABLE prices (
|
|
9
|
+
model_id TEXT NOT NULL, -- source: prices.json model_id (exact message.model string)
|
|
10
|
+
effective_from TEXT NOT NULL, -- source: prices.json effective_from, YYYY-MM-DD; 0000-01-01 = since release (D-020)
|
|
11
|
+
input_per_mtok REAL NOT NULL, -- source: pricing page, base input USD per million tokens
|
|
12
|
+
output_per_mtok REAL NOT NULL, -- source: pricing page, output
|
|
13
|
+
cache_write_5m_per_mtok REAL NOT NULL, -- source: pricing page, 5-minute cache writes
|
|
14
|
+
cache_write_1h_per_mtok REAL NOT NULL, -- source: pricing page, 1-hour cache writes
|
|
15
|
+
cache_read_per_mtok REAL NOT NULL, -- source: pricing page, cache hits and refreshes
|
|
16
|
+
fast_input_per_mtok REAL, -- source: pricing page, fast mode input; NULL when not published
|
|
17
|
+
fast_output_per_mtok REAL, -- source: pricing page, fast mode output
|
|
18
|
+
fast_cache_write_5m_per_mtok REAL, -- source: pricing page, caching multipliers on fast input
|
|
19
|
+
fast_cache_write_1h_per_mtok REAL, -- source: pricing page, caching multipliers on fast input
|
|
20
|
+
fast_cache_read_per_mtok REAL, -- source: pricing page, caching multipliers on fast input
|
|
21
|
+
standard_rate_max_input_tokens INTEGER, -- source: prices.json; NULL when the full context window is standard-priced
|
|
22
|
+
source_url TEXT NOT NULL, -- source: page the rates were read from
|
|
23
|
+
verified_on TEXT NOT NULL, -- source: day the page was read, YYYY-MM-DD
|
|
24
|
+
PRIMARY KEY (model_id, effective_from)
|
|
25
|
+
) STRICT;
|
|
26
|
+
|
|
27
|
+
ALTER TABLE requests ADD COLUMN service_tier TEXT; -- source: message.usage.service_tier when it's a string
|
|
28
|
+
ALTER TABLE requests ADD COLUMN inference_geo TEXT; -- source: message.usage.inference_geo when it's a string
|
|
29
|
+
|
|
30
|
+
-- Projected: each deduplicated request at API list price, or NULL cost with the reason it can't be priced.
|
|
31
|
+
CREATE VIEW request_costs AS
|
|
32
|
+
WITH matched AS (
|
|
33
|
+
SELECT
|
|
34
|
+
d.*,
|
|
35
|
+
r.service_tier,
|
|
36
|
+
r.inference_geo,
|
|
37
|
+
substr(d.timestamp_utc, 1, 10) AS day_utc,
|
|
38
|
+
d.input_tokens + d.cache_read_tokens + COALESCE(d.cache_write_5m_tokens, 0)
|
|
39
|
+
+ COALESCE(d.cache_write_1h_tokens, 0) + COALESCE(d.cache_write_unsplit_tokens, 0) AS total_input_tokens,
|
|
40
|
+
(SELECT p.effective_from FROM prices p
|
|
41
|
+
WHERE p.model_id = d.model AND p.effective_from <= substr(d.timestamp_utc, 1, 10)
|
|
42
|
+
ORDER BY p.effective_from DESC LIMIT 1) AS price_effective_from
|
|
43
|
+
FROM requests_dedup d
|
|
44
|
+
JOIN requests r ON r.raw_line_id = d.raw_line_id
|
|
45
|
+
),
|
|
46
|
+
rated AS (
|
|
47
|
+
SELECT
|
|
48
|
+
m.*,
|
|
49
|
+
p.verified_on,
|
|
50
|
+
p.standard_rate_max_input_tokens,
|
|
51
|
+
CASE
|
|
52
|
+
WHEN m.timestamp_utc IS NULL THEN 'unparsed_timestamp'
|
|
53
|
+
WHEN p.model_id IS NULL THEN 'no_price_row'
|
|
54
|
+
WHEN m.speed = 'fast' AND p.fast_input_per_mtok IS NULL THEN 'fast_rate_unknown'
|
|
55
|
+
WHEN m.service_tier IS NOT NULL AND m.service_tier <> 'standard' THEN 'service_tier_not_standard'
|
|
56
|
+
WHEN p.standard_rate_max_input_tokens IS NOT NULL
|
|
57
|
+
AND m.total_input_tokens > p.standard_rate_max_input_tokens THEN 'long_context_rate_unverified'
|
|
58
|
+
ELSE NULL
|
|
59
|
+
END AS unpriced_reason,
|
|
60
|
+
CASE WHEN m.speed = 'fast' THEN p.fast_input_per_mtok ELSE p.input_per_mtok END AS rate_input,
|
|
61
|
+
CASE WHEN m.speed = 'fast' THEN p.fast_output_per_mtok ELSE p.output_per_mtok END AS rate_output,
|
|
62
|
+
CASE WHEN m.speed = 'fast' THEN p.fast_cache_write_5m_per_mtok ELSE p.cache_write_5m_per_mtok END AS rate_write_5m,
|
|
63
|
+
CASE WHEN m.speed = 'fast' THEN p.fast_cache_write_1h_per_mtok ELSE p.cache_write_1h_per_mtok END AS rate_write_1h,
|
|
64
|
+
CASE WHEN m.speed = 'fast' THEN p.fast_cache_read_per_mtok ELSE p.cache_read_per_mtok END AS rate_read,
|
|
65
|
+
CASE WHEN m.inference_geo = 'us' THEN 1.1 ELSE 1.0 END AS geo_multiplier
|
|
66
|
+
FROM matched m
|
|
67
|
+
LEFT JOIN prices p ON p.model_id = m.model AND p.effective_from = m.price_effective_from
|
|
68
|
+
)
|
|
69
|
+
SELECT
|
|
70
|
+
raw_line_id, dedup_key, model, day_utc, timestamp_utc, speed, service_tier, inference_geo,
|
|
71
|
+
input_tokens, output_tokens, cache_read_tokens,
|
|
72
|
+
cache_write_5m_tokens, cache_write_1h_tokens, cache_write_unsplit_tokens,
|
|
73
|
+
unpriced_reason,
|
|
74
|
+
price_effective_from,
|
|
75
|
+
verified_on,
|
|
76
|
+
-- 1 when unsplit cache writes were priced at the 5-minute rate, so the cost is a lower bound.
|
|
77
|
+
CASE WHEN unpriced_reason IS NULL AND cache_write_unsplit_tokens > 0 THEN 1 ELSE 0 END AS is_lower_bound,
|
|
78
|
+
CASE WHEN unpriced_reason IS NULL THEN input_tokens * rate_input * geo_multiplier / 1e6 END AS input_usd,
|
|
79
|
+
CASE WHEN unpriced_reason IS NULL THEN output_tokens * rate_output * geo_multiplier / 1e6 END AS output_usd,
|
|
80
|
+
CASE WHEN unpriced_reason IS NULL THEN cache_read_tokens * rate_read * geo_multiplier / 1e6 END AS cache_read_usd,
|
|
81
|
+
CASE WHEN unpriced_reason IS NULL
|
|
82
|
+
THEN (COALESCE(cache_write_5m_tokens, 0) + COALESCE(cache_write_unsplit_tokens, 0)) * rate_write_5m * geo_multiplier / 1e6 END AS cache_write_5m_usd,
|
|
83
|
+
CASE WHEN unpriced_reason IS NULL THEN COALESCE(cache_write_1h_tokens, 0) * rate_write_1h * geo_multiplier / 1e6 END AS cache_write_1h_usd,
|
|
84
|
+
CASE WHEN unpriced_reason IS NULL THEN
|
|
85
|
+
(input_tokens * rate_input + output_tokens * rate_output + cache_read_tokens * rate_read
|
|
86
|
+
+ (COALESCE(cache_write_5m_tokens, 0) + COALESCE(cache_write_unsplit_tokens, 0)) * rate_write_5m
|
|
87
|
+
+ COALESCE(cache_write_1h_tokens, 0) * rate_write_1h) * geo_multiplier / 1e6
|
|
88
|
+
END AS total_usd
|
|
89
|
+
FROM rated;
|
|
90
|
+
|
|
91
|
+
-- Observed: requests that couldn't be priced, and why (D-005: never $0, never a family fallback).
|
|
92
|
+
CREATE VIEW unpriced_requests AS
|
|
93
|
+
SELECT raw_line_id, dedup_key, model, day_utc, speed, service_tier, unpriced_reason
|
|
94
|
+
FROM request_costs
|
|
95
|
+
WHERE unpriced_reason IS NOT NULL;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
-- 008_interruptions: conversation links, resolved reset times, and the interruption metrics (P6.1).
|
|
2
|
+
--
|
|
3
|
+
-- Rules: D-004 (limit hits), D-021 (mid-task), D-022 (resumption), D-023 (lockout, merging hits).
|
|
4
|
+
-- Every metric has a summary view (obs_<metric>) returning covers_from/covers_to, and an events
|
|
5
|
+
-- view listing the rows behind its number, so each number can be re-summed from its events
|
|
6
|
+
-- (README principles 4 and 5). Timestamps are UTC text, `YYYY-MM-DDTHH:MM:SS.sssZ`, which sorts
|
|
7
|
+
-- chronologically. Spans subtract whole milliseconds, which is the timestamps' precision, so
|
|
8
|
+
-- they carry no floating-point noise.
|
|
9
|
+
|
|
10
|
+
ALTER TABLE parsed_lines ADD COLUMN uuid TEXT; -- source: uuid when it's a non-empty string
|
|
11
|
+
ALTER TABLE parsed_lines ADD COLUMN parent_uuid TEXT; -- source: parentUuid when it's a non-empty string (D-021)
|
|
12
|
+
ALTER TABLE parsed_lines ADD COLUMN origin_kind TEXT; -- source: origin.kind when it's a string (D-022)
|
|
13
|
+
ALTER TABLE parsed_lines ADD COLUMN user_content TEXT CHECK (user_content IN ('tool_result', 'prompt')); -- derived: user lines only; 'tool_result' when message.content holds a tool_result block
|
|
14
|
+
ALTER TABLE parsed_lines ADD COLUMN is_meta INTEGER NOT NULL DEFAULT 0 CHECK (is_meta IN (0, 1)); -- source: isMeta === true (1) or anything else (0)
|
|
15
|
+
ALTER TABLE events ADD COLUMN reset_at_utc TEXT; -- derived: limit hits only; reset_text resolved against the hit's timestamp (D-023)
|
|
16
|
+
|
|
17
|
+
-- Parent lookups and "next line in this session" lookups.
|
|
18
|
+
CREATE INDEX parsed_lines_session_uuid ON parsed_lines (session_id, uuid);
|
|
19
|
+
CREATE INDEX parsed_lines_session_time ON parsed_lines (session_id, timestamp_utc);
|
|
20
|
+
|
|
21
|
+
-- Observed: the time span each source's data covers, from the data present (README principle 5).
|
|
22
|
+
CREATE VIEW source_coverage AS
|
|
23
|
+
SELECT 'session_logs' AS source, MIN(timestamp_utc) AS covers_from, MAX(timestamp_utc) AS covers_to
|
|
24
|
+
FROM parsed_lines
|
|
25
|
+
WHERE timestamp_utc IS NOT NULL
|
|
26
|
+
UNION ALL
|
|
27
|
+
SELECT
|
|
28
|
+
'status_line',
|
|
29
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', MIN(captured_at_s), 'unixepoch'),
|
|
30
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', MAX(captured_at_s), 'unixepoch')
|
|
31
|
+
FROM status_readings
|
|
32
|
+
WHERE status = 'ok';
|
|
33
|
+
|
|
34
|
+
-- Observed: logged limit hits, one per distinct line (copies of a uuid in one session count once), with the D-021 position.
|
|
35
|
+
CREATE VIEW logged_limit_hits AS
|
|
36
|
+
WITH copies AS (
|
|
37
|
+
SELECT
|
|
38
|
+
p.raw_line_id, p.session_id, p.uuid, p.parent_uuid, p.timestamp_utc, p.is_sidechain,
|
|
39
|
+
e.window, e.reset_text, e.reset_at_utc,
|
|
40
|
+
ROW_NUMBER() OVER (
|
|
41
|
+
-- A line without a uuid is its own group: NULL uuids must not merge with each other.
|
|
42
|
+
PARTITION BY p.session_id, p.uuid, CASE WHEN p.uuid IS NULL THEN p.raw_line_id END
|
|
43
|
+
ORDER BY f.root, f.relative_path, l.first_run_id, l.line_number
|
|
44
|
+
) AS copy_rank
|
|
45
|
+
FROM events e
|
|
46
|
+
JOIN parsed_lines p ON p.raw_line_id = e.raw_line_id
|
|
47
|
+
JOIN raw_lines l ON l.id = e.raw_line_id
|
|
48
|
+
JOIN source_files f ON f.id = l.source_file_id
|
|
49
|
+
WHERE e.class = 'limit_hit'
|
|
50
|
+
)
|
|
51
|
+
SELECT
|
|
52
|
+
c.raw_line_id, c.session_id, c.uuid, c.parent_uuid, c.timestamp_utc, c.is_sidechain,
|
|
53
|
+
c.window, c.reset_text, c.reset_at_utc,
|
|
54
|
+
COALESCE((
|
|
55
|
+
SELECT CASE
|
|
56
|
+
WHEN par.user_content = 'tool_result' THEN 'mid_task'
|
|
57
|
+
WHEN par.user_content = 'prompt' AND par.is_meta = 0 THEN 'turn_start'
|
|
58
|
+
ELSE 'unknown'
|
|
59
|
+
END
|
|
60
|
+
FROM parsed_lines par
|
|
61
|
+
JOIN raw_lines pl ON pl.id = par.raw_line_id
|
|
62
|
+
JOIN source_files pf ON pf.id = pl.source_file_id
|
|
63
|
+
WHERE par.session_id = c.session_id AND par.uuid = c.parent_uuid
|
|
64
|
+
ORDER BY pf.root, pf.relative_path, pl.first_run_id, pl.line_number
|
|
65
|
+
LIMIT 1
|
|
66
|
+
), 'unknown') AS position
|
|
67
|
+
FROM copies c
|
|
68
|
+
WHERE c.copy_rank = 1;
|
|
69
|
+
|
|
70
|
+
-- Observed: status line windows that reached 100%, one row per (window, resets_at), from init onward (D-023).
|
|
71
|
+
CREATE VIEW status_limit_groups AS
|
|
72
|
+
WITH at_limit AS (
|
|
73
|
+
SELECT
|
|
74
|
+
w.raw_line_id, w.window, w.resets_at, s.session_id, s.captured_at_s,
|
|
75
|
+
ROW_NUMBER() OVER (PARTITION BY w.window, w.resets_at ORDER BY s.captured_at_s, w.raw_line_id) AS reading_rank,
|
|
76
|
+
COUNT(*) OVER (PARTITION BY w.window, w.resets_at) AS readings_at_limit
|
|
77
|
+
FROM rate_limit_windows w
|
|
78
|
+
JOIN status_readings s ON s.raw_line_id = w.raw_line_id
|
|
79
|
+
WHERE s.status = 'ok' AND w.validity = 'valid'
|
|
80
|
+
AND w.window IN ('five_hour', 'seven_day') AND w.used_percentage >= 100
|
|
81
|
+
)
|
|
82
|
+
SELECT
|
|
83
|
+
raw_line_id, window, resets_at, session_id, readings_at_limit,
|
|
84
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', captured_at_s, 'unixepoch') AS first_at_utc,
|
|
85
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', resets_at, 'unixepoch') AS reset_at_utc,
|
|
86
|
+
CASE window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END AS window_seconds
|
|
87
|
+
FROM at_limit
|
|
88
|
+
WHERE reading_rank = 1;
|
|
89
|
+
|
|
90
|
+
-- Observed: every rate-limit interruption, logged hits merged with status line groups without double-counting (D-004, D-023).
|
|
91
|
+
CREATE VIEW obs_limit_hits_events AS
|
|
92
|
+
WITH logged AS (
|
|
93
|
+
SELECT
|
|
94
|
+
h.*,
|
|
95
|
+
(SELECT g.raw_line_id FROM status_limit_groups g
|
|
96
|
+
WHERE (h.window IS NULL OR h.window = g.window)
|
|
97
|
+
AND unixepoch(h.timestamp_utc, 'subsec') > g.resets_at - g.window_seconds
|
|
98
|
+
AND unixepoch(h.timestamp_utc, 'subsec') <= g.resets_at
|
|
99
|
+
ORDER BY g.resets_at, g.window
|
|
100
|
+
LIMIT 1) AS status_raw_line_id
|
|
101
|
+
FROM logged_limit_hits h
|
|
102
|
+
),
|
|
103
|
+
merged AS (
|
|
104
|
+
SELECT
|
|
105
|
+
'session_log' AS source,
|
|
106
|
+
l.raw_line_id,
|
|
107
|
+
l.status_raw_line_id,
|
|
108
|
+
l.session_id,
|
|
109
|
+
l.timestamp_utc AS hit_at_utc,
|
|
110
|
+
COALESCE(l.window, g.window) AS window,
|
|
111
|
+
-- The payload's resets_at is exact; reset text has minute precision.
|
|
112
|
+
COALESCE(g.reset_at_utc, l.reset_at_utc) AS reset_at_utc,
|
|
113
|
+
CASE WHEN g.raw_line_id IS NOT NULL THEN 'status_line' WHEN l.reset_at_utc IS NOT NULL THEN 'log_text' END AS reset_source,
|
|
114
|
+
l.reset_text,
|
|
115
|
+
l.position,
|
|
116
|
+
l.is_sidechain
|
|
117
|
+
FROM logged l
|
|
118
|
+
LEFT JOIN status_limit_groups g ON g.raw_line_id = l.status_raw_line_id
|
|
119
|
+
UNION ALL
|
|
120
|
+
SELECT 'status_line', g.raw_line_id, g.raw_line_id, g.session_id, g.first_at_utc, g.window,
|
|
121
|
+
g.reset_at_utc, 'status_line', NULL, 'unknown', 0
|
|
122
|
+
FROM status_limit_groups g
|
|
123
|
+
WHERE NOT EXISTS (SELECT 1 FROM logged l WHERE l.status_raw_line_id = g.raw_line_id)
|
|
124
|
+
)
|
|
125
|
+
SELECT
|
|
126
|
+
m.*,
|
|
127
|
+
(round(unixepoch(m.reset_at_utc, 'subsec') * 1000) - round(unixepoch(m.hit_at_utc, 'subsec') * 1000)) / 1000.0 AS hit_to_reset_seconds,
|
|
128
|
+
(SELECT MIN(d.timestamp_utc) FROM requests_dedup d
|
|
129
|
+
WHERE d.session_id = m.session_id AND d.timestamp_utc > m.hit_at_utc) AS next_session_request_at_utc,
|
|
130
|
+
(SELECT p.timestamp_utc FROM parsed_lines p
|
|
131
|
+
WHERE p.session_id = m.session_id AND p.user_content = 'prompt' AND p.is_meta = 0
|
|
132
|
+
AND p.timestamp_utc > m.hit_at_utc
|
|
133
|
+
ORDER BY p.timestamp_utc, p.raw_line_id LIMIT 1) AS next_session_prompt_at_utc,
|
|
134
|
+
-- origin.kind as written (D-022): never relabeled as automatic or manual.
|
|
135
|
+
(SELECT p.origin_kind FROM parsed_lines p
|
|
136
|
+
WHERE p.session_id = m.session_id AND p.user_content = 'prompt' AND p.is_meta = 0
|
|
137
|
+
AND p.timestamp_utc > m.hit_at_utc
|
|
138
|
+
ORDER BY p.timestamp_utc, p.raw_line_id LIMIT 1) AS next_session_prompt_origin_kind,
|
|
139
|
+
m.reset_at_utc > (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS reset_after_coverage
|
|
140
|
+
FROM merged m;
|
|
141
|
+
|
|
142
|
+
-- Observed: count of rate-limit interruptions, by source and window.
|
|
143
|
+
CREATE VIEW obs_limit_hits AS
|
|
144
|
+
SELECT
|
|
145
|
+
COUNT(e.raw_line_id) AS hits,
|
|
146
|
+
COUNT(e.raw_line_id) FILTER (WHERE e.source = 'session_log') AS logged_hits,
|
|
147
|
+
COUNT(e.raw_line_id) FILTER (WHERE e.source = 'status_line') AS status_line_only_hits,
|
|
148
|
+
COUNT(e.raw_line_id) FILTER (WHERE e.window = 'five_hour') AS five_hour_hits,
|
|
149
|
+
COUNT(e.raw_line_id) FILTER (WHERE e.window = 'seven_day') AS seven_day_hits,
|
|
150
|
+
COUNT(e.raw_line_id) FILTER (WHERE e.window IS NULL) AS unknown_window_hits,
|
|
151
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
|
|
152
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to,
|
|
153
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'status_line') AS status_line_covers_from,
|
|
154
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'status_line') AS status_line_covers_to
|
|
155
|
+
FROM obs_limit_hits_events e;
|
|
156
|
+
|
|
157
|
+
-- Observed: the interruptions that stopped the model while it was continuing its own tool loop (D-021).
|
|
158
|
+
CREATE VIEW obs_mid_task_interruptions_events AS
|
|
159
|
+
SELECT * FROM obs_limit_hits_events WHERE position = 'mid_task';
|
|
160
|
+
|
|
161
|
+
-- Observed: mid-task interruptions, beside turn-start and unknown-position counts (D-021).
|
|
162
|
+
CREATE VIEW obs_mid_task_interruptions AS
|
|
163
|
+
SELECT
|
|
164
|
+
COUNT(e.raw_line_id) FILTER (WHERE e.position = 'mid_task') AS mid_task,
|
|
165
|
+
COUNT(e.raw_line_id) FILTER (WHERE e.position = 'turn_start') AS turn_start,
|
|
166
|
+
COUNT(e.raw_line_id) FILTER (WHERE e.position = 'unknown') AS position_unknown,
|
|
167
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
|
|
168
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
|
|
169
|
+
FROM obs_limit_hits_events e;
|
|
170
|
+
|
|
171
|
+
-- Observed: which merged lockout interval each hit with a known reset belongs to (D-023).
|
|
172
|
+
CREATE VIEW obs_lockout_interval_hits AS
|
|
173
|
+
WITH spans AS (
|
|
174
|
+
SELECT
|
|
175
|
+
raw_line_id,
|
|
176
|
+
hit_at_utc AS start_utc,
|
|
177
|
+
-- A reset in the hit's own minute can precede the hit by seconds; the span is then empty.
|
|
178
|
+
MAX(reset_at_utc, hit_at_utc) AS end_utc
|
|
179
|
+
FROM obs_limit_hits_events
|
|
180
|
+
WHERE hit_at_utc IS NOT NULL AND reset_at_utc IS NOT NULL
|
|
181
|
+
),
|
|
182
|
+
ordered AS (
|
|
183
|
+
SELECT
|
|
184
|
+
s.*,
|
|
185
|
+
MAX(end_utc) OVER (ORDER BY start_utc, end_utc, raw_line_id
|
|
186
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS previous_end_utc
|
|
187
|
+
FROM spans s
|
|
188
|
+
)
|
|
189
|
+
SELECT
|
|
190
|
+
raw_line_id, start_utc, end_utc,
|
|
191
|
+
-- A span starting after every earlier span has ended opens a new interval; touching spans merge.
|
|
192
|
+
SUM(CASE WHEN previous_end_utc IS NULL OR start_utc > previous_end_utc THEN 1 ELSE 0 END)
|
|
193
|
+
OVER (ORDER BY start_utc, end_utc, raw_line_id) AS interval_number
|
|
194
|
+
FROM ordered;
|
|
195
|
+
|
|
196
|
+
-- Observed: merged lockout intervals, hit → reset, with the first Claude Code request at or after each reset (D-023).
|
|
197
|
+
CREATE VIEW obs_lockout_intervals AS
|
|
198
|
+
WITH grouped AS (
|
|
199
|
+
SELECT
|
|
200
|
+
interval_number,
|
|
201
|
+
MIN(start_utc) AS locked_from_utc,
|
|
202
|
+
MAX(end_utc) AS locked_until_utc,
|
|
203
|
+
COUNT(*) AS hits
|
|
204
|
+
FROM obs_lockout_interval_hits
|
|
205
|
+
GROUP BY interval_number
|
|
206
|
+
),
|
|
207
|
+
with_next AS (
|
|
208
|
+
SELECT
|
|
209
|
+
g.*,
|
|
210
|
+
(SELECT MIN(d.timestamp_utc) FROM requests_dedup d WHERE d.timestamp_utc >= g.locked_until_utc) AS next_request_at_utc
|
|
211
|
+
FROM grouped g
|
|
212
|
+
)
|
|
213
|
+
SELECT
|
|
214
|
+
w.*,
|
|
215
|
+
(round(unixepoch(w.locked_until_utc, 'subsec') * 1000) - round(unixepoch(w.locked_from_utc, 'subsec') * 1000)) / 1000.0 AS lockout_seconds,
|
|
216
|
+
(round(unixepoch(w.next_request_at_utc, 'subsec') * 1000) - round(unixepoch(w.locked_until_utc, 'subsec') * 1000)) / 1000.0 AS reset_to_next_request_seconds,
|
|
217
|
+
w.locked_until_utc > (SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS reset_after_coverage
|
|
218
|
+
FROM with_next w;
|
|
219
|
+
|
|
220
|
+
-- Observed: total elapsed lockout time (hit → reset, overlaps counted once), beside hits whose reset is unknown.
|
|
221
|
+
CREATE VIEW obs_lockout_time AS
|
|
222
|
+
SELECT
|
|
223
|
+
(SELECT COUNT(*) FROM obs_lockout_intervals) AS intervals,
|
|
224
|
+
(SELECT TOTAL(lockout_seconds) FROM obs_lockout_intervals) AS lockout_seconds,
|
|
225
|
+
(SELECT COUNT(*) FROM obs_lockout_interval_hits) AS hits_with_known_reset,
|
|
226
|
+
(SELECT COUNT(*) FROM obs_limit_hits_events WHERE reset_at_utc IS NULL OR hit_at_utc IS NULL) AS hits_with_unknown_reset,
|
|
227
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
|
|
228
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to;
|
|
229
|
+
|
|
230
|
+
-- Observed: sessions whose last limit hit has no later request in that session (D-022).
|
|
231
|
+
CREATE VIEW obs_sessions_not_resumed_events AS
|
|
232
|
+
WITH ranked AS (
|
|
233
|
+
SELECT
|
|
234
|
+
e.session_id, e.raw_line_id, e.hit_at_utc, e.reset_at_utc, e.reset_after_coverage,
|
|
235
|
+
ROW_NUMBER() OVER (PARTITION BY e.session_id ORDER BY e.hit_at_utc DESC, e.raw_line_id DESC) AS hit_rank,
|
|
236
|
+
COUNT(*) OVER (PARTITION BY e.session_id) AS hits
|
|
237
|
+
FROM obs_limit_hits_events e
|
|
238
|
+
WHERE e.session_id IS NOT NULL AND e.session_id <> '<missing>' AND e.hit_at_utc IS NOT NULL
|
|
239
|
+
)
|
|
240
|
+
SELECT
|
|
241
|
+
session_id,
|
|
242
|
+
raw_line_id AS last_hit_raw_line_id,
|
|
243
|
+
hit_at_utc AS last_hit_at_utc,
|
|
244
|
+
reset_at_utc,
|
|
245
|
+
reset_after_coverage,
|
|
246
|
+
hits
|
|
247
|
+
FROM ranked r
|
|
248
|
+
WHERE hit_rank = 1
|
|
249
|
+
AND NOT EXISTS (
|
|
250
|
+
SELECT 1 FROM requests_dedup d WHERE d.session_id = r.session_id AND d.timestamp_utc > r.hit_at_utc
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
-- Observed: sessions not resumed after a limit hit, with those whose reset was after log coverage counted separately.
|
|
254
|
+
CREATE VIEW obs_sessions_not_resumed AS
|
|
255
|
+
SELECT
|
|
256
|
+
(SELECT COUNT(DISTINCT session_id) FROM obs_limit_hits_events
|
|
257
|
+
WHERE session_id IS NOT NULL AND session_id <> '<missing>' AND hit_at_utc IS NOT NULL) AS sessions_with_hits,
|
|
258
|
+
COUNT(n.session_id) AS sessions_not_resumed,
|
|
259
|
+
COUNT(n.session_id) FILTER (WHERE n.reset_after_coverage = 1) AS reset_after_coverage,
|
|
260
|
+
COUNT(n.session_id) FILTER (WHERE n.reset_at_utc IS NULL) AS reset_unknown,
|
|
261
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
|
|
262
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
|
|
263
|
+
FROM obs_sessions_not_resumed_events n;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
-- 009_headroom: last observed usage and peak usage per rate-limit window, from init onward (P6.2, D-024).
|
|
2
|
+
--
|
|
3
|
+
-- A window instance is (window, resets_at). Readings come only from the status line, so these
|
|
4
|
+
-- metrics cover the status line's span, not the logs' (README principle 5). The last reading is a
|
|
5
|
+
-- lower bound on the window's final usage: budget spent after the last Claude Code turn isn't seen.
|
|
6
|
+
|
|
7
|
+
-- Observed: valid plan-window readings, each with its window instance and whether it was captured after that reset.
|
|
8
|
+
CREATE VIEW window_readings AS
|
|
9
|
+
SELECT
|
|
10
|
+
w.raw_line_id,
|
|
11
|
+
w.window,
|
|
12
|
+
w.resets_at,
|
|
13
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', w.resets_at, 'unixepoch') AS reset_at_utc,
|
|
14
|
+
w.used_percentage,
|
|
15
|
+
s.session_id,
|
|
16
|
+
s.captured_at_s,
|
|
17
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', s.captured_at_s, 'unixepoch') AS captured_at_utc,
|
|
18
|
+
s.captured_at_s > w.resets_at AS captured_after_reset
|
|
19
|
+
FROM rate_limit_windows w
|
|
20
|
+
JOIN status_readings s ON s.raw_line_id = w.raw_line_id
|
|
21
|
+
WHERE s.status = 'ok' AND w.validity = 'valid' AND w.window IN ('five_hour', 'seven_day');
|
|
22
|
+
|
|
23
|
+
-- Observed: per window instance, reading counts, whether it is still open, and the status line coverage.
|
|
24
|
+
CREATE VIEW window_instances AS
|
|
25
|
+
SELECT
|
|
26
|
+
window,
|
|
27
|
+
resets_at,
|
|
28
|
+
reset_at_utc,
|
|
29
|
+
COUNT(*) FILTER (WHERE captured_after_reset = 0) AS readings,
|
|
30
|
+
COUNT(*) FILTER (WHERE captured_after_reset = 1) AS readings_after_reset,
|
|
31
|
+
MIN(captured_at_utc) FILTER (WHERE captured_after_reset = 0) AS first_reading_at_utc,
|
|
32
|
+
-- Open: no status line reading at all was captured at or after this reset, so later usage may still come.
|
|
33
|
+
resets_at > (SELECT MAX(captured_at_s) FROM status_readings WHERE status = 'ok') AS window_open,
|
|
34
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'status_line') AS covers_from,
|
|
35
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'status_line') AS covers_to
|
|
36
|
+
FROM window_readings
|
|
37
|
+
GROUP BY window, resets_at;
|
|
38
|
+
|
|
39
|
+
-- Observed: last observed usage before each window's reset, with that reading's time (a lower bound on final usage).
|
|
40
|
+
CREATE VIEW obs_window_headroom AS
|
|
41
|
+
WITH ranked AS (
|
|
42
|
+
SELECT
|
|
43
|
+
r.*,
|
|
44
|
+
ROW_NUMBER() OVER (PARTITION BY r.window, r.resets_at ORDER BY r.captured_at_s DESC, r.raw_line_id DESC) AS latest_rank
|
|
45
|
+
FROM window_readings r
|
|
46
|
+
WHERE r.captured_after_reset = 0
|
|
47
|
+
)
|
|
48
|
+
SELECT
|
|
49
|
+
i.window,
|
|
50
|
+
i.reset_at_utc,
|
|
51
|
+
r.used_percentage AS last_used_percentage,
|
|
52
|
+
r.captured_at_utc AS last_reading_at_utc,
|
|
53
|
+
r.raw_line_id AS last_reading_raw_line_id,
|
|
54
|
+
i.readings,
|
|
55
|
+
i.readings_after_reset,
|
|
56
|
+
i.first_reading_at_utc,
|
|
57
|
+
i.window_open,
|
|
58
|
+
i.covers_from,
|
|
59
|
+
i.covers_to
|
|
60
|
+
FROM window_instances i
|
|
61
|
+
LEFT JOIN ranked r ON r.window = i.window AND r.resets_at = i.resets_at AND r.latest_rank = 1;
|
|
62
|
+
|
|
63
|
+
-- Observed: highest observed usage in each window, with the reading it came from.
|
|
64
|
+
CREATE VIEW obs_window_peak AS
|
|
65
|
+
WITH ranked AS (
|
|
66
|
+
SELECT
|
|
67
|
+
r.*,
|
|
68
|
+
ROW_NUMBER() OVER (PARTITION BY r.window, r.resets_at ORDER BY r.used_percentage DESC, r.captured_at_s, r.raw_line_id) AS peak_rank
|
|
69
|
+
FROM window_readings r
|
|
70
|
+
WHERE r.captured_after_reset = 0
|
|
71
|
+
)
|
|
72
|
+
SELECT
|
|
73
|
+
i.window,
|
|
74
|
+
i.reset_at_utc,
|
|
75
|
+
r.used_percentage AS peak_used_percentage,
|
|
76
|
+
r.captured_at_utc AS peak_reading_at_utc,
|
|
77
|
+
r.raw_line_id AS peak_reading_raw_line_id,
|
|
78
|
+
i.readings,
|
|
79
|
+
i.readings_after_reset,
|
|
80
|
+
i.window_open,
|
|
81
|
+
i.covers_from,
|
|
82
|
+
i.covers_to
|
|
83
|
+
FROM window_instances i
|
|
84
|
+
LEFT JOIN ranked r ON r.window = i.window AND r.resets_at = i.resets_at AND r.peak_rank = 1;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
-- 010_attribution: where observed usage goes, by model and repository, and budget used outside Claude Code (P6.3).
|
|
2
|
+
--
|
|
3
|
+
-- Model and repository metrics are log-derived and cover the logs' span; unattributed usage is
|
|
4
|
+
-- status-line-derived and covers the status line's span (README principle 5). Token sums are
|
|
5
|
+
-- per request after dedup (D-001, D-007). A token type with no requests reporting it sums to NULL.
|
|
6
|
+
|
|
7
|
+
-- Observed: every deduplicated request with the model and repository it's attributed to.
|
|
8
|
+
CREATE VIEW obs_usage_events AS
|
|
9
|
+
SELECT
|
|
10
|
+
d.raw_line_id, d.dedup_key, d.session_id, d.timestamp_utc, d.model,
|
|
11
|
+
r.repository, r.repo_kind,
|
|
12
|
+
d.input_tokens, d.output_tokens, d.cache_read_tokens,
|
|
13
|
+
d.cache_write_5m_tokens, d.cache_write_1h_tokens, d.cache_write_unsplit_tokens
|
|
14
|
+
FROM requests_dedup d
|
|
15
|
+
JOIN request_repositories r ON r.raw_line_id = d.raw_line_id;
|
|
16
|
+
|
|
17
|
+
-- Observed: tokens per model per token type, with request counts and unknowns.
|
|
18
|
+
CREATE VIEW obs_usage_by_model AS
|
|
19
|
+
SELECT
|
|
20
|
+
model,
|
|
21
|
+
COUNT(*) AS requests,
|
|
22
|
+
SUM(input_tokens) AS input_tokens,
|
|
23
|
+
SUM(output_tokens) AS output_tokens,
|
|
24
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
25
|
+
SUM(cache_write_5m_tokens) AS cache_write_5m_tokens,
|
|
26
|
+
SUM(cache_write_1h_tokens) AS cache_write_1h_tokens,
|
|
27
|
+
SUM(cache_write_unsplit_tokens) AS cache_write_unsplit_tokens,
|
|
28
|
+
COUNT(*) FILTER (WHERE dedup_key IS NULL) AS unkeyed_requests,
|
|
29
|
+
COUNT(*) FILTER (WHERE timestamp_utc IS NULL) AS requests_without_timestamp,
|
|
30
|
+
MIN(timestamp_utc) AS first_request_at_utc,
|
|
31
|
+
MAX(timestamp_utc) AS last_request_at_utc,
|
|
32
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
|
|
33
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
|
|
34
|
+
FROM obs_usage_events
|
|
35
|
+
GROUP BY model;
|
|
36
|
+
|
|
37
|
+
-- Observed: tokens per repository (git root, or the working directory outside git; NULL when no cwd) per token type.
|
|
38
|
+
CREATE VIEW obs_usage_by_repo AS
|
|
39
|
+
SELECT
|
|
40
|
+
repository,
|
|
41
|
+
repo_kind,
|
|
42
|
+
COUNT(*) AS requests,
|
|
43
|
+
SUM(input_tokens) AS input_tokens,
|
|
44
|
+
SUM(output_tokens) AS output_tokens,
|
|
45
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
46
|
+
SUM(cache_write_5m_tokens) AS cache_write_5m_tokens,
|
|
47
|
+
SUM(cache_write_1h_tokens) AS cache_write_1h_tokens,
|
|
48
|
+
SUM(cache_write_unsplit_tokens) AS cache_write_unsplit_tokens,
|
|
49
|
+
COUNT(*) FILTER (WHERE timestamp_utc IS NULL) AS requests_without_timestamp,
|
|
50
|
+
(SELECT covers_from FROM source_coverage WHERE source = 'session_logs') AS covers_from,
|
|
51
|
+
(SELECT covers_to FROM source_coverage WHERE source = 'session_logs') AS covers_to
|
|
52
|
+
FROM obs_usage_events
|
|
53
|
+
GROUP BY repository, repo_kind;
|
|
54
|
+
|
|
55
|
+
-- Observed: consecutive readings of one window, the change between them, and Claude Code requests in between (D-025).
|
|
56
|
+
CREATE VIEW window_reading_pairs AS
|
|
57
|
+
WITH ordered AS (
|
|
58
|
+
SELECT
|
|
59
|
+
r.*,
|
|
60
|
+
LAG(r.raw_line_id) OVER w AS previous_raw_line_id,
|
|
61
|
+
LAG(r.used_percentage) OVER w AS previous_used_percentage,
|
|
62
|
+
LAG(r.captured_at_s) OVER w AS previous_captured_at_s
|
|
63
|
+
FROM window_readings r
|
|
64
|
+
WHERE r.captured_after_reset = 0
|
|
65
|
+
WINDOW w AS (PARTITION BY r.window, r.resets_at ORDER BY r.captured_at_s, r.raw_line_id)
|
|
66
|
+
)
|
|
67
|
+
SELECT
|
|
68
|
+
o.window,
|
|
69
|
+
o.reset_at_utc,
|
|
70
|
+
o.previous_raw_line_id,
|
|
71
|
+
o.raw_line_id,
|
|
72
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', o.previous_captured_at_s, 'unixepoch') AS previous_captured_at_utc,
|
|
73
|
+
o.captured_at_utc,
|
|
74
|
+
o.used_percentage - o.previous_used_percentage AS change_percentage_points,
|
|
75
|
+
-- Captures are whole seconds: include the previous capture's second, and one second past this capture.
|
|
76
|
+
(SELECT COUNT(*) FROM requests_dedup d
|
|
77
|
+
WHERE d.timestamp_utc >= strftime('%Y-%m-%dT%H:%M:%fZ', o.previous_captured_at_s, 'unixepoch')
|
|
78
|
+
AND d.timestamp_utc < strftime('%Y-%m-%dT%H:%M:%fZ', o.captured_at_s + 1, 'unixepoch')) AS requests_between
|
|
79
|
+
FROM ordered o
|
|
80
|
+
WHERE o.previous_raw_line_id IS NOT NULL;
|
|
81
|
+
|
|
82
|
+
-- Observed: reading pairs whose usage rose with no Claude Code request in between (D-025).
|
|
83
|
+
CREATE VIEW obs_unattributed_usage_events AS
|
|
84
|
+
SELECT * FROM window_reading_pairs
|
|
85
|
+
WHERE requests_between = 0 AND change_percentage_points > 0;
|
|
86
|
+
|
|
87
|
+
-- Observed: per window, percentage points consumed with no Claude Code request in between (a lower bound), beside pair counts.
|
|
88
|
+
CREATE VIEW obs_unattributed_usage AS
|
|
89
|
+
SELECT
|
|
90
|
+
i.window,
|
|
91
|
+
i.reset_at_utc,
|
|
92
|
+
i.readings,
|
|
93
|
+
(SELECT COUNT(*) FROM window_reading_pairs p WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc) AS pairs,
|
|
94
|
+
(SELECT COUNT(*) FROM window_reading_pairs p
|
|
95
|
+
WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc AND p.requests_between = 0) AS pairs_without_requests,
|
|
96
|
+
(SELECT COUNT(*) FROM window_reading_pairs p
|
|
97
|
+
WHERE p.window = i.window AND p.reset_at_utc = i.reset_at_utc AND p.change_percentage_points < 0) AS decreasing_pairs,
|
|
98
|
+
-- Fewer than two readings means no pair was observed: unknown, not 0.
|
|
99
|
+
CASE WHEN i.readings >= 2 THEN
|
|
100
|
+
(SELECT TOTAL(e.change_percentage_points) FROM obs_unattributed_usage_events e
|
|
101
|
+
WHERE e.window = i.window AND e.reset_at_utc = i.reset_at_utc)
|
|
102
|
+
END AS unattributed_percentage_points,
|
|
103
|
+
i.window_open,
|
|
104
|
+
i.covers_from,
|
|
105
|
+
i.covers_to
|
|
106
|
+
FROM window_instances i;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
-- 011_projections: projections derived from observations, kept apart from observed views (P6.4, D-026, D-027).
|
|
2
|
+
--
|
|
3
|
+
-- Every view here is a projection: it carries the proj_ prefix, and no obs_ view may read from it
|
|
4
|
+
-- (README principle 2; enforced by tests/unit/core/metrics/structure.test.ts). plan_prices is user
|
|
5
|
+
-- data entered with the plan-price command: not derived, never rebuilt, never committed.
|
|
6
|
+
|
|
7
|
+
CREATE TABLE plan_prices (
|
|
8
|
+
effective_month TEXT PRIMARY KEY, -- source: user entry, YYYY-MM (local calendar month from which the price applies)
|
|
9
|
+
plan_name TEXT NOT NULL, -- source: user entry, the plan's name as the user writes it
|
|
10
|
+
usd_per_month REAL NOT NULL, -- source: user entry, the plan's USD list price per month (README, Currency)
|
|
11
|
+
entered_at TEXT NOT NULL, -- derived: ISO-8601 UTC time the row was written
|
|
12
|
+
CHECK (effective_month GLOB '[0-9][0-9][0-9][0-9]-[0-1][0-9]'),
|
|
13
|
+
CHECK (usd_per_month >= 0)
|
|
14
|
+
) STRICT;
|
|
15
|
+
|
|
16
|
+
-- Projected: when each window's limit would be reached at its average rate since the window began (D-026).
|
|
17
|
+
CREATE VIEW proj_burn_rate AS
|
|
18
|
+
WITH timed AS (
|
|
19
|
+
SELECT
|
|
20
|
+
h.*,
|
|
21
|
+
unixepoch(h.reset_at_utc) - CASE h.window WHEN 'five_hour' THEN 5 * 3600 ELSE 7 * 86400 END AS window_start_s,
|
|
22
|
+
unixepoch(h.last_reading_at_utc, 'subsec') AS last_reading_s
|
|
23
|
+
FROM obs_window_headroom h
|
|
24
|
+
)
|
|
25
|
+
SELECT
|
|
26
|
+
window,
|
|
27
|
+
reset_at_utc,
|
|
28
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', window_start_s, 'unixepoch') AS window_start_utc,
|
|
29
|
+
last_used_percentage,
|
|
30
|
+
last_reading_at_utc,
|
|
31
|
+
window_open,
|
|
32
|
+
last_used_percentage >= 100 AS limit_reached,
|
|
33
|
+
-- Unknown without a reading after the window start: the premise (0% at the start) can't apply.
|
|
34
|
+
CASE WHEN last_reading_s > window_start_s
|
|
35
|
+
THEN last_used_percentage * 3600.0 / (last_reading_s - window_start_s)
|
|
36
|
+
END AS projected_percentage_points_per_hour,
|
|
37
|
+
CASE WHEN last_reading_s > window_start_s AND last_used_percentage > 0 AND last_used_percentage < 100
|
|
38
|
+
THEN strftime('%Y-%m-%dT%H:%M:%fZ',
|
|
39
|
+
window_start_s + (last_reading_s - window_start_s) * 100.0 / last_used_percentage,
|
|
40
|
+
'unixepoch')
|
|
41
|
+
END AS projected_limit_at_utc,
|
|
42
|
+
CASE WHEN last_reading_s > window_start_s AND last_used_percentage > 0 AND last_used_percentage < 100
|
|
43
|
+
THEN window_start_s + (last_reading_s - window_start_s) * 100.0 / last_used_percentage < unixepoch(reset_at_utc)
|
|
44
|
+
END AS projected_limit_before_reset,
|
|
45
|
+
covers_from,
|
|
46
|
+
covers_to
|
|
47
|
+
FROM timed;
|
|
48
|
+
|
|
49
|
+
-- Projected: observed tokens at API list price per local calendar month, beside the plan price entered for that month (D-027).
|
|
50
|
+
CREATE VIEW proj_api_list_price AS
|
|
51
|
+
WITH months AS (
|
|
52
|
+
SELECT
|
|
53
|
+
strftime('%Y-%m', timestamp_utc, 'localtime') AS month,
|
|
54
|
+
COUNT(*) AS requests,
|
|
55
|
+
COUNT(*) FILTER (WHERE unpriced_reason IS NULL) AS priced_requests,
|
|
56
|
+
COUNT(*) FILTER (WHERE unpriced_reason IS NOT NULL) AS unpriced_requests,
|
|
57
|
+
COUNT(*) FILTER (WHERE is_lower_bound = 1) AS lower_bound_requests,
|
|
58
|
+
COUNT(*) FILTER (WHERE unpriced_reason IS NULL AND day_utc < verified_on) AS priced_before_verified_requests,
|
|
59
|
+
MAX(verified_on) AS verified_on,
|
|
60
|
+
TOTAL(total_usd) AS priced_usd,
|
|
61
|
+
MIN(timestamp_utc) AS covers_from,
|
|
62
|
+
MAX(timestamp_utc) AS covers_to
|
|
63
|
+
FROM request_costs
|
|
64
|
+
WHERE timestamp_utc IS NOT NULL
|
|
65
|
+
GROUP BY month
|
|
66
|
+
)
|
|
67
|
+
SELECT
|
|
68
|
+
m.month,
|
|
69
|
+
m.requests,
|
|
70
|
+
m.priced_requests,
|
|
71
|
+
m.unpriced_requests,
|
|
72
|
+
m.lower_bound_requests,
|
|
73
|
+
m.priced_before_verified_requests,
|
|
74
|
+
m.verified_on,
|
|
75
|
+
-- No priced request in the month: the cost is unknown, not $0.
|
|
76
|
+
CASE WHEN m.priced_requests > 0 THEN m.priced_usd END AS api_list_price_usd,
|
|
77
|
+
(SELECT p.plan_name FROM plan_prices p WHERE p.effective_month <= m.month
|
|
78
|
+
ORDER BY p.effective_month DESC LIMIT 1) AS plan_name,
|
|
79
|
+
(SELECT p.usd_per_month FROM plan_prices p WHERE p.effective_month <= m.month
|
|
80
|
+
ORDER BY p.effective_month DESC LIMIT 1) AS plan_usd_per_month,
|
|
81
|
+
m.covers_from,
|
|
82
|
+
m.covers_to
|
|
83
|
+
FROM months m;
|