ruby_llm-agents 3.5.4 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +155 -10
  4. data/app/helpers/ruby_llm/agents/application_helper.rb +15 -1
  5. data/app/models/ruby_llm/agents/execution/replayable.rb +124 -0
  6. data/app/models/ruby_llm/agents/execution/scopes.rb +42 -1
  7. data/app/models/ruby_llm/agents/execution.rb +50 -1
  8. data/app/models/ruby_llm/agents/tenant/budgetable.rb +28 -4
  9. data/app/views/layouts/ruby_llm/agents/application.html.erb +41 -28
  10. data/app/views/ruby_llm/agents/agents/show.html.erb +16 -1
  11. data/app/views/ruby_llm/agents/dashboard/_top_tenants.html.erb +47 -0
  12. data/app/views/ruby_llm/agents/dashboard/index.html.erb +397 -100
  13. data/lib/generators/ruby_llm_agents/rename_agent_generator.rb +53 -0
  14. data/lib/generators/ruby_llm_agents/templates/rename_agent_migration.rb.tt +19 -0
  15. data/lib/ruby_llm/agents/agent_tool.rb +125 -0
  16. data/lib/ruby_llm/agents/audio/speaker.rb +5 -3
  17. data/lib/ruby_llm/agents/audio/speech_pricing.rb +63 -187
  18. data/lib/ruby_llm/agents/audio/transcriber.rb +5 -3
  19. data/lib/ruby_llm/agents/audio/transcription_pricing.rb +5 -7
  20. data/lib/ruby_llm/agents/base_agent.rb +144 -5
  21. data/lib/ruby_llm/agents/core/configuration.rb +178 -53
  22. data/lib/ruby_llm/agents/core/errors.rb +3 -77
  23. data/lib/ruby_llm/agents/core/instrumentation.rb +0 -17
  24. data/lib/ruby_llm/agents/core/version.rb +1 -1
  25. data/lib/ruby_llm/agents/dsl/base.rb +0 -8
  26. data/lib/ruby_llm/agents/dsl/queryable.rb +124 -0
  27. data/lib/ruby_llm/agents/dsl.rb +1 -0
  28. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -1
  29. data/lib/ruby_llm/agents/image/generator/pricing.rb +75 -217
  30. data/lib/ruby_llm/agents/image/generator.rb +5 -3
  31. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +8 -0
  32. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +4 -2
  33. data/lib/ruby_llm/agents/pipeline/builder.rb +43 -0
  34. data/lib/ruby_llm/agents/pipeline/context.rb +11 -1
  35. data/lib/ruby_llm/agents/pipeline/executor.rb +1 -25
  36. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +26 -1
  37. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +18 -0
  38. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +130 -3
  39. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +29 -0
  40. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +11 -4
  41. data/lib/ruby_llm/agents/pipeline.rb +0 -92
  42. data/lib/ruby_llm/agents/results/background_removal_result.rb +11 -1
  43. data/lib/ruby_llm/agents/results/base.rb +23 -1
  44. data/lib/ruby_llm/agents/results/embedding_result.rb +14 -1
  45. data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -1
  46. data/lib/ruby_llm/agents/results/image_edit_result.rb +11 -1
  47. data/lib/ruby_llm/agents/results/image_generation_result.rb +12 -3
  48. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +11 -1
  49. data/lib/ruby_llm/agents/results/image_transform_result.rb +11 -1
  50. data/lib/ruby_llm/agents/results/image_upscale_result.rb +11 -1
  51. data/lib/ruby_llm/agents/results/image_variation_result.rb +11 -1
  52. data/lib/ruby_llm/agents/results/speech_result.rb +20 -1
  53. data/lib/ruby_llm/agents/results/transcription_result.rb +20 -1
  54. data/lib/ruby_llm/agents/text/embedder.rb +23 -18
  55. data/lib/ruby_llm/agents.rb +70 -5
  56. data/lib/tasks/ruby_llm_agents.rake +21 -0
  57. metadata +7 -6
  58. data/lib/ruby_llm/agents/infrastructure/reliability/breaker_manager.rb +0 -80
  59. data/lib/ruby_llm/agents/infrastructure/reliability/execution_constraints.rb +0 -69
  60. data/lib/ruby_llm/agents/infrastructure/reliability/executor.rb +0 -125
  61. data/lib/ruby_llm/agents/infrastructure/reliability/fallback_routing.rb +0 -72
  62. data/lib/ruby_llm/agents/infrastructure/reliability/retry_strategy.rb +0 -82
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Wraps an agent class as a RubyLLM::Tool so it can be used
6
+ # in another agent's `tools` list. The LLM sees the sub-agent
7
+ # as a callable tool and can invoke it with the agent's declared params.
8
+ module AgentTool
9
+ MAX_AGENT_TOOL_DEPTH = 5
10
+
11
+ # Wraps an agent class as a RubyLLM::Tool subclass.
12
+ #
13
+ # @param agent_class [Class] A BaseAgent subclass
14
+ # @return [Class] An anonymous RubyLLM::Tool subclass
15
+ def self.for(agent_class)
16
+ tool_name = derive_tool_name(agent_class)
17
+ tool_desc = agent_class.respond_to?(:description) ? agent_class.description : nil
18
+ agent_params = agent_class.respond_to?(:params) ? agent_class.params : {}
19
+ captured_agent_class = agent_class
20
+
21
+ Class.new(RubyLLM::Tool) do
22
+ description tool_desc if tool_desc
23
+
24
+ # Map agent params to tool params
25
+ agent_params.each do |name, config|
26
+ next if name.to_s.start_with?("_")
27
+
28
+ param name,
29
+ desc: config[:desc] || "#{name} parameter",
30
+ required: config[:required] == true,
31
+ type: AgentTool.map_type(config[:type])
32
+ end
33
+
34
+ # Store references on the class
35
+ define_singleton_method(:agent_class) { captured_agent_class }
36
+ define_singleton_method(:tool_name) { tool_name }
37
+
38
+ # Instance #name returns the derived tool name
39
+ define_method(:name) { tool_name }
40
+
41
+ define_method(:execute) do |**kwargs|
42
+ depth = (Thread.current[:ruby_llm_agents_tool_depth] || 0) + 1
43
+ if depth > MAX_AGENT_TOOL_DEPTH
44
+ return "Error calling #{captured_agent_class.name}: Agent tool depth exceeded (max #{MAX_AGENT_TOOL_DEPTH})"
45
+ end
46
+
47
+ Thread.current[:ruby_llm_agents_tool_depth] = depth
48
+
49
+ # Inject hierarchy context from thread-local (set by calling agent)
50
+ caller_ctx = Thread.current[:ruby_llm_agents_caller_context]
51
+
52
+ call_kwargs = kwargs.dup
53
+ if caller_ctx
54
+ call_kwargs[:_parent_execution_id] = caller_ctx.execution_id
55
+ call_kwargs[:_root_execution_id] = caller_ctx.root_execution_id || caller_ctx.execution_id
56
+ call_kwargs[:tenant] = caller_ctx.tenant_object if caller_ctx.tenant_id && !call_kwargs.key?(:tenant)
57
+ end
58
+
59
+ result = captured_agent_class.call(**call_kwargs)
60
+ content = result.respond_to?(:content) ? result.content : result
61
+ case content
62
+ when String then content
63
+ when Hash then content.to_json
64
+ when nil then "(no response)"
65
+ else content.to_s
66
+ end
67
+ rescue => e
68
+ "Error calling #{captured_agent_class.name}: #{e.message}"
69
+ ensure
70
+ Thread.current[:ruby_llm_agents_tool_depth] = depth - 1
71
+ end
72
+ end
73
+ end
74
+
75
+ # Converts agent class name to tool name.
76
+ #
77
+ # @example
78
+ # ResearchAgent -> "research"
79
+ # CodeReviewAgent -> "code_review"
80
+ #
81
+ # @param agent_class [Class] The agent class
82
+ # @return [String] Snake-cased tool name
83
+ def self.derive_tool_name(agent_class)
84
+ raw = agent_class.name.to_s.split("::").last
85
+ raw.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
86
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
87
+ .downcase
88
+ .sub(/_agent$/, "")
89
+ end
90
+
91
+ # Maps Ruby types to JSON Schema types for tool parameters.
92
+ #
93
+ # @param type [Class, Symbol, nil] Ruby type
94
+ # @return [Symbol] JSON Schema type
95
+ def self.map_type(type)
96
+ case type
97
+ when :integer then :integer
98
+ when :number, :float then :number
99
+ when :boolean then :boolean
100
+ when :array then :array
101
+ when :object then :object
102
+ else
103
+ # Handle class objects (Integer, Float, Array, Hash, etc.)
104
+ if type.is_a?(Class)
105
+ if type <= Integer
106
+ :integer
107
+ elsif type <= Float
108
+ :number
109
+ elsif type <= Array
110
+ :array
111
+ elsif type <= Hash
112
+ :object
113
+ elsif type == TrueClass || type == FalseClass
114
+ :boolean
115
+ else
116
+ :string
117
+ end
118
+ else
119
+ :string
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -352,7 +352,8 @@ module RubyLLM
352
352
  started_at: context.started_at || execution_started_at,
