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

Commit 68aa440

Browse files
committed
most of the backend for a quota system is now done
1 parent 6d3faf1 commit 68aa440

File tree

4 files changed

+166
-12
lines changed

4 files changed

+166
-12
lines changed

app/models/llm_quota.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,31 @@ class LlmQuota < ActiveRecord::Base
1616
validate :at_least_one_limit
1717

1818
def self.within_quota?(llm, user)
19+
return true if user.blank?
20+
quotas = joins(:group).where(llm_model: llm).where(group: user.groups)
21+
22+
return true if quotas.empty?
23+
quotas.each do |quota|
24+
usage = LlmQuotaUsage.find_or_create_for(user: user, llm_quota: quota)
25+
begin
26+
usage.check_quota!
27+
rescue LlmQuotaUsage::QuotaExceededError
28+
return false
29+
end
30+
end
31+
32+
true
1933
end
2034

2135
def self.log_usage(llm, user, input_tokens, output_tokens)
36+
return if user.blank?
37+
38+
quotas = joins(:group).where(llm_model: llm).where(group: user.groups)
39+
40+
quotas.each do |quota|
41+
usage = LlmQuotaUsage.find_or_create_for(user: user, llm_quota: quota)
42+
usage.increment_usage!(input_tokens: input_tokens, output_tokens: output_tokens)
43+
end
2244
end
2345

2446
def available_tokens
@@ -51,3 +73,8 @@ def at_least_one_limit
5173
# created_at :datetime not null
5274
# updated_at :datetime not null
5375
#
76+
# Indexes
77+
#
78+
# index_llm_quotas_on_group_id_and_llm_model_id (group_id,llm_model_id) UNIQUE
79+
# index_llm_quotas_on_llm_model_id (llm_model_id)
80+
#

app/models/llm_quota_usage.rb

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,19 @@ def percentage_usages_used
103103
#
104104
# Table name: llm_quota_usages
105105
#
106-
# id :bigint not null, primary key
107-
# user_id :bigint not null
108-
# llm_quota_id :bigint not null
109-
# input_tokens_used :integer not null
110-
# output_tokens_used:integer not null
111-
# usages :integer not null
112-
# started_at :datetime not null
113-
# reset_at :datetime not null
114-
# created_at :datetime not null
115-
# updated_at :datetime not null
106+
# id :bigint not null, primary key
107+
# user_id :bigint not null
108+
# llm_quota_id :bigint not null
109+
# input_tokens_used :integer not null
110+
# output_tokens_used :integer not null
111+
# usages :integer not null
112+
# started_at :datetime not null
113+
# reset_at :datetime not null
114+
# created_at :datetime not null
115+
# updated_at :datetime not null
116+
#
117+
# Indexes
118+
#
119+
# index_llm_quota_usages_on_llm_quota_id (llm_quota_id)
120+
# index_llm_quota_usages_on_user_id_and_llm_quota_id (user_id,llm_quota_id) UNIQUE
116121
#

