diff --git a/app/models/ai/usage_cost.rb b/app/models/ai/usage_cost.rb new file mode 100644 index 000000000..442be5480 --- /dev/null +++ b/app/models/ai/usage_cost.rb @@ -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 diff --git a/app/models/conversation/message/respondable.rb b/app/models/conversation/message/respondable.rb index d3d8f03a2..6485a25e8 100644 --- a/app/models/conversation/message/respondable.rb +++ b/app/models/conversation/message/respondable.rb @@ -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) diff --git a/app/models/conversation/message/response_generator/response.rb b/app/models/conversation/message/response_generator/response.rb index a62bf5c74..a4b1981a3 100644 --- a/app/models/conversation/message/response_generator/response.rb +++ b/app/models/conversation/message/response_generator/response.rb @@ -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 diff --git a/app/models/event/summarizer.rb b/app/models/event/summarizer.rb index cf10b57c6..6c9ff20d2 100644 --- a/app/models/event/summarizer.rb +++ b/app/models/event/summarizer.rb @@ -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 diff --git a/test/models/ai/response_cost_test.rb b/test/models/ai/response_cost_test.rb new file mode 100644 index 000000000..a0f5109a7 --- /dev/null +++ b/test/models/ai/response_cost_test.rb @@ -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 diff --git a/test/models/conversation/response_generator/response_test.rb b/test/models/conversation/response_generator/response_test.rb deleted file mode 100644 index daa752736..000000000 --- a/test/models/conversation/response_generator/response_test.rb +++ /dev/null @@ -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