353
353
  completed_at: execution_completed_at,
354
354
  duration_ms: duration_ms,
355
- tenant_id: context.tenant_id
355
+ tenant_id: context.tenant_id,
356
+ execution_id: context.execution_id
356
357
  )
357
358
  end
358
359
 
@@ -527,7 +528,7 @@ module RubyLLM
527
528
  end
528
529
 
529
530
  # Builds the final result object
530
- def build_result(raw_result, original_text, started_at:, completed_at:, duration_ms:, tenant_id:)
531
+ def build_result(raw_result, original_text, started_at:, completed_at:, duration_ms:, tenant_id:, execution_id: nil)
531
532
  SpeechResult.new(
532
533
  audio: raw_result[:audio],
533
534
  duration: raw_result[:duration],
@@ -544,7 +545,8 @@ module RubyLLM
544
545
  completed_at: completed_at,
545
546
  total_cost: calculate_cost(raw_result),
546
547
  status: :success,
547
- tenant_id: tenant_id
548
+ tenant_id: tenant_id,
549
+ execution_id: execution_id
548
550
  )
549
551
  end
550
552
 
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
- require "json"
3
+ require_relative "../pricing/data_store"
4
+ require_relative "../pricing/ruby_llm_adapter"
5
+ require_relative "../pricing/litellm_adapter"
5
6
 
