Class: SpreeMenuChat::TokenBudget

Inherits:
Object
  • Object
show all
Defined in:
app/services/spree_menu_chat/token_budget.rb

Overview

Per-store daily cap on Gemini generation token spend (see SpreeMenuChat::Configuration#daily_token_budget) — the other half of M5's guardrails alongside SpreeMenuChat::RateLimiter. Once a store's daily total is exhausted, SpreeMenuChat::AnswerGenerator stops calling Gemini for the rest of the day and falls back to a "come back tomorrow / contact us" response instead, rather than continuing to spend.

Deliberately tracks generation tokens only, using Gemini's own reported usageMetadata.totalTokenCount (not embedding tokens, and not an estimate). Voyage's embedding call in the live chat path is just the query embedding (SpreeMenuChat::Retriever) — a few dozen tokens, dwarfed by a real generation call's typical few-hundred-token cost. Folding embedding spend in too would be more complete, but it would mean EmbeddingClient/Retriever both starting to return usage alongside vectors — real plumbing, not just a config tweak — so it's left as a follow-up rather than blocking this guardrail on it.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(store:) ⇒ TokenBudget

Returns a new instance of TokenBudget.



27
28
29
# File 'app/services/spree_menu_chat/token_budget.rb', line 27

def initialize(store:)
  @store = store
end

Class Method Details

.exceeded?(store: Spree::Store.default) ⇒ Boolean

Returns:

  • (Boolean)


19
20
21
# File 'app/services/spree_menu_chat/token_budget.rb', line 19

def self.exceeded?(store: Spree::Store.default)
  new(store: store).exceeded?
end

.record!(tokens, store: Spree::Store.default) ⇒ Object



23
24
25
# File 'app/services/spree_menu_chat/token_budget.rb', line 23

def self.record!(tokens, store: Spree::Store.default)
  new(store: store).record!(tokens)
end

Instance Method Details

#exceeded?Boolean

Returns:

  • (Boolean)


31
32
33
34
# File 'app/services/spree_menu_chat/token_budget.rb', line 31

def exceeded?
  used = SpreeMenuChat::TokenUsage.find_by(store: @store, date: Date.current)&.tokens_used || 0
  used >= SpreeMenuChat::Config.daily_token_budget
end

#record!(tokens) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'app/services/spree_menu_chat/token_budget.rb', line 36

def record!(tokens)
  tokens = tokens.to_i
  return if tokens <= 0

  ActiveRecord::Base.transaction do
    usage = SpreeMenuChat::TokenUsage.lock.find_or_initialize_by(store: @store, date: Date.current)
    usage.tokens_used = usage.tokens_used.to_i + tokens
    usage.save!
  end
rescue ActiveRecord::RecordNotUnique
  # Lost a race with a concurrent first-request-of-the-day for this
  # store — the row exists now, retry against it. Same pattern as
  # SpreeMenuChat::RateLimiter#check!.
  retry
end