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.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +155 -10
- data/app/helpers/ruby_llm/agents/application_helper.rb +15 -1
- data/app/models/ruby_llm/agents/execution/replayable.rb +124 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +42 -1
- data/app/models/ruby_llm/agents/execution.rb +50 -1
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +28 -4
- data/app/views/layouts/ruby_llm/agents/application.html.erb +41 -28
- data/app/views/ruby_llm/agents/agents/show.html.erb +16 -1
- data/app/views/ruby_llm/agents/dashboard/_top_tenants.html.erb +47 -0
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +397 -100
- data/lib/generators/ruby_llm_agents/rename_agent_generator.rb +53 -0
- data/lib/generators/ruby_llm_agents/templates/rename_agent_migration.rb.tt +19 -0
- data/lib/ruby_llm/agents/agent_tool.rb +125 -0
- data/lib/ruby_llm/agents/audio/speaker.rb +5 -3
- data/lib/ruby_llm/agents/audio/speech_pricing.rb +63 -187
- data/lib/ruby_llm/agents/audio/transcriber.rb +5 -3
- data/lib/ruby_llm/agents/audio/transcription_pricing.rb +5 -7
- data/lib/ruby_llm/agents/base_agent.rb +144 -5
- data/lib/ruby_llm/agents/core/configuration.rb +178 -53
- data/lib/ruby_llm/agents/core/errors.rb +3 -77
- data/lib/ruby_llm/agents/core/instrumentation.rb +0 -17
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +0 -8
- data/lib/ruby_llm/agents/dsl/queryable.rb +124 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -1
- data/lib/ruby_llm/agents/image/generator/pricing.rb +75 -217
- data/lib/ruby_llm/agents/image/generator.rb +5 -3
- data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +8 -0
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +4 -2
- data/lib/ruby_llm/agents/pipeline/builder.rb +43 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +11 -1
- data/lib/ruby_llm/agents/pipeline/executor.rb +1 -25
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +26 -1
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +18 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +130 -3
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +29 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +11 -4
- data/lib/ruby_llm/agents/pipeline.rb +0 -92
- data/lib/ruby_llm/agents/results/background_removal_result.rb +11 -1
- data/lib/ruby_llm/agents/results/base.rb +23 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +14 -1
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_edit_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_generation_result.rb +12 -3
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_transform_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_variation_result.rb +11 -1
- data/lib/ruby_llm/agents/results/speech_result.rb +20 -1
- data/lib/ruby_llm/agents/results/transcription_result.rb +20 -1
- data/lib/ruby_llm/agents/text/embedder.rb +23 -18
- data/lib/ruby_llm/agents.rb +70 -5
- data/lib/tasks/ruby_llm_agents.rake +21 -0
- metadata +7 -6
- data/lib/ruby_llm/agents/infrastructure/reliability/breaker_manager.rb +0 -80
- data/lib/ruby_llm/agents/infrastructure/reliability/execution_constraints.rb +0 -69
- data/lib/ruby_llm/agents/infrastructure/reliability/executor.rb +0 -125
- data/lib/ruby_llm/agents/infrastructure/reliability/fallback_routing.rb +0 -72
- 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
|
-
|
|
4
|
-
|
|
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
|
|
12
|
-
# 1.
|
|
13
|
-
# 2.
|
|
14
|
-
# 3. ElevenLabs API - dynamic multiplier × base rate
|
|
15
|
-
#
|
|
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:
|
|
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
|
|
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
|
-
#
|
|
70
|
-
|
|
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
|
|
72
|
+
# Force refresh of cached pricing data
|
|
74
73
|
def refresh!
|
|
75
|
-
|
|
76
|
-
@litellm_fetched_at = nil
|
|
77
|
-
litellm_data
|
|
74
|
+
Pricing::DataStore.refresh!
|
|
78
75
|
end
|
|
79
76
|
|
|
80
|
-
# Expose all known pricing for debugging/
|
|
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:
|
|
89
|
+
# Tier 1: User configuration
|
|
94
90
|
# ============================================================
|
|
95
91
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
return nil unless
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
key.to_s.downcase.include?(normalized.downcase)
|
|
123
|
-
end&.last
|
|
101
|
+
price if price.is_a?(Numeric)
|
|
124
102
|
end
|
|
125
103
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
143
|
-
|
|
108
|
+
def from_litellm(model_id)
|
|
109
|
+
data = Pricing::LiteLLMAdapter.find_model(model_id)
|
|
110
|
+
return nil unless data
|
|
144
111
|
|
|
145
|
-
|
|
146
|
-
@litellm_fetched_at = Time.now
|
|
147
|
-
@litellm_data
|
|
112
|
+
extract_tts_price(data)
|
|
148
113
|
end
|
|
149
114
|
|
|
150
|
-
def
|
|
151
|
-
if
|
|
152
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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/
|
|
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: {},
|
|
87
|
+
ruby_llm: {},
|
|
90
88
|
litellm: litellm_transcription_models,
|
|
91
|
-
portkey: {},
|
|
92
|
-
openrouter: {},
|
|
93
|
-
helicone: {},
|
|
89
|
+
portkey: {},
|
|
90
|
+
openrouter: {},
|
|
91
|
+
helicone: {},
|
|
94
92
|
configured: config.transcription_model_pricing || {}
|
|
95
93
|
}
|
|
96
94
|
end
|