6
7
  module RubyLLM
7
8
  module Agents
8
9
  module Audio
9
10
  # Dynamic pricing resolution for text-to-speech models.
10
11
  #
11
- # Uses a four-tier pricing cascade:
12
- # 1. LiteLLM JSON (primary) - future-proof, auto-updating
13
- # 2. Configurable pricing table - user overrides via config.tts_model_pricing
14
- # 3. ElevenLabs API - dynamic multiplier × base rate from /v1/models
15
- # 4. Hardcoded fallbacks - per-model defaults
12
+ # Uses a three-tier pricing cascade (no hardcoded prices):
13
+ # 1. Configurable pricing table - user overrides via config.tts_model_pricing
14
+ # 2. LiteLLM (via shared DataStore) - comprehensive, community-maintained
15
+ # 3. ElevenLabs API - dynamic multiplier × user-configured base rate
16
+ #
17
+ # When no pricing is found, returns 0 to signal unknown cost.
16
18
  #
17
19
  # All prices are per 1,000 characters.
18
20
  #
@@ -31,9 +33,6 @@ module RubyLLM
31
33
  module SpeechPricing
32
34
  extend self
33
35
 
34
- LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
35
- DEFAULT_CACHE_TTL = 24 * 60 * 60 # 24 hours
36
-
37
36
  # Calculate total cost for a speech operation
38
37
  #
39
38
  # @param provider [Symbol] :openai or :elevenlabs
@@ -49,150 +48,91 @@ module RubyLLM
49
48
  #
50
49
  # @param provider [Symbol] Provider identifier
51
50
  # @param model_id [String] Model identifier
52
- # @return [Float] Cost per 1K characters in USD
51
+ # @return [Float] Cost per 1K characters in USD (0 if unknown)
53
52
  def cost_per_1k_characters(provider, model_id)
54
- # Tier 1: LiteLLM
55
- if (litellm_price = from_litellm(model_id))
56
- return litellm_price
57
- end
58
-
59
- # Tier 2: User config overrides
53
+ # Tier 1: User config overrides (highest priority)
60
54
  if (config_price = from_config(model_id))
61
55
  return config_price
62
56
  end
63
57
 
64
- # Tier 3: ElevenLabs API multiplier × base rate
58
+ # Tier 2: LiteLLM (via shared adapter/DataStore)
59
+ if (litellm_price = from_litellm(model_id))
60
+ return litellm_price
61
+ end
62
+
63
+ # Tier 3: ElevenLabs API multiplier × user-configured base rate
65
64
  if provider == :elevenlabs && (api_price = from_elevenlabs_api(model_id))
