diff --git a/grafana/dashboards/claude-usage.json b/grafana/dashboards/claude-usage.json index 5cab598cf3..b2933c4bcc 100644 --- a/grafana/dashboards/claude-usage.json +++ b/grafana/dashboards/claude-usage.json @@ -4,10 +4,10 @@ "tags": ["gittensory", "claude", "ai"], "timezone": "browser", "schemaVersion": 39, - "version": 3, + "version": 4, "refresh": "30s", "time": { "from": "now-7d", "to": "now" }, - "description": "Claude Code review-CLI usage via OpenTelemetry. Each headless review is a session-scoped cumulative counter, so totals use last_over_time (sum of each session's final value) — increase()/rate() would read flat because a session exports its total in one shot.", + "description": "Claude Code CLI/session telemetry via OpenTelemetry — NOT the durable ORB ai_usage_events DB attribution (see the 'Gittensory - ORB AI usage' dashboard for that, filterable by provider/model/repo/PR). Each headless review is a session-scoped cumulative counter, so totals use last_over_time (sum of each session's final value) — increase()/rate() would read flat because a session exports its total in one shot.", "templating": { "list": [ { diff --git a/grafana/dashboards/codex-usage.json b/grafana/dashboards/codex-usage.json index c314967c8e..72e67ac105 100644 --- a/grafana/dashboards/codex-usage.json +++ b/grafana/dashboards/codex-usage.json @@ -4,10 +4,10 @@ "tags": ["gittensory", "codex", "ai"], "timezone": "browser", "schemaVersion": 39, - "version": 1, + "version": 4, "refresh": "30s", "time": { "from": "now-7d", "to": "now" }, - "description": "Codex review observability for the self-hosted stack. Prometheus panels are subscription-CLI attempt counters for the selected range; durable review records come from ai_usage_events and must be read with their status. USD cost appears only when the Codex CLI emits a cost field.", + "description": "Codex review observability for the self-hosted stack. Prometheus panels are subscription-CLI attempt counters for the selected range. Durable review records come from ai_usage_events (migration 0109) and must be read with their status. Two different filters are used deliberately: the token/cost panels (real usage sums) filter strictly on provider='codex', since an unattributed row always contributes 0 to a sum either way. The record-count/status/recent-events panels instead match 'codex' as a reviewer-name token in either provider OR the model label (\"+\"-joined reviewer names, e.g. 'codex', 'codex:o4-mini', 'codex+claude-code') — a quota-blocked call never gets provider attribution (no call ever ran) but IS attributable via its configured model label, so filtering by provider alone silently drops every quota_exceeded row for a codex-configured repo. estimated_neurons is a heuristic budget proxy, not billing-grade accounting.", "panels": [ { "id": 1, @@ -52,7 +52,7 @@ "id": 5, "type": "stat", "title": "Successful review records", - "description": "Durable Codex-attributed ai_review_pr records with status=ok.", + "description": "Durable ai_review_pr records with status=ok where codex was the reviewer — matched by provider attribution OR the configured model label (see dashboard description), so this stays consistent with the status-breakdown panel below rather than under-counting successes where the CLI's response reported no usage envelope at all.", "gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 }, "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "fixed", "fixedColor": "green" } } }, @@ -61,8 +61,8 @@ { "refId": "A", "queryType": "table", - "queryText": "SELECT count(*) AS reviews FROM ai_usage_events WHERE feature = 'ai_review_pr' AND status = 'ok' AND (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%')", - "rawQueryText": "SELECT count(*) AS reviews FROM ai_usage_events WHERE feature = 'ai_review_pr' AND status = 'ok' AND (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%')" + "queryText": "SELECT count(*) AS reviews FROM ai_usage_events WHERE feature = 'ai_review_pr' AND status = 'ok' AND (provider = 'codex' OR (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%'))", + "rawQueryText": "SELECT count(*) AS reviews FROM ai_usage_events WHERE feature = 'ai_review_pr' AND status = 'ok' AND (provider = 'codex' OR (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%'))" } ] }, @@ -118,18 +118,19 @@ { "id": 10, "type": "timeseries", - "title": "Estimated neuron usage by day", + "title": "Durable token usage by day", + "description": "Real input/output tokens from ai_usage_events (migration 0109), populated when the Codex CLI reports usage. Replaces the old estimated-neuron-only proxy for this panel. Filters strictly on provider='codex' (not the broader model-label match used by the status/recent-events panels below): a quota-blocked or otherwise unattributed row contributes 0 tokens regardless of which filter is used, so the stricter filter is equivalent here and simpler.", "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, - "fieldConfig": { "defaults": { "unit": "short", "custom": { "drawStyle": "bars", "fillOpacity": 70, "lineWidth": 1 } } }, + "fieldConfig": { "defaults": { "unit": "short", "custom": { "drawStyle": "bars", "fillOpacity": 70, "lineWidth": 1, "stacking": { "mode": "normal" } } } }, "options": { "legend": { "showLegend": true, "placement": "bottom" }, "tooltip": { "mode": "multi" } }, "targets": [ { "refId": "A", "queryType": "time series", "timeColumns": ["time"], - "queryText": "SELECT date(created_at) AS time, sum(estimated_neurons) AS estimated_neurons FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%') GROUP BY date(created_at) ORDER BY time", - "rawQueryText": "SELECT date(created_at) AS time, sum(estimated_neurons) AS estimated_neurons FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%') GROUP BY date(created_at) ORDER BY time" + "queryText": "SELECT date(created_at) AS time, coalesce(sum(input_tokens), 0) AS input_tokens, coalesce(sum(output_tokens), 0) AS output_tokens FROM ai_usage_events WHERE feature = 'ai_review_pr' AND provider = 'codex' GROUP BY date(created_at) ORDER BY time", + "rawQueryText": "SELECT date(created_at) AS time, coalesce(sum(input_tokens), 0) AS input_tokens, coalesce(sum(output_tokens), 0) AS output_tokens FROM ai_usage_events WHERE feature = 'ai_review_pr' AND provider = 'codex' GROUP BY date(created_at) ORDER BY time" } ] }, @@ -137,6 +138,7 @@ "id": 11, "type": "piechart", "title": "Review record status", + "description": "Includes quota_exceeded rows for a codex-configured repo: those rows never get provider attribution (the call never ran), but the model label already reflects the configured reviewer at write time (record() computes it via reviewerModelLabel() regardless of status), so matching 'codex' as a reviewer-name token there recovers them. A plain provider='codex' filter would silently make every quota-blocked codex review invisible here.", "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "fieldConfig": { "defaults": { "unit": "short" } }, @@ -145,8 +147,27 @@ { "refId": "A", "queryType": "table", - "queryText": "SELECT status, count(*) AS count FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%') GROUP BY status ORDER BY count DESC", - "rawQueryText": "SELECT status, count(*) AS count FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%') GROUP BY status ORDER BY count DESC" + "queryText": "SELECT status, count(*) AS count FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (provider = 'codex' OR (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%')) GROUP BY status ORDER BY count DESC", + "rawQueryText": "SELECT status, count(*) AS count FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (provider = 'codex' OR (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%')) GROUP BY status ORDER BY count DESC" + } + ] + }, + { + "id": 13, + "type": "timeseries", + "title": "Durable cost by day", + "description": "Real USD cost from ai_usage_events (migration 0109), populated when the Codex CLI reports a cost field. Filters strictly on provider='codex' for the same reason as the token-usage panel above — an unattributed row contributes $0 regardless of filter.", + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "fieldConfig": { "defaults": { "unit": "currencyUSD", "decimals": 4, "custom": { "drawStyle": "bars", "fillOpacity": 70, "lineWidth": 1 } } }, + "options": { "legend": { "showLegend": true, "placement": "bottom" }, "tooltip": { "mode": "multi" } }, + "targets": [ + { + "refId": "A", + "queryType": "time series", + "timeColumns": ["time"], + "queryText": "SELECT date(created_at) AS time, coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE feature = 'ai_review_pr' AND provider = 'codex' GROUP BY date(created_at) ORDER BY time", + "rawQueryText": "SELECT date(created_at) AS time, coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE feature = 'ai_review_pr' AND provider = 'codex' GROUP BY date(created_at) ORDER BY time" } ] }, @@ -154,7 +175,8 @@ "id": 12, "type": "table", "title": "Recent Codex-attributed review events", - "gridPos": { "h": 10, "w": 24, "x": 0, "y": 24 }, + "description": "Includes quota-blocked codex-configured rows (matched via the model label, not just provider attribution — see the status-breakdown panel's description) so an operator can see recent failures/blocks, not only rows with a completed usage response. The provider column itself is not displayed here; the model column already reflects the configured reviewer for every row, including blocked ones.", + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 32 }, "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "options": { "showHeader": true, "cellHeight": "sm", "sortBy": [{ "displayName": "created_at", "desc": true }] }, "fieldConfig": { "defaults": { "custom": { "filterable": true } } }, @@ -162,8 +184,8 @@ { "refId": "A", "queryType": "table", - "queryText": "SELECT created_at, model, status, estimated_neurons, detail, json_extract(metadata_json, '$.repoFullName') AS repo, json_extract(metadata_json, '$.pullNumber') AS pr FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%') ORDER BY created_at DESC LIMIT 100", - "rawQueryText": "SELECT created_at, model, status, estimated_neurons, detail, json_extract(metadata_json, '$.repoFullName') AS repo, json_extract(metadata_json, '$.pullNumber') AS pr FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%') ORDER BY created_at DESC LIMIT 100" + "queryText": "SELECT created_at, model, effort, status, input_tokens, output_tokens, total_tokens, cost_usd, estimated_neurons, detail, json_extract(metadata_json, '$.repoFullName') AS repo, json_extract(metadata_json, '$.pullNumber') AS pr FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (provider = 'codex' OR (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%')) ORDER BY created_at DESC LIMIT 100", + "rawQueryText": "SELECT created_at, model, effort, status, input_tokens, output_tokens, total_tokens, cost_usd, estimated_neurons, detail, json_extract(metadata_json, '$.repoFullName') AS repo, json_extract(metadata_json, '$.pullNumber') AS pr FROM ai_usage_events WHERE feature = 'ai_review_pr' AND (provider = 'codex' OR (('+' || model || '+') LIKE '%+codex+%' OR ('+' || model || '+') LIKE '%+codex:%')) ORDER BY created_at DESC LIMIT 100" } ] } diff --git a/grafana/dashboards/orb-ai-usage.json b/grafana/dashboards/orb-ai-usage.json new file mode 100644 index 0000000000..823163314b --- /dev/null +++ b/grafana/dashboards/orb-ai-usage.json @@ -0,0 +1,169 @@ +{ + "uid": "gittensory-orb-ai-usage", + "title": "Gittensory - ORB AI usage", + "tags": ["gittensory", "ai", "orb"], + "timezone": "browser", + "schemaVersion": 39, + "version": 4, + "refresh": "30s", + "time": { "from": "now-7d", "to": "now" }, + "description": "Durable, provider-neutral AI usage from ai_usage_events (migration 0109) — every configured provider (Codex, Claude Code, OpenAI-compatible, Ollama, Anthropic BYOK), not just Codex. Every AI feature (ai_review_pr, agent_private_summary, agent_public_summary, pr_intelligence_comment, ai_slop_pr, issue_plan) threads real usage through recordAiUsageEvent on a completed call; a row still lands as estimated_neurons-only when the call never happened (quota_exceeded/disabled/unavailable), is a BYOK call (not yet usage-tracked), or the provider omitted a parseable usage field — see the 'Events missing real usage' panel. See 'Gittensory - Codex usage (self-host)' for a Codex-focused view and 'Gittensory — Claude usage (OTEL)' for Claude Code's own CLI/session telemetry (a separate, non-durable data source).", + "panels": [ + { + "id": 1, + "type": "row", + "title": "Summary (all providers)", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 } + }, + { + "id": 2, + "type": "stat", + "title": "Total AI events", + "description": "Every ai_usage_events row with a non-null provider (i.e. a real, attributed AI call), across all features.", + "gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "fixed", "fixedColor": "blue" } } }, + "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "justifyMode": "center" }, + "targets": [ + { + "refId": "A", + "queryType": "table", + "queryText": "SELECT count(*) AS events FROM ai_usage_events WHERE provider IS NOT NULL", + "rawQueryText": "SELECT count(*) AS events FROM ai_usage_events WHERE provider IS NOT NULL" + } + ] + }, + { + "id": 3, + "type": "stat", + "title": "Total tokens", + "gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "fixed", "fixedColor": "purple" } } }, + "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "justifyMode": "center" }, + "targets": [ + { + "refId": "A", + "queryType": "table", + "queryText": "SELECT coalesce(sum(total_tokens), 0) AS total_tokens FROM ai_usage_events WHERE provider IS NOT NULL", + "rawQueryText": "SELECT coalesce(sum(total_tokens), 0) AS total_tokens FROM ai_usage_events WHERE provider IS NOT NULL" + } + ] + }, + { + "id": 4, + "type": "stat", + "title": "Total cost", + "gridPos": { "h": 5, "w": 6, "x": 12, "y": 1 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "fieldConfig": { "defaults": { "unit": "currencyUSD", "decimals": 4, "color": { "mode": "fixed", "fixedColor": "green" } } }, + "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "justifyMode": "center" }, + "targets": [ + { + "refId": "A", + "queryType": "table", + "queryText": "SELECT coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE provider IS NOT NULL", + "rawQueryText": "SELECT coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE provider IS NOT NULL" + } + ] + }, + { + "id": 5, + "type": "stat", + "title": "Events missing real usage", + "description": "Completed AI calls (status='ok', a response was actually returned) with no real usage captured — either no provider attribution at all, or a provider attributed with input_tokens/output_tokens/total_tokens/cost_usd all still at their zero default. (input_tokens/output_tokens/total_tokens/cost_usd are NOT NULL DEFAULT 0 per migration 0109 — recordAiUsageEvent's finiteNumber() coercion means they can never be SQL NULL, so a zero-value check, not an IS NULL check, is the real \"no data\" signal here; coerceAiUsage extracts provider independently of the numeric fields, so a provider name with no parseable token counts is a real, reachable row shape.) Excludes quota_exceeded/disabled/unavailable rows (no call was attempted, so no usage was ever possible) and ai_key_change audit rows (not an AI call at all — recordAiKeyChange's status is always \"set\"/\"replace\"/\"delete\", never \"ok\", but the feature is excluded explicitly here too so this panel doesn't rely on that invariant holding in another file). A nonzero count here means either a BYOK call (callAiProvider does not yet capture usage) or a provider response that omitted a parseable usage field.", + "gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "fieldConfig": { "defaults": { "unit": "short", "color": { "mode": "fixed", "fixedColor": "orange" } } }, + "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none", "justifyMode": "center" }, + "targets": [ + { + "refId": "A", + "queryType": "table", + "queryText": "SELECT count(*) AS unattributed FROM ai_usage_events WHERE status = 'ok' AND feature <> 'ai_key_change' AND (provider IS NULL OR (input_tokens = 0 AND output_tokens = 0 AND total_tokens = 0 AND cost_usd = 0))", + "rawQueryText": "SELECT count(*) AS unattributed FROM ai_usage_events WHERE status = 'ok' AND feature <> 'ai_key_change' AND (provider IS NULL OR (input_tokens = 0 AND output_tokens = 0 AND total_tokens = 0 AND cost_usd = 0))" + } + ] + }, + { + "id": 6, + "type": "row", + "title": "By provider / model / effort", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 } + }, + { + "id": 7, + "type": "table", + "title": "Usage by provider", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "options": { "showHeader": true, "cellHeight": "sm", "sortBy": [{ "displayName": "events", "desc": true }] }, + "fieldConfig": { "defaults": { "custom": { "filterable": true } } }, + "targets": [ + { + "refId": "A", + "queryType": "table", + "queryText": "SELECT provider, count(*) AS events, coalesce(sum(input_tokens), 0) AS input_tokens, coalesce(sum(output_tokens), 0) AS output_tokens, coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE provider IS NOT NULL GROUP BY provider ORDER BY events DESC", + "rawQueryText": "SELECT provider, count(*) AS events, coalesce(sum(input_tokens), 0) AS input_tokens, coalesce(sum(output_tokens), 0) AS output_tokens, coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE provider IS NOT NULL GROUP BY provider ORDER BY events DESC" + } + ] + }, + { + "id": 8, + "type": "table", + "title": "Usage by provider + model + effort", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "options": { "showHeader": true, "cellHeight": "sm", "sortBy": [{ "displayName": "events", "desc": true }] }, + "fieldConfig": { "defaults": { "custom": { "filterable": true } } }, + "targets": [ + { + "refId": "A", + "queryType": "table", + "queryText": "SELECT provider, model, effort, count(*) AS events, coalesce(sum(total_tokens), 0) AS total_tokens, coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE provider IS NOT NULL GROUP BY provider, model, effort ORDER BY events DESC LIMIT 50", + "rawQueryText": "SELECT provider, model, effort, count(*) AS events, coalesce(sum(total_tokens), 0) AS total_tokens, coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE provider IS NOT NULL GROUP BY provider, model, effort ORDER BY events DESC LIMIT 50" + } + ] + }, + { + "id": 9, + "type": "row", + "title": "By repo / PR", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 15 } + }, + { + "id": 10, + "type": "table", + "title": "Top repos by AI usage", + "gridPos": { "h": 10, "w": 12, "x": 0, "y": 16 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "options": { "showHeader": true, "cellHeight": "sm", "sortBy": [{ "displayName": "events", "desc": true }] }, + "fieldConfig": { "defaults": { "custom": { "filterable": true } } }, + "targets": [ + { + "refId": "A", + "queryType": "table", + "queryText": "SELECT json_extract(metadata_json, '$.repoFullName') AS repo, provider, count(*) AS events, coalesce(sum(total_tokens), 0) AS total_tokens, coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE provider IS NOT NULL AND json_extract(metadata_json, '$.repoFullName') IS NOT NULL GROUP BY repo, provider ORDER BY events DESC LIMIT 50", + "rawQueryText": "SELECT json_extract(metadata_json, '$.repoFullName') AS repo, provider, count(*) AS events, coalesce(sum(total_tokens), 0) AS total_tokens, coalesce(sum(cost_usd), 0) AS cost_usd FROM ai_usage_events WHERE provider IS NOT NULL AND json_extract(metadata_json, '$.repoFullName') IS NOT NULL GROUP BY repo, provider ORDER BY events DESC LIMIT 50" + } + ] + }, + { + "id": 11, + "type": "table", + "title": "Recent AI events (all providers)", + "gridPos": { "h": 10, "w": 12, "x": 12, "y": 16 }, + "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, + "options": { "showHeader": true, "cellHeight": "sm", "sortBy": [{ "displayName": "created_at", "desc": true }] }, + "fieldConfig": { "defaults": { "custom": { "filterable": true } } }, + "targets": [ + { + "refId": "A", + "queryType": "table", + "queryText": "SELECT created_at, feature, provider, model, effort, status, input_tokens, output_tokens, total_tokens, cost_usd, json_extract(metadata_json, '$.repoFullName') AS repo, json_extract(metadata_json, '$.pullNumber') AS pr FROM ai_usage_events WHERE provider IS NOT NULL ORDER BY created_at DESC LIMIT 100", + "rawQueryText": "SELECT created_at, feature, provider, model, effort, status, input_tokens, output_tokens, total_tokens, cost_usd, json_extract(metadata_json, '$.repoFullName') AS repo, json_extract(metadata_json, '$.pullNumber') AS pr FROM ai_usage_events WHERE provider IS NOT NULL ORDER BY created_at DESC LIMIT 100" + } + ] + } + ] +}