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.
- checksums.yaml +4 -4
- data/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/ui.rb +11 -0
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +118 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- 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
|