ruby_llm-agents 3.3.0 → 3.5.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 +49 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +27 -4
- data/app/services/ruby_llm/agents/agent_registry.rb +3 -1
- data/app/views/ruby_llm/agents/agents/_config_router.html.erb +110 -0
- data/app/views/ruby_llm/agents/agents/index.html.erb +6 -0
- data/app/views/ruby_llm/agents/executions/show.html.erb +10 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +8 -0
- data/lib/ruby_llm/agents/audio/elevenlabs/model_registry.rb +187 -0
- data/lib/ruby_llm/agents/audio/speaker.rb +38 -0
- data/lib/ruby_llm/agents/audio/speech_client.rb +26 -2
- data/lib/ruby_llm/agents/audio/speech_pricing.rb +44 -3
- data/lib/ruby_llm/agents/audio/transcriber.rb +26 -15
- data/lib/ruby_llm/agents/audio/transcription_pricing.rb +226 -0
- data/lib/ruby_llm/agents/core/configuration.rb +32 -1
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/pricing/data_store.rb +339 -0
- data/lib/ruby_llm/agents/pricing/helicone_adapter.rb +88 -0
- data/lib/ruby_llm/agents/pricing/litellm_adapter.rb +105 -0
- data/lib/ruby_llm/agents/pricing/llmpricing_adapter.rb +73 -0
- data/lib/ruby_llm/agents/pricing/openrouter_adapter.rb +90 -0
- data/lib/ruby_llm/agents/pricing/portkey_adapter.rb +94 -0
- data/lib/ruby_llm/agents/pricing/ruby_llm_adapter.rb +94 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +19 -16
- data/lib/ruby_llm/agents/routing/class_methods.rb +92 -0
- data/lib/ruby_llm/agents/routing/result.rb +74 -0
- data/lib/ruby_llm/agents/routing.rb +140 -0
- data/lib/ruby_llm/agents.rb +3 -0
- metadata +14 -1
|
@@ -8,10 +8,11 @@ module RubyLLM
|
|
|
8
8
|
module Audio
|
|
9
9
|
# Dynamic pricing resolution for text-to-speech models.
|
|
10
10
|
#
|
|
11
|
-
# Uses
|
|
11
|
+
# Uses a four-tier pricing cascade:
|
|
12
12
|
# 1. LiteLLM JSON (primary) - future-proof, auto-updating
|
|
13
13
|
# 2. Configurable pricing table - user overrides via config.tts_model_pricing
|
|
14
|
-
# 3.
|
|
14
|
+
# 3. ElevenLabs API - dynamic multiplier × base rate from /v1/models
|
|
15
|
+
# 4. Hardcoded fallbacks - per-model defaults
|
|
15
16
|
#
|
|
16
17
|
# All prices are per 1,000 characters.
|
|
17
18
|
#
|
|
@@ -50,14 +51,22 @@ module RubyLLM
|
|
|
50
51
|
# @param model_id [String] Model identifier
|
|
51
52
|
# @return [Float] Cost per 1K characters in USD
|
|
52
53
|
def cost_per_1k_characters(provider, model_id)
|
|
54
|
+
# Tier 1: LiteLLM
|
|
53
55
|
if (litellm_price = from_litellm(model_id))
|
|
54
56
|
return litellm_price
|
|
55
57
|
end
|
|
56
58
|
|
|
59
|
+
# Tier 2: User config overrides
|
|
57
60
|
if (config_price = from_config(model_id))
|
|
58
61
|
return config_price
|
|
59
62
|
end
|
|
60
63
|
|
|
64
|
+
# Tier 3: ElevenLabs API multiplier × base rate
|
|
65
|
+
if provider == :elevenlabs && (api_price = from_elevenlabs_api(model_id))
|
|
66
|
+
return api_price
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Tier 4: Hardcoded fallbacks
|
|
61
70
|
fallback_price(provider, model_id)
|
|
62
71
|
end
|
|
63
72
|
|
|
@@ -73,6 +82,7 @@ module RubyLLM
|
|
|
73
82
|
{
|
|
74
83
|
litellm: litellm_tts_models,
|
|
75
84
|
configured: config.tts_model_pricing || {},
|
|
85
|
+
elevenlabs_api: elevenlabs_api_pricing,
|
|
76
86
|
fallbacks: fallback_pricing_table
|
|
77
87
|
}
|
|
78
88
|
end
|
|
@@ -190,6 +200,19 @@ module RubyLLM
|
|
|
190
200
|
end
|
|
191
201
|
end
|
|
192
202
|
|
|
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
|
+
|
|
193
216
|
# ============================================================
|
|
194
217
|
# Tier 2: User configuration
|
|
195
218
|
# ============================================================
|
|
@@ -207,7 +230,25 @@ module RubyLLM
|
|
|
207
230
|
end
|
|
208
231
|
|
|
209
232
|
# ============================================================
|
|
210
|
-
# Tier 3:
|
|
233
|
+
# Tier 3: ElevenLabs API (dynamic multiplier × base rate)
|
|
234
|
+
# ============================================================
|
|
235
|
+
|
|
236
|
+
def from_elevenlabs_api(model_id)
|
|
237
|
+
return nil unless defined?(ElevenLabs::ModelRegistry)
|
|
238
|
+
|
|
239
|
+
model = ElevenLabs::ModelRegistry.find(model_id)
|
|
240
|
+
return nil unless model
|
|
241
|
+
|
|
242
|
+
multiplier = model.dig("model_rates", "character_cost_multiplier") || 1.0
|
|
243
|
+
base = config.elevenlabs_base_cost_per_1k || 0.30
|
|
244
|
+
(base * multiplier).round(6)
|
|
245
|
+
rescue => e
|
|
246
|
+
warn "[RubyLLM::Agents] Failed to get ElevenLabs API pricing: #{e.message}"
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# ============================================================
|
|
251
|
+
# Tier 4: Hardcoded fallbacks
|
|
211
252
|
# ============================================================
|
|
212
253
|
|
|
213
254
|
def fallback_price(provider, model_id)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require_relative "../results/transcription_result"
|
|
5
|
+
require_relative "transcription_pricing"
|
|
5
6
|
|
|
6
7
|
module RubyLLM
|
|
7
8
|
module Agents
|
|
@@ -318,6 +319,12 @@ module RubyLLM
|
|
|
318
319
|
context.output_tokens = 0
|
|
319
320
|
context.total_cost = calculate_cost(raw_result)
|
|
320
321
|
|
|
322
|
+
# Store pricing warning if cost calculation returned nil
|
|
323
|
+
if @pricing_warning
|
|
324
|
+
context[:pricing_warning] = @pricing_warning
|
|
325
|
+
Rails.logger.warn(@pricing_warning) if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
326
|
+
end
|
|
327
|
+
|
|
321
328
|
# Store transcription-specific metadata for execution tracking
|
|
322
329
|
context[:language] = resolved_language if resolved_language
|
|
323
330
|
context[:detected_language] = raw_result[:language] if raw_result[:language]
|
|
@@ -615,30 +622,34 @@ module RubyLLM
|
|
|
615
622
|
# Calculates cost for transcription
|
|
616
623
|
#
|
|
617
624
|
# @param raw_result [Hash] Raw transcription result
|
|
618
|
-
# @return [Float] Cost in USD
|
|
625
|
+
# @return [Float] Cost in USD (0 if no pricing found)
|
|
619
626
|
def calculate_cost(raw_result)
|
|
620
|
-
|
|
621
|
-
duration_minutes = raw_result[:duration] ? raw_result[:duration] / 60.0 : 0
|
|
627
|
+
@pricing_warning = nil
|
|
622
628
|
|
|
623
|
-
# Check if response has cost info
|
|
629
|
+
# Check if response has cost info from the API
|
|
624
630
|
if raw_result[:raw_response].respond_to?(:cost) && raw_result[:raw_response].cost
|
|
625
631
|
return raw_result[:raw_response].cost
|
|
626
632
|
end
|
|
627
633
|
|
|
628
|
-
#
|
|
634
|
+
# Delegate to TranscriptionPricing (2-tier: LiteLLM + user config)
|
|
629
635
|
model = raw_result[:model].to_s
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
636
|
+
duration = raw_result[:duration] || 0
|
|
637
|
+
|
|
638
|
+
cost = Audio::TranscriptionPricing.calculate_cost(
|
|
639
|
+
model_id: model,
|
|
640
|
+
duration_seconds: duration
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
if cost.nil?
|
|
644
|
+
@pricing_warning = "[RubyLLM::Agents] No pricing found for transcription model '#{model}'. " \
|
|
645
|
+
"Cost recorded as $0. Add pricing to your config:\n" \
|
|
646
|
+
" RubyLLM::Agents.configure do |c|\n" \
|
|
647
|
+
" c.transcription_model_pricing = { \"#{model}\" => 0.006 } # price per minute\n" \
|
|
648
|
+
" end"
|
|
649
|
+
return 0
|
|
639
650
|
end
|
|
640
651
|
|
|
641
|
-
|
|
652
|
+
cost
|
|
642
653
|
end
|
|
643
654
|
|
|
644
655
|
# Resolves the model to use
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../pricing/data_store"
|
|
4
|
+
require_relative "../pricing/ruby_llm_adapter"
|
|
5
|
+
require_relative "../pricing/litellm_adapter"
|
|
6
|
+
require_relative "../pricing/portkey_adapter"
|
|
7
|
+
require_relative "../pricing/openrouter_adapter"
|
|
8
|
+
require_relative "../pricing/helicone_adapter"
|
|
9
|
+
require_relative "../pricing/llmpricing_adapter"
|
|
10
|
+
|
|
11
|
+
module RubyLLM
|
|
12
|
+
module Agents
|
|
13
|
+
module Audio
|
|
14
|
+
# Dynamic pricing resolution for audio transcription models.
|
|
15
|
+
#
|
|
16
|
+
# Cascades through multiple pricing sources to maximize coverage:
|
|
17
|
+
# 1. User config (instant, always wins)
|
|
18
|
+
# 2. RubyLLM gem (local, no HTTP, already a dependency)
|
|
19
|
+
# 3. LiteLLM (bulk, most comprehensive for transcription)
|
|
20
|
+
# 4. Portkey AI (per-model, good transcription coverage)
|
|
21
|
+
# 5. OpenRouter (bulk, audio-capable chat models only)
|
|
22
|
+
# 6. Helicone (text LLM only — pass-through, future-proof)
|
|
23
|
+
# 7. LLM Pricing AI (text LLM only — pass-through, future-proof)
|
|
24
|
+
#
|
|
25
|
+
# When no pricing is found, methods return nil to signal the caller
|
|
26
|
+
# should warn the user with actionable configuration instructions.
|
|
27
|
+
#
|
|
28
|
+
# All prices are per minute of audio.
|
|
29
|
+
#
|
|
30
|
+
# @example Get cost for a transcription
|
|
31
|
+
# TranscriptionPricing.calculate_cost(model_id: "whisper-1", duration_seconds: 120)
|
|
32
|
+
# # => 0.012 (or nil if no pricing found)
|
|
33
|
+
#
|
|
34
|
+
# @example User-configured pricing
|
|
35
|
+
# RubyLLM::Agents.configure do |c|
|
|
36
|
+
# c.transcription_model_pricing = { "whisper-1" => 0.006 }
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
module TranscriptionPricing
|
|
40
|
+
extend self
|
|
41
|
+
|
|
42
|
+
LITELLM_PRICING_URL = Pricing::DataStore::LITELLM_URL
|
|
43
|
+
|
|
44
|
+
SOURCES = [:config, :ruby_llm, :litellm, :portkey, :openrouter, :helicone, :llmpricing].freeze
|
|
45
|
+
|
|
46
|
+
# Calculate total cost for a transcription operation
|
|
47
|
+
#
|
|
48
|
+
# @param model_id [String] The model identifier
|
|
49
|
+
# @param duration_seconds [Numeric] Duration of audio in seconds
|
|
50
|
+
# @return [Float, nil] Total cost in USD, or nil if no pricing found
|
|
51
|
+
def calculate_cost(model_id:, duration_seconds:)
|
|
52
|
+
price = cost_per_minute(model_id)
|
|
53
|
+
return nil unless price
|
|
54
|
+
|
|
55
|
+
duration_minutes = duration_seconds / 60.0
|
|
56
|
+
(duration_minutes * price).round(6)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get cost per minute for a transcription model
|
|
60
|
+
#
|
|
61
|
+
# @param model_id [String] Model identifier
|
|
62
|
+
# @return [Float, nil] Cost per minute in USD, or nil if not found
|
|
63
|
+
def cost_per_minute(model_id)
|
|
64
|
+
SOURCES.each do |source|
|
|
65
|
+
price = send(:"from_#{source}", model_id)
|
|
66
|
+
return price if price
|
|
67
|
+
end
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check whether pricing is available for a model
|
|
72
|
+
#
|
|
73
|
+
# @param model_id [String] Model identifier
|
|
74
|
+
# @return [Boolean] true if pricing is available
|
|
75
|
+
def pricing_found?(model_id)
|
|
76
|
+
!cost_per_minute(model_id).nil?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Force refresh of cached pricing data
|
|
80
|
+
def refresh!
|
|
81
|
+
Pricing::DataStore.refresh!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Expose all known pricing for debugging/dashboard
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash] Pricing from all tiers
|
|
87
|
+
def all_pricing
|
|
88
|
+
{
|
|
89
|
+
ruby_llm: {}, # local gem, per-model lookup
|
|
90
|
+
litellm: litellm_transcription_models,
|
|
91
|
+
portkey: {}, # per-model, populated on demand
|
|
92
|
+
openrouter: {}, # no dedicated transcription models
|
|
93
|
+
helicone: {}, # no transcription models
|
|
94
|
+
configured: config.transcription_model_pricing || {}
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# ============================================================
|
|
101
|
+
# Tier 1: User configuration (highest priority)
|
|
102
|
+
# ============================================================
|
|
103
|
+
|
|
104
|
+
def from_config(model_id)
|
|
105
|
+
table = config.transcription_model_pricing
|
|
106
|
+
return nil unless table.is_a?(Hash) && !table.empty?
|
|
107
|
+
|
|
108
|
+
normalized = normalize_model_id(model_id)
|
|
109
|
+
|
|
110
|
+
price = table[model_id] || table[normalized] ||
|
|
111
|
+
table[model_id.to_sym] || table[normalized.to_sym]
|
|
112
|
+
|
|
113
|
+
price if price.is_a?(Numeric)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# ============================================================
|
|
117
|
+
# Tier 2: RubyLLM gem (local, no HTTP)
|
|
118
|
+
# ============================================================
|
|
119
|
+
|
|
120
|
+
def from_ruby_llm(model_id)
|
|
121
|
+
data = Pricing::RubyLLMAdapter.find_model(model_id)
|
|
122
|
+
return nil unless data
|
|
123
|
+
|
|
124
|
+
extract_per_minute(data)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ============================================================
|
|
128
|
+
# Tier 3: LiteLLM
|
|
129
|
+
# ============================================================
|
|
130
|
+
|
|
131
|
+
def from_litellm(model_id)
|
|
132
|
+
data = Pricing::LiteLLMAdapter.find_model(model_id)
|
|
133
|
+
return nil unless data
|
|
134
|
+
|
|
135
|
+
extract_per_minute(data)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# ============================================================
|
|
139
|
+
# Tier 4: Portkey AI
|
|
140
|
+
# ============================================================
|
|
141
|
+
|
|
142
|
+
def from_portkey(model_id)
|
|
143
|
+
data = Pricing::PortkeyAdapter.find_model(model_id)
|
|
144
|
+
return nil unless data
|
|
145
|
+
|
|
146
|
+
extract_per_minute(data)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# ============================================================
|
|
150
|
+
# Tier 5: OpenRouter (audio-capable chat models only)
|
|
151
|
+
# ============================================================
|
|
152
|
+
|
|
153
|
+
def from_openrouter(model_id)
|
|
154
|
+
data = Pricing::OpenRouterAdapter.find_model(model_id)
|
|
155
|
+
return nil unless data
|
|
156
|
+
|
|
157
|
+
extract_per_minute(data)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# ============================================================
|
|
161
|
+
# Tier 6: Helicone (text LLM only — future-proof)
|
|
162
|
+
# ============================================================
|
|
163
|
+
|
|
164
|
+
def from_helicone(model_id)
|
|
165
|
+
data = Pricing::HeliconeAdapter.find_model(model_id)
|
|
166
|
+
return nil unless data
|
|
167
|
+
|
|
168
|
+
extract_per_minute(data)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# ============================================================
|
|
172
|
+
# Tier 7: LLM Pricing AI (text LLM only — future-proof)
|
|
173
|
+
# ============================================================
|
|
174
|
+
|
|
175
|
+
def from_llmpricing(model_id)
|
|
176
|
+
data = Pricing::LLMPricingAdapter.find_model(model_id)
|
|
177
|
+
return nil unless data
|
|
178
|
+
|
|
179
|
+
extract_per_minute(data)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# ============================================================
|
|
183
|
+
# Price extraction
|
|
184
|
+
# ============================================================
|
|
185
|
+
|
|
186
|
+
def extract_per_minute(data)
|
|
187
|
+
# Per-second pricing (most common for transcription: whisper-1, etc.)
|
|
188
|
+
if data[:input_cost_per_second]
|
|
189
|
+
return (data[:input_cost_per_second] * 60).round(6)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Per-audio-token pricing (GPT-4o-transcribe models)
|
|
193
|
+
# ~25 audio tokens/second = 1500 tokens/minute
|
|
194
|
+
if data[:input_cost_per_audio_token]
|
|
195
|
+
return (data[:input_cost_per_audio_token] * 1500).round(6)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def litellm_transcription_models
|
|
202
|
+
data = Pricing::DataStore.litellm_data
|
|
203
|
+
return {} unless data.is_a?(Hash)
|
|
204
|
+
|
|
205
|
+
data.select do |key, value|
|
|
206
|
+
value.is_a?(Hash) && (
|
|
207
|
+
value["mode"] == "audio_transcription" ||
|
|
208
|
+
value["input_cost_per_second"] ||
|
|
209
|
+
key.to_s.match?(/whisper|transcri/i)
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def normalize_model_id(model_id)
|
|
215
|
+
model_id.to_s.downcase
|
|
216
|
+
.gsub(/[^a-z0-9._-]/, "-").squeeze("-")
|
|
217
|
+
.gsub(/^-|-$/, "")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def config
|
|
221
|
+
RubyLLM::Agents.configuration
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -453,7 +453,20 @@ module RubyLLM
|
|
|
453
453
|
:root_namespace,
|
|
454
454
|
:tool_result_max_length,
|
|
455
455
|
:redaction,
|
|
456
|
-
:persist_audio_data
|
|
456
|
+
:persist_audio_data,
|
|
457
|
+
:elevenlabs_base_cost_per_1k,
|
|
458
|
+
:elevenlabs_models_cache_ttl,
|
|
459
|
+
:transcription_model_pricing,
|
|
460
|
+
:default_transcription_cost,
|
|
461
|
+
:pricing_cache_ttl,
|
|
462
|
+
:portkey_pricing_enabled,
|
|
463
|
+
:portkey_pricing_url,
|
|
464
|
+
:openrouter_pricing_enabled,
|
|
465
|
+
:openrouter_pricing_url,
|
|
466
|
+
:helicone_pricing_enabled,
|
|
467
|
+
:helicone_pricing_url,
|
|
468
|
+
:llmpricing_enabled,
|
|
469
|
+
:llmpricing_url
|
|
457
470
|
|
|
458
471
|
# Attributes with validation (readers only, custom setters below)
|
|
459
472
|
attr_reader :default_temperature,
|
|
@@ -672,6 +685,19 @@ module RubyLLM
|
|
|
672
685
|
# Transcription defaults
|
|
673
686
|
@default_transcription_model = "whisper-1"
|
|
674
687
|
@track_transcriptions = true
|
|
688
|
+
@transcription_model_pricing = {}
|
|
689
|
+
@default_transcription_cost = nil # nil = no default, will trigger warning
|
|
690
|
+
|
|
691
|
+
# Multi-source pricing defaults
|
|
692
|
+
@pricing_cache_ttl = nil # nil = use DataStore default (24h)
|
|
693
|
+
@portkey_pricing_enabled = true
|
|
694
|
+
@portkey_pricing_url = nil # nil = use default
|
|
695
|
+
@openrouter_pricing_enabled = true
|
|
696
|
+
@openrouter_pricing_url = nil
|
|
697
|
+
@helicone_pricing_enabled = true
|
|
698
|
+
@helicone_pricing_url = nil
|
|
699
|
+
@llmpricing_enabled = true
|
|
700
|
+
@llmpricing_url = nil
|
|
675
701
|
|
|
676
702
|
# TTS/Speech defaults
|
|
677
703
|
@default_tts_provider = :openai
|
|
@@ -738,6 +764,11 @@ module RubyLLM
|
|
|
738
764
|
|
|
739
765
|
# Audio data persistence (disabled by default — base64 audio can be large)
|
|
740
766
|
@persist_audio_data = false
|
|
767
|
+
|
|
768
|
+
# ElevenLabs dynamic pricing: base cost per 1K characters (Pro plan overage rate)
|
|
769
|
+
@elevenlabs_base_cost_per_1k = 0.30
|
|
770
|
+
# ElevenLabs models cache TTL in seconds (6 hours)
|
|
771
|
+
@elevenlabs_models_cache_ttl = 21_600
|
|
741
772
|
end
|
|
742
773
|
|
|
743
774
|
# Returns the configured cache store, falling back to Rails.cache
|