ruby_llm-agents 0.5.0 → 1.0.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 +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
|
@@ -1,733 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "cache_helper"
|
|
4
|
-
|
|
5
|
-
module RubyLLM
|
|
6
|
-
module Agents
|
|
7
|
-
# Cache-based budget tracking for cost governance
|
|
8
|
-
#
|
|
9
|
-
# Tracks spending against configured budget limits using cache counters.
|
|
10
|
-
# Supports daily and monthly budgets at both global and per-agent levels.
|
|
11
|
-
# In multi-tenant mode, budgets are tracked separately per tenant.
|
|
12
|
-
#
|
|
13
|
-
# Note: Uses best-effort enforcement with cache counters. In high-concurrency
|
|
14
|
-
# scenarios, slight overruns may occur due to race conditions. This is an
|
|
15
|
-
# acceptable trade-off for performance.
|
|
16
|
-
#
|
|
17
|
-
# @example Checking budget before execution
|
|
18
|
-
# BudgetTracker.check_budget!("MyAgent") # raises BudgetExceededError if over limit
|
|
19
|
-
#
|
|
20
|
-
# @example Recording spend after execution
|
|
21
|
-
# BudgetTracker.record_spend!("MyAgent", 0.05)
|
|
22
|
-
#
|
|
23
|
-
# @example Multi-tenant usage
|
|
24
|
-
# BudgetTracker.check_budget!("MyAgent", tenant_id: "acme_corp")
|
|
25
|
-
# BudgetTracker.record_spend!("MyAgent", 0.05, tenant_id: "acme_corp")
|
|
26
|
-
#
|
|
27
|
-
# @see RubyLLM::Agents::Configuration
|
|
28
|
-
# @see RubyLLM::Agents::Reliability::BudgetExceededError
|
|
29
|
-
# @see RubyLLM::Agents::TenantBudget
|
|
30
|
-
# @api public
|
|
31
|
-
module BudgetTracker
|
|
32
|
-
extend CacheHelper
|
|
33
|
-
|
|
34
|
-
class << self
|
|
35
|
-
# Checks if the current spend exceeds budget limits
|
|
36
|
-
#
|
|
37
|
-
# @param agent_type [String] The agent class name
|
|
38
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
39
|
-
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
40
|
-
# @raise [Reliability::BudgetExceededError] If hard cap is exceeded
|
|
41
|
-
# @return [void]
|
|
42
|
-
def check_budget!(agent_type, tenant_id: nil, tenant_config: nil)
|
|
43
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
44
|
-
budget_config = resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
45
|
-
|
|
46
|
-
return unless budget_config[:enabled]
|
|
47
|
-
return unless budget_config[:enforcement] == :hard
|
|
48
|
-
|
|
49
|
-
check_budget_limits!(agent_type, tenant_id, budget_config)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Checks if the current token usage exceeds budget limits
|
|
53
|
-
#
|
|
54
|
-
# @param agent_type [String] The agent class name
|
|
55
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
56
|
-
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
57
|
-
# @raise [Reliability::BudgetExceededError] If hard cap is exceeded
|
|
58
|
-
# @return [void]
|
|
59
|
-
def check_token_budget!(agent_type, tenant_id: nil, tenant_config: nil)
|
|
60
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
61
|
-
budget_config = resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
62
|
-
|
|
63
|
-
return unless budget_config[:enabled]
|
|
64
|
-
return unless budget_config[:enforcement] == :hard
|
|
65
|
-
|
|
66
|
-
check_token_limits!(agent_type, tenant_id, budget_config)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Records spend and checks for soft cap alerts
|
|
70
|
-
#
|
|
71
|
-
# @param agent_type [String] The agent class name
|
|
72
|
-
# @param amount [Float] The amount spent in USD
|
|
73
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
74
|
-
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
75
|
-
# @return [void]
|
|
76
|
-
def record_spend!(agent_type, amount, tenant_id: nil, tenant_config: nil)
|
|
77
|
-
return if amount.nil? || amount <= 0
|
|
78
|
-
|
|
79
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
80
|
-
|
|
81
|
-
# Increment all relevant counters
|
|
82
|
-
increment_spend(:global, :daily, amount, tenant_id: tenant_id)
|
|
83
|
-
increment_spend(:global, :monthly, amount, tenant_id: tenant_id)
|
|
84
|
-
increment_spend(:agent, :daily, amount, agent_type: agent_type, tenant_id: tenant_id)
|
|
85
|
-
increment_spend(:agent, :monthly, amount, agent_type: agent_type, tenant_id: tenant_id)
|
|
86
|
-
|
|
87
|
-
# Check for soft cap alerts
|
|
88
|
-
budget_config = resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
89
|
-
check_soft_cap_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Records token usage and checks for soft cap alerts
|
|
93
|
-
#
|
|
94
|
-
# @param agent_type [String] The agent class name
|
|
95
|
-
# @param tokens [Integer] The number of tokens used
|
|
96
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
97
|
-
# @param tenant_config [Hash, nil] Optional runtime tenant config (takes priority over resolver/DB)
|
|
98
|
-
# @return [void]
|
|
99
|
-
def record_tokens!(agent_type, tokens, tenant_id: nil, tenant_config: nil)
|
|
100
|
-
return if tokens.nil? || tokens <= 0
|
|
101
|
-
|
|
102
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
103
|
-
|
|
104
|
-
# Increment all relevant token counters
|
|
105
|
-
increment_tokens(:global, :daily, tokens, tenant_id: tenant_id)
|
|
106
|
-
increment_tokens(:global, :monthly, tokens, tenant_id: tenant_id)
|
|
107
|
-
increment_tokens(:agent, :daily, tokens, agent_type: agent_type, tenant_id: tenant_id)
|
|
108
|
-
increment_tokens(:agent, :monthly, tokens, agent_type: agent_type, tenant_id: tenant_id)
|
|
109
|
-
|
|
110
|
-
# Check for soft cap alerts
|
|
111
|
-
budget_config = resolve_budget_config(tenant_id, runtime_config: tenant_config)
|
|
112
|
-
check_soft_token_alerts(agent_type, tenant_id, budget_config) if budget_config[:enabled]
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Returns the current spend for a scope and period
|
|
116
|
-
#
|
|
117
|
-
# @param scope [Symbol] :global or :agent
|
|
118
|
-
# @param period [Symbol] :daily or :monthly
|
|
119
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
120
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
121
|
-
# @return [Float] Current spend in USD
|
|
122
|
-
def current_spend(scope, period, agent_type: nil, tenant_id: nil)
|
|
123
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
124
|
-
key = budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
125
|
-
(BudgetTracker.cache_read(key) || 0).to_f
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Returns the current token usage for a period (global only)
|
|
129
|
-
#
|
|
130
|
-
# @param period [Symbol] :daily or :monthly
|
|
131
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
132
|
-
# @return [Integer] Current token usage
|
|
133
|
-
def current_tokens(period, tenant_id: nil)
|
|
134
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
135
|
-
key = token_cache_key(period, tenant_id: tenant_id)
|
|
136
|
-
(BudgetTracker.cache_read(key) || 0).to_i
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Returns the remaining budget for a scope and period
|
|
140
|
-
#
|
|
141
|
-
# @param scope [Symbol] :global or :agent
|
|
142
|
-
# @param period [Symbol] :daily or :monthly
|
|
143
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
144
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
145
|
-
# @return [Float, nil] Remaining budget in USD, or nil if no limit configured
|
|
146
|
-
def remaining_budget(scope, period, agent_type: nil, tenant_id: nil)
|
|
147
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
148
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
149
|
-
|
|
150
|
-
limit = case [scope, period]
|
|
151
|
-
when [:global, :daily]
|
|
152
|
-
budget_config[:global_daily]
|
|
153
|
-
when [:global, :monthly]
|
|
154
|
-
budget_config[:global_monthly]
|
|
155
|
-
when [:agent, :daily]
|
|
156
|
-
budget_config[:per_agent_daily]&.dig(agent_type)
|
|
157
|
-
when [:agent, :monthly]
|
|
158
|
-
budget_config[:per_agent_monthly]&.dig(agent_type)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
return nil unless limit
|
|
162
|
-
|
|
163
|
-
[limit - current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id), 0].max
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Returns the remaining token budget for a period (global only)
|
|
167
|
-
#
|
|
168
|
-
# @param period [Symbol] :daily or :monthly
|
|
169
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
170
|
-
# @return [Integer, nil] Remaining token budget, or nil if no limit configured
|
|
171
|
-
def remaining_token_budget(period, tenant_id: nil)
|
|
172
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
173
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
174
|
-
|
|
175
|
-
limit = case period
|
|
176
|
-
when :daily
|
|
177
|
-
budget_config[:global_daily_tokens]
|
|
178
|
-
when :monthly
|
|
179
|
-
budget_config[:global_monthly_tokens]
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
return nil unless limit
|
|
183
|
-
|
|
184
|
-
[limit - current_tokens(period, tenant_id: tenant_id), 0].max
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Returns a summary of all budget statuses
|
|
188
|
-
#
|
|
189
|
-
# @param agent_type [String, nil] Optional agent type for per-agent budgets
|
|
190
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
191
|
-
# @return [Hash] Budget status information
|
|
192
|
-
def status(agent_type: nil, tenant_id: nil)
|
|
193
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
194
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
tenant_id: tenant_id,
|
|
198
|
-
enabled: budget_config[:enabled],
|
|
199
|
-
enforcement: budget_config[:enforcement],
|
|
200
|
-
# Cost budgets
|
|
201
|
-
global_daily: budget_status(:global, :daily, budget_config[:global_daily], tenant_id: tenant_id),
|
|
202
|
-
global_monthly: budget_status(:global, :monthly, budget_config[:global_monthly], tenant_id: tenant_id),
|
|
203
|
-
per_agent_daily: agent_type ? budget_status(:agent, :daily, budget_config[:per_agent_daily]&.dig(agent_type), agent_type: agent_type, tenant_id: tenant_id) : nil,
|
|
204
|
-
per_agent_monthly: agent_type ? budget_status(:agent, :monthly, budget_config[:per_agent_monthly]&.dig(agent_type), agent_type: agent_type, tenant_id: tenant_id) : nil,
|
|
205
|
-
# Token budgets (global only)
|
|
206
|
-
global_daily_tokens: token_status(:daily, budget_config[:global_daily_tokens], tenant_id: tenant_id),
|
|
207
|
-
global_monthly_tokens: token_status(:monthly, budget_config[:global_monthly_tokens], tenant_id: tenant_id),
|
|
208
|
-
forecast: calculate_forecast(tenant_id: tenant_id)
|
|
209
|
-
}.compact
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Calculates budget forecasts based on current spending trends
|
|
213
|
-
#
|
|
214
|
-
# @param tenant_id [String, nil] Optional tenant identifier (uses resolver if not provided)
|
|
215
|
-
# @return [Hash, nil] Forecast information
|
|
216
|
-
def calculate_forecast(tenant_id: nil)
|
|
217
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
218
|
-
budget_config = resolve_budget_config(tenant_id)
|
|
219
|
-
|
|
220
|
-
return nil unless budget_config[:enabled]
|
|
221
|
-
return nil unless budget_config[:global_daily] || budget_config[:global_monthly]
|
|
222
|
-
|
|
223
|
-
daily_current = current_spend(:global, :daily, tenant_id: tenant_id)
|
|
224
|
-
monthly_current = current_spend(:global, :monthly, tenant_id: tenant_id)
|
|
225
|
-
|
|
226
|
-
# Calculate hours elapsed today and days elapsed this month
|
|
227
|
-
hours_elapsed = Time.current.hour + (Time.current.min / 60.0)
|
|
228
|
-
hours_elapsed = [hours_elapsed, 1].max # Avoid division by zero
|
|
229
|
-
days_in_month = Time.current.end_of_month.day
|
|
230
|
-
day_of_month = Time.current.day
|
|
231
|
-
days_elapsed = day_of_month - 1 + (hours_elapsed / 24.0)
|
|
232
|
-
days_elapsed = [days_elapsed, 1].max
|
|
233
|
-
|
|
234
|
-
forecast = {}
|
|
235
|
-
|
|
236
|
-
# Daily forecast
|
|
237
|
-
if budget_config[:global_daily]
|
|
238
|
-
daily_rate = daily_current / hours_elapsed
|
|
239
|
-
projected_daily = daily_rate * 24
|
|
240
|
-
forecast[:daily] = {
|
|
241
|
-
current: daily_current.round(4),
|
|
242
|
-
projected: projected_daily.round(4),
|
|
243
|
-
limit: budget_config[:global_daily],
|
|
244
|
-
on_track: projected_daily <= budget_config[:global_daily],
|
|
245
|
-
hours_remaining: (24 - hours_elapsed).round(1),
|
|
246
|
-
rate_per_hour: daily_rate.round(6)
|
|
247
|
-
}
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# Monthly forecast
|
|
251
|
-
if budget_config[:global_monthly]
|
|
252
|
-
monthly_rate = monthly_current / days_elapsed
|
|
253
|
-
projected_monthly = monthly_rate * days_in_month
|
|
254
|
-
days_remaining = days_in_month - day_of_month
|
|
255
|
-
forecast[:monthly] = {
|
|
256
|
-
current: monthly_current.round(4),
|
|
257
|
-
projected: projected_monthly.round(4),
|
|
258
|
-
limit: budget_config[:global_monthly],
|
|
259
|
-
on_track: projected_monthly <= budget_config[:global_monthly],
|
|
260
|
-
days_remaining: days_remaining,
|
|
261
|
-
rate_per_day: monthly_rate.round(4)
|
|
262
|
-
}
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
forecast.presence
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Resets all budget counters (useful for testing)
|
|
269
|
-
#
|
|
270
|
-
# @param tenant_id [String, nil] Optional tenant identifier to reset only that tenant's counters
|
|
271
|
-
# @return [void]
|
|
272
|
-
def reset!(tenant_id: nil)
|
|
273
|
-
tenant_id = resolve_tenant_id(tenant_id)
|
|
274
|
-
today = Date.current.to_s
|
|
275
|
-
month = Date.current.strftime("%Y-%m")
|
|
276
|
-
|
|
277
|
-
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
278
|
-
|
|
279
|
-
BudgetTracker.cache_delete(BudgetTracker.cache_key("budget", tenant_part, today))
|
|
280
|
-
BudgetTracker.cache_delete(BudgetTracker.cache_key("budget", tenant_part, month))
|
|
281
|
-
|
|
282
|
-
# Reset memoized table existence check (useful for testing)
|
|
283
|
-
remove_instance_variable(:@tenant_budget_table_exists) if defined?(@tenant_budget_table_exists)
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
private
|
|
287
|
-
|
|
288
|
-
# Resolves the current tenant ID
|
|
289
|
-
#
|
|
290
|
-
# @param explicit_tenant_id [String, nil] Explicitly passed tenant ID
|
|
291
|
-
# @return [String, nil] Resolved tenant ID or nil if multi-tenancy disabled
|
|
292
|
-
def resolve_tenant_id(explicit_tenant_id)
|
|
293
|
-
config = RubyLLM::Agents.configuration
|
|
294
|
-
|
|
295
|
-
# Ignore tenant_id entirely when multi-tenancy is disabled
|
|
296
|
-
return nil unless config.multi_tenancy_enabled?
|
|
297
|
-
|
|
298
|
-
# Use explicit tenant_id if provided, otherwise use resolver
|
|
299
|
-
return explicit_tenant_id if explicit_tenant_id.present?
|
|
300
|
-
|
|
301
|
-
config.tenant_resolver&.call
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
# Resolves budget configuration for a tenant
|
|
305
|
-
#
|
|
306
|
-
# Priority order:
|
|
307
|
-
# 1. runtime_config (passed to run())
|
|
308
|
-
# 2. tenant_config_resolver (configured lambda)
|
|
309
|
-
# 3. TenantBudget database record
|
|
310
|
-
# 4. Global configuration
|
|
311
|
-
#
|
|
312
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
313
|
-
# @param runtime_config [Hash, nil] Runtime config passed to run()
|
|
314
|
-
# @return [Hash] Budget configuration
|
|
315
|
-
def resolve_budget_config(tenant_id, runtime_config: nil)
|
|
316
|
-
config = RubyLLM::Agents.configuration
|
|
317
|
-
|
|
318
|
-
# Priority 1: Runtime config passed directly to run()
|
|
319
|
-
if runtime_config.present?
|
|
320
|
-
return normalize_budget_config(runtime_config, config)
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# If multi-tenancy is disabled or no tenant, use global config
|
|
324
|
-
if tenant_id.nil? || !config.multi_tenancy_enabled?
|
|
325
|
-
return global_budget_config(config)
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Priority 2: tenant_config_resolver lambda
|
|
329
|
-
if config.tenant_config_resolver.present?
|
|
330
|
-
resolved_config = config.tenant_config_resolver.call(tenant_id)
|
|
331
|
-
if resolved_config.present?
|
|
332
|
-
return normalize_budget_config(resolved_config, config)
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# Priority 3: Look up tenant-specific budget from database
|
|
337
|
-
tenant_budget = lookup_tenant_budget(tenant_id)
|
|
338
|
-
|
|
339
|
-
if tenant_budget
|
|
340
|
-
tenant_budget.to_budget_config
|
|
341
|
-
else
|
|
342
|
-
# Priority 4: Fall back to global config for unknown tenants
|
|
343
|
-
global_budget_config(config)
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
# Builds global budget config from configuration
|
|
348
|
-
#
|
|
349
|
-
# @param config [Configuration] The configuration object
|
|
350
|
-
# @return [Hash] Budget configuration
|
|
351
|
-
def global_budget_config(config)
|
|
352
|
-
{
|
|
353
|
-
enabled: config.budgets_enabled?,
|
|
354
|
-
enforcement: config.budget_enforcement,
|
|
355
|
-
global_daily: config.budgets&.dig(:global_daily),
|
|
356
|
-
global_monthly: config.budgets&.dig(:global_monthly),
|
|
357
|
-
per_agent_daily: config.budgets&.dig(:per_agent_daily),
|
|
358
|
-
per_agent_monthly: config.budgets&.dig(:per_agent_monthly),
|
|
359
|
-
global_daily_tokens: config.budgets&.dig(:global_daily_tokens),
|
|
360
|
-
global_monthly_tokens: config.budgets&.dig(:global_monthly_tokens)
|
|
361
|
-
}
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
# Normalizes runtime/resolver config to standard budget config format
|
|
365
|
-
#
|
|
366
|
-
# @param raw_config [Hash] Raw config from runtime or resolver
|
|
367
|
-
# @param global_config [Configuration] Global config for fallbacks
|
|
368
|
-
# @return [Hash] Normalized budget configuration
|
|
369
|
-
def normalize_budget_config(raw_config, global_config)
|
|
370
|
-
enforcement = raw_config[:enforcement]&.to_sym || global_config.budget_enforcement
|
|
371
|
-
|
|
372
|
-
{
|
|
373
|
-
enabled: enforcement != :none,
|
|
374
|
-
enforcement: enforcement,
|
|
375
|
-
# Cost/budget limits (USD)
|
|
376
|
-
global_daily: raw_config[:daily_budget_limit],
|
|
377
|
-
global_monthly: raw_config[:monthly_budget_limit],
|
|
378
|
-
per_agent_daily: raw_config[:per_agent_daily] || {},
|
|
379
|
-
per_agent_monthly: raw_config[:per_agent_monthly] || {},
|
|
380
|
-
# Token limits
|
|
381
|
-
global_daily_tokens: raw_config[:daily_token_limit],
|
|
382
|
-
global_monthly_tokens: raw_config[:monthly_token_limit]
|
|
383
|
-
}
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
# Safely looks up tenant budget, handling missing table
|
|
387
|
-
#
|
|
388
|
-
# @param tenant_id [String] The tenant identifier
|
|
389
|
-
# @return [TenantBudget, nil] The tenant budget or nil
|
|
390
|
-
def lookup_tenant_budget(tenant_id)
|
|
391
|
-
return nil unless tenant_budget_table_exists?
|
|
392
|
-
|
|
393
|
-
TenantBudget.for_tenant(tenant_id)
|
|
394
|
-
rescue StandardError => e
|
|
395
|
-
Rails.logger.warn("[RubyLLM::Agents] Failed to lookup tenant budget: #{e.message}")
|
|
396
|
-
nil
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
# Checks if the tenant_budgets table exists
|
|
400
|
-
#
|
|
401
|
-
# @return [Boolean] true if table exists
|
|
402
|
-
def tenant_budget_table_exists?
|
|
403
|
-
return @tenant_budget_table_exists if defined?(@tenant_budget_table_exists)
|
|
404
|
-
|
|
405
|
-
@tenant_budget_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenant_budgets)
|
|
406
|
-
rescue StandardError
|
|
407
|
-
@tenant_budget_table_exists = false
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
# Resets the memoized tenant budget table existence check (useful for testing)
|
|
411
|
-
#
|
|
412
|
-
# @return [void]
|
|
413
|
-
def reset_tenant_budget_table_check!
|
|
414
|
-
remove_instance_variable(:@tenant_budget_table_exists) if defined?(@tenant_budget_table_exists)
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
# Checks budget limits and raises error if exceeded
|
|
418
|
-
#
|
|
419
|
-
# @param agent_type [String] The agent class name
|
|
420
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
421
|
-
# @param budget_config [Hash] The budget configuration
|
|
422
|
-
# @raise [Reliability::BudgetExceededError] If limit exceeded
|
|
423
|
-
# @return [void]
|
|
424
|
-
def check_budget_limits!(agent_type, tenant_id, budget_config)
|
|
425
|
-
# Check global daily budget
|
|
426
|
-
if budget_config[:global_daily]
|
|
427
|
-
current = current_spend(:global, :daily, tenant_id: tenant_id)
|
|
428
|
-
if current >= budget_config[:global_daily]
|
|
429
|
-
raise Reliability::BudgetExceededError.new(:global_daily, budget_config[:global_daily], current, tenant_id: tenant_id)
|
|
430
|
-
end
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
# Check global monthly budget
|
|
434
|
-
if budget_config[:global_monthly]
|
|
435
|
-
current = current_spend(:global, :monthly, tenant_id: tenant_id)
|
|
436
|
-
if current >= budget_config[:global_monthly]
|
|
437
|
-
raise Reliability::BudgetExceededError.new(:global_monthly, budget_config[:global_monthly], current, tenant_id: tenant_id)
|
|
438
|
-
end
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
# Check per-agent daily budget
|
|
442
|
-
agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
|
|
443
|
-
if agent_daily_limit
|
|
444
|
-
current = current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id)
|
|
445
|
-
if current >= agent_daily_limit
|
|
446
|
-
raise Reliability::BudgetExceededError.new(:per_agent_daily, agent_daily_limit, current, agent_type: agent_type, tenant_id: tenant_id)
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
# Check per-agent monthly budget
|
|
451
|
-
agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
|
|
452
|
-
if agent_monthly_limit
|
|
453
|
-
current = current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id)
|
|
454
|
-
if current >= agent_monthly_limit
|
|
455
|
-
raise Reliability::BudgetExceededError.new(:per_agent_monthly, agent_monthly_limit, current, agent_type: agent_type, tenant_id: tenant_id)
|
|
456
|
-
end
|
|
457
|
-
end
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
# Increments the spend counter for a scope and period
|
|
461
|
-
#
|
|
462
|
-
# @param scope [Symbol] :global or :agent
|
|
463
|
-
# @param period [Symbol] :daily or :monthly
|
|
464
|
-
# @param amount [Float] Amount to add
|
|
465
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
466
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
467
|
-
# @return [Float] New total
|
|
468
|
-
def increment_spend(scope, period, amount, agent_type: nil, tenant_id: nil)
|
|
469
|
-
key = budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
470
|
-
ttl = period == :daily ? 1.day : 31.days
|
|
471
|
-
|
|
472
|
-
# Read-modify-write for float values (cache increment is for integers)
|
|
473
|
-
current = (BudgetTracker.cache_read(key) || 0).to_f
|
|
474
|
-
new_total = current + amount
|
|
475
|
-
BudgetTracker.cache_write(key, new_total, expires_in: ttl)
|
|
476
|
-
new_total
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
# Generates a cache key for budget tracking
|
|
480
|
-
#
|
|
481
|
-
# @param scope [Symbol] :global or :agent
|
|
482
|
-
# @param period [Symbol] :daily or :monthly
|
|
483
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
484
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
485
|
-
# @return [String] Cache key
|
|
486
|
-
def budget_cache_key(scope, period, agent_type: nil, tenant_id: nil)
|
|
487
|
-
date_part = period == :daily ? Date.current.to_s : Date.current.strftime("%Y-%m")
|
|
488
|
-
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
489
|
-
|
|
490
|
-
case scope
|
|
491
|
-
when :global
|
|
492
|
-
BudgetTracker.cache_key("budget", tenant_part, date_part)
|
|
493
|
-
when :agent
|
|
494
|
-
BudgetTracker.cache_key("budget", tenant_part, "agent", agent_type, date_part)
|
|
495
|
-
else
|
|
496
|
-
raise ArgumentError, "Unknown scope: #{scope}"
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
# Returns budget status for a scope/period
|
|
501
|
-
#
|
|
502
|
-
# @param scope [Symbol] :global or :agent
|
|
503
|
-
# @param period [Symbol] :daily or :monthly
|
|
504
|
-
# @param limit [Float, nil] The budget limit
|
|
505
|
-
# @param agent_type [String, nil] Required when scope is :agent
|
|
506
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
507
|
-
# @return [Hash, nil] Status hash or nil if no limit
|
|
508
|
-
def budget_status(scope, period, limit, agent_type: nil, tenant_id: nil)
|
|
509
|
-
return nil unless limit
|
|
510
|
-
|
|
511
|
-
current = current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
512
|
-
{
|
|
513
|
-
limit: limit,
|
|
514
|
-
current: current.round(6),
|
|
515
|
-
remaining: [limit - current, 0].max.round(6),
|
|
516
|
-
percentage_used: ((current / limit) * 100).round(2)
|
|
517
|
-
}
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
# Checks for soft cap alerts after recording spend
|
|
521
|
-
#
|
|
522
|
-
# @param agent_type [String] The agent class name
|
|
523
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
524
|
-
# @param budget_config [Hash] Budget configuration
|
|
525
|
-
# @return [void]
|
|
526
|
-
def check_soft_cap_alerts(agent_type, tenant_id, budget_config)
|
|
527
|
-
config = RubyLLM::Agents.configuration
|
|
528
|
-
return unless config.alerts_enabled?
|
|
529
|
-
return unless config.alert_events.include?(:budget_soft_cap) || config.alert_events.include?(:budget_hard_cap)
|
|
530
|
-
|
|
531
|
-
# Check global daily
|
|
532
|
-
check_budget_alert(:global_daily, budget_config[:global_daily],
|
|
533
|
-
current_spend(:global, :daily, tenant_id: tenant_id),
|
|
534
|
-
agent_type, tenant_id, budget_config)
|
|
535
|
-
|
|
536
|
-
# Check global monthly
|
|
537
|
-
check_budget_alert(:global_monthly, budget_config[:global_monthly],
|
|
538
|
-
current_spend(:global, :monthly, tenant_id: tenant_id),
|
|
539
|
-
agent_type, tenant_id, budget_config)
|
|
540
|
-
|
|
541
|
-
# Check per-agent daily
|
|
542
|
-
agent_daily_limit = budget_config[:per_agent_daily]&.dig(agent_type)
|
|
543
|
-
if agent_daily_limit
|
|
544
|
-
check_budget_alert(:per_agent_daily, agent_daily_limit,
|
|
545
|
-
current_spend(:agent, :daily, agent_type: agent_type, tenant_id: tenant_id),
|
|
546
|
-
agent_type, tenant_id, budget_config)
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
# Check per-agent monthly
|
|
550
|
-
agent_monthly_limit = budget_config[:per_agent_monthly]&.dig(agent_type)
|
|
551
|
-
if agent_monthly_limit
|
|
552
|
-
check_budget_alert(:per_agent_monthly, agent_monthly_limit,
|
|
553
|
-
current_spend(:agent, :monthly, agent_type: agent_type, tenant_id: tenant_id),
|
|
554
|
-
agent_type, tenant_id, budget_config)
|
|
555
|
-
end
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
# Checks if an alert should be fired for a budget
|
|
559
|
-
#
|
|
560
|
-
# @param scope [Symbol] Budget scope
|
|
561
|
-
# @param limit [Float, nil] Budget limit
|
|
562
|
-
# @param current [Float] Current spend
|
|
563
|
-
# @param agent_type [String] Agent type
|
|
564
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
565
|
-
# @param budget_config [Hash] Budget configuration
|
|
566
|
-
# @return [void]
|
|
567
|
-
def check_budget_alert(scope, limit, current, agent_type, tenant_id, budget_config)
|
|
568
|
-
return unless limit
|
|
569
|
-
return if current <= limit
|
|
570
|
-
|
|
571
|
-
event = budget_config[:enforcement] == :hard ? :budget_hard_cap : :budget_soft_cap
|
|
572
|
-
config = RubyLLM::Agents.configuration
|
|
573
|
-
return unless config.alert_events.include?(event)
|
|
574
|
-
|
|
575
|
-
# Prevent duplicate alerts by using a cache key (include tenant for isolation)
|
|
576
|
-
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
577
|
-
alert_key = BudgetTracker.cache_key("budget_alert", tenant_part, scope, Date.current.to_s)
|
|
578
|
-
return if BudgetTracker.cache_exist?(alert_key)
|
|
579
|
-
|
|
580
|
-
BudgetTracker.cache_write(alert_key, true, expires_in: 1.hour)
|
|
581
|
-
|
|
582
|
-
AlertManager.notify(event, {
|
|
583
|
-
scope: scope,
|
|
584
|
-
limit: limit,
|
|
585
|
-
total: current.round(6),
|
|
586
|
-
agent_type: agent_type,
|
|
587
|
-
tenant_id: tenant_id,
|
|
588
|
-
timestamp: Date.current.to_s
|
|
589
|
-
})
|
|
590
|
-
end
|
|
591
|
-
|
|
592
|
-
# Increments the token counter for a period
|
|
593
|
-
#
|
|
594
|
-
# @param scope [Symbol] :global (only global supported for tokens)
|
|
595
|
-
# @param period [Symbol] :daily or :monthly
|
|
596
|
-
# @param tokens [Integer] Tokens to add
|
|
597
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
598
|
-
# @return [Integer] New total
|
|
599
|
-
def increment_tokens(scope, period, tokens, agent_type: nil, tenant_id: nil)
|
|
600
|
-
# For now, we only track global token usage (not per-agent)
|
|
601
|
-
key = token_cache_key(period, tenant_id: tenant_id)
|
|
602
|
-
ttl = period == :daily ? 1.day : 31.days
|
|
603
|
-
|
|
604
|
-
current = (BudgetTracker.cache_read(key) || 0).to_i
|
|
605
|
-
new_total = current + tokens
|
|
606
|
-
BudgetTracker.cache_write(key, new_total, expires_in: ttl)
|
|
607
|
-
new_total
|
|
608
|
-
end
|
|
609
|
-
|
|
610
|
-
# Generates a cache key for token tracking
|
|
611
|
-
#
|
|
612
|
-
# @param period [Symbol] :daily or :monthly
|
|
613
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
614
|
-
# @return [String] Cache key
|
|
615
|
-
def token_cache_key(period, tenant_id: nil)
|
|
616
|
-
date_part = period == :daily ? Date.current.to_s : Date.current.strftime("%Y-%m")
|
|
617
|
-
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
618
|
-
|
|
619
|
-
BudgetTracker.cache_key("tokens", tenant_part, date_part)
|
|
620
|
-
end
|
|
621
|
-
|
|
622
|
-
# Checks token limits and raises error if exceeded
|
|
623
|
-
#
|
|
624
|
-
# @param agent_type [String] The agent class name
|
|
625
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
626
|
-
# @param budget_config [Hash] The budget configuration
|
|
627
|
-
# @raise [Reliability::BudgetExceededError] If limit exceeded
|
|
628
|
-
# @return [void]
|
|
629
|
-
def check_token_limits!(agent_type, tenant_id, budget_config)
|
|
630
|
-
# Check global daily token budget
|
|
631
|
-
if budget_config[:global_daily_tokens]
|
|
632
|
-
current = current_tokens(:daily, tenant_id: tenant_id)
|
|
633
|
-
if current >= budget_config[:global_daily_tokens]
|
|
634
|
-
raise Reliability::BudgetExceededError.new(
|
|
635
|
-
:global_daily_tokens,
|
|
636
|
-
budget_config[:global_daily_tokens],
|
|
637
|
-
current,
|
|
638
|
-
tenant_id: tenant_id
|
|
639
|
-
)
|
|
640
|
-
end
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
# Check global monthly token budget
|
|
644
|
-
if budget_config[:global_monthly_tokens]
|
|
645
|
-
current = current_tokens(:monthly, tenant_id: tenant_id)
|
|
646
|
-
if current >= budget_config[:global_monthly_tokens]
|
|
647
|
-
raise Reliability::BudgetExceededError.new(
|
|
648
|
-
:global_monthly_tokens,
|
|
649
|
-
budget_config[:global_monthly_tokens],
|
|
650
|
-
current,
|
|
651
|
-
tenant_id: tenant_id
|
|
652
|
-
)
|
|
653
|
-
end
|
|
654
|
-
end
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
# Checks for soft cap token alerts after recording usage
|
|
658
|
-
#
|
|
659
|
-
# @param agent_type [String] The agent class name
|
|
660
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
661
|
-
# @param budget_config [Hash] Budget configuration
|
|
662
|
-
# @return [void]
|
|
663
|
-
def check_soft_token_alerts(agent_type, tenant_id, budget_config)
|
|
664
|
-
config = RubyLLM::Agents.configuration
|
|
665
|
-
return unless config.alerts_enabled?
|
|
666
|
-
return unless config.alert_events.include?(:token_soft_cap) || config.alert_events.include?(:token_hard_cap)
|
|
667
|
-
|
|
668
|
-
# Check global daily tokens
|
|
669
|
-
check_token_alert(:global_daily_tokens, budget_config[:global_daily_tokens],
|
|
670
|
-
current_tokens(:daily, tenant_id: tenant_id),
|
|
671
|
-
agent_type, tenant_id, budget_config)
|
|
672
|
-
|
|
673
|
-
# Check global monthly tokens
|
|
674
|
-
check_token_alert(:global_monthly_tokens, budget_config[:global_monthly_tokens],
|
|
675
|
-
current_tokens(:monthly, tenant_id: tenant_id),
|
|
676
|
-
agent_type, tenant_id, budget_config)
|
|
677
|
-
end
|
|
678
|
-
|
|
679
|
-
# Checks if a token alert should be fired
|
|
680
|
-
#
|
|
681
|
-
# @param scope [Symbol] Token scope
|
|
682
|
-
# @param limit [Integer, nil] Token limit
|
|
683
|
-
# @param current [Integer] Current token usage
|
|
684
|
-
# @param agent_type [String] Agent type
|
|
685
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
686
|
-
# @param budget_config [Hash] Budget configuration
|
|
687
|
-
# @return [void]
|
|
688
|
-
def check_token_alert(scope, limit, current, agent_type, tenant_id, budget_config)
|
|
689
|
-
return unless limit
|
|
690
|
-
return if current <= limit
|
|
691
|
-
|
|
692
|
-
event = budget_config[:enforcement] == :hard ? :token_hard_cap : :token_soft_cap
|
|
693
|
-
config = RubyLLM::Agents.configuration
|
|
694
|
-
return unless config.alert_events.include?(event)
|
|
695
|
-
|
|
696
|
-
# Prevent duplicate alerts
|
|
697
|
-
tenant_part = tenant_id.present? ? "tenant:#{tenant_id}" : "global"
|
|
698
|
-
alert_key = BudgetTracker.cache_key("token_alert", tenant_part, scope, Date.current.to_s)
|
|
699
|
-
return if BudgetTracker.cache_exist?(alert_key)
|
|
700
|
-
|
|
701
|
-
BudgetTracker.cache_write(alert_key, true, expires_in: 1.hour)
|
|
702
|
-
|
|
703
|
-
AlertManager.notify(event, {
|
|
704
|
-
scope: scope,
|
|
705
|
-
limit: limit,
|
|
706
|
-
total: current,
|
|
707
|
-
agent_type: agent_type,
|
|
708
|
-
tenant_id: tenant_id,
|
|
709
|
-
timestamp: Date.current.to_s
|
|
710
|
-
})
|
|
711
|
-
end
|
|
712
|
-
|
|
713
|
-
# Returns token status for a period
|
|
714
|
-
#
|
|
715
|
-
# @param period [Symbol] :daily or :monthly
|
|
716
|
-
# @param limit [Integer, nil] The token limit
|
|
717
|
-
# @param tenant_id [String, nil] The tenant identifier
|
|
718
|
-
# @return [Hash, nil] Status hash or nil if no limit
|
|
719
|
-
def token_status(period, limit, tenant_id: nil)
|
|
720
|
-
return nil unless limit
|
|
721
|
-
|
|
722
|
-
current = current_tokens(period, tenant_id: tenant_id)
|
|
723
|
-
{
|
|
724
|
-
limit: limit,
|
|
725
|
-
current: current,
|
|
726
|
-
remaining: [limit - current, 0].max,
|
|
727
|
-
percentage_used: ((current.to_f / limit) * 100).round(2)
|
|
728
|
-
}
|
|
729
|
-
end
|
|
730
|
-
end
|
|
731
|
-
end
|
|
732
|
-
end
|
|
733
|
-
end
|