Merge pull request #984 from basecamp/refactor-rubby-llm-costs

Unify AI usage cost calculations
This commit is contained in:
Jorge Manrubia
2025-08-27 13:19:34 +02:00
committed by GitHub
6 changed files with 71 additions and 89 deletions
+43
View File
@@ -0,0 +1,43 @@
class Ai::UsageCost
attr_reader :model_id, :input_tokens, :output_tokens
class << self
def from_llm_response(response)
new \
model_id: response.model_id,
input_tokens: response.input_tokens,
output_tokens: response.output_tokens
end
end
def initialize(model_id:, input_tokens:, output_tokens:)
@model_id = model_id
@input_tokens = input_tokens
@output_tokens = output_tokens
end
def in_microcents
input_cost_in_microcents + output_cost_in_microcents
end
def input_cost_in_microcents
calculate_token_cost(input_tokens, model_info.input_price_per_million)
end
def output_cost_in_microcents
calculate_token_cost(output_tokens, model_info.output_price_per_million)
end
private
def calculate_token_cost(token_count, price_per_million)
return 0 unless price_per_million
single_token_price = price_per_million.to_d / 1_000_000
token_cost_dollars = token_count * single_token_price
Ai::Quota::Money.wrap(token_cost_dollars).in_microcents
end
def model_info
@model_info ||= RubyLLM.models.find(model_id)
end
end
@@ -16,9 +16,9 @@ module Conversation::Message::Respondable
model_id: response.model_id,
input_tokens: response.input_tokens,
output_tokens: response.output_tokens,
input_cost_in_microcents: response.input_cost_in_microcents,
output_cost_in_microcents: response.output_cost_in_microcents,
cost_in_microcents: response.cost_in_microcents
input_cost_in_microcents: response.cost.input_cost_in_microcents,
output_cost_in_microcents: response.cost.output_cost_in_microcents,
cost_in_microcents: response.cost.in_microcents
}
conversation.respond(response.answer, **message_attributes)
@@ -8,41 +8,7 @@ class Conversation::Message::ResponseGenerator::Response
@model_id = model_id
end
def cost_in_microcents
input_cost_in_microcents + output_cost_in_microcents
def cost
@cost ||= Ai::UsageCost.from_llm_response(self)
end
def input_cost_in_microcents
return unless token_price = input_token_price_microcents
(input_tokens * token_price).to_i
end
def input_token_price_microcents
return unless model_info
price_per_million_tokens_in_microcents(model_info.input_price_per_million)
end
def output_cost_in_microcents
return unless token_price = output_token_price_microcents
(output_tokens * token_price).to_i
end
def output_token_price_microcents
return unless model_info
price_per_million_tokens_in_microcents(model_info.output_price_per_million)
end
def model_info
@model_info ||= RubyLLM.models.find(model_id)
end
private
def price_per_million_tokens_in_microcents(price)
single_token_price = price.to_d / 1_000_000
Ai::Quota::Money.wrap(single_token_price).in_microcents
end
end
+1 -18
View File
@@ -29,7 +29,7 @@ class Event::Summarizer
def summarize
response = chat.ask join_prompts("Summarize the following content:", summarizable_content)
[ response.content, calculate_cost_in_microcents(response) ]
[ response.content, Ai::UsageCost.from_llm_response(response).in_microcents ]
end
def summarizable_content
@@ -43,21 +43,4 @@ class Event::Summarizer
chat = RubyLLM.chat(model: llm_model)
chat.with_instructions(join_prompts(prompt, domain_model_prompt, user_data_injection_prompt))
end
def calculate_cost_in_microcents(response)
model_info = RubyLLM.models.find(response.model_id)
input_cost = calculate_token_cost(response.input_tokens, model_info.input_price_per_million)
output_cost = calculate_token_cost(response.output_tokens, model_info.output_price_per_million)
input_cost + output_cost
end
def calculate_token_cost(token_count, price_per_million)
return 0 unless price_per_million
single_token_price = price_per_million.to_d / 1_000_000
token_cost_dollars = token_count * single_token_price
Ai::Quota::Money.wrap(token_cost_dollars).in_microcents
end
end
+22
View File
@@ -0,0 +1,22 @@
require "test_helper"
class Ai::ResponseCostTest < ActiveSupport::TestCase
test "price calculations" do
response_cost = Ai::UsageCost.new(
model_id: "gpt-4",
input_tokens: 198,
output_tokens: 2
)
# We've got 198 input tokens, so that's
# 198 * 3000 = 594000
assert_equal 594000, response_cost.input_cost_in_microcents
# We've got 2 output tokens, so that's
# 2 * 6000 = 12000
assert_equal 12000, response_cost.output_cost_in_microcents
# So the total is 594000 + 12000 micro-cents
assert_equal 606000, response_cost.in_microcents
end
end
@@ -1,32 +0,0 @@
require "test_helper"
class Conversation::Message::ResponseGenerator::ResponseTest < ActiveSupport::TestCase
test "price calculations" do
response = Conversation::Message::ResponseGenerator::Response.new(
answer: "Hi!",
input_tokens: 198,
output_tokens: 2,
model_id: "gpt-4"
)
# The price of an input token is 30 USD per million tokens
# and 60 USD per million output tokens
# That's 0.00003 cents per input token and 0.00006 cents
# per output token
# Which is 3000 micro-cents per input token and 6000 micro-cents
# per output token
assert_equal "3000.0".to_d, response.input_token_price_microcents
assert_equal "6000.0".to_d, response.output_token_price_microcents
# We've got 198 input tokens, so that's
# 198 * 3000 = 594000
assert_equal 594000, response.input_cost_in_microcents
# We've got 2 output tokens, so that's
# 2 * 6000 = 12
assert_equal 12000, response.output_cost_in_microcents
# So the total is 594000 + 12000 micro-cents
assert_equal 606000, response.cost_in_microcents
end
end