66
65
  return api_price
67
66
  end
68
67
 
69
- # Tier 4: Hardcoded fallbacks
70
- fallback_price(provider, model_id)
68
+ # No pricing found — return user-configured default or 0
69
+ config.default_tts_cost || 0
71
70
  end
72
71
 
73
- # Force refresh of cached LiteLLM data
72
+ # Force refresh of cached pricing data
74
73
  def refresh!
75
- @litellm_data = nil
76
- @litellm_fetched_at = nil
77
- litellm_data
74
+ Pricing::DataStore.refresh!
78
75
  end
79
76
 
80
- # Expose all known pricing for debugging/dashboard
77
+ # Expose all known pricing for debugging/console inspection
81
78
  def all_pricing
82
79
  {
83
80
  litellm: litellm_tts_models,
84
81
  configured: config.tts_model_pricing || {},
85
- elevenlabs_api: elevenlabs_api_pricing,
86
- fallbacks: fallback_pricing_table
82
+ elevenlabs_api: elevenlabs_api_pricing
87
83
  }
88
84
  end
89
85
 
90
86
  private
91
87
 
92
88
  # ============================================================
93
- # Tier 1: LiteLLM
89
+ # Tier 1: User configuration
94
90
  # ============================================================
95
91
 
96
- def from_litellm(model_id)
97
- data = litellm_data
98
- return nil unless data
99
-
100
- model_data = find_litellm_model(data, model_id)
101
- return nil unless model_data
102
-
103
- extract_litellm_tts_price(model_data)
104
- end
92
+ def from_config(model_id)
93
+ table = config.tts_model_pricing
94
+ return nil unless table.is_a?(Hash) && !table.empty?
105
95
 
106
- def find_litellm_model(data, model_id)
107
96
  normalized = normalize_model_id(model_id)
108
97
 
109
- candidates = [
110
- model_id,
111
- normalized,
112
- "tts/#{model_id}",
113
- "openai/#{model_id}",
114
- "elevenlabs/#{model_id}"
115
- ]
116
-
117
- candidates.each do |key|
118
- return data[key] if data[key]
119
- end
98
+ price = table[model_id] || table[normalized] ||
99
+ table[model_id.to_sym] || table[normalized.to_sym]
120
100
 
121
- data.find do |key, _|
122
- key.to_s.downcase.include?(normalized.downcase)
123
- end&.last
101
+ price if price.is_a?(Numeric)
124
102
  end
125
103
 
126
- def extract_litellm_tts_price(model_data)
127
- if model_data["input_cost_per_character"]
128
- return model_data["input_cost_per_character"] * 1000
129
- end
130
-
131
- if model_data["output_cost_per_character"]
132
- return model_data["output_cost_per_character"] * 1000
133
- end
134
-
135
- if model_data["output_cost_per_audio_token"]
136
- return model_data["output_cost_per_audio_token"] * 250
137
- end
138
-
139
- nil
140
- end
104
+ # ============================================================
105
+ # Tier 2: LiteLLM (via shared DataStore + adapter)
106
+ # ============================================================
141
107
 
142
- def litellm_data
143
- return @litellm_data if @litellm_data && !cache_expired?
108
+ def from_litellm(model_id)
109
+ data = Pricing::LiteLLMAdapter.find_model(model_id)
110
+ return nil unless data
144
111
 
145
- @litellm_data = fetch_litellm_data
146
- @litellm_fetched_at = Time.now
147
- @litellm_data
112
+ extract_tts_price(data)
148
113
  end
149
114
 
150
- def fetch_litellm_data
151
- if defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
152
- Rails.cache.fetch("litellm_tts_pricing_data", expires_in: cache_ttl) do
153
- fetch_from_url
154
- end
155
- else
156
- fetch_from_url
115
+ def extract_tts_price(data)
116
+ if data[:input_cost_per_character]
117
+ return (data[:input_cost_per_character] * 1000).round(6)
157
118
  end
158
- rescue => e
159
- warn "[RubyLLM::Agents] Failed to fetch LiteLLM TTS pricing: #{e.message}"
160
- {}
161
- end
162
-
163
- def fetch_from_url
164
- uri = URI(config.litellm_pricing_url || LITELLM_PRICING_URL)
165
- http = Net::HTTP.new(uri.host, uri.port)
166
- http.use_ssl = uri.scheme == "https"
167
- http.open_timeout = 5
168
- http.read_timeout = 10
169
119
 
