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.
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
@@ -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