Class: ActionAgent::ModelPricing

Inherits:
Object
  • Object
show all
Defined in:
app/models/action_agent/model_pricing.rb

Overview

Estimates LLM spend from token counts. The activeagent gem's telemetry records tokens only; the platform layers pricing on top for the cost figures shown in Traces and Metrics.

Rates come from RubyLLM's model registry (USD per million tokens, maintained upstream per model) when the model is known there; the static pattern table below is the fallback for aliases/self-hosted models, and a conservative blended rate covers everything else so totals stay meaningful. Costs are always presented as estimates.

Constant Summary collapse

PRICES =
[
  # [pattern, input $/1M, output $/1M]
  [ /gpt-4o-mini/i, 0.15, 0.60 ],
  [ /gpt-4o/i, 2.50, 10.00 ],
  [ /gpt-4\.1-nano/i, 0.10, 0.40 ],
  [ /gpt-4\.1-mini/i, 0.40, 1.60 ],
  [ /gpt-4\.1/i, 2.00, 8.00 ],
  [ /o3-mini|o4-mini/i, 1.10, 4.40 ],
  [ /claude.*(fable|mythos)/i, 10.00, 50.00 ],
  [ /claude.*haiku-?4/i, 1.00, 5.00 ],
  [ /claude.*(haiku)/i, 0.80, 4.00 ],
  [ /claude.*(sonnet)/i, 3.00, 15.00 ],
  [ /claude.*opus-(5|4-[5-9])/i, 5.00, 25.00 ],
  [ /claude.*(opus)/i, 15.00, 75.00 ],
  [ /gemini.*flash/i, 0.10, 0.40 ],
  [ /gemini.*pro/i, 1.25, 10.00 ],
  [ /llama|mistral|mixtral|qwen|deepseek/i, 0.20, 0.60 ],
  # Zero-prices "mock-*" traces recorded before the mock fallback was
  # removed, so legacy rows never register as real spend.
  [ /mock/i, 0.0, 0.0 ]
].freeze
DEFAULT_RATE =

Fallback blended rate for unknown models ($/1M input, $/1M output)

[ 1.00, 4.00 ].freeze

Class Method Summary collapse

Class Method Details

.estimate(model:, input_tokens:, output_tokens:) ⇒ Float?

Returns estimated USD cost, nil when there is nothing to price.

Returns:

  • (Float, nil)

    estimated USD cost, nil when there is nothing to price



40
41
42
43
44
45
46
47
# File 'app/models/action_agent/model_pricing.rb', line 40

def self.estimate(model:, input_tokens:, output_tokens:)
  input = input_tokens.to_i
  output = output_tokens.to_i
  return nil if input.zero? && output.zero?

  input_rate, output_rate = rate_for(model)
  ((input * input_rate) + (output * output_rate)) / 1_000_000.0
end

.rate_for(model) ⇒ Object



49
50
51
52
53
# File 'app/models/action_agent/model_pricing.rb', line 49

def self.rate_for(model)
  return DEFAULT_RATE if model.blank?

  registry_rate(model) || static_rate(model)
end

.registry_rate(model) ⇒ Object

Exact per-model rates from RubyLLM's registry. Lookups are memoized — the registry scan is not free and trace serialization calls this per row.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/models/action_agent/model_pricing.rb', line 58

def self.registry_rate(model)
  @registry_rates ||= {}
  return @registry_rates[model] if @registry_rates.key?(model)

  @registry_rates[model] = begin
    info = RubyLLM.models.find(model.to_s)
    tokens = info&.pricing&.text_tokens
    if tokens&.input && tokens&.output
      [ tokens.input, tokens.output ]
    end
  rescue StandardError
    nil
  end
end

.static_rate(model) ⇒ Object



73
74
75
76
77
78
# File 'app/models/action_agent/model_pricing.rb', line 73

def self.static_rate(model)
  PRICES.each do |pattern, input_rate, output_rate|
    return [ input_rate, output_rate ] if model.to_s.match?(pattern)
  end
  DEFAULT_RATE
end