170
- request = Net::HTTP::Get.new(uri)
171
- response = http.request(request)
172
-
173
- if response.is_a?(Net::HTTPSuccess)
174
- JSON.parse(response.body)
175
- else
176
- {}
120
+ if data[:output_cost_per_character]
121
+ return (data[:output_cost_per_character] * 1000).round(6)
177
122
  end
178
- rescue => e
179
- warn "[RubyLLM::Agents] HTTP error fetching LiteLLM pricing: #{e.message}"
180
- {}
181
- end
182
123
 
183
- def cache_expired?
184
- return true unless @litellm_fetched_at
185
- Time.now - @litellm_fetched_at > cache_ttl
186
- end
124
+ if data[:output_cost_per_audio_token]
125
+ return (data[:output_cost_per_audio_token] * 250).round(6)
126
+ end
187
127
 
188
- def cache_ttl
189
- ttl = config.litellm_pricing_cache_ttl
190
- return DEFAULT_CACHE_TTL unless ttl
191
- ttl.respond_to?(:to_i) ? ttl.to_i : ttl
128
+ nil
192
129
  end
193
130
 
194
131
  def litellm_tts_models
195
- litellm_data.select do |key, value|
132
+ data = Pricing::DataStore.litellm_data
133
+ return {} unless data.is_a?(Hash)
134
+
135
+ data.select do |key, value|
196
136
  value.is_a?(Hash) && (
197
137
  value["input_cost_per_character"] ||
198
138
  key.to_s.match?(/tts|speech|eleven/i)
@@ -200,35 +140,6 @@ module RubyLLM
200
140
  end
201
141
  end
202
142
 
203
- def elevenlabs_api_pricing
204
- return {} unless defined?(ElevenLabs::ModelRegistry)
205
-
206
- base = config.elevenlabs_base_cost_per_1k || 0.30
207
- ElevenLabs::ModelRegistry.models.each_with_object({}) do |model, hash|
208
- multiplier = model.dig("model_rates", "character_cost_multiplier") || 1.0
209
- hash[model["model_id"]] = (base * multiplier).round(6)
210
- end
211
- rescue => e
212
- warn "[RubyLLM::Agents] Failed to get ElevenLabs API pricing: #{e.message}"
213
- {}
214
- end
215
-
216
- # ============================================================
217
- # Tier 2: User configuration
218
- # ============================================================
219
-
220
- def from_config(model_id)
221
- table = config.tts_model_pricing
222
- return nil unless table.is_a?(Hash) && !table.empty?
223
-
224
- normalized = normalize_model_id(model_id)
225
-
226
- price = table[model_id] || table[normalized] ||
227
- table[model_id.to_sym] || table[normalized.to_sym]
228
-
229
- price if price.is_a?(Numeric)
230
- end
231
-
232
143
  # ============================================================
233
144
  # Tier 3: ElevenLabs API (dynamic multiplier × base rate)
234
145
  # ============================================================
@@ -236,67 +147,32 @@ module RubyLLM
236
147
  def from_elevenlabs_api(model_id)
237
148
  return nil unless defined?(ElevenLabs::ModelRegistry)
238
149
 
150
+ base = config.elevenlabs_base_cost_per_1k
151
+ return nil unless base
152
+
239
153
  model = ElevenLabs::ModelRegistry.find(model_id)
240
154
  return nil unless model
241
155
 
242
156
  multiplier = model.dig("model_rates", "character_cost_multiplier") || 1.0
243
- base = config.elevenlabs_base_cost_per_1k || 0.30
244
157
  (base * multiplier).round(6)
245
158
  rescue => e
246
159
  warn "[RubyLLM::Agents] Failed to get ElevenLabs API pricing: #{e.message}"
247
160
  nil
248
161
  end
249
162
 
250
- # ============================================================
251
- # Tier 4: Hardcoded fallbacks
252
- # ============================================================
253
-
254
- def fallback_price(provider, model_id)
255
- normalized = normalize_model_id(model_id)
256
-
257
- case provider
258
- when :openai
259
- openai_fallback_price(normalized)
260
- when :elevenlabs
261
- elevenlabs_fallback_price(normalized)
262
- else
263
- config.default_tts_cost || 0.015
264
- end
265
- end
163
+ def elevenlabs_api_pricing
164
+ return {} unless defined?(ElevenLabs::ModelRegistry)
266
165
 
267
- def openai_fallback_price(model_id)
268
- case model_id
269
- when /tts-1-hd/ then 0.030
270
- when /tts-1/ then 0.015
271
- else 0.015
272
- end
273
- end
166
+ base = config.elevenlabs_base_cost_per_1k
167
+ return {} unless base
274
168
 
275
- def elevenlabs_fallback_price(model_id)
276
- case model_id
277
- when /eleven_flash_v2/ then 0.15
278
- when /eleven_turbo_v2/ then 0.15
279
- when /eleven_v3/ then 0.30
280
- when /eleven_multilingual_v2/ then 0.30
281
- when /eleven_multilingual_v1/ then 0.30
282
- when /eleven_monolingual_v1/ then 0.30
283
- else 0.30
169
+ ElevenLabs::ModelRegistry.models.each_with_object({}) do |model, hash|
170
+ multiplier = model.dig("model_rates", "character_cost_multiplier") || 1.0
171
+ hash[model["model_id"]] = (base * multiplier).round(6)
284
172
  end
285
- end
286
-
287
- def fallback_pricing_table
288
- {
289
- "tts-1" => 0.015,
290
- "tts-1-hd" => 0.030,
291
- "eleven_monolingual_v1" => 0.30,
292
- "eleven_multilingual_v1" => 0.30,
293
- "eleven_multilingual_v2" => 0.30,
294
- "eleven_turbo_v2" => 0.15,
295
- "eleven_flash_v2" => 0.15,
296
- "eleven_turbo_v2_5" => 0.15,
297
- "eleven_flash_v2_5" => 0.15,
298
- "eleven_v3" => 0.30
299
- }
173
+ rescue => e
174
+ warn "[RubyLLM::Agents] Failed to get ElevenLabs API pricing: #{e.message}"
175
+ {}
300
176
  end
301
177
 
302
178
  def normalize_model_id(model_id)
@@ -341,7 +341,8 @@ module RubyLLM
341
341
  started_at: context.started_at || execution_started_at,
342
342
  completed_at: execution_completed_at,
343
343
  duration_ms: duration_ms,
344
- tenant_id: context.tenant_id
344
+ tenant_id: context.tenant_id,
345
+ execution_id: context.execution_id
345
346
  )
