aidp 0.34.0 → 0.39.3

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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/ui.rb +11 -0
  61. data/lib/aidp/harness/usage_limit.rb +179 -0
  62. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  63. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  64. data/lib/aidp/harness/usage_period.rb +234 -0
  65. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  66. data/lib/aidp/init/runner.rb +25 -4
  67. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  68. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  69. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  70. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  71. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  72. data/lib/aidp/logger.rb +25 -7
  73. data/lib/aidp/orchestration_adapter.rb +279 -0
  74. data/lib/aidp/pr_worktree_manager.rb +4 -15
  75. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  76. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  77. data/lib/aidp/prompts/template_evolver.rb +305 -0
  78. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  79. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  80. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  81. data/lib/aidp/setup/wizard.rb +616 -176
  82. data/lib/aidp/shell_executor.rb +58 -0
  83. data/lib/aidp/style_guide/selector.rb +56 -1
  84. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  85. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  86. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  87. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  88. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  89. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  90. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  91. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  92. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  93. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  94. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  95. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  96. data/lib/aidp/temporal/configuration.rb +169 -0
  97. data/lib/aidp/temporal/connection.rb +93 -0
  98. data/lib/aidp/temporal/worker.rb +119 -0
  99. data/lib/aidp/temporal/workflow_client.rb +171 -0
  100. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  101. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  102. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  103. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  104. data/lib/aidp/temporal.rb +120 -0
  105. data/lib/aidp/version.rb +1 -1
  106. data/lib/aidp/watch/base_processor.rb +35 -0
  107. data/lib/aidp/watch/build_processor.rb +132 -0
  108. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  109. data/lib/aidp/watch/rebase_processor.rb +245 -0
  110. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  111. data/lib/aidp/watch/runner.rb +366 -285
  112. data/lib/aidp/watch/state_store.rb +80 -0
  113. data/lib/aidp/watch/work_item.rb +96 -0
  114. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  115. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  116. data/lib/aidp/workflows/guided_agent.rb +1 -1
  117. data/lib/aidp/worktree.rb +62 -0
  118. data/templates/aidp.yml.example +70 -0
  119. metadata +118 -16
  120. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  121. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  122. data/lib/aidp/providers/adapter.rb +0 -239
  123. data/lib/aidp/providers/aider.rb +0 -264
  124. data/lib/aidp/providers/anthropic.rb +0 -614
  125. data/lib/aidp/providers/base.rb +0 -617
  126. data/lib/aidp/providers/capability_registry.rb +0 -204
  127. data/lib/aidp/providers/codex.rb +0 -267
  128. data/lib/aidp/providers/cursor.rb +0 -304
  129. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  130. data/lib/aidp/providers/gemini.rb +0 -110
  131. data/lib/aidp/providers/github_copilot.rb +0 -227
  132. data/lib/aidp/providers/kilocode.rb +0 -157
  133. data/lib/aidp/providers/opencode.rb +0 -139
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Harness
5
+ # Immutable value object representing usage limit configuration
6
+ # Encapsulates limit settings for a provider with tier-based limits
7
+ class UsageLimit
8
+ VALID_PERIODS = %w[daily weekly monthly].freeze
9
+ VALID_TIERS = %w[mini standard thinking advanced pro max].freeze
10
+ DEFAULT_PERIOD = "monthly"
11
+ DEFAULT_RESET_DAY = 1
12
+
13
+ attr_reader :enabled, :period, :reset_day, :tier_limits, :max_tokens, :max_cost
14
+
15
+ # Create a new UsageLimit from configuration hash
16
+ #
17
+ # @param config [Hash] Configuration hash with usage limit settings
18
+ # @return [UsageLimit] Immutable usage limit instance
19
+ def self.from_config(config)
20
+ return new(enabled: false) unless config.is_a?(Hash)
21
+
22
+ new(
23
+ enabled: config[:enabled] == true,
24
+ period: normalize_period(config[:period]),
25
+ reset_day: config[:reset_day] || DEFAULT_RESET_DAY,
26
+ tier_limits: parse_tier_limits(config[:tier_limits]),
27
+ max_tokens: config[:max_tokens],
28
+ max_cost: config[:max_cost]
29
+ )
30
+ end
31
+
32
+ def initialize(enabled: true, period: DEFAULT_PERIOD, reset_day: DEFAULT_RESET_DAY,
33
+ tier_limits: {}, max_tokens: nil, max_cost: nil)
34
+ @enabled = enabled
35
+ @period = validate_period(period)
36
+ @reset_day = validate_reset_day(reset_day)
37
+ @tier_limits = freeze_tier_limits(tier_limits)
38
+ @max_tokens = max_tokens&.to_i
39
+ @max_cost = max_cost&.to_f
40
+ freeze
41
+ end
42
+
43
+ # Check if limits are enabled and configured
44
+ def enabled?
45
+ @enabled
46
+ end
47
+
48
+ # Get limits for a specific tier
49
+ #
50
+ # @param tier [String, Symbol] The tier name (mini, standard, advanced, etc.)
51
+ # @return [Hash] Tier-specific limits or global limits
52
+ def limits_for_tier(tier)
53
+ tier_key = tier.to_s.downcase
54
+
55
+ # Map thinking tier names to usage limit tiers
56
+ tier_key = case tier_key
57
+ when "thinking", "pro", "max" then "advanced"
58
+ when "standard" then "standard"
59
+ else tier_key
60
+ end
61
+
62
+ tier_config = @tier_limits[tier_key] || @tier_limits[tier_key.to_sym]
63
+
64
+ if tier_config
65
+ {
66
+ max_tokens: tier_config[:max_tokens],
67
+ max_cost: tier_config[:max_cost]
68
+ }
69
+ else
70
+ # Fall back to global limits
71
+ {
72
+ max_tokens: @max_tokens,
73
+ max_cost: @max_cost
74
+ }
75
+ end
76
+ end
77
+
78
+ # Check if usage would exceed limits
79
+ #
80
+ # @param current_tokens [Integer] Current token count
81
+ # @param current_cost [Float] Current cost
82
+ # @param tier [String] The tier to check against
83
+ # @return [Hash] Result with :exceeded boolean and :reason
84
+ def exceeds_limit?(current_tokens:, current_cost:, tier: "standard")
85
+ return {exceeded: false, reason: nil} unless enabled?
86
+
87
+ limits = limits_for_tier(tier)
88
+
89
+ if limits[:max_tokens] && current_tokens >= limits[:max_tokens]
90
+ return {
91
+ exceeded: true,
92
+ reason: "Token limit exceeded: #{current_tokens}/#{limits[:max_tokens]}"
93
+ }
94
+ end
95
+
96
+ if limits[:max_cost] && current_cost >= limits[:max_cost]
97
+ return {
98
+ exceeded: true,
99
+ reason: "Cost limit exceeded: $#{format("%.2f", current_cost)}/$#{format("%.2f", limits[:max_cost])}"
100
+ }
101
+ end
102
+
103
+ {exceeded: false, reason: nil}
104
+ end
105
+
106
+ # Value equality
107
+ def ==(other)
108
+ return false unless other.is_a?(UsageLimit)
109
+
110
+ enabled == other.enabled &&
111
+ period == other.period &&
112
+ reset_day == other.reset_day &&
113
+ tier_limits == other.tier_limits &&
114
+ max_tokens == other.max_tokens &&
115
+ max_cost == other.max_cost
116
+ end
117
+
118
+ alias_method :eql?, :==
119
+
120
+ def hash
121
+ [enabled, period, reset_day, tier_limits, max_tokens, max_cost].hash
122
+ end
123
+
124
+ # Convert to configuration hash
125
+ def to_h
126
+ {
127
+ enabled: @enabled,
128
+ period: @period,
129
+ reset_day: @reset_day,
130
+ tier_limits: @tier_limits.transform_values(&:to_h),
131
+ max_tokens: @max_tokens,
132
+ max_cost: @max_cost
133
+ }.compact
134
+ end
135
+
136
+ def self.normalize_period(period)
137
+ return DEFAULT_PERIOD unless period
138
+
139
+ period_str = period.to_s.downcase
140
+ VALID_PERIODS.include?(period_str) ? period_str : DEFAULT_PERIOD
141
+ end
142
+
143
+ def self.parse_tier_limits(tier_limits)
144
+ return {} unless tier_limits.is_a?(Hash)
145
+
146
+ tier_limits.transform_values do |limits|
147
+ next {} unless limits.is_a?(Hash)
148
+
149
+ {
150
+ max_tokens: limits[:max_tokens] || limits["max_tokens"],
151
+ max_cost: limits[:max_cost] || limits["max_cost"]
152
+ }.compact
153
+ end
154
+ end
155
+
156
+ private_class_method :normalize_period, :parse_tier_limits
157
+
158
+ private
159
+
160
+ def validate_period(period)
161
+ period_str = period.to_s.downcase
162
+ VALID_PERIODS.include?(period_str) ? period_str : DEFAULT_PERIOD
163
+ end
164
+
165
+ def validate_reset_day(day)
166
+ day_int = day.to_i
167
+ day_int.between?(1, 28) ? day_int : DEFAULT_RESET_DAY
168
+ end
169
+
170
+ def freeze_tier_limits(limits)
171
+ return {}.freeze unless limits.is_a?(Hash)
172
+
173
+ limits.transform_values do |tier_config|
174
+ tier_config.is_a?(Hash) ? tier_config.freeze : {}
175
+ end.freeze
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "usage_limit"
4
+ require_relative "usage_limit_tracker"
5
+
6
+ module Aidp
7
+ module Harness
8
+ # Exception raised when usage limits are exceeded
9
+ # Contains context about the exceeded limit for fallback handling
10
+ class UsageLimitExceededError < StandardError
11
+ attr_reader :provider_name, :tier, :current_tokens, :current_cost,
12
+ :max_tokens, :max_cost, :period_description
13
+
14
+ def initialize(provider_name:, tier:, current_tokens:, current_cost:,
15
+ max_tokens:, max_cost:, period_description:, message: nil)
16
+ @provider_name = provider_name
17
+ @tier = tier
18
+ @current_tokens = current_tokens
19
+ @current_cost = current_cost
20
+ @max_tokens = max_tokens
21
+ @max_cost = max_cost
22
+ @period_description = period_description
23
+
24
+ super(message || default_message)
25
+ end
26
+
27
+ def to_h
28
+ {
29
+ type: "usage_limit_exceeded",
30
+ provider: @provider_name,
31
+ tier: @tier,
32
+ current_tokens: @current_tokens,
33
+ current_cost: @current_cost,
34
+ max_tokens: @max_tokens,
35
+ max_cost: @max_cost,
36
+ period: @period_description,
37
+ timestamp: Time.now.iso8601
38
+ }
39
+ end
40
+
41
+ private
42
+
43
+ def default_message
44
+ parts = []
45
+ parts << "Usage limit exceeded for #{@provider_name} (tier: #{@tier})"
46
+ parts << "Period: #{@period_description}"
47
+
48
+ if @max_tokens && @current_tokens >= @max_tokens
49
+ parts << "Tokens: #{@current_tokens}/#{@max_tokens}"
50
+ end
51
+
52
+ if @max_cost && @current_cost >= @max_cost
53
+ parts << "Cost: $#{format("%.2f", @current_cost)}/$#{format("%.2f", @max_cost)}"
54
+ end
55
+
56
+ parts.join(". ")
57
+ end
58
+ end
59
+
60
+ # Application service for enforcing usage limits
61
+ # Coordinates between UsageLimit config and UsageLimitTracker
62
+ class UsageLimitEnforcer
63
+ attr_reader :provider_name, :usage_limit, :tracker
64
+
65
+ # Initialize the enforcer with configuration
66
+ #
67
+ # @param provider_name [String] Name of the provider
68
+ # @param usage_limit [UsageLimit] Usage limit configuration
69
+ # @param tracker [UsageLimitTracker] Tracker instance for usage data
70
+ def initialize(provider_name:, usage_limit:, tracker:)
71
+ @provider_name = provider_name.to_s
72
+ @usage_limit = usage_limit
73
+ @tracker = tracker
74
+
75
+ Aidp.log_debug("usage_limit_enforcer", "initialized",
76
+ provider: @provider_name,
77
+ enabled: @usage_limit.enabled?,
78
+ period: @usage_limit.period)
79
+ end
80
+
81
+ # Check if usage limits allow a request before making API call
82
+ #
83
+ # @param tier [String] The tier for this request
84
+ # @raise [UsageLimitExceededError] If limits are exceeded
85
+ def check_before_request(tier: "standard")
86
+ Aidp.log_debug("usage_limit_enforcer", "checking_limits",
87
+ provider: @provider_name,
88
+ tier: tier)
89
+
90
+ return unless @usage_limit.enabled?
91
+
92
+ current = @tracker.current_usage(
93
+ period_type: @usage_limit.period,
94
+ reset_day: @usage_limit.reset_day
95
+ )
96
+
97
+ limits = @usage_limit.limits_for_tier(tier)
98
+ check_result = @usage_limit.exceeds_limit?(
99
+ current_tokens: current[:total_tokens],
100
+ current_cost: current[:total_cost],
101
+ tier: tier
102
+ )
103
+
104
+ if check_result[:exceeded]
105
+ Aidp.log_warn("usage_limit_enforcer", "limit_exceeded",
106
+ provider: @provider_name,
107
+ tier: tier,
108
+ reason: check_result[:reason])
109
+
110
+ raise UsageLimitExceededError.new(
111
+ provider_name: @provider_name,
112
+ tier: tier,
113
+ current_tokens: current[:total_tokens],
114
+ current_cost: current[:total_cost],
115
+ max_tokens: limits[:max_tokens],
116
+ max_cost: limits[:max_cost],
117
+ period_description: current[:period_description]
118
+ )
119
+ end
120
+
121
+ Aidp.log_debug("usage_limit_enforcer", "limits_ok",
122
+ provider: @provider_name,
123
+ tier: tier,
124
+ tokens: current[:total_tokens],
125
+ cost: current[:total_cost])
126
+ end
127
+
128
+ # Record usage after a successful API request
129
+ #
130
+ # @param tokens [Integer] Tokens used in the request
131
+ # @param cost [Float] Cost of the request
132
+ # @param tier [String] Tier used for the request
133
+ def record_after_request(tokens:, cost:, tier: "standard")
134
+ return unless @usage_limit.enabled?
135
+
136
+ @tracker.record_usage(
137
+ tokens: tokens,
138
+ cost: cost,
139
+ tier: tier,
140
+ period_type: @usage_limit.period,
141
+ reset_day: @usage_limit.reset_day
142
+ )
143
+ end
144
+
145
+ # Check if limits would be exceeded by additional usage
146
+ # Non-raising version for checking without blocking
147
+ #
148
+ # @param additional_tokens [Integer] Additional tokens to add
149
+ # @param additional_cost [Float] Additional cost to add
150
+ # @param tier [String] The tier to check
151
+ # @return [Hash] Result with :would_exceed and :headroom
152
+ def check_headroom(additional_tokens: 0, additional_cost: 0.0, tier: "standard")
153
+ return {would_exceed: false, headroom: {tokens: nil, cost: nil}} unless @usage_limit.enabled?
154
+
155
+ current = @tracker.current_usage(
156
+ period_type: @usage_limit.period,
157
+ reset_day: @usage_limit.reset_day
158
+ )
159
+
160
+ limits = @usage_limit.limits_for_tier(tier)
161
+
162
+ projected_tokens = current[:total_tokens] + additional_tokens
163
+ projected_cost = current[:total_cost] + additional_cost
164
+
165
+ token_headroom = limits[:max_tokens] ? limits[:max_tokens] - current[:total_tokens] : nil
166
+ cost_headroom = limits[:max_cost] ? limits[:max_cost] - current[:total_cost] : nil
167
+
168
+ would_exceed = false
169
+ would_exceed = true if limits[:max_tokens] && projected_tokens >= limits[:max_tokens]
170
+ would_exceed = true if limits[:max_cost] && projected_cost >= limits[:max_cost]
171
+
172
+ {
173
+ would_exceed: would_exceed,
174
+ headroom: {
175
+ tokens: token_headroom,
176
+ cost: cost_headroom
177
+ },
178
+ current: {
179
+ tokens: current[:total_tokens],
180
+ cost: current[:total_cost]
181
+ },
182
+ limits: limits
183
+ }
184
+ end
185
+
186
+ # Get usage summary for display
187
+ #
188
+ # @return [Hash] Usage summary with current values, limits, and percentages
189
+ def usage_summary
190
+ return {enabled: false} unless @usage_limit.enabled?
191
+
192
+ current = @tracker.current_usage(
193
+ period_type: @usage_limit.period,
194
+ reset_day: @usage_limit.reset_day
195
+ )
196
+
197
+ # Aggregate across common tiers
198
+ tiers_summary = %w[mini standard advanced].map do |tier|
199
+ limits = @usage_limit.limits_for_tier(tier)
200
+ tier_usage = @tracker.tier_usage(
201
+ tier: tier,
202
+ period_type: @usage_limit.period,
203
+ reset_day: @usage_limit.reset_day
204
+ )
205
+
206
+ {
207
+ tier: tier,
208
+ tokens: tier_usage[:tokens],
209
+ cost: tier_usage[:cost],
210
+ requests: tier_usage[:requests],
211
+ max_tokens: limits[:max_tokens],
212
+ max_cost: limits[:max_cost],
213
+ token_percent: calculate_percent(tier_usage[:tokens], limits[:max_tokens]),
214
+ cost_percent: calculate_percent(tier_usage[:cost], limits[:max_cost])
215
+ }
216
+ end
217
+
218
+ {
219
+ enabled: true,
220
+ provider: @provider_name,
221
+ period: @usage_limit.period,
222
+ period_description: current[:period_description],
223
+ total_tokens: current[:total_tokens],
224
+ total_cost: current[:total_cost],
225
+ total_requests: current[:request_count],
226
+ remaining_seconds: current[:remaining_seconds],
227
+ tiers: tiers_summary
228
+ }
229
+ end
230
+
231
+ private
232
+
233
+ def calculate_percent(current, max)
234
+ return nil unless max&.positive?
235
+
236
+ ((current.to_f / max) * 100).round(1)
237
+ end
238
+ end
239
+ end
240
+ end