llm_cost_tracker 0.7.3 → 0.8.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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +66 -1
- data/README.md +58 -225
- data/app/assets/llm_cost_tracker/application.css +218 -41
- data/app/controllers/llm_cost_tracker/application_controller.rb +30 -17
- data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +19 -14
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +10 -2
- data/app/helpers/llm_cost_tracker/application_helper.rb +11 -24
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +20 -7
- data/app/models/llm_cost_tracker/call.rb +169 -0
- data/app/models/llm_cost_tracker/call_line_item.rb +22 -0
- data/app/models/llm_cost_tracker/call_rollup.rb +9 -0
- data/app/models/llm_cost_tracker/call_tag.rb +16 -0
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +13 -0
- data/app/models/llm_cost_tracker/ingestion/lease.rb +1 -1
- data/app/models/llm_cost_tracker/provider_invoice.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +121 -30
- data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -2
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
- data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
- data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
- data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
- data/app/views/llm_cost_tracker/calls/show.html.erb +62 -7
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -50
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +103 -126
- data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
- data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +63 -0
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
- data/app/views/llm_cost_tracker/tags/show.html.erb +5 -37
- data/lib/llm_cost_tracker/billing/components.rb +53 -0
- data/lib/llm_cost_tracker/billing/components.yml +117 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
- data/lib/llm_cost_tracker/budget.rb +23 -35
- data/lib/llm_cost_tracker/capture/stream_collector.rb +47 -33
- data/lib/llm_cost_tracker/configuration.rb +36 -19
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +54 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +24 -32
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
- data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
- data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +31 -0
- data/lib/llm_cost_tracker/doctor.rb +43 -45
- data/lib/llm_cost_tracker/errors.rb +5 -19
- data/lib/llm_cost_tracker/event.rb +10 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +157 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -23
- data/lib/llm_cost_tracker/ingestion/worker.rb +14 -5
- data/lib/llm_cost_tracker/ingestion.rb +28 -22
- data/lib/llm_cost_tracker/integrations/anthropic.rb +45 -38
- data/lib/llm_cost_tracker/integrations/base.rb +36 -29
- data/lib/llm_cost_tracker/integrations/openai.rb +85 -40
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +5 -5
- data/lib/llm_cost_tracker/integrations.rb +2 -2
- data/lib/llm_cost_tracker/ledger/period/totals.rb +12 -9
- data/lib/llm_cost_tracker/ledger/period.rb +5 -5
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +2 -2
- data/lib/llm_cost_tracker/ledger/rollups.rb +76 -25
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +50 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +34 -23
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
- data/lib/llm_cost_tracker/ledger/store.rb +96 -13
- data/lib/llm_cost_tracker/ledger/tags/query.rb +4 -10
- data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
- data/lib/llm_cost_tracker/ledger.rb +4 -2
- data/lib/llm_cost_tracker/logging.rb +2 -5
- data/lib/llm_cost_tracker/middleware/faraday.rb +7 -6
- data/lib/llm_cost_tracker/parsers/anthropic.rb +52 -7
- data/lib/llm_cost_tracker/parsers/base.rb +8 -3
- data/lib/llm_cost_tracker/parsers/gemini.rb +101 -15
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +10 -2
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +87 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +48 -21
- data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
- data/lib/llm_cost_tracker/parsers.rb +1 -1
- data/lib/llm_cost_tracker/prices.json +105 -20
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +57 -19
- data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
- data/lib/llm_cost_tracker/pricing/lookup.rb +38 -34
- data/lib/llm_cost_tracker/pricing/registry.rb +65 -45
- data/lib/llm_cost_tracker/pricing/service_charges.rb +204 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
- data/lib/llm_cost_tracker/pricing/sync.rb +57 -10
- data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
- data/lib/llm_cost_tracker/pricing.rb +190 -26
- data/lib/llm_cost_tracker/railtie.rb +0 -8
- data/lib/llm_cost_tracker/report/data.rb +16 -8
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +8 -8
- data/lib/llm_cost_tracker/tags/context.rb +2 -4
- data/lib/llm_cost_tracker/tags/key.rb +4 -0
- data/lib/llm_cost_tracker/tags/sanitizer.rb +12 -17
- data/lib/llm_cost_tracker/timing.rb +15 -0
- data/lib/llm_cost_tracker/token_usage.rb +56 -42
- data/lib/llm_cost_tracker/tracker.rb +67 -24
- data/lib/llm_cost_tracker/usage_capture.rb +29 -8
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +36 -35
- data/lib/tasks/llm_cost_tracker.rake +22 -17
- metadata +36 -41
- data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
- data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
- data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
- data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
- data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
- data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
- data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
- data/lib/llm_cost_tracker/pricing/components.rb +0 -37
- data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
|
@@ -16,10 +16,31 @@
|
|
|
16
16
|
--lct-danger: #dc2626;
|
|
17
17
|
--lct-danger-soft: #fee2e2;
|
|
18
18
|
--lct-danger-border: #fca5a5;
|
|
19
|
+
--lct-warning-copy: #9a3412;
|
|
20
|
+
--lct-warning-strong: #7c2d12;
|
|
21
|
+
--lct-danger-copy: #b91c1c;
|
|
22
|
+
--lct-danger-strong: #991b1b;
|
|
19
23
|
--lct-row-hover: #f8fafc;
|
|
24
|
+
--lct-th-bg: #fbfcfe;
|
|
25
|
+
--lct-toolbar-bg: rgba(255, 255, 255, 0.72);
|
|
26
|
+
--lct-toolbar-border: #e7ecf3;
|
|
27
|
+
--lct-border-strong: #d4dae2;
|
|
28
|
+
--lct-chart-secondary: rgba(10, 37, 64, 0.42);
|
|
29
|
+
--lct-chart-marker: rgba(10, 37, 64, 0.45);
|
|
30
|
+
--lct-chip-remove: rgba(10, 37, 64, 0.58);
|
|
20
31
|
--lct-shadow: 0 1px 2px rgba(10, 37, 64, 0.05), 0 1px 1px rgba(10, 37, 64, 0.03);
|
|
32
|
+
--lct-shadow-sm: 0 1px 2px rgba(10, 37, 64, 0.06);
|
|
33
|
+
--lct-shadow-md: 0 4px 12px rgba(10, 37, 64, 0.08), 0 1px 2px rgba(10, 37, 64, 0.04);
|
|
34
|
+
--lct-shadow-lg: 0 12px 32px rgba(10, 37, 64, 0.12), 0 2px 6px rgba(10, 37, 64, 0.05);
|
|
21
35
|
--lct-focus-ring: 0 0 0 3px rgba(99, 91, 255, 0.25);
|
|
22
36
|
|
|
37
|
+
--sp-1: 4px;
|
|
38
|
+
--sp-2: 8px;
|
|
39
|
+
--sp-3: 12px;
|
|
40
|
+
--sp-4: 16px;
|
|
41
|
+
--sp-5: 24px;
|
|
42
|
+
--sp-6: 32px;
|
|
43
|
+
|
|
23
44
|
--fs-2xs: 10px;
|
|
24
45
|
--fs-xs: 11px;
|
|
25
46
|
--fs-sm: 13px;
|
|
@@ -34,6 +55,53 @@
|
|
|
34
55
|
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
35
56
|
}
|
|
36
57
|
|
|
58
|
+
@media (prefers-color-scheme: dark) {
|
|
59
|
+
:root {
|
|
60
|
+
--lct-bg: #0b1220;
|
|
61
|
+
--lct-panel: #111a2e;
|
|
62
|
+
--lct-surface: #1a253c;
|
|
63
|
+
--lct-border: #233149;
|
|
64
|
+
--lct-text: #e6ecf5;
|
|
65
|
+
--lct-muted: #8a98b3;
|
|
66
|
+
--lct-accent: #8a82ff;
|
|
67
|
+
--lct-accent-hover: #a39cff;
|
|
68
|
+
--lct-accent-soft: rgba(138, 130, 255, 0.16);
|
|
69
|
+
--lct-success: #34d399;
|
|
70
|
+
--lct-success-soft: rgba(52, 211, 153, 0.16);
|
|
71
|
+
--lct-warning: #fbbf24;
|
|
72
|
+
--lct-warning-soft: rgba(251, 191, 36, 0.16);
|
|
73
|
+
--lct-warning-border: rgba(251, 191, 36, 0.4);
|
|
74
|
+
--lct-danger: #f87171;
|
|
75
|
+
--lct-danger-soft: rgba(248, 113, 113, 0.16);
|
|
76
|
+
--lct-danger-border: rgba(248, 113, 113, 0.4);
|
|
77
|
+
--lct-row-hover: #182238;
|
|
78
|
+
--lct-th-bg: #142036;
|
|
79
|
+
--lct-toolbar-bg: rgba(17, 26, 46, 0.78);
|
|
80
|
+
--lct-toolbar-border: #233149;
|
|
81
|
+
--lct-border-strong: #2f3f5c;
|
|
82
|
+
--lct-warning-copy: #fcd34d;
|
|
83
|
+
--lct-warning-strong: #fde68a;
|
|
84
|
+
--lct-danger-copy: #fca5a5;
|
|
85
|
+
--lct-danger-strong: #fecaca;
|
|
86
|
+
--lct-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 1px rgba(0, 0, 0, 0.25);
|
|
87
|
+
--lct-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
88
|
+
--lct-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45), 0 1px 2px rgba(0, 0, 0, 0.25);
|
|
89
|
+
--lct-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
|
|
90
|
+
--lct-focus-ring: 0 0 0 3px rgba(138, 130, 255, 0.35);
|
|
91
|
+
--lct-chart-secondary: rgba(230, 236, 245, 0.32);
|
|
92
|
+
--lct-chart-marker: rgba(230, 236, 245, 0.45);
|
|
93
|
+
--lct-chip-remove: rgba(230, 236, 245, 0.58);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.lct-field select {
|
|
97
|
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%238a98b3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M2.5 4.5l3.5 3.5 3.5-3.5'/></svg>");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.lct-field input[type="date"]::-webkit-calendar-picker-indicator {
|
|
101
|
+
filter: invert(70%) sepia(8%) saturate(437%) hue-rotate(176deg) brightness(95%) contrast(86%);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
37
105
|
.lct-body { margin: 0; }
|
|
38
106
|
|
|
39
107
|
.lct-app {
|
|
@@ -93,6 +161,9 @@
|
|
|
93
161
|
|
|
94
162
|
.lct-panel-tight { padding: 16px 18px; }
|
|
95
163
|
|
|
164
|
+
.lct-panel + .lct-panel,
|
|
165
|
+
.lct-stat-grid + .lct-panel { margin-top: 16px; }
|
|
166
|
+
|
|
96
167
|
.lct-banner {
|
|
97
168
|
align-items: center;
|
|
98
169
|
border-radius: 8px;
|
|
@@ -108,13 +179,13 @@
|
|
|
108
179
|
.lct-banner-title { font-size: var(--fs-md); font-weight: 600; margin: 0; }
|
|
109
180
|
.lct-banner-muted { color: var(--lct-muted); font-weight: 400; }
|
|
110
181
|
|
|
111
|
-
.lct-banner-warning { background: var(--lct-warning-soft); border-color: var(--lct-warning-border); color:
|
|
182
|
+
.lct-banner-warning { background: var(--lct-warning-soft); border-color: var(--lct-warning-border); color: var(--lct-warning-strong); }
|
|
112
183
|
.lct-banner-warning .lct-banner-muted,
|
|
113
|
-
.lct-banner-warning .lct-banner-copy { color:
|
|
184
|
+
.lct-banner-warning .lct-banner-copy { color: var(--lct-warning-copy); }
|
|
114
185
|
|
|
115
|
-
.lct-banner-danger { background: var(--lct-danger-soft); border-color: var(--lct-danger-border); color:
|
|
186
|
+
.lct-banner-danger { background: var(--lct-danger-soft); border-color: var(--lct-danger-border); color: var(--lct-danger-strong); }
|
|
116
187
|
.lct-banner-danger .lct-banner-muted,
|
|
117
|
-
.lct-banner-danger .lct-banner-copy { color:
|
|
188
|
+
.lct-banner-danger .lct-banner-copy { color: var(--lct-danger-copy); }
|
|
118
189
|
|
|
119
190
|
.lct-muted { color: var(--lct-muted); }
|
|
120
191
|
|
|
@@ -268,16 +339,26 @@
|
|
|
268
339
|
.lct-table tbody tr:last-child td { border-bottom: 0; }
|
|
269
340
|
|
|
270
341
|
.lct-table th {
|
|
271
|
-
background:
|
|
342
|
+
background: var(--lct-th-bg);
|
|
272
343
|
color: var(--lct-muted);
|
|
273
344
|
font-weight: 600;
|
|
274
345
|
font-size: var(--fs-xs);
|
|
275
346
|
letter-spacing: 0;
|
|
276
347
|
text-transform: none;
|
|
348
|
+
position: sticky;
|
|
349
|
+
top: 0;
|
|
350
|
+
z-index: 1;
|
|
277
351
|
}
|
|
278
352
|
|
|
353
|
+
.lct-table tbody tr {
|
|
354
|
+
transition: background-color 0.1s ease;
|
|
355
|
+
}
|
|
279
356
|
.lct-table tbody tr:hover { background: var(--lct-row-hover); }
|
|
280
357
|
|
|
358
|
+
@media (prefers-reduced-motion: reduce) {
|
|
359
|
+
.lct-table tbody tr { transition: none; }
|
|
360
|
+
}
|
|
361
|
+
|
|
281
362
|
.lct-table td:last-child,
|
|
282
363
|
.lct-table th:last-child,
|
|
283
364
|
.lct-calls-table td:last-child,
|
|
@@ -304,8 +385,10 @@
|
|
|
304
385
|
.lct-stack-fill-input { background: var(--lct-accent); }
|
|
305
386
|
.lct-stack-fill-cache-read { background: #22c55e; }
|
|
306
387
|
.lct-stack-fill-cache-write { background: #f59e0b; }
|
|
307
|
-
.lct-stack-fill-cache-write-
|
|
388
|
+
.lct-stack-fill-cache-write-extended { background: #a855f7; }
|
|
389
|
+
.lct-stack-fill-audio-input { background: #ec4899; }
|
|
308
390
|
.lct-stack-fill-output { background: #0ea5e9; }
|
|
391
|
+
.lct-stack-fill-audio-output { background: #14b8a6; }
|
|
309
392
|
|
|
310
393
|
.lct-budget { display: grid; gap: 10px; }
|
|
311
394
|
.lct-budget-head { align-items: baseline; display: flex; gap: 10px; justify-content: space-between; }
|
|
@@ -318,7 +401,7 @@
|
|
|
318
401
|
.lct-budget-of { color: var(--lct-muted); font-size: var(--fs-sm); font-weight: 500; }
|
|
319
402
|
.lct-budget-percent { font-size: var(--fs-md); font-variant-numeric: tabular-nums; font-weight: 700; }
|
|
320
403
|
.lct-budget-marker {
|
|
321
|
-
border-left: 2px dashed
|
|
404
|
+
border-left: 2px dashed var(--lct-chart-marker);
|
|
322
405
|
bottom: 0;
|
|
323
406
|
position: absolute;
|
|
324
407
|
top: 0;
|
|
@@ -335,7 +418,7 @@
|
|
|
335
418
|
}
|
|
336
419
|
.lct-budget-projection strong { color: var(--lct-text); }
|
|
337
420
|
.lct-budget-projection-status { font-weight: 600; }
|
|
338
|
-
.lct-budget-projection-status--over { color:
|
|
421
|
+
.lct-budget-projection-status--over { color: var(--lct-warning-copy); }
|
|
339
422
|
.lct-budget-projection-status--under { color: var(--lct-muted); }
|
|
340
423
|
.lct-budget-meta { color: var(--lct-muted); font-size: var(--fs-xs); }
|
|
341
424
|
|
|
@@ -344,7 +427,7 @@
|
|
|
344
427
|
.lct-chart-line-secondary {
|
|
345
428
|
fill: none;
|
|
346
429
|
opacity: 0.75;
|
|
347
|
-
stroke:
|
|
430
|
+
stroke: var(--lct-chart-secondary);
|
|
348
431
|
stroke-dasharray: 5 4;
|
|
349
432
|
stroke-linejoin: round;
|
|
350
433
|
stroke-linecap: round;
|
|
@@ -365,7 +448,7 @@
|
|
|
365
448
|
.lct-chart-legend-compare { align-items: center; display: inline-flex; gap: 12px; }
|
|
366
449
|
.lct-chart-key { align-items: center; display: inline-flex; gap: 6px; }
|
|
367
450
|
.lct-chart-key-line { background: var(--lct-accent); border-radius: 999px; display: inline-block; height: 2px; width: 16px; }
|
|
368
|
-
.lct-chart-key-line-secondary { background:
|
|
451
|
+
.lct-chart-key-line-secondary { background: var(--lct-chart-secondary); position: relative; }
|
|
369
452
|
.lct-chart-key-line-secondary::after {
|
|
370
453
|
background: linear-gradient(90deg, transparent 40%, var(--lct-bg) 40%, var(--lct-bg) 60%, transparent 60%);
|
|
371
454
|
content: "";
|
|
@@ -399,15 +482,21 @@
|
|
|
399
482
|
.lct-tag-chip-more { background: transparent; border: 1px dashed var(--lct-border); color: var(--lct-muted); }
|
|
400
483
|
.lct-tag-empty { color: var(--lct-muted); font-size: var(--fs-sm); font-style: italic; }
|
|
401
484
|
|
|
402
|
-
.lct-empty {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
485
|
+
.lct-empty {
|
|
486
|
+
padding: 56px 28px;
|
|
487
|
+
text-align: center;
|
|
488
|
+
background:
|
|
489
|
+
radial-gradient(ellipse 60% 50% at 50% 0%, rgba(99, 91, 255, 0.04), transparent 70%),
|
|
490
|
+
var(--lct-panel);
|
|
491
|
+
}
|
|
492
|
+
.lct-state-title { font-size: var(--fs-lg); font-weight: 600; margin: 0 0 6px; letter-spacing: -0.005em; }
|
|
493
|
+
.lct-state-copy { color: var(--lct-muted); margin: 0 auto; max-width: 480px; font-size: var(--fs-sm); line-height: 1.55; }
|
|
494
|
+
.lct-state-actions { display: flex; gap: 8px; justify-content: center; margin-top: 20px; }
|
|
406
495
|
|
|
407
496
|
.lct-toolbar {
|
|
408
|
-
background:
|
|
497
|
+
background: var(--lct-toolbar-bg);
|
|
409
498
|
backdrop-filter: blur(10px);
|
|
410
|
-
border-color:
|
|
499
|
+
border-color: var(--lct-toolbar-border);
|
|
411
500
|
box-shadow: none;
|
|
412
501
|
padding: 16px;
|
|
413
502
|
margin-bottom: 16px;
|
|
@@ -429,52 +518,124 @@
|
|
|
429
518
|
.lct-toolbar-actions .lct-button,
|
|
430
519
|
.lct-banner .lct-button { font-size: var(--fs-sm); height: 32px; padding: 0 12px; }
|
|
431
520
|
|
|
432
|
-
.lct-filters { display:
|
|
521
|
+
.lct-filters { display: block; }
|
|
433
522
|
|
|
434
|
-
.lct-filter-row {
|
|
435
|
-
|
|
436
|
-
|
|
523
|
+
.lct-filter-row {
|
|
524
|
+
align-items: flex-end;
|
|
525
|
+
display: flex;
|
|
526
|
+
flex-wrap: wrap;
|
|
527
|
+
gap: 12px;
|
|
528
|
+
}
|
|
437
529
|
|
|
438
530
|
.lct-filter-actions {
|
|
439
|
-
align-items: end;
|
|
531
|
+
align-items: flex-end;
|
|
440
532
|
display: flex;
|
|
441
533
|
flex-wrap: wrap;
|
|
442
534
|
gap: 8px;
|
|
443
|
-
|
|
535
|
+
margin-left: auto;
|
|
444
536
|
}
|
|
445
537
|
|
|
446
|
-
.lct-field {
|
|
538
|
+
.lct-field {
|
|
539
|
+
display: flex;
|
|
540
|
+
flex: 1 1 160px;
|
|
541
|
+
flex-direction: column;
|
|
542
|
+
gap: 6px;
|
|
543
|
+
min-width: 140px;
|
|
544
|
+
}
|
|
447
545
|
|
|
448
546
|
.lct-field input,
|
|
449
547
|
.lct-field select {
|
|
450
|
-
|
|
548
|
+
appearance: none;
|
|
549
|
+
-webkit-appearance: none;
|
|
550
|
+
-moz-appearance: none;
|
|
551
|
+
background: var(--lct-panel);
|
|
552
|
+
border: 1px solid var(--lct-border);
|
|
451
553
|
border-radius: 8px;
|
|
554
|
+
box-sizing: border-box;
|
|
452
555
|
color: var(--lct-text);
|
|
453
556
|
font-family: inherit;
|
|
454
557
|
font-size: var(--fs-md);
|
|
455
|
-
background: var(--lct-panel);
|
|
456
|
-
box-shadow: inset 0 0 0 1px var(--lct-border);
|
|
457
|
-
box-sizing: border-box;
|
|
458
558
|
height: var(--lct-control-height);
|
|
459
559
|
line-height: 1.2;
|
|
460
560
|
padding: 0 12px;
|
|
461
561
|
width: 100%;
|
|
462
562
|
}
|
|
463
563
|
|
|
464
|
-
.lct-field
|
|
564
|
+
.lct-field select {
|
|
565
|
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23697386' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M2.5 4.5l3.5 3.5 3.5-3.5'/></svg>");
|
|
566
|
+
background-position: right 10px center;
|
|
567
|
+
background-repeat: no-repeat;
|
|
568
|
+
background-size: 12px 12px;
|
|
569
|
+
padding-right: 32px;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.lct-field input[type="date"] {
|
|
573
|
+
font-variant-numeric: tabular-nums;
|
|
574
|
+
padding-right: 8px;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.lct-field input[type="date"]::-webkit-calendar-picker-indicator {
|
|
578
|
+
cursor: pointer;
|
|
579
|
+
filter: invert(45%) sepia(8%) saturate(437%) hue-rotate(176deg) brightness(95%) contrast(86%);
|
|
580
|
+
opacity: 0.85;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.lct-field input::placeholder { color: var(--lct-muted); }
|
|
584
|
+
|
|
585
|
+
.lct-field input:hover,
|
|
586
|
+
.lct-field select:hover { border-color: var(--lct-border-strong); }
|
|
465
587
|
|
|
466
588
|
.lct-field input:focus,
|
|
467
589
|
.lct-field select:focus {
|
|
468
|
-
|
|
590
|
+
border-color: var(--lct-accent);
|
|
591
|
+
box-shadow: var(--lct-focus-ring);
|
|
469
592
|
outline: none;
|
|
470
593
|
}
|
|
471
594
|
|
|
595
|
+
.lct-results-toolbar {
|
|
596
|
+
align-items: center;
|
|
597
|
+
display: flex;
|
|
598
|
+
flex-wrap: wrap;
|
|
599
|
+
gap: 12px;
|
|
600
|
+
margin: -4px 0 8px;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.lct-results-toolbar .lct-pagination-per { order: 0; margin-right: auto; }
|
|
604
|
+
.lct-results-toolbar .lct-sort { order: 1; margin-left: auto; }
|
|
605
|
+
|
|
606
|
+
.lct-sort {
|
|
607
|
+
align-items: center;
|
|
608
|
+
background: var(--lct-surface);
|
|
609
|
+
border-radius: 6px;
|
|
610
|
+
display: inline-flex;
|
|
611
|
+
flex-wrap: wrap;
|
|
612
|
+
gap: 2px;
|
|
613
|
+
padding: 2px;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.lct-sort-option {
|
|
617
|
+
border-radius: 4px;
|
|
618
|
+
color: var(--lct-muted);
|
|
619
|
+
display: inline-block;
|
|
620
|
+
font-size: var(--fs-xs);
|
|
621
|
+
font-weight: 500;
|
|
622
|
+
padding: 4px 8px;
|
|
623
|
+
text-decoration: none;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.lct-sort-option:hover { background: var(--lct-panel); color: var(--lct-text); }
|
|
627
|
+
.lct-sort-option.is-active {
|
|
628
|
+
background: var(--lct-panel);
|
|
629
|
+
box-shadow: var(--lct-shadow);
|
|
630
|
+
color: var(--lct-text);
|
|
631
|
+
}
|
|
632
|
+
|
|
472
633
|
.lct-button {
|
|
473
634
|
align-items: center;
|
|
474
635
|
background: var(--lct-accent);
|
|
475
636
|
border: 1px solid var(--lct-accent);
|
|
476
637
|
border-radius: 8px;
|
|
477
|
-
box-shadow:
|
|
638
|
+
box-shadow: var(--lct-shadow-sm);
|
|
478
639
|
color: #ffffff;
|
|
479
640
|
cursor: pointer;
|
|
480
641
|
display: inline-flex;
|
|
@@ -486,17 +647,26 @@
|
|
|
486
647
|
height: var(--lct-control-height);
|
|
487
648
|
padding: 0 14px;
|
|
488
649
|
text-decoration: none;
|
|
489
|
-
transition: background-color 0.12s ease, border-color 0.12s ease;
|
|
650
|
+
transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease, transform 0.08s ease;
|
|
490
651
|
white-space: nowrap;
|
|
491
652
|
}
|
|
492
653
|
|
|
493
|
-
.lct-button:hover {
|
|
654
|
+
.lct-button:hover {
|
|
655
|
+
background: var(--lct-accent-hover);
|
|
656
|
+
border-color: var(--lct-accent-hover);
|
|
657
|
+
box-shadow: var(--lct-shadow-md);
|
|
658
|
+
transform: translateY(-1px);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.lct-button:active {
|
|
662
|
+
transform: translateY(0);
|
|
663
|
+
box-shadow: var(--lct-shadow-sm);
|
|
664
|
+
}
|
|
494
665
|
|
|
495
666
|
.lct-button:focus-visible,
|
|
496
667
|
.lct-chip-remove:focus-visible,
|
|
497
668
|
.lct-clear-link:focus-visible,
|
|
498
669
|
.lct-nav a:focus-visible {
|
|
499
|
-
border-radius: 6px;
|
|
500
670
|
box-shadow: var(--lct-focus-ring);
|
|
501
671
|
outline: none;
|
|
502
672
|
}
|
|
@@ -508,7 +678,15 @@
|
|
|
508
678
|
}
|
|
509
679
|
.lct-button-secondary:hover {
|
|
510
680
|
background: var(--lct-row-hover);
|
|
511
|
-
border-color: var(--lct-border);
|
|
681
|
+
border-color: var(--lct-border-strong);
|
|
682
|
+
}
|
|
683
|
+
.lct-button-secondary:active {
|
|
684
|
+
background: var(--lct-surface);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
@media (prefers-reduced-motion: reduce) {
|
|
688
|
+
.lct-button { transition: none; }
|
|
689
|
+
.lct-button:hover { transform: none; }
|
|
512
690
|
}
|
|
513
691
|
|
|
514
692
|
.lct-button-compact {
|
|
@@ -536,7 +714,7 @@
|
|
|
536
714
|
}
|
|
537
715
|
|
|
538
716
|
.lct-chip-remove {
|
|
539
|
-
color:
|
|
717
|
+
color: var(--lct-chip-remove);
|
|
540
718
|
font-size: var(--fs-lg);
|
|
541
719
|
line-height: 1;
|
|
542
720
|
text-decoration: none;
|
|
@@ -586,14 +764,15 @@
|
|
|
586
764
|
|
|
587
765
|
.lct-pagination-info strong { color: var(--lct-text); font-weight: 600; }
|
|
588
766
|
|
|
589
|
-
.lct-pagination-per { display: inline-flex; align-items: center; gap:
|
|
590
|
-
.lct-pagination-per-label { color: var(--lct-muted); font-size: var(--fs-
|
|
767
|
+
.lct-pagination-per { display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; }
|
|
768
|
+
.lct-pagination-per-label { color: var(--lct-muted); font-size: var(--fs-xs); font-weight: 500; }
|
|
591
769
|
|
|
592
770
|
.lct-pagination-per-option {
|
|
593
771
|
color: var(--lct-muted);
|
|
594
772
|
text-decoration: none;
|
|
595
773
|
padding: 2px 8px;
|
|
596
774
|
border-radius: 999px;
|
|
775
|
+
font-size: var(--fs-xs);
|
|
597
776
|
font-variant-numeric: tabular-nums;
|
|
598
777
|
}
|
|
599
778
|
|
|
@@ -651,7 +830,6 @@
|
|
|
651
830
|
|
|
652
831
|
.lct-pagination-nav > *:last-child { border-right: 0; }
|
|
653
832
|
|
|
654
|
-
.lct-tags { display: inline-block; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
655
833
|
.lct-nowrap { white-space: nowrap; }
|
|
656
834
|
|
|
657
835
|
.lct-detail-grid { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
|
@@ -663,7 +841,7 @@
|
|
|
663
841
|
margin: 0;
|
|
664
842
|
}
|
|
665
843
|
|
|
666
|
-
.lct-dl dd { margin: 0; }
|
|
844
|
+
.lct-dl dd { margin: 0; min-width: 0; overflow-wrap: anywhere; word-break: break-word; }
|
|
667
845
|
|
|
668
846
|
.lct-pre {
|
|
669
847
|
background: #0f172a;
|
|
@@ -740,8 +918,7 @@
|
|
|
740
918
|
.lct-filters { gap: 12px; }
|
|
741
919
|
|
|
742
920
|
.lct-filter-row,
|
|
743
|
-
.lct-filter-row-basic
|
|
744
|
-
.lct-filter-row-with-sort { align-items: stretch; grid-template-columns: 1fr; }
|
|
921
|
+
.lct-filter-row-basic { align-items: stretch; grid-template-columns: 1fr; }
|
|
745
922
|
|
|
746
923
|
.lct-filter-actions { justify-content: flex-start; width: 100%; }
|
|
747
924
|
|
|
@@ -11,30 +11,43 @@ module LlmCostTracker
|
|
|
11
11
|
rescue_from ActiveRecord::StatementInvalid, with: :render_database_error
|
|
12
12
|
rescue_from LlmCostTracker::InvalidFilterError, with: :render_invalid_filter
|
|
13
13
|
|
|
14
|
+
SCHEMA_CHECKS = [
|
|
15
|
+
[
|
|
16
|
+
LlmCostTracker::Ledger::Schema::Calls,
|
|
17
|
+
"The llm_cost_tracker_calls table does not match the current LLM Cost Tracker schema."
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
LlmCostTracker::Ledger::Schema::CallRollups,
|
|
21
|
+
"The llm_cost_tracker_call_rollups table does not match the current LLM Cost Tracker schema."
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
LlmCostTracker::Ledger::Schema::CallLineItems,
|
|
25
|
+
"The llm_cost_tracker_call_line_items table does not match the current LLM Cost Tracker schema."
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
LlmCostTracker::Ledger::Schema::CallTags,
|
|
29
|
+
"The llm_cost_tracker_call_tags table does not match the current LLM Cost Tracker schema."
|
|
30
|
+
]
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
private_constant :SCHEMA_CHECKS
|
|
34
|
+
|
|
14
35
|
private
|
|
15
36
|
|
|
16
37
|
def ensure_current_schema
|
|
17
|
-
unless LlmCostTracker::
|
|
18
|
-
@setup_message = "The
|
|
38
|
+
unless LlmCostTracker::Call.table_exists?
|
|
39
|
+
@setup_message = "The llm_cost_tracker_calls table is not available yet."
|
|
19
40
|
return render template: "llm_cost_tracker/shared/setup_required"
|
|
20
41
|
end
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@setup_details = schema_errors
|
|
26
|
-
render template: "llm_cost_tracker/shared/setup_required"
|
|
27
|
-
return
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
period_total_errors = LlmCostTracker::Ledger::Schema::PeriodTotals.current_schema_errors
|
|
31
|
-
return if period_total_errors.empty?
|
|
43
|
+
SCHEMA_CHECKS.each do |schema, message|
|
|
44
|
+
errors = schema.current_schema_errors
|
|
45
|
+
next if errors.empty?
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
render template: "llm_cost_tracker/shared/setup_required"
|
|
47
|
+
@setup_message = message
|
|
48
|
+
@setup_details = errors + ["See docs/upgrading.md for the migration path."]
|
|
49
|
+
return render template: "llm_cost_tracker/shared/setup_required"
|
|
50
|
+
end
|
|
38
51
|
end
|
|
39
52
|
|
|
40
53
|
def render_database_error(error)
|
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
class AssetsController < ActionController::Base
|
|
5
5
|
def stylesheet
|
|
6
|
-
response.set_header("Cache-Control",
|
|
6
|
+
response.set_header("Cache-Control", cache_control_header)
|
|
7
7
|
send_file LlmCostTracker::Assets::STYLESHEET_PATH, type: "text/css", disposition: "inline"
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def cache_control_header
|
|
13
|
+
if Rails.env.development?
|
|
14
|
+
"no-store"
|
|
15
|
+
else
|
|
16
|
+
"public, max-age=31536000, immutable"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
9
19
|
end
|
|
10
20
|
end
|
|
@@ -19,7 +19,7 @@ module LlmCostTracker
|
|
|
19
19
|
format.html do
|
|
20
20
|
@page = Dashboard::Pagination.call(params)
|
|
21
21
|
@calls_count = scope.count
|
|
22
|
-
@calls = ordered_scope.limit(@page.limit).offset(@page.offset).to_a
|
|
22
|
+
@calls = ordered_scope.includes(:tag_records).limit(@page.limit).offset(@page.offset).to_a
|
|
23
23
|
end
|
|
24
24
|
format.csv do
|
|
25
25
|
send_data render_csv(ordered_scope.limit(CSV_EXPORT_LIMIT)),
|
|
@@ -30,7 +30,7 @@ module LlmCostTracker
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def show
|
|
33
|
-
@call =
|
|
33
|
+
@call = LlmCostTracker::Call.find(params[:id])
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private
|
|
@@ -55,33 +55,38 @@ module LlmCostTracker
|
|
|
55
55
|
CSV.generate do |csv|
|
|
56
56
|
csv << fields.map(&:to_s)
|
|
57
57
|
|
|
58
|
-
relation.
|
|
59
|
-
csv << fields.
|
|
58
|
+
relation.includes(:tag_records).each do |call|
|
|
59
|
+
csv << fields.map { |field| csv_value(field, call) }
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def csv_fields
|
|
65
65
|
%i[tracked_at provider model] +
|
|
66
|
-
TokenUsage
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
TokenUsage.members +
|
|
67
|
+
%i[
|
|
68
|
+
total_cost cost_status pricing_snapshot latency_ms provider_response_id provider_project_id
|
|
69
|
+
provider_api_key_id provider_workspace_id batch tags
|
|
70
|
+
]
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
def csv_value(field,
|
|
73
|
+
def csv_value(field, call)
|
|
72
74
|
case field
|
|
73
75
|
when :tracked_at
|
|
74
|
-
|
|
75
|
-
when :provider, :model, :provider_response_id
|
|
76
|
-
|
|
76
|
+
call.tracked_at&.utc&.iso8601
|
|
77
|
+
when :provider, :model, :provider_response_id, :provider_project_id, :provider_api_key_id,
|
|
78
|
+
:provider_workspace_id, :cost_status
|
|
79
|
+
csv_safe(call[field])
|
|
80
|
+
when :pricing_snapshot
|
|
81
|
+
csv_safe(csv_json(call.pricing_snapshot))
|
|
77
82
|
when :tags
|
|
78
|
-
csv_safe(
|
|
83
|
+
csv_safe(call.parsed_tags.to_json)
|
|
79
84
|
else
|
|
80
|
-
|
|
85
|
+
call[field]
|
|
81
86
|
end
|
|
82
87
|
end
|
|
83
88
|
|
|
84
|
-
def
|
|
89
|
+
def csv_json(value)
|
|
85
90
|
return value.transform_keys(&:to_s).to_json if value.is_a?(Hash)
|
|
86
91
|
|
|
87
92
|
JSON.parse(value || "{}").to_json
|
|
@@ -5,9 +5,17 @@ module LlmCostTracker
|
|
|
5
5
|
def index
|
|
6
6
|
scope = Dashboard::Filter.call(params: params)
|
|
7
7
|
@stats = Dashboard::DataQuality.call(scope: scope)
|
|
8
|
-
@
|
|
8
|
+
@summary = Dashboard::DataQuality.summary(@stats)
|
|
9
|
+
@usage_rows = Dashboard::DataQuality.usage_rows(
|
|
10
|
+
@stats,
|
|
11
|
+
component_costs: Dashboard::DataQuality.component_costs(scope)
|
|
12
|
+
)
|
|
9
13
|
@hidden_output_summary = Dashboard::DataQuality.hidden_output_summary(@stats)
|
|
10
|
-
@unknown_pricing_by_model = Dashboard::DataQuality.unknown_pricing_by_model(
|
|
14
|
+
@unknown_pricing_by_model = Dashboard::DataQuality.unknown_pricing_by_model(
|
|
15
|
+
scope,
|
|
16
|
+
total_calls: @summary.total
|
|
17
|
+
)
|
|
18
|
+
@service_charge_rows = Dashboard::DataQuality.service_charge_rows(scope).to_a
|
|
11
19
|
end
|
|
12
20
|
end
|
|
13
21
|
end
|