ruby_llm-agents 0.5.0 → 1.0.0.beta.1

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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +189 -31
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  5. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  7. data/app/models/ruby_llm/agents/execution.rb +3 -0
  8. data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
  9. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
  11. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  12. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  13. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  14. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  15. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  16. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  17. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  18. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  19. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  20. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  21. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  22. data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
  23. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  24. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  25. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  26. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  27. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  28. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  29. data/config/routes.rb +1 -0
  30. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  31. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  32. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  33. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  34. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  35. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  36. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  37. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  38. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  39. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  40. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  41. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  42. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  43. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  44. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  45. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  46. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  47. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  48. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  49. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  50. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  51. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  52. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  53. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  54. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  55. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  56. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  57. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  58. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  60. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  61. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  62. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  63. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  64. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  65. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  66. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  67. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  68. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  69. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  70. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  71. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  72. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  73. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  74. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  75. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  76. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  77. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  78. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  79. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  80. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  81. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  82. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  83. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  84. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  85. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  86. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  87. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  88. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  89. data/lib/ruby_llm/agents/core/base.rb +135 -0
  90. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  91. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  92. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
  93. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  94. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  95. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  96. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  97. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  98. data/lib/ruby_llm/agents/dsl.rb +41 -0
  99. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  100. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  101. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  102. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  103. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  104. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  105. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  106. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  107. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  108. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  109. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  110. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  111. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  112. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  113. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  114. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  115. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  116. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  117. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  118. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  119. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  120. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  121. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  122. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  123. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  124. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  125. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  126. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  127. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  128. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  129. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  130. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  131. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  132. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  133. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  134. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  135. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  136. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  137. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  138. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  139. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  140. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  141. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  142. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  143. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  144. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  145. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  146. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  147. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
  148. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  149. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  150. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  151. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  152. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  153. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  154. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  155. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  156. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  157. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  158. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  159. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  160. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  161. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  162. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  163. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  164. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  165. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  166. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  167. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  168. data/lib/ruby_llm/agents.rb +86 -20
  169. metadata +172 -34
  170. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  171. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  172. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  173. data/lib/ruby_llm/agents/base/execution.rb +0 -366
  174. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  175. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  176. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  177. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  178. data/lib/ruby_llm/agents/base.rb +0 -210
  179. data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
  180. data/lib/ruby_llm/agents/configuration.rb +0 -394
  181. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  182. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  183. /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
  184. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  185. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  186. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  187. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  188. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  189. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  190. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ class ImageGenerator