lib/completions/endpoints/base.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ def perform_completion!(
6565
partial_tool_calls: false,
6666
&blk
6767
)
68+
if !LlmQuota.within_quota?(@llm_model, user)
69+
raise LlmQuotaUsage::QuotaExceededError.new(
70+
I18n.t("discourse_ai.errors.quota_exceeded"),
71+
)
72+
end
73+
6874
@partial_tool_calls = partial_tool_calls
6975
model_params = normalize_model_params(model_params)
7076
orig_blk = blk
@@ -188,10 +194,9 @@ def perform_completion!(
188194
if log
189195
log.raw_response_payload = response_raw
190196
final_log_update(log)
191-
192197
log.response_tokens = tokenizer.size(partials_raw) if log.response_tokens.blank?
193198
log.save!
194-
199+
LlmQuota.log_usage(@llm_model, user, log.request_tokens, log.response_tokens)
195200
if Rails.env.development?
196201
puts "#{self.class.name}: request_tokens #{log.request_tokens} response_tokens #{log.response_tokens}"
197202
end

spec/models/llm_quota_spec.rb

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# frozen_string_literal: true
2+
#
3+
RSpec.describe LlmQuota do
4+
fab!(:group)
5+
fab!(:user)
6+
fab!(:llm_model)
7+
8+
before { group.add(user) }
9+
10+
describe ".within_quota?" do
11+
it "returns true when user is nil" do
12+
expect(described_class.within_quota?(llm_model, nil)).to be true
13+
end
14+
15+
it "returns true when no quotas exist for the user's groups" do
16+
expect(described_class.within_quota?(llm_model, user)).to be true
17+
end
18+
19+
it "returns true when usage is within limits" do
20+
quota = Fabricate(:llm_quota, group: group, llm_model: llm_model)
21+
_usage =
22+
Fabricate(
23+
:llm_quota_usage,
24+
user: user,
25+
llm_quota: quota,
26+
input_tokens_used: quota.max_tokens - 100,
27+
)
28+
29+
expect(described_class.within_quota?(llm_model, user)).to be true
30+
end
31+
32+
it "returns false when usage exceeds token limit" do
33+
quota = Fabricate(:llm_quota, group: group, llm_model: llm_model, max_tokens: 1000)
34+
_usage = Fabricate(:llm_quota_usage, user: user, llm_quota: quota, input_tokens_used: 1100)
35+
36+
expect(described_class.within_quota?(llm_model, user)).to be false
37+
end
38+
39+
it "returns false when usage exceeds usage limit" do
40+
quota = Fabricate(:llm_quota, group: group, llm_model: llm_model, max_usages: 10)
41+
_usage = Fabricate(:llm_quota_usage, user: user, llm_quota: quota, usages: 11)
42+
43+
expect(described_class.within_quota?(llm_model, user)).to be false
44+
end
45+
46+
it "checks all quotas from user's groups" do
47+
group2 = Fabricate(:group)
48+
group2.add(user)
49+
50+
quota1 = Fabricate(:llm_quota, group: group, llm_model: llm_model, max_tokens: 1000)
51+
quota2 = Fabricate(:llm_quota, group: group2, llm_model: llm_model, max_tokens: 500)
52+
53+
Fabricate(:llm_quota_usage, user: user, llm_quota: quota1, input_tokens_used: 900)
54+
Fabricate(:llm_quota_usage, user: user, llm_quota: quota2, input_tokens_used: 600)
55+
56+
expect(described_class.within_quota?(llm_model, user)).to be false
57+
end
58+
end
59+
60+
describe ".log_usage" do
61+
it "does nothing when user is nil" do
62+
expect { described_class.log_usage(llm_model, nil, 100, 50) }.not_to change(
63+
LlmQuotaUsage,
64+
:count,
65+
)
66+
end
67+
68+
it "creates usage records when none exist" do
69+
_quota = Fabricate(:llm_quota, group: group, llm_model: llm_model)
70+
71+
expect { described_class.log_usage(llm_model, user, 100, 50) }.to change(
72+
LlmQuotaUsage,
73+
:count,
74+
).by(1)
75+
76+
usage = LlmQuotaUsage.last
77+
expect(usage.input_tokens_used).to eq(100)
78+
expect(usage.output_tokens_used).to eq(50)
79+
expect(usage.usages).to eq(1)
80+
end
81+
82+
it "updates existing usage records" do
83+
quota = Fabricate(:llm_quota, group: group, llm_model: llm_model)
84+
usage =
85+
Fabricate(
86+
:llm_quota_usage,
87+
user: user,
88+
llm_quota: quota,
89+
input_tokens_used: 100,
90+
output_tokens_used: 50,
91+
usages: 1,
92+
)
93+
94+
described_class.log_usage(llm_model, user, 50, 25)
95+
96+
usage.reload
97+
expect(usage.input_tokens_used).to eq(150)
98+
expect(usage.output_tokens_used).to eq(75)
99+
expect(usage.usages).to eq(2)
100+
end
101+
102+
it "logs usage for all quotas from user's groups" do
103+
group2 = Fabricate(:group)
104+
group2.add(user)
105+
106+
_quota1 = Fabricate(:llm_quota, group: group, llm_model: llm_model)
107+
_quota2 = Fabricate(:llm_quota, group: group2, llm_model: llm_model)
108+
109+
expect { described_class.log_usage(llm_model, user, 100, 50) }.to change(
110+
LlmQuotaUsage,
111+
:count,
112+
).by(2)
113+
114+
expect(LlmQuotaUsage.where(user: user).count).to eq(2)
115+
end
116+
end
117+
end

0 commit comments

Comments
 (0)