Module: Legion::LLM::Metering

Extended by:
Legion::Logging::Helper
Defined in:
lib/legion/llm/metering.rb,
lib/legion/llm/metering/tokens.rb,
lib/legion/llm/metering/tracker.rb,
lib/legion/llm/metering/estimator.rb

Defined Under Namespace

Modules: Pricing, Recorder, Tokens

Class Method Summary collapse

Class Method Details

.const_missing(name) ⇒ Object

Backward-compat: resolve old Legion::LLM::Metering::Exchange, ::Event



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/legion/llm/metering.rb', line 139

def self.const_missing(name)
  case name
  when :Exchange
    require_relative 'transport/exchanges/metering'
    Transport::Exchanges::Metering
  when :Event
    require_relative 'transport/messages/metering_event'
    Transport::Messages::MeteringEvent
  else
    super
  end
end

.emit(event) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/legion/llm/metering.rb', line 23

def emit(event)
  event_class = metering_event_class if transport_connected?

  if event_class
    event_class.new(**event).publish
    log.info("[llm][metering] published provider=#{event[:provider]} model=#{event[:model_id]}")
    :published
  elsif spool_available?
    spool_event(event)
    log.info("[llm][metering] spooled provider=#{event[:provider]} model=#{event[:model_id]}")
    :spooled
  else
    log.warn("[llm][metering] dropped provider=#{event[:provider]} model=#{event[:model_id]}")
    :dropped
  end
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.metering.emit')
  :dropped
end

.extract_model(response) ⇒ Object



123
124
125
126
127
# File 'lib/legion/llm/metering.rb', line 123

def extract_model(response)
  return nil unless response.is_a?(Hash)

  settings_value(settings_value(response, :meta), :model) || settings_value(response, :model)
end

.extract_provider(response) ⇒ Object



117
118
119
120
121
# File 'lib/legion/llm/metering.rb', line 117

def extract_provider(response)
  return nil unless response.is_a?(Hash)

  settings_value(settings_value(response, :meta), :provider) || settings_value(response, :provider)
end

.extract_usage(response) ⇒ Object



107
108
109
110
111
112
113
114
115
# File 'lib/legion/llm/metering.rb', line 107

def extract_usage(response)
  return { input_tokens: 0, output_tokens: 0 } unless response.is_a?(Hash)

  usage = settings_value(response, :usage) || {}
  {
    input_tokens:  settings_value(usage, :input_tokens) || settings_value(usage, :prompt_tokens) || 0,
    output_tokens: settings_value(usage, :output_tokens) || settings_value(usage, :completion_tokens) || 0
  }
end

.flush_spoolObject



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/legion/llm/metering.rb', line 43

def flush_spool
  return 0 unless spool_available? && transport_connected?

  spool = Legion::Data::Spool.for(Legion::LLM)
  flushed = spool.flush(:metering) { |event| emit(event) }
  log.info("[llm][metering] spool_flushed count=#{flushed}")
  flushed
rescue StandardError => e
  handle_exception(e, level: :warn, operation: 'llm.metering.flush_spool')
  0
end

.install_hookObject



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/legion/llm/metering.rb', line 55

def install_hook
  Legion::LLM::Hooks.after_chat do |response:, model:, **|
    usage = extract_usage(response)
    next if usage[:input_tokens].zero? && usage[:output_tokens].zero?

    resolved_model    = (extract_model(response) || model).to_s
    resolved_provider = extract_provider(response)

    Metering::Recorder.record(
      model:         resolved_model,
      input_tokens:  usage[:input_tokens],
      output_tokens: usage[:output_tokens],
      provider:      resolved_provider
    )

    emit(
      provider:      resolved_provider,
      model_id:      resolved_model,
      input_tokens:  usage[:input_tokens],
      output_tokens: usage[:output_tokens],
      event_type:    'llm_completion',
      status:        response.is_a?(Hash) && response[:error] ? 'failure' : 'success'
    )
    nil
  end
end

.load_transportObject



14
15
16
17
18
19
# File 'lib/legion/llm/metering.rb', line 14

def self.load_transport
  return unless defined?(Legion::Transport::Message)

  require_relative 'transport/exchanges/metering'
  require_relative 'transport/messages/metering_event'
end

.metering_event_classObject



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/legion/llm/metering.rb', line 86

def metering_event_class
  return Legion::LLM::Transport::Messages::MeteringEvent if defined?(Legion::LLM::Transport::Messages::MeteringEvent)

  load_transport
  return Legion::LLM::Transport::Messages::MeteringEvent if defined?(Legion::LLM::Transport::Messages::MeteringEvent)

  Legion::LLM::Metering::Event
rescue NameError, LoadError => e
  handle_exception(e, level: :warn, handled: true, operation: 'llm.metering.event_class')
  nil
end

.settings_value(hash, key) ⇒ Object



129
130
131
132
133
134
135
136
# File 'lib/legion/llm/metering.rb', line 129

def settings_value(hash, key)
  return nil unless hash.respond_to?(:key?)

  string_key = key.to_s
  return hash[string_key] if hash.key?(string_key)

  hash[key] if hash.key?(key)
end

.spool_available?Boolean

Returns:

  • (Boolean)


98
99
100
# File 'lib/legion/llm/metering.rb', line 98

def spool_available?
  !!defined?(Legion::Data::Spool)
end

.spool_event(event) ⇒ Object



102
103
104
105
# File 'lib/legion/llm/metering.rb', line 102

def spool_event(event)
  spool = Legion::Data::Spool.for(Legion::LLM)
  spool.write(:metering, event)
end

.transport_connected?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/legion/llm/metering.rb', line 82

def transport_connected?
  Legion::LLM::Settings.transport_connected?
end