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.
- checksums.yaml +4 -4
- data/README.md +189 -31
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +1 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +172 -34
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -366
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -210
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
- data/lib/ruby_llm/agents/configuration.rb +0 -394
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /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
|