346
347
  end
347
348
 
@@ -597,7 +598,7 @@ module RubyLLM
597
598
  end
598
599
 
599
600
  # Builds the final result object
600
- def build_result(raw_result, started_at:, completed_at:, duration_ms:, tenant_id:)
601
+ def build_result(raw_result, started_at:, completed_at:, duration_ms:, tenant_id:, execution_id: nil)
601
602
  # Apply post-processing
602
603
  text = raw_result[:text] ? postprocess_text(raw_result[:text]) : nil
603
604
 
@@ -615,7 +616,8 @@ module RubyLLM
615
616
  total_cost: calculate_cost(raw_result),
616
617
  audio_minutes: raw_result[:duration] ? raw_result[:duration] / 60.0 : nil,
617
618
  status: :success,
618
- tenant_id: tenant_id
619
+ tenant_id: tenant_id,
620
+ execution_id: execution_id
619
621
  )
620
622
  end
621
623
 
@@ -39,8 +39,6 @@ module RubyLLM
39
39
  module TranscriptionPricing
40
40
  extend self
41
41
 
42
- LITELLM_PRICING_URL = Pricing::DataStore::LITELLM_URL
43
-
44
42
  SOURCES = [:config, :ruby_llm, :litellm, :portkey, :openrouter, :helicone, :llmpricing].freeze
45
43
 
46
44
  # Calculate total cost for a transcription operation
@@ -81,16 +79,16 @@ module RubyLLM
81
79
  Pricing::DataStore.refresh!
82
80
  end
83
81
 
84
- # Expose all known pricing for debugging/dashboard
82
+ # Expose all known pricing for debugging/console inspection
85
83
  #
86
84
  # @return [Hash] Pricing from all tiers
87
85
  def all_pricing
88
86
  {
89
- ruby_llm: {}, # local gem, per-model lookup
87
+ ruby_llm: {},
90
88
  litellm: litellm_transcription_models,
91
- portkey: {}, # per-model, populated on demand
92
- openrouter: {}, # no dedicated transcription models
93
- helicone: {}, # no transcription models
89
+ portkey: {},
90
+ openrouter: {},
91
+ helicone: {},
94
92
  configured: config.transcription_model_pricing || {}
95
93
  }
96
94
  end