Class: TurnKit::Cost

Inherits:
Object
  • Object
show all
Defined in:
lib/turnkit/cost.rb

Constant Summary collapse

COMPONENTS =
%i[input output cache_read cache_write].freeze
PER_MILLION =
1_000_000.0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input: nil, output: nil, cache_read: nil, cache_write: nil, total: nil, strict: false) ⇒ Cost

Returns a new instance of Cost.



122
123
124
125
126
127
128
129
# File 'lib/turnkit/cost.rb', line 122

def initialize(input: nil, output: nil, cache_read: nil, cache_write: nil, total: nil, strict: false)
  @input = number(input)
  @output = number(output)
  @cache_read = number(cache_read)
  @cache_write = number(cache_write)
  @total = number(total)
  @strict = strict
end

Instance Attribute Details

#cache_readObject (readonly)

Returns the value of attribute cache_read.



8
9
10
# File 'lib/turnkit/cost.rb', line 8

def cache_read
  @cache_read
end

#cache_writeObject (readonly)

Returns the value of attribute cache_write.



8
9
10
# File 'lib/turnkit/cost.rb', line 8

def cache_write
  @cache_write
end

#inputObject (readonly)

Returns the value of attribute input.



8
9
10
# File 'lib/turnkit/cost.rb', line 8

def input
  @input
end

#outputObject (readonly)

Returns the value of attribute output.



8
9
10
# File 'lib/turnkit/cost.rb', line 8

def output
  @output
end

Class Method Details

.aggregate(costs) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/turnkit/cost.rb', line 10

def self.aggregate(costs)
  costs = costs.compact
  return new unless costs.any?

  if costs.any? { |cost| COMPONENTS.any? { |component| !cost.public_send(component).nil? } }
    values = COMPONENTS.to_h do |component|
      amounts = costs.filter_map { |cost| cost.public_send(component) }
      [ component, amounts.any? ? amounts.sum : nil ]
    end
    return new(**values)
  end

  totals = costs.map(&:total)
  return new(total: totals.sum) if totals.none?(&:nil?)

  new
end

.amount(tokens, price) ⇒ Object



115
116
117
118
119
120
# File 'lib/turnkit/cost.rb', line 115

def self.amount(tokens, price)
  return nil if tokens.to_i.positive? && price.nil?
  return 0.0 if tokens.to_i.zero?

  tokens.to_i * price.to_f / PER_MILLION
end

.custom_cost(usage, model) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/turnkit/cost.rb', line 99

def self.custom_cost(usage, model)
  return unless TurnKit.cost_calculator

  value = TurnKit.cost_calculator.call(usage, model)
  case value
  when nil
    nil
  when Cost
    value
  when Hash
    from_hash(value)
  else
    new(total: value)
  end
end

.from_hash(hash) ⇒ Object



88
89
90
91
92
93
94
95
96
97
# File 'lib/turnkit/cost.rb', line 88

def self.from_hash(hash)
  hash = hash.transform_keys(&:to_sym)
  new(
    input: hash[:input],
    output: hash[:output],
    cache_read: hash[:cache_read] || hash[:cached_input],
    cache_write: hash[:cache_write] || hash[:cache_creation],
    total: hash[:total]
  )
end

.from_rates(usage, rates) ⇒ Object



51
52
53
54
55
56
57
58
59
60
# File 'lib/turnkit/cost.rb', line 51

def self.from_rates(usage, rates)
  rates = rates.transform_keys(&:to_sym)
  new(
    input: amount(usage.input_tokens, rates[:input] || rates[:input_per_million]),
    output: amount(usage.output_tokens, rates[:output] || rates[:output_per_million]),
    cache_read: amount(usage.cached_tokens, rates[:cache_read] || rates[:cached_input] || rates[:cache_read_input_per_million] || rates[:cached_input_per_million]),
    cache_write: amount(usage.cache_write_tokens, rates[:cache_write] || rates[:cache_creation] || rates[:cache_write_input_per_million] || rates[:cache_creation_input_per_million]),
    strict: true
  )
end

.from_record(record) ⇒ Object



42
43
44
45
46
47
48
49
# File 'lib/turnkit/cost.rb', line 42

def self.from_record(record)
  attrs = record.transform_keys(&:to_s)
  usage = attrs["usage"] || {}
  return from_hash(usage["cost_details"] || usage[:cost_details]) if usage["cost_details"] || usage[:cost_details]
  return new(total: attrs["cost"]) if attrs["cost"]

  from_usage(Usage.from_h(usage), model: attrs["model"])
end

.from_records(records) ⇒ Object



38
39
40
# File 'lib/turnkit/cost.rb', line 38

def self.from_records(records)
  aggregate(records.map { |record| from_record(record) })
end

.from_ruby_llm(usage, model) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/turnkit/cost.rb', line 62

def self.from_ruby_llm(usage, model)
  require "ruby_llm"

  model_info = ::RubyLLM.models.find(model) if model
  return new unless model_info

  if defined?(::RubyLLM::Cost)
    tokens = ::RubyLLM::Tokens.new(
      input: usage.input_tokens,
      output: usage.output_tokens,
      cached: usage.cached_tokens,
      cache_creation: usage.cache_write_tokens
    )
    from_hash(::RubyLLM::Cost.new(tokens: tokens, model: model_info).to_h)
  else
    from_rates(
      usage,
      input: model_info.input_price_per_million,
      output: model_info.output_price_per_million,
      cached_input: model_info.pricing&.text_tokens&.cached_input
    )
  end
rescue LoadError, StandardError
  new
end

.from_usage(usage, model: nil) ⇒ Object



28
29
30
31
32
33
34
35
36
# File 'lib/turnkit/cost.rb', line 28

def self.from_usage(usage, model: nil)
  return new(total: usage.cost) if usage.cost

  custom = custom_cost(usage, model)
  return custom if custom

  rates = TurnKit.cost_rates[model.to_s] || TurnKit.cost_rates[model&.to_sym]
  rates ? from_rates(usage, rates) : from_ruby_llm(usage, model)
end

Instance Method Details

#to_hObject



139
140
141
142
143
144
145
146
147
# File 'lib/turnkit/cost.rb', line 139

def to_h
  {
    "input" => input,
    "output" => output,
    "cache_read" => cache_read,
    "cache_write" => cache_write,
    "total" => total
  }.compact
end

#totalObject



131
132
133
134
135
136
137
# File 'lib/turnkit/cost.rb', line 131

def total
  return @total if @total
  return nil if @strict && COMPONENTS.any? { |component| public_send(component).nil? }

  values = COMPONENTS.filter_map { |component| public_send(component) }
  values.empty? ? nil : values.sum
end