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

Commit 44581d5

Browse files
committed
DEV: Add total spending tile
1 parent 1c522db commit 44581d5

File tree

4 files changed

+66
-1
lines changed

4 files changed

+66
-1
lines changed

app/serializers/ai_usage_serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def summary
6060
total_request_tokens: object.total_request_tokens,
6161
total_response_tokens: object.total_response_tokens,
6262
total_requests: object.total_requests,
63+
total_spending: object.total_spending,
6364
date_range: {
6465
start: object.start_date,
6566
end: object.end_date,

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fn, hash } from "@ember/helper";
44
import { action } from "@ember/object";
55
import { LinkTo } from "@ember/routing";
66
import { service } from "@ember/service";
7+
import { modifier } from "ember-modifier";
78
import { eq, gt, lt } from "truth-helpers";
89
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
910
import DButton from "discourse/components/d-button";
@@ -33,6 +34,22 @@ export default class AiUsage extends Component {
3334
@tracked isCustomDateActive = false;
3435
@tracked loadingData = true;
3536

37+
// TODO: currently doing dollar, but how should we handle other currencies?
38+
addCurrency = modifier((element) => {
39+
element.querySelectorAll(".d-stat-tile__label").forEach((label) => {
40+
if (
41+
label.innerText.trim() === i18n("discourse_ai.usage.total_spending")
42+
) {
43+
const valueElement = label
44+
.closest(".d-stat-tile")
45+
?.querySelector(".d-stat-tile__value");
46+
if (valueElement) {
47+
valueElement.innerText = `$${valueElement.innerText}`;
48+
}
49+
}
50+
});
51+
});
52+
3653
constructor() {
3754
super(...arguments);
3855
this.fetchData();
@@ -153,6 +170,11 @@ export default class AiUsage extends Component {
153170
value: this.data.summary.total_cached_tokens,
154171
tooltip: i18n("discourse_ai.usage.stat_tooltips.cached_tokens"),
155172
},
173+
{
174+
label: i18n("discourse_ai.usage.total_spending"),
175+
value: this.data.summary.total_spending,
176+
tooltip: i18n("discourse_ai.usage.stat_tooltips.total_spending"),
177+
},
156178
];
157179
}
158180

@@ -376,9 +398,10 @@ export default class AiUsage extends Component {
376398
class="ai-usage__summary"
377399
>
378400
<:content>
379-
<DStatTiles as |tiles|>
401+
<DStatTiles {{this.addCurrency}} as |tiles|>
380402
{{#each this.metrics as |metric|}}
381403
<tiles.Tile
404+
class="bar"
382405
@label={{metric.label}}
383406
@href={{metric.href}}
384407
@value={{metric.value}}

config/locales/client.en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ en:
239239
net_request_tokens: "Net request tokens"
240240
cached_tokens: "Cached tokens"
241241
cached_request_tokens: "Cached request tokens"
242+
total_spending: "Total spending"
242243
no_users: "No user usage data found"
243244
no_models: "No model usage data found"
244245
no_features: "No feature usage data found"
@@ -249,6 +250,7 @@ en:
249250
request_tokens: "Tokens used when the LLM tries to understand what you are saying"
250251
response_tokens: "Tokens used when the LLM responds to your prompt"
251252
cached_tokens: "Previously processed request tokens that the LLM reuses to optimize performance and cost"
253+
total_spending: "Cumulative cost of all tokens used by the LLMs"
252254
periods:
253255
last_day: "Last 24 hours"
254256
last_week: "Last week"

lib/completions/report.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ def total_requests
3333
stats.total_requests || 0
3434
end
3535

36+
def total_spending
37+
total = total_input_spending + total_output_spending + total_cached_input_spending
38+
total.round(2)
39+
end
40+
41+
def total_input_spending
42+
model_costs.sum { |row| row.input_cost.to_f * row.total_request_tokens.to_i / 1_000_000.0 }
43+
end
44+
45+
def total_output_spending
46+
model_costs.sum do |row|
47+
row.output_cost.to_f * row.total_response_tokens.to_i / 1_000_000.0
48+
end
49+
end
50+
51+
def total_cached_input_spending
52+
model_costs.sum do |row|
53+
row.cached_input_cost.to_f * row.total_cached_tokens.to_i / 1_000_000.0
54+
end
55+
end
56+
3657
def stats
3758
@stats ||=
3859
base_query.select(
@@ -46,6 +67,24 @@ def stats
4667
]
4768
end
4869

70+
def model_costs
71+
@model_costs ||=
72+
base_query
73+
.joins("LEFT JOIN llm_models ON llm_models.name = language_model")
74+
.group(
75+
"llm_models.name, llm_models.input_cost, llm_models.output_cost, llm_models.cached_input_cost",
76+
)
77+
.select(
78+
"llm_models.name",
79+
"llm_models.input_cost",
80+
"llm_models.output_cost",
81+
"llm_models.cached_input_cost",
82+
"SUM(COALESCE(request_tokens, 0)) as total_request_tokens",
83+
"SUM(COALESCE(response_tokens, 0)) as total_response_tokens",
84+
"SUM(COALESCE(cached_tokens, 0)) as total_cached_tokens",
85+
)
86+
end
87+
4988
def guess_period(period = nil)
5089
period = nil if %i[day month hour].include?(period)
5190
period ||

0 commit comments

Comments
 (0)