Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 952fc3f

Browse files
committed
Add usage per user
1 parent 6f49653 commit 952fc3f

File tree

6 files changed

+103
-10
lines changed

6 files changed

+103
-10
lines changed

app/models/ai_api_audit_log.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
class AiApiAuditLog < ActiveRecord::Base
44
belongs_to :post
55
belongs_to :topic
6+
belongs_to :user
67

78
module Provider
89
OpenAI = 1

app/serializers/ai_usage_serializer.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
class AiUsageSerializer < ApplicationSerializer
4-
attributes :data, :features, :models, :summary
4+
attributes :data, :features, :models, :users, :summary
55

66
def data
77
object.tokens_by_period.as_json(
@@ -35,6 +35,19 @@ def models
3535
)
3636
end
3737

38+
def users
39+
object.user_breakdown.as_json(
40+
only: %i[
41+
username
42+
usage_count
43+
total_tokens
44+
total_cached_tokens
45+
total_request_tokens
46+
total_response_tokens
47+
],
48+
)
49+
end
50+
3851
def summary
3952
{
4053
total_tokens: object.total_tokens,

assets/javascripts/discourse/components/ai-usage.gjs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,22 @@ export default class AiUsage extends Component {
8888
return normalized;
8989
}
9090

91-
// Then modify the chartConfig getter to use this normalized data:
9291
get chartConfig() {
9392
if (!this.data?.data) {
9493
return;
9594
}
9695

9796
const normalizedData = this.normalizeTimeSeriesData(this.data.data);
9897

98+
const chartEl = document.querySelector(".ai-usage__chart");
99+
const computedStyle = getComputedStyle(chartEl);
100+
101+
const colors = {
102+
response: computedStyle.getPropertyValue("--chart-response-color").trim(),
103+
request: computedStyle.getPropertyValue("--chart-request-color").trim(),
104+
cached: computedStyle.getPropertyValue("--chart-cached-color").trim(),
105+
};
106+
99107
return {
100108
type: "bar",
101109
data: {
@@ -107,17 +115,19 @@ export default class AiUsage extends Component {
107115
{
108116
label: "Response Tokens",
109117
data: normalizedData.map((row) => row.total_response_tokens),
110-
backgroundColor: "rgba(75, 192, 192, 0.8)",
118+
backgroundColor: colors.response,
111119
},
112120
{
113-
label: "Request Tokens",
114-
data: normalizedData.map((row) => row.total_request_tokens),
115-
backgroundColor: "rgba(153, 102, 255, 0.8)",
121+
label: "Net Request Tokens",
122+
data: normalizedData.map(
123+
(row) => row.total_request_tokens - row.total_cached_tokens
124+
),
125+
backgroundColor: colors.request,
116126
},
117127
{
118-
label: "Cached Tokens",
128+
label: "Cached Request Tokens",
119129
data: normalizedData.map((row) => row.total_cached_tokens),
120-
backgroundColor: "rgba(255, 159, 64, 0.8)",
130+
backgroundColor: colors.cached,
121131
},
122132
],
123133
},
@@ -296,6 +306,37 @@ export default class AiUsage extends Component {
296306
</div>
297307

298308
<div class="ai-usage__breakdowns">
309+
310+
<div class="ai-usage__users">
311+
<h3 class="ai-usage__users-title">
312+
{{i18n "discourse_ai.usage.users_breakdown"}}
313+
</h3>
314+
<table class="ai-usage__users-table">
315+
<thead>
316+
<tr>
317+
<th>{{i18n "discourse_ai.usage.username"}}</th>
318+
<th>{{i18n "discourse_ai.usage.usage_count"}}</th>
319+
<th>{{i18n "discourse_ai.usage.total_tokens"}}</th>
320+
</tr>
321+
</thead>
322+
<tbody>
323+
{{#each this.data.users as |user|}}
324+
<tr class="ai-usage__users-row">
325+
<td
326+
class="ai-usage__users-cell"
327+
>{{user.username}}</td>
328+
<td
329+
class="ai-usage__users-cell"
330+
>{{user.usage_count}}</td>
331+
<td
332+
class="ai-usage__users-cell"
333+
>{{user.total_tokens}}</td>
334+
</tr>
335+
{{/each}}
336+
</tbody>
337+
</table>
338+
</div>
339+
299340
<div class="ai-usage__features">
300341
<h3 class="ai-usage__features-title">
301342
{{i18n "discourse_ai.usage.features_breakdown"}}

assets/stylesheets/modules/llms/common/usage.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
.ai-usage {
2+
--chart-response-color: rgba(75, 192, 192, 0.8);
3+
--chart-request-color: rgba(153, 102, 255, 0.8);
4+
--chart-cached-color: rgba(153, 102, 255, 0.4);
5+
26
padding: 1em;
37

48
&__filters-dates {
@@ -64,6 +68,10 @@
6468
margin-top: 2em;
6569
}
6670

71+
&__chart {
72+
position: relative;
73+
}
74+
6775
&__chart-container {
6876
margin-bottom: 2em;
6977
}
@@ -84,18 +92,21 @@
8492
}
8593

8694
&__features,
95+
&__users,
8796
&__models {
8897
background: var(--primary-very-low);
8998
padding: 1em;
9099
border-radius: 0.5em;
91100
}
92101

93102
&__features-title,
103+
&__users-title,
94104
&__models-title {
95105
margin-bottom: 1em;
96106
}
97107

98108
&__features-table,
109+
&__users-table,
99110
&__models-table {
100111
width: 100%;
101112
border-collapse: collapse;
@@ -108,13 +119,15 @@
108119
}
109120

110121
&__features-row,
122+
&__users-row,
111123
&__models-row {
112124
&:hover {
113125
background: var(--primary-low);
114126
}
115127
}
116128

117129
&__features-cell,
130+
&__users-cell,
118131
&__models-cell {
119132
padding: 0.5em;
120133
border-bottom: 1px solid var(--primary-low);

config/locales/client.en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,10 @@ en:
136136
usage_count: "Usage count"
137137
model: "Model"
138138
models_breakdown: "Usage per model"
139+
users_breakdown: "Usage per user"
139140
all_features: "All features"
140141
all_models: "All models"
142+
username: "Username"
141143

142144

143145
ai_persona:

lib/completions/report.rb

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
module DiscourseAi
22
module Completions
33
class Report
4+
UNKNOWN_FEATURE = "unknown"
5+
USER_LIMIT = 50
6+
47
attr_reader :start_date, :end_date, :base_query
58

69
def initialize(start_date: 30.days.ago, end_date: Time.current)
@@ -40,12 +43,28 @@ def tokens_by_period(period = nil)
4043
)
4144
end
4245

46+
def user_breakdown
47+
base_query
48+
.joins(:user)
49+
.group(:user_id, "users.username")
50+
.order("usage_count DESC")
51+
.limit(USER_LIMIT)
52+
.select(
53+
"users.username",
54+
"COUNT(*) as usage_count",
55+
"SUM(request_tokens + response_tokens) as total_tokens",
56+
"SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
57+
"SUM(request_tokens) as total_request_tokens",
58+
"SUM(response_tokens) as total_response_tokens",
59+
)
60+
end
61+
4362
def feature_breakdown
4463
base_query
4564
.group(:feature_name)
4665
.order("usage_count DESC")
4766
.select(
48-
"feature_name",
67+
"case when coalesce(feature_name, '') = '' then '#{UNKNOWN_FEATURE}' else feature_name end as feature_name",
4968
"COUNT(*) as usage_count",
5069
"SUM(request_tokens + response_tokens) as total_tokens",
5170
"SUM(COALESCE(cached_tokens,0)) as total_cached_tokens",
@@ -81,7 +100,11 @@ def tokens_per_month
81100
end
82101

83102
def filter_by_feature(feature_name)
84-
@base_query = base_query.where(feature_name: feature_name)
103+
if feature_name == UNKNOWN_FEATURE
104+
@base_query = base_query.where("coalesce(feature_name, '') = ''")
105+
else
106+
@base_query = base_query.where(feature_name: feature_name)
107+
end
85108
self
86109
end
87110

0 commit comments

Comments
 (0)