9
+ # Dynamic pricing resolution for image generation models
10
+ #
11
+ # Uses a three-tier strategy:
12
+ # 1. LiteLLM JSON (primary) - comprehensive, community-maintained
13
+ # 2. Configurable pricing table - user overrides
14
+ # 3. Hardcoded fallbacks - last resort
15
+ #
16
+ # @example Get price for a model
17
+ # Pricing.cost_per_image("gpt-image-1", size: "1024x1024", quality: "hd")
18
+ # # => 0.08
19
+ #
20
+ # @example Calculate total cost
21
+ # Pricing.calculate_cost(model_id: "dall-e-3", size: "1024x1024", count: 4)
22
+ # # => 0.16
23
+ #
24
+ module Pricing
25
+ extend self
26
+
27
+ LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
28
+ DEFAULT_CACHE_TTL = 24 * 60 * 60 # 24 hours in seconds
29
+
30
+ # Calculate total cost for image generation
31
+ #
32
+ # @param model_id [String] The model identifier
33
+ # @param size [String] Image size (e.g., "1024x1024")
34
+ # @param quality [String] Quality setting ("standard", "hd")
35
+ # @param count [Integer] Number of images
36
+ # @return [Float] Total cost in USD
37
+ def calculate_cost(model_id:, size: nil, quality: nil, count: 1)
38
+ cost = cost_per_image(model_id, size: size, quality: quality)
39
+ (cost * count).round(6)
40
+ end
41
+
42
+ # Get cost for a single image
43
+ #
44
+ # @param model_id [String] The model identifier
45
+ # @param size [String] Image size
46
+ # @param quality [String] Quality setting
47
+ # @return [Float] Cost per image in USD
48
+ def cost_per_image(model_id, size: nil, quality: nil)
49
+ # 1. Try LiteLLM pricing data
50
+ if (litellm_price = from_litellm(model_id, size, quality))
51
+ return litellm_price
52
+ end
53
+
54
+ # 2. Try configurable pricing table
55
+ if (config_price = from_config(model_id, size, quality))
56
+ return config_price
57
+ end
58
+
59
+ # 3. Fall back to hardcoded estimates
60
+ fallback_price(model_id, size, quality)
61
+ end
62
+
63
+ # Refresh pricing data from LiteLLM
64
+ #
65
+ # @return [Hash] The fetched pricing data
66
+ def refresh!
67
+ @litellm_data = nil
68
+ @litellm_fetched_at = nil
69
+ litellm_data
70
+ end
71
+
72
+ # Get all known pricing for debugging/display
73
+ #
74
+ # @return [Hash] Merged pricing from all sources
75
+ def all_pricing
76
+ {
77
+ litellm: litellm_image_models,
78
+ configured: config.image_model_pricing || {},
79
+ fallbacks: fallback_pricing_table
80
+ }
81
+ end
82
+
83
+ private
84
+
85
+ # Fetch from LiteLLM JSON
86
+ def from_litellm(model_id, size, quality)
87
+ data = litellm_data
88
+ return nil unless data
89
+
90
+ # Try exact match first
91
+ model_data = find_litellm_model(data, model_id, size, quality)
92
+ return nil unless model_data
93
+
94
+ extract_litellm_price(model_data, size, quality)
95
+ end
96
+
97
+ def find_litellm_model(data, model_id, size, quality)
98
+ normalized = normalize_model_id(model_id)
99
+
100
+ # Try various key formats LiteLLM uses
101
+ candidates = [
102
+ model_id,
103
+ normalized,
104
+ "#{size}/#{model_id}",
105
+ "#{size}/#{quality}/#{model_id}",
106
+ "aiml/#{normalized}",
107
+ "together_ai/#{normalized}"
108
+ ]
109
+
110
+ candidates.each do |key|
111
+ return data[key] if data[key]
112
+ end
113
+
114
+ # Fuzzy match by model name pattern
115
+ data.find do |key, _value|
116
+ key_lower = key.to_s.downcase
117
+ normalized_lower = normalized.downcase
118
+
119
+ key_lower.include?(normalized_lower) ||
120
+ normalized_lower.include?(key_lower.split("/").last.to_s)
121
+ end&.last
122
+ end
123
+
124
+ def extract_litellm_price(model_data, size, quality)
125
+ # LiteLLM uses different pricing fields for images
126
+ if model_data["input_cost_per_image"]
127
+ return model_data["input_cost_per_image"]
128
+ end
129
+
130
+ if model_data["input_cost_per_pixel"] && size
131
+ width, height = size.split("x").map(&:to_i)
132
+ pixels = width * height
133
+ return (model_data["input_cost_per_pixel"] * pixels).round(6)
134
+ end
135
+
136
+ # Some models have quality-based pricing
137
+ if quality == "hd" && model_data["input_cost_per_image_hd"]
138
+ return model_data["input_cost_per_image_hd"]
139
+ end
140
+
141
+ nil
142
+ end
143
+
144
+ def litellm_data
145
+ return @litellm_data if @litellm_data && !cache_expired?
146
+
147
+ @litellm_data = fetch_litellm_data
148
+ @litellm_fetched_at = Time.now
149
+ @litellm_data
150
+ end
151
+
152
+ def fetch_litellm_data
153
+ # Use Rails cache if available
154
+ if defined?(Rails) && Rails.cache
155
+ Rails.cache.fetch("litellm_pricing_data", expires_in: cache_ttl) do
156
+ fetch_from_url
157
+ end
158
+ else
159
+ fetch_from_url
160
+ end
161
+ rescue StandardError => e
162
+ warn "[RubyLLM::Agents] Failed to fetch LiteLLM pricing: #{e.message}"
163
+ {}
164
+ end
165
+
166
+ def fetch_from_url
167
+ uri = URI(config.litellm_pricing_url || LITELLM_PRICING_URL)
168
+ http = Net::HTTP.new(uri.host, uri.port)
169
+ http.use_ssl = uri.scheme == "https"
170
+ http.open_timeout = 5
171
+ http.read_timeout = 10
172
+
173
+ request = Net::HTTP::Get.new(uri)
174
+ response = http.request(request)
175
+
176
+ if response.is_a?(Net::HTTPSuccess)
177
+ JSON.parse(response.body)
178
+ else
179
+ {}
180
+ end
181
+ rescue StandardError => e
182
+ warn "[RubyLLM::Agents] HTTP error fetching LiteLLM pricing: #{e.message}"
183
+ {}
184
+ end
185
+
186
+ def cache_expired?
187
+ return true unless @litellm_fetched_at
188
+ Time.now - @litellm_fetched_at > cache_ttl
189
+ end
190
+
191
+ def cache_ttl
192
+ ttl = config.litellm_pricing_cache_ttl
193
+ return DEFAULT_CACHE_TTL unless ttl
194
+
195
+ # Handle ActiveSupport::Duration
196
+ ttl.respond_to?(:to_i) ? ttl.to_i : ttl
197
+ end
198
+
199
+ # Get image-specific models from LiteLLM data
200
+ def litellm_image_models
201
+ litellm_data.select do |key, value|
202
+ value.is_a?(Hash) && (
203
+ value["input_cost_per_image"] ||
204
+ value["input_cost_per_pixel"] ||
205
+ key.to_s.match?(/dall-e|flux|sdxl|stable|imagen|image/i)
206
+ )
207
+ end
208
+ end
209
+
210
+ # Fetch from configurable pricing table
211
+ def from_config(model_id, size, quality)
212
+ table = config.image_model_pricing
213
+ return nil unless table
214
+
215
+ normalized = normalize_model_id(model_id)
216
+
217
+ # Try exact match, then normalized
218
+ pricing = table[model_id] || table[normalized] || table[model_id.to_sym] || table[normalized.to_sym]
219
+ return nil unless pricing
220
+
221
+ resolve_config_price(pricing, size, quality)
222
+ end
223
+
224
+ def resolve_config_price(pricing, size, quality)
225
+ return pricing if pricing.is_a?(Numeric)
226
+ return nil unless pricing.is_a?(Hash)
227
+
228
+ # Size/quality combined key (e.g., "1024x1024/hd")
229
+ combined_key = [size, quality].compact.join("/")
230
+ if combined_key.present? && (pricing[combined_key] || pricing[combined_key.to_sym])
231
+ return pricing[combined_key] || pricing[combined_key.to_sym]
232
+ end
233
+
234
+ # Size-specific pricing
235
+ if size && (pricing[size] || pricing[size.to_sym])
236
+ return pricing[size] || pricing[size.to_sym]
237
+ end
238
+
239
+ # Quality-specific pricing
240
+ if quality == "hd"
241
+ if pricing[:hd] || pricing["hd"]
242
+ pixels = parse_pixels(size)
243
+ if pixels && pixels >= 1_000_000 && (pricing[:large_hd] || pricing["large_hd"])
244
+ return pricing[:large_hd] || pricing["large_hd"]
245
+ end
246
+ return pricing[:hd] || pricing["hd"]
247
+ end
248
+ end
249
+
250
+ # Base price
251
+ pricing[:base] || pricing["base"] || pricing[:default] || pricing["default"] || pricing[:standard] || pricing["standard"]
252
+ end
253
+
254
+ # Hardcoded fallback prices
255
+ def fallback_price(model_id, size, quality)
256
+ normalized = normalize_model_id(model_id)
257
+
258
+ case normalized
259
+ when /gpt-image-1|dall-e-3/i
260
+ dalle3_price(size, quality)
261
+ when /dall-e-2/i
262
+ dalle2_price(size)
263
+ when /imagen/i
264
+ 0.02
265
+ when /flux.*pro.*ultra/i
266
+ 0.063
267
+ when /flux.*pro/i
268
+ 0.05
269
+ when /flux.*dev/i
270
+ 0.025
271
+ when /flux.*schnell/i
272
+ 0.003
273
+ when /sdxl.*lightning/i
274
+ 0.002
275
+ when /sdxl|stable-diffusion-xl/i
276
+ 0.04
277
+ when /stable-diffusion/i
278
+ 0.02
279
+ when /ideogram/i
280
+ 0.04
281
+ when /recraft/i
282
+ 0.04
283
+ when /real-esrgan|upscal/i
284
+ 0.01
285
+ when /blip|caption|analyz/i
286
+ 0.001
287
+ when /segment|background|rembg/i
288
+ 0.01
289
+ else
290
+ config.default_image_cost || 0.04
291
+ end
292
+ end
293
+
294
+ def dalle3_price(size, quality)
295
+ pixels = parse_pixels(size)
296
+ is_large = pixels && pixels >= 1_000_000
297
+
298
+ case quality
299
+ when "hd"
300
+ is_large ? 0.12 : 0.08
301
+ else
302
+ is_large ? 0.08 : 0.04
303
+ end
304
+ end
305
+
306
+ def dalle2_price(size)
307
+ case size
308
+ when "1024x1024" then 0.02
309
+ when "512x512" then 0.018
310
+ when "256x256" then 0.016
311
+ else 0.02
312
+ end
313
+ end
314
+
315
+ def fallback_pricing_table
316
+ {
317
+ "gpt-image-1" => { standard: 0.04, hd: 0.08, large_hd: 0.12 },
318
+ "dall-e-3" => { standard: 0.04, hd: 0.08, large_hd: 0.12 },
319
+ "dall-e-2" => { "1024x1024" => 0.02, "512x512" => 0.018, "256x256" => 0.016 },
320
+ "flux-pro" => 0.05,
321
+ "flux-dev" => 0.025,
322
+ "flux-schnell" => 0.003,
323
+ "sdxl" => 0.04,
324
+ "stable-diffusion-3.5" => 0.03,
325
+ "imagen-3" => 0.02,
326
+ "ideogram-2" => 0.04
327
+ }
328
+ end
329
+
330
+ def parse_pixels(size)
331
+ return nil unless size
332
+ width, height = size.to_s.split("x").map(&:to_i)
333
+ return nil if width.zero? || height.zero?
334
+ width * height
335
+ rescue StandardError
336
+ nil
337
+ end
338
+
339
+ def normalize_model_id(model_id)
340
+ model_id.to_s
341
+ .downcase
342
+ .gsub(/[^a-z0-9.-]/, "-")
343
+ .gsub(/-+/, "-")
344
+ .gsub(/^-|-$/, "")
345
+ end
346
+
347
+ def config
348
+ RubyLLM::Agents.configuration
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class ImageGenerator
6
+ # Prompt template support for image generators
7
+ #
8
+ # Allows defining reusable prompt templates that wrap user input
9
+ # with consistent styling, quality, or context instructions.
10
+ #
11
+ # @example Using templates in a generator
12
+ # class ProductPhotoGenerator < RubyLLM::Agents::ImageGenerator
13
+ # model "gpt-image-1"
14
+ # template "Professional product photography of {prompt}, " \
15
+ # "white background, studio lighting, 8k resolution"
16
+ # end
17
+ #
18
+ # result = ProductPhotoGenerator.call(prompt: "a red sneaker")
19
+ # # Actual prompt: "Professional product photography of a red sneaker, ..."
20
+ #
21
+ # @example Template with multiple placeholders
22
+ # class StyleTransferGenerator < RubyLLM::Agents::ImageGenerator
23
+ # def build_prompt
24
+ # Templates.apply(
25
+ # "{prompt} in the style of {style}, detailed, high quality",
26
+ # prompt: @prompt,
27
+ # style: options[:style] || "impressionism"
28
+ # )
29
+ # end
30
+ # end
31
+ #
32
+ module Templates
33
+ # Common prompt templates for different use cases
34
+ PRESETS = {
35
+ # Photography styles
36
+ product: "Professional product photography of {prompt}, " \
37
+ "white background, studio lighting, high resolution, commercial quality",
38
+
39
+ portrait: "Professional portrait of {prompt}, " \
40
+ "soft lighting, shallow depth of field, 85mm lens, studio quality",
41
+
42
+ landscape: "Stunning landscape photograph of {prompt}, " \
43
+ "golden hour lighting, dramatic sky, high dynamic range",
44
+
45
+ # Artistic styles
46
+ watercolor: "Watercolor painting of {prompt}, " \
47
+ "soft brushstrokes, muted colors, artistic, on textured paper",
48
+
49
+ oil_painting: "Oil painting of {prompt}, " \
50
+ "rich colors, visible brushwork, classical style, museum quality",
51
+
52
+ digital_art: "Digital art of {prompt}, " \
53
+ "vibrant colors, detailed, trending on artstation, 4k",
54
+
55
+ anime: "Anime style illustration of {prompt}, " \
56
+ "detailed, Studio Ghibli inspired, beautiful lighting",
57
+
58
+ # Technical styles
59
+ isometric: "Isometric 3D render of {prompt}, " \
60
+ "clean lines, bright colors, game asset style",
61
+
62
+ blueprint: "Technical blueprint of {prompt}, " \
63
+ "detailed engineering drawing, white lines on blue background",
64
+
65
+ wireframe: "3D wireframe render of {prompt}, " \
66
+ "clean geometric lines, technical visualization",
67
+
68
+ # UI/Design
69
+ icon: "App icon design of {prompt}, " \
70
+ "flat design, bold colors, minimal, iOS style, high resolution",
71
+
72
+ logo: "Minimalist logo design for {prompt}, " \
73
+ "clean lines, professional, vector style, brand identity",
74
+
75
+ ui_mockup: "Modern UI mockup of {prompt}, " \
76
+ "clean design, shadows, glass morphism, Figma style"
77
+ }.freeze
78
+
79
+ class << self
80
+ # Apply a template to a prompt with variable substitution
81
+ #
82
+ # @param template [String] Template string with {placeholder} syntax
83
+ # @param vars [Hash] Variables to substitute
84
+ # @return [String] The rendered template
85
+ def apply(template, **vars)
86
+ result = template.dup
87
+ vars.each do |key, value|
88
+ result.gsub!("{#{key}}", value.to_s)
89
+ end
90
+ result
91
+ end
92
+
93
+ # Get a preset template by name
94
+ #
95
+ # @param name [Symbol] Preset name
96
+ # @return [String, nil] The template string or nil
97
+ def preset(name)
98
+ PRESETS[name.to_sym]
99
+ end
100
+
101
+ # List all available preset names
102
+ #
103
+ # @return [Array<Symbol>] Preset names
104
+ def preset_names
105
+ PRESETS.keys
106
+ end
107
+
108
+ # Apply a preset template to a prompt
109
+ #
110
+ # @param name [Symbol] Preset name
111
+ # @param prompt [String] User prompt
112
+ # @return [String] Rendered template
113
+ # @raise [ArgumentError] If preset not found
114
+ def apply_preset(name, prompt)
115
+ template = preset(name)
116
+ raise ArgumentError, "Unknown template preset: #{name}" unless template
117
+
118
+ apply(template, prompt: prompt)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end