aidp 0.5.0 → 0.8.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 +128 -151
- data/bin/aidp +1 -1
- data/lib/aidp/analysis/kb_inspector.rb +471 -0
- data/lib/aidp/analysis/seams.rb +159 -0
- data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
- data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
- data/lib/aidp/analyze/error_handler.rb +2 -78
- data/lib/aidp/analyze/json_file_storage.rb +292 -0
- data/lib/aidp/analyze/progress.rb +12 -0
- data/lib/aidp/analyze/progress_visualizer.rb +12 -17
- data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
- data/lib/aidp/analyze/runner.rb +256 -87
- data/lib/aidp/analyze/steps.rb +6 -0
- data/lib/aidp/cli/jobs_command.rb +103 -435
- data/lib/aidp/cli.rb +317 -191
- data/lib/aidp/config.rb +298 -10
- data/lib/aidp/debug_logger.rb +195 -0
- data/lib/aidp/debug_mixin.rb +187 -0
- data/lib/aidp/execute/progress.rb +9 -0
- data/lib/aidp/execute/runner.rb +221 -40
- data/lib/aidp/execute/steps.rb +17 -7
- data/lib/aidp/execute/workflow_selector.rb +211 -0
- data/lib/aidp/harness/completion_checker.rb +268 -0
- data/lib/aidp/harness/condition_detector.rb +1526 -0
- data/lib/aidp/harness/config_loader.rb +373 -0
- data/lib/aidp/harness/config_manager.rb +382 -0
- data/lib/aidp/harness/config_schema.rb +1006 -0
- data/lib/aidp/harness/config_validator.rb +355 -0
- data/lib/aidp/harness/configuration.rb +477 -0
- data/lib/aidp/harness/enhanced_runner.rb +494 -0
- data/lib/aidp/harness/error_handler.rb +616 -0
- data/lib/aidp/harness/provider_config.rb +423 -0
- data/lib/aidp/harness/provider_factory.rb +306 -0
- data/lib/aidp/harness/provider_manager.rb +1269 -0
- data/lib/aidp/harness/provider_type_checker.rb +88 -0
- data/lib/aidp/harness/runner.rb +411 -0
- data/lib/aidp/harness/state/errors.rb +28 -0
- data/lib/aidp/harness/state/metrics.rb +219 -0
- data/lib/aidp/harness/state/persistence.rb +128 -0
- data/lib/aidp/harness/state/provider_state.rb +132 -0
- data/lib/aidp/harness/state/ui_state.rb +68 -0
- data/lib/aidp/harness/state/workflow_state.rb +123 -0
- data/lib/aidp/harness/state_manager.rb +586 -0
- data/lib/aidp/harness/status_display.rb +888 -0
- data/lib/aidp/harness/ui/base.rb +16 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
- data/lib/aidp/harness/ui/error_handler.rb +132 -0
- data/lib/aidp/harness/ui/frame_manager.rb +361 -0
- data/lib/aidp/harness/ui/job_monitor.rb +500 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
- data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
- data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
- data/lib/aidp/harness/ui/progress_display.rb +280 -0
- data/lib/aidp/harness/ui/question_collector.rb +141 -0
- data/lib/aidp/harness/ui/spinner_group.rb +184 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
- data/lib/aidp/harness/ui/status_manager.rb +312 -0
- data/lib/aidp/harness/ui/status_widget.rb +280 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
- data/lib/aidp/harness/user_interface.rb +2381 -0
- data/lib/aidp/provider_manager.rb +131 -7
- data/lib/aidp/providers/anthropic.rb +28 -109
- data/lib/aidp/providers/base.rb +170 -0
- data/lib/aidp/providers/cursor.rb +52 -183
- data/lib/aidp/providers/gemini.rb +24 -109
- data/lib/aidp/providers/macos_ui.rb +99 -5
- data/lib/aidp/providers/opencode.rb +194 -0
- data/lib/aidp/storage/csv_storage.rb +172 -0
- data/lib/aidp/storage/file_manager.rb +214 -0
- data/lib/aidp/storage/json_storage.rb +140 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +56 -35
- data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
- data/templates/COMMON/AGENT_BASE.md +11 -0
- data/templates/EXECUTE/00_PRD.md +4 -4
- data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
- data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
- data/templates/EXECUTE/08_TASKS.md +4 -4
- data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
- data/templates/README.md +279 -0
- data/templates/aidp-development.yml.example +373 -0
- data/templates/aidp-minimal.yml.example +48 -0
- data/templates/aidp-production.yml.example +475 -0
- data/templates/aidp.yml.example +598 -0
- metadata +106 -64
- data/lib/aidp/analyze/agent_personas.rb +0 -71
- data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
- data/lib/aidp/analyze/data_retention_manager.rb +0 -426
- data/lib/aidp/analyze/database.rb +0 -260
- data/lib/aidp/analyze/dependencies.rb +0 -335
- data/lib/aidp/analyze/export_manager.rb +0 -425
- data/lib/aidp/analyze/focus_guidance.rb +0 -517
- data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
- data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
- data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
- data/lib/aidp/analyze/memory_manager.rb +0 -365
- data/lib/aidp/analyze/metrics_storage.rb +0 -336
- data/lib/aidp/analyze/parallel_processor.rb +0 -460
- data/lib/aidp/analyze/performance_optimizer.rb +0 -694
- data/lib/aidp/analyze/repository_chunker.rb +0 -704
- data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
- data/lib/aidp/analyze/storage.rb +0 -662
- data/lib/aidp/analyze/tool_configuration.rb +0 -456
- data/lib/aidp/analyze/tool_modernization.rb +0 -750
- data/lib/aidp/database/pg_adapter.rb +0 -148
- data/lib/aidp/database_config.rb +0 -69
- data/lib/aidp/database_connection.rb +0 -72
- data/lib/aidp/database_migration.rb +0 -158
- data/lib/aidp/job_manager.rb +0 -41
- data/lib/aidp/jobs/base_job.rb +0 -47
- data/lib/aidp/jobs/provider_execution_job.rb +0 -96
- data/lib/aidp/project_detector.rb +0 -117
- data/lib/aidp/providers/agent_supervisor.rb +0 -348
- data/lib/aidp/providers/supervised_base.rb +0 -317
- data/lib/aidp/providers/supervised_cursor.rb +0 -22
- data/lib/aidp/sync.rb +0 -13
- data/lib/aidp/workspace.rb +0 -19
@@ -0,0 +1,1269 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "provider_factory"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
module Harness
|
7
|
+
# Manages provider switching and fallback logic
|
8
|
+
class ProviderManager
|
9
|
+
def initialize(configuration)
|
10
|
+
@configuration = configuration
|
11
|
+
@current_provider = nil
|
12
|
+
@current_model = nil
|
13
|
+
@provider_history = []
|
14
|
+
@rate_limit_info = {}
|
15
|
+
@provider_metrics = {}
|
16
|
+
@fallback_chains = {}
|
17
|
+
@provider_health = {}
|
18
|
+
@retry_counts = {}
|
19
|
+
@max_retries = 3
|
20
|
+
@circuit_breaker_threshold = 5
|
21
|
+
@circuit_breaker_timeout = 300 # 5 minutes
|
22
|
+
@provider_weights = {}
|
23
|
+
@load_balancing_enabled = true
|
24
|
+
@sticky_sessions = {}
|
25
|
+
@session_timeout = 1800 # 30 minutes
|
26
|
+
@model_configs = {}
|
27
|
+
@model_health = {}
|
28
|
+
@model_metrics = {}
|
29
|
+
@model_fallback_chains = {}
|
30
|
+
@model_switching_enabled = true
|
31
|
+
@model_weights = {}
|
32
|
+
initialize_fallback_chains
|
33
|
+
initialize_provider_health
|
34
|
+
initialize_model_configs
|
35
|
+
initialize_model_health
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get current provider
|
39
|
+
def current_provider
|
40
|
+
@current_provider ||= @configuration.default_provider
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get current model
|
44
|
+
def current_model
|
45
|
+
@current_model ||= get_default_model(current_provider)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get current provider and model combination
|
49
|
+
def current_provider_model
|
50
|
+
"#{current_provider}:#{current_model}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get configured providers from configuration
|
54
|
+
def configured_providers
|
55
|
+
@configuration.configured_providers
|
56
|
+
end
|
57
|
+
|
58
|
+
# Switch to next available provider with sophisticated fallback logic
|
59
|
+
def switch_provider(reason = "manual_switch", context = {})
|
60
|
+
# Get fallback chain for current provider
|
61
|
+
fallback_chain = get_fallback_chain(current_provider)
|
62
|
+
|
63
|
+
# Find next healthy provider in fallback chain
|
64
|
+
next_provider = find_next_healthy_provider(fallback_chain, current_provider)
|
65
|
+
|
66
|
+
if next_provider
|
67
|
+
success = set_current_provider(next_provider, reason, context)
|
68
|
+
if success
|
69
|
+
log_provider_switch(current_provider, next_provider, reason, context)
|
70
|
+
return next_provider
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# If no provider in fallback chain, try load balancing
|
75
|
+
if @load_balancing_enabled
|
76
|
+
next_provider = select_provider_by_load_balancing
|
77
|
+
if next_provider
|
78
|
+
success = set_current_provider(next_provider, reason, context)
|
79
|
+
if success
|
80
|
+
log_provider_switch(current_provider, next_provider, reason, context)
|
81
|
+
return next_provider
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Last resort: try any available provider
|
87
|
+
next_provider = find_any_available_provider
|
88
|
+
if next_provider
|
89
|
+
success = set_current_provider(next_provider, reason, context)
|
90
|
+
if success
|
91
|
+
log_provider_switch(current_provider, next_provider, reason, context)
|
92
|
+
return next_provider
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# No providers available
|
97
|
+
log_no_providers_available(reason, context)
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
# Switch provider for specific error type
|
102
|
+
def switch_provider_for_error(error_type, error_details = {})
|
103
|
+
case error_type
|
104
|
+
when "rate_limit"
|
105
|
+
switch_provider("rate_limit", error_details)
|
106
|
+
when "authentication"
|
107
|
+
switch_provider("authentication_error", error_details)
|
108
|
+
when "network"
|
109
|
+
switch_provider("network_error", error_details)
|
110
|
+
when "server_error"
|
111
|
+
switch_provider("server_error", error_details)
|
112
|
+
when "timeout"
|
113
|
+
switch_provider("timeout", error_details)
|
114
|
+
else
|
115
|
+
switch_provider("error", {error_type: error_type}.merge(error_details))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Switch provider with retry logic
|
120
|
+
def switch_provider_with_retry(reason = "retry", max_retries = @max_retries)
|
121
|
+
retry_count = 0
|
122
|
+
|
123
|
+
while retry_count < max_retries
|
124
|
+
next_provider = switch_provider(reason, {retry_count: retry_count})
|
125
|
+
|
126
|
+
if next_provider
|
127
|
+
return next_provider
|
128
|
+
end
|
129
|
+
|
130
|
+
retry_count += 1
|
131
|
+
|
132
|
+
# Wait before retrying
|
133
|
+
delay = calculate_retry_delay(retry_count)
|
134
|
+
if ENV["RACK_ENV"] == "test" || defined?(RSpec)
|
135
|
+
sleep(delay)
|
136
|
+
else
|
137
|
+
Async::Task.current.sleep(delay)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
|
144
|
+
# Switch to next available model within current provider
|
145
|
+
def switch_model(reason = "manual_switch", context = {})
|
146
|
+
return nil unless @model_switching_enabled
|
147
|
+
|
148
|
+
# Get fallback chain for current provider's models
|
149
|
+
model_chain = get_model_fallback_chain(current_provider)
|
150
|
+
|
151
|
+
# Find next healthy model in fallback chain
|
152
|
+
next_model = find_next_healthy_model(model_chain, current_model)
|
153
|
+
|
154
|
+
if next_model
|
155
|
+
success = set_current_model(next_model, reason, context)
|
156
|
+
if success
|
157
|
+
log_model_switch(current_model, next_model, reason, context)
|
158
|
+
return next_model
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# If no model in fallback chain, try load balancing
|
163
|
+
if @load_balancing_enabled
|
164
|
+
next_model = select_model_by_load_balancing(current_provider)
|
165
|
+
if next_model
|
166
|
+
success = set_current_model(next_model, reason, context)
|
167
|
+
if success
|
168
|
+
log_model_switch(current_model, next_model, reason, context)
|
169
|
+
return next_model
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Last resort: try any available model
|
175
|
+
next_model = find_any_available_model(current_provider)
|
176
|
+
if next_model
|
177
|
+
success = set_current_model(next_model, reason, context)
|
178
|
+
if success
|
179
|
+
log_model_switch(current_model, next_model, reason, context)
|
180
|
+
return next_model
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# No models available
|
185
|
+
log_no_models_available(current_provider, reason, context)
|
186
|
+
nil
|
187
|
+
end
|
188
|
+
|
189
|
+
# Switch model for specific error type
|
190
|
+
def switch_model_for_error(error_type, error_details = {})
|
191
|
+
return nil unless @model_switching_enabled
|
192
|
+
|
193
|
+
case error_type
|
194
|
+
when "rate_limit"
|
195
|
+
switch_model("rate_limit", error_details)
|
196
|
+
when "model_unavailable"
|
197
|
+
switch_model("model_unavailable", error_details)
|
198
|
+
when "model_error"
|
199
|
+
switch_model("model_error", error_details)
|
200
|
+
when "timeout"
|
201
|
+
switch_model("timeout", error_details)
|
202
|
+
else
|
203
|
+
switch_model("error", {error_type: error_type}.merge(error_details))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Switch model with retry logic
|
208
|
+
def switch_model_with_retry(reason = "retry", max_retries = @max_retries)
|
209
|
+
return nil unless @model_switching_enabled
|
210
|
+
|
211
|
+
retry_count = 0
|
212
|
+
|
213
|
+
while retry_count < max_retries
|
214
|
+
next_model = switch_model(reason, {retry_count: retry_count})
|
215
|
+
|
216
|
+
if next_model
|
217
|
+
return next_model
|
218
|
+
end
|
219
|
+
|
220
|
+
retry_count += 1
|
221
|
+
|
222
|
+
# Wait before retrying
|
223
|
+
delay = calculate_retry_delay(retry_count)
|
224
|
+
if ENV["RACK_ENV"] == "test" || defined?(RSpec)
|
225
|
+
sleep(delay)
|
226
|
+
else
|
227
|
+
Async::Task.current.sleep(delay)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
|
234
|
+
# Set current model with enhanced validation
|
235
|
+
def set_current_model(model_name, reason = "manual_switch", context = {})
|
236
|
+
return false unless model_available?(current_provider, model_name)
|
237
|
+
return false unless is_model_healthy?(current_provider, model_name)
|
238
|
+
return false if is_model_circuit_breaker_open?(current_provider, model_name)
|
239
|
+
|
240
|
+
# Update model health
|
241
|
+
update_model_health(current_provider, model_name, "switched_to")
|
242
|
+
|
243
|
+
# Record model switch
|
244
|
+
@model_history ||= []
|
245
|
+
@model_history << {
|
246
|
+
provider: current_provider,
|
247
|
+
model: model_name,
|
248
|
+
switched_at: Time.now,
|
249
|
+
reason: reason,
|
250
|
+
context: context,
|
251
|
+
previous_model: @current_model
|
252
|
+
}
|
253
|
+
|
254
|
+
@current_model = model_name
|
255
|
+
true
|
256
|
+
end
|
257
|
+
|
258
|
+
# Set current provider with enhanced validation
|
259
|
+
def set_current_provider(provider_name, reason = "manual_switch", context = {})
|
260
|
+
return false unless @configuration.provider_configured?(provider_name)
|
261
|
+
return false unless is_provider_healthy?(provider_name)
|
262
|
+
return false if is_provider_circuit_breaker_open?(provider_name)
|
263
|
+
|
264
|
+
# Update provider health
|
265
|
+
update_provider_health(provider_name, "switched_to")
|
266
|
+
|
267
|
+
# Record provider switch
|
268
|
+
@provider_history << {
|
269
|
+
provider: provider_name,
|
270
|
+
switched_at: Time.now,
|
271
|
+
reason: reason,
|
272
|
+
context: context,
|
273
|
+
previous_provider: @current_provider
|
274
|
+
}
|
275
|
+
|
276
|
+
# Update sticky session if enabled
|
277
|
+
update_sticky_session(provider_name) if context[:session_id]
|
278
|
+
|
279
|
+
# Reset current model when switching providers
|
280
|
+
@current_model = get_default_model(provider_name)
|
281
|
+
|
282
|
+
@current_provider = provider_name
|
283
|
+
true
|
284
|
+
end
|
285
|
+
|
286
|
+
# Get available providers (not rate limited, healthy, and circuit breaker closed)
|
287
|
+
def get_available_providers
|
288
|
+
all_providers = @configuration.configured_providers
|
289
|
+
all_providers.select do |provider|
|
290
|
+
!is_rate_limited?(provider) &&
|
291
|
+
is_provider_healthy?(provider) &&
|
292
|
+
!is_provider_circuit_breaker_open?(provider)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Get available models for a provider
|
297
|
+
def get_available_models(provider_name)
|
298
|
+
models = get_provider_models(provider_name)
|
299
|
+
models.select do |model|
|
300
|
+
model_available?(provider_name, model) &&
|
301
|
+
is_model_healthy?(provider_name, model) &&
|
302
|
+
!is_model_circuit_breaker_open?(provider_name, model)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Check if model is available
|
307
|
+
def model_available?(provider_name, model_name)
|
308
|
+
# Check if model is configured for provider
|
309
|
+
return false unless model_configured?(provider_name, model_name)
|
310
|
+
|
311
|
+
# Check if model is not rate limited
|
312
|
+
!is_model_rate_limited?(provider_name, model_name)
|
313
|
+
end
|
314
|
+
|
315
|
+
# Check if model is configured for provider
|
316
|
+
def model_configured?(provider_name, model_name)
|
317
|
+
models = get_provider_models(provider_name)
|
318
|
+
models.include?(model_name)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Get models for a provider
|
322
|
+
def get_provider_models(provider_name)
|
323
|
+
@model_configs[provider_name] || []
|
324
|
+
end
|
325
|
+
|
326
|
+
# Get default model for provider
|
327
|
+
def get_default_model(provider_name)
|
328
|
+
models = get_provider_models(provider_name)
|
329
|
+
return models.first if models.any?
|
330
|
+
|
331
|
+
# Fallback to provider-specific defaults
|
332
|
+
case provider_name
|
333
|
+
when "claude"
|
334
|
+
"claude-3-5-sonnet-20241022"
|
335
|
+
when "gemini"
|
336
|
+
"gemini-1.5-pro"
|
337
|
+
when "cursor"
|
338
|
+
"cursor-default"
|
339
|
+
else
|
340
|
+
"default"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Get fallback chain for a provider
|
345
|
+
def get_fallback_chain(provider_name)
|
346
|
+
@fallback_chains[provider_name] || build_default_fallback_chain(provider_name)
|
347
|
+
end
|
348
|
+
|
349
|
+
# Get fallback chain for models within a provider
|
350
|
+
def get_model_fallback_chain(provider_name)
|
351
|
+
@model_fallback_chains[provider_name] || build_default_model_fallback_chain(provider_name)
|
352
|
+
end
|
353
|
+
|
354
|
+
# Build default model fallback chain
|
355
|
+
def build_default_model_fallback_chain(provider_name)
|
356
|
+
models = get_provider_models(provider_name)
|
357
|
+
@model_fallback_chains[provider_name] = models.dup
|
358
|
+
models
|
359
|
+
end
|
360
|
+
|
361
|
+
# Find next healthy model in fallback chain
|
362
|
+
def find_next_healthy_model(model_chain, current_model)
|
363
|
+
current_index = model_chain.index(current_model) || -1
|
364
|
+
|
365
|
+
# Start from next model in chain
|
366
|
+
(current_index + 1...model_chain.size).each do |index|
|
367
|
+
model = model_chain[index]
|
368
|
+
if model_available?(current_provider, model)
|
369
|
+
return model
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
nil
|
374
|
+
end
|
375
|
+
|
376
|
+
# Find any available model for provider
|
377
|
+
def find_any_available_model(provider_name)
|
378
|
+
available_models = get_available_models(provider_name)
|
379
|
+
return nil if available_models.empty?
|
380
|
+
|
381
|
+
# Use weighted selection if weights are configured
|
382
|
+
if @model_weights[provider_name]&.any?
|
383
|
+
select_model_by_weight(provider_name, available_models)
|
384
|
+
else
|
385
|
+
# Simple round-robin selection
|
386
|
+
available_models.first
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Select model by load balancing
|
391
|
+
def select_model_by_load_balancing(provider_name)
|
392
|
+
available_models = get_available_models(provider_name)
|
393
|
+
return nil if available_models.empty?
|
394
|
+
|
395
|
+
# Calculate load for each model
|
396
|
+
model_loads = available_models.map do |model|
|
397
|
+
load = calculate_model_load(provider_name, model)
|
398
|
+
[model, load]
|
399
|
+
end
|
400
|
+
|
401
|
+
# Select model with lowest load
|
402
|
+
model_loads.min_by { |_, load| load }&.first
|
403
|
+
end
|
404
|
+
|
405
|
+
# Select model by weight
|
406
|
+
def select_model_by_weight(provider_name, available_models)
|
407
|
+
weights = @model_weights[provider_name] || {}
|
408
|
+
total_weight = available_models.sum { |model| weights[model] || 1 }
|
409
|
+
return available_models.first if total_weight == 0
|
410
|
+
|
411
|
+
random_value = rand(total_weight)
|
412
|
+
current_weight = 0
|
413
|
+
|
414
|
+
available_models.each do |model|
|
415
|
+
weight = weights[model] || 1
|
416
|
+
current_weight += weight
|
417
|
+
return model if random_value < current_weight
|
418
|
+
end
|
419
|
+
|
420
|
+
available_models.last
|
421
|
+
end
|
422
|
+
|
423
|
+
# Calculate model load
|
424
|
+
def calculate_model_load(provider_name, model_name)
|
425
|
+
metrics = get_model_metrics(provider_name, model_name)
|
426
|
+
return 0 if metrics.empty?
|
427
|
+
|
428
|
+
# Calculate load based on success rate, response time, and current usage
|
429
|
+
success_rate = metrics[:successful_requests].to_f / [metrics[:total_requests], 1].max
|
430
|
+
avg_response_time = metrics[:total_duration] / [metrics[:successful_requests], 1].max
|
431
|
+
current_usage = calculate_model_current_usage(provider_name, model_name)
|
432
|
+
|
433
|
+
# Load formula: higher is worse
|
434
|
+
(1 - success_rate) * 100 + avg_response_time + current_usage
|
435
|
+
end
|
436
|
+
|
437
|
+
# Calculate current usage for model
|
438
|
+
def calculate_model_current_usage(provider_name, model_name)
|
439
|
+
metrics = get_model_metrics(provider_name, model_name)
|
440
|
+
return 0 if metrics.empty?
|
441
|
+
|
442
|
+
last_used = metrics[:last_used]
|
443
|
+
return 0 unless last_used
|
444
|
+
|
445
|
+
# Higher usage if used recently
|
446
|
+
time_since_last_use = Time.now - last_used
|
447
|
+
if time_since_last_use < 60 # Used within last minute
|
448
|
+
10
|
449
|
+
elsif time_since_last_use < 300 # Used within last 5 minutes
|
450
|
+
5
|
451
|
+
else
|
452
|
+
0
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
# Build default fallback chain
|
457
|
+
def build_default_fallback_chain(provider_name)
|
458
|
+
all_providers = @configuration.configured_providers
|
459
|
+
fallback_chain = all_providers.dup
|
460
|
+
fallback_chain.delete(provider_name)
|
461
|
+
fallback_chain.unshift(provider_name) # Put current provider first
|
462
|
+
@fallback_chains[provider_name] = fallback_chain
|
463
|
+
fallback_chain
|
464
|
+
end
|
465
|
+
|
466
|
+
# Find next healthy provider in fallback chain
|
467
|
+
def find_next_healthy_provider(fallback_chain, current_provider)
|
468
|
+
current_index = fallback_chain.index(current_provider) || -1
|
469
|
+
|
470
|
+
# Start from next provider in chain
|
471
|
+
(current_index + 1...fallback_chain.size).each do |index|
|
472
|
+
provider = fallback_chain[index]
|
473
|
+
if is_provider_available?(provider)
|
474
|
+
return provider
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
nil
|
479
|
+
end
|
480
|
+
|
481
|
+
# Find any available provider
|
482
|
+
def find_any_available_provider
|
483
|
+
available_providers = get_available_providers
|
484
|
+
return nil if available_providers.empty?
|
485
|
+
|
486
|
+
# Use weighted selection if weights are configured
|
487
|
+
if @provider_weights.any?
|
488
|
+
select_provider_by_weight(available_providers)
|
489
|
+
else
|
490
|
+
# Simple round-robin selection
|
491
|
+
available_providers.first
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# Select provider by load balancing
|
496
|
+
def select_provider_by_load_balancing
|
497
|
+
available_providers = get_available_providers
|
498
|
+
return nil if available_providers.empty?
|
499
|
+
|
500
|
+
# Calculate load for each provider
|
501
|
+
provider_loads = available_providers.map do |provider|
|
502
|
+
load = calculate_provider_load(provider)
|
503
|
+
[provider, load]
|
504
|
+
end
|
505
|
+
|
506
|
+
# Select provider with lowest load
|
507
|
+
provider_loads.min_by { |_, load| load }&.first
|
508
|
+
end
|
509
|
+
|
510
|
+
# Select provider by weight
|
511
|
+
def select_provider_by_weight(available_providers)
|
512
|
+
total_weight = available_providers.sum { |provider| @provider_weights[provider] || 1 }
|
513
|
+
return available_providers.first if total_weight == 0
|
514
|
+
|
515
|
+
random_value = rand(total_weight)
|
516
|
+
current_weight = 0
|
517
|
+
|
518
|
+
available_providers.each do |provider|
|
519
|
+
weight = @provider_weights[provider] || 1
|
520
|
+
current_weight += weight
|
521
|
+
return provider if random_value < current_weight
|
522
|
+
end
|
523
|
+
|
524
|
+
available_providers.last
|
525
|
+
end
|
526
|
+
|
527
|
+
# Calculate provider load
|
528
|
+
def calculate_provider_load(provider_name)
|
529
|
+
metrics = get_metrics(provider_name)
|
530
|
+
return 0 if metrics.empty?
|
531
|
+
|
532
|
+
# Calculate load based on success rate, response time, and current usage
|
533
|
+
success_rate = metrics[:successful_requests].to_f / [metrics[:total_requests], 1].max
|
534
|
+
avg_response_time = metrics[:total_duration] / [metrics[:successful_requests], 1].max
|
535
|
+
current_usage = calculate_current_usage(provider_name)
|
536
|
+
|
537
|
+
# Load formula: higher is worse
|
538
|
+
(1 - success_rate) * 100 + avg_response_time + current_usage
|
539
|
+
end
|
540
|
+
|
541
|
+
# Calculate current usage for provider
|
542
|
+
def calculate_current_usage(provider_name)
|
543
|
+
# Simple usage calculation based on recent activity
|
544
|
+
metrics = get_metrics(provider_name)
|
545
|
+
return 0 if metrics.empty?
|
546
|
+
|
547
|
+
last_used = metrics[:last_used]
|
548
|
+
return 0 unless last_used
|
549
|
+
|
550
|
+
# Higher usage if used recently
|
551
|
+
time_since_last_use = Time.now - last_used
|
552
|
+
if time_since_last_use < 60 # Used within last minute
|
553
|
+
10
|
554
|
+
elsif time_since_last_use < 300 # Used within last 5 minutes
|
555
|
+
5
|
556
|
+
else
|
557
|
+
0
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
# Check if provider is available (not rate limited, healthy, circuit breaker closed)
|
562
|
+
def is_provider_available?(provider_name)
|
563
|
+
!is_rate_limited?(provider_name) &&
|
564
|
+
is_provider_healthy?(provider_name) &&
|
565
|
+
!is_provider_circuit_breaker_open?(provider_name)
|
566
|
+
end
|
567
|
+
|
568
|
+
# Check if model is rate limited
|
569
|
+
def is_model_rate_limited?(provider_name, model_name)
|
570
|
+
info = @model_rate_limit_info ||= {}
|
571
|
+
model_key = "#{provider_name}:#{model_name}"
|
572
|
+
rate_limit_info = info[model_key]
|
573
|
+
return false unless rate_limit_info
|
574
|
+
|
575
|
+
reset_time = rate_limit_info[:reset_time]
|
576
|
+
reset_time && Time.now < reset_time
|
577
|
+
end
|
578
|
+
|
579
|
+
# Mark model as rate limited
|
580
|
+
def mark_model_rate_limited(provider_name, model_name, reset_time = nil)
|
581
|
+
@model_rate_limit_info ||= {}
|
582
|
+
model_key = "#{provider_name}:#{model_name}"
|
583
|
+
@model_rate_limit_info[model_key] = {
|
584
|
+
rate_limited_at: Time.now,
|
585
|
+
reset_time: reset_time || calculate_model_reset_time(provider_name, model_name),
|
586
|
+
error_count: (@model_rate_limit_info[model_key]&.dig(:error_count) || 0) + 1
|
587
|
+
}
|
588
|
+
|
589
|
+
# Update model health
|
590
|
+
update_model_health(provider_name, model_name, "rate_limited")
|
591
|
+
|
592
|
+
# Switch to next model if current one is rate limited
|
593
|
+
if provider_name == current_provider && model_name == current_model
|
594
|
+
switch_model("rate_limit", {provider: provider_name, model: model_name})
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
# Clear rate limit for model
|
599
|
+
def clear_model_rate_limit(provider_name, model_name)
|
600
|
+
@model_rate_limit_info ||= {}
|
601
|
+
model_key = "#{provider_name}:#{model_name}"
|
602
|
+
@model_rate_limit_info.delete(model_key)
|
603
|
+
end
|
604
|
+
|
605
|
+
# Check if model is healthy
|
606
|
+
def is_model_healthy?(provider_name, model_name)
|
607
|
+
health = @model_health[provider_name]&.dig(model_name)
|
608
|
+
return true unless health # Default to healthy if no health info
|
609
|
+
|
610
|
+
health[:status] == "healthy"
|
611
|
+
end
|
612
|
+
|
613
|
+
# Check if model circuit breaker is open
|
614
|
+
def is_model_circuit_breaker_open?(provider_name, model_name)
|
615
|
+
health = @model_health[provider_name]&.dig(model_name)
|
616
|
+
return false unless health
|
617
|
+
|
618
|
+
if health[:circuit_breaker_open]
|
619
|
+
# Check if timeout has passed
|
620
|
+
if health[:circuit_breaker_opened_at] &&
|
621
|
+
Time.now - health[:circuit_breaker_opened_at] > @circuit_breaker_timeout
|
622
|
+
# Reset circuit breaker
|
623
|
+
reset_model_circuit_breaker(provider_name, model_name)
|
624
|
+
return false
|
625
|
+
end
|
626
|
+
return true
|
627
|
+
end
|
628
|
+
|
629
|
+
false
|
630
|
+
end
|
631
|
+
|
632
|
+
# Update model health
|
633
|
+
def update_model_health(provider_name, model_name, event, _details = {})
|
634
|
+
@model_health[provider_name] ||= {}
|
635
|
+
@model_health[provider_name][model_name] ||= {
|
636
|
+
status: "healthy",
|
637
|
+
last_updated: Time.now,
|
638
|
+
error_count: 0,
|
639
|
+
success_count: 0,
|
640
|
+
circuit_breaker_open: false,
|
641
|
+
circuit_breaker_opened_at: nil
|
642
|
+
}
|
643
|
+
|
644
|
+
health = @model_health[provider_name][model_name]
|
645
|
+
health[:last_updated] = Time.now
|
646
|
+
|
647
|
+
case event
|
648
|
+
when "success"
|
649
|
+
health[:success_count] += 1
|
650
|
+
health[:error_count] = [health[:error_count] - 1, 0].max # Decay errors
|
651
|
+
health[:status] = "healthy"
|
652
|
+
|
653
|
+
# Reset circuit breaker on success
|
654
|
+
if health[:circuit_breaker_open]
|
655
|
+
reset_model_circuit_breaker(provider_name, model_name)
|
656
|
+
end
|
657
|
+
|
658
|
+
when "error"
|
659
|
+
health[:error_count] += 1
|
660
|
+
|
661
|
+
# Check if circuit breaker should open
|
662
|
+
if health[:error_count] >= @circuit_breaker_threshold
|
663
|
+
open_model_circuit_breaker(provider_name, model_name)
|
664
|
+
end
|
665
|
+
|
666
|
+
# Mark as unhealthy if too many errors
|
667
|
+
if health[:error_count] > @circuit_breaker_threshold * 2
|
668
|
+
health[:status] = "unhealthy"
|
669
|
+
end
|
670
|
+
|
671
|
+
when "switched_to"
|
672
|
+
# Model was selected, update last used
|
673
|
+
health[:last_used] = Time.now
|
674
|
+
|
675
|
+
when "rate_limited"
|
676
|
+
# Rate limiting doesn't affect health status
|
677
|
+
health[:last_rate_limited] = Time.now
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
# Open circuit breaker for model
|
682
|
+
def open_model_circuit_breaker(provider_name, model_name)
|
683
|
+
health = @model_health[provider_name]&.dig(model_name)
|
684
|
+
return unless health
|
685
|
+
|
686
|
+
health[:circuit_breaker_open] = true
|
687
|
+
health[:circuit_breaker_opened_at] = Time.now
|
688
|
+
health[:status] = "circuit_breaker_open"
|
689
|
+
|
690
|
+
log_model_circuit_breaker_event(provider_name, model_name, "opened")
|
691
|
+
end
|
692
|
+
|
693
|
+
# Reset circuit breaker for model
|
694
|
+
def reset_model_circuit_breaker(provider_name, model_name)
|
695
|
+
health = @model_health[provider_name]&.dig(model_name)
|
696
|
+
return unless health
|
697
|
+
|
698
|
+
was_open = health[:circuit_breaker_open]
|
699
|
+
health[:circuit_breaker_open] = false
|
700
|
+
health[:circuit_breaker_opened_at] = nil
|
701
|
+
health[:error_count] = 0
|
702
|
+
health[:status] = "healthy"
|
703
|
+
|
704
|
+
log_model_circuit_breaker_event(provider_name, model_name, "reset") if was_open
|
705
|
+
end
|
706
|
+
|
707
|
+
# Check if provider is rate limited
|
708
|
+
def is_rate_limited?(provider_name)
|
709
|
+
info = @rate_limit_info[provider_name]
|
710
|
+
return false unless info
|
711
|
+
|
712
|
+
reset_time = info[:reset_time]
|
713
|
+
reset_time && Time.now < reset_time
|
714
|
+
end
|
715
|
+
|
716
|
+
# Check if provider is healthy
|
717
|
+
def is_provider_healthy?(provider_name)
|
718
|
+
health = @provider_health[provider_name]
|
719
|
+
return true unless health # Default to healthy if no health info
|
720
|
+
|
721
|
+
health[:status] == "healthy"
|
722
|
+
end
|
723
|
+
|
724
|
+
# Check if provider circuit breaker is open
|
725
|
+
def is_provider_circuit_breaker_open?(provider_name)
|
726
|
+
health = @provider_health[provider_name]
|
727
|
+
return false unless health
|
728
|
+
|
729
|
+
if health[:circuit_breaker_open]
|
730
|
+
# Check if timeout has passed
|
731
|
+
if health[:circuit_breaker_opened_at] &&
|
732
|
+
Time.now - health[:circuit_breaker_opened_at] > @circuit_breaker_timeout
|
733
|
+
# Reset circuit breaker
|
734
|
+
reset_circuit_breaker(provider_name)
|
735
|
+
return false
|
736
|
+
end
|
737
|
+
return true
|
738
|
+
end
|
739
|
+
|
740
|
+
false
|
741
|
+
end
|
742
|
+
|
743
|
+
# Update provider health
|
744
|
+
def update_provider_health(provider_name, event, _details = {})
|
745
|
+
@provider_health[provider_name] ||= {
|
746
|
+
status: "healthy",
|
747
|
+
last_updated: Time.now,
|
748
|
+
error_count: 0,
|
749
|
+
success_count: 0,
|
750
|
+
circuit_breaker_open: false,
|
751
|
+
circuit_breaker_opened_at: nil
|
752
|
+
}
|
753
|
+
|
754
|
+
health = @provider_health[provider_name]
|
755
|
+
health[:last_updated] = Time.now
|
756
|
+
|
757
|
+
case event
|
758
|
+
when "success"
|
759
|
+
health[:success_count] += 1
|
760
|
+
health[:error_count] = [health[:error_count] - 1, 0].max # Decay errors
|
761
|
+
health[:status] = "healthy"
|
762
|
+
|
763
|
+
# Reset circuit breaker on success
|
764
|
+
if health[:circuit_breaker_open]
|
765
|
+
reset_circuit_breaker(provider_name)
|
766
|
+
end
|
767
|
+
|
768
|
+
when "error"
|
769
|
+
health[:error_count] += 1
|
770
|
+
|
771
|
+
# Check if circuit breaker should open
|
772
|
+
if health[:error_count] >= @circuit_breaker_threshold
|
773
|
+
open_circuit_breaker(provider_name)
|
774
|
+
end
|
775
|
+
|
776
|
+
# Mark as unhealthy if too many errors
|
777
|
+
if health[:error_count] > @circuit_breaker_threshold * 2
|
778
|
+
health[:status] = "unhealthy"
|
779
|
+
end
|
780
|
+
|
781
|
+
when "switched_to"
|
782
|
+
# Provider was selected, update last used
|
783
|
+
health[:last_used] = Time.now
|
784
|
+
|
785
|
+
when "rate_limited"
|
786
|
+
# Rate limiting doesn't affect health status
|
787
|
+
health[:last_rate_limited] = Time.now
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
# Open circuit breaker for provider
|
792
|
+
def open_circuit_breaker(provider_name)
|
793
|
+
health = @provider_health[provider_name]
|
794
|
+
return unless health
|
795
|
+
|
796
|
+
health[:circuit_breaker_open] = true
|
797
|
+
health[:circuit_breaker_opened_at] = Time.now
|
798
|
+
health[:status] = "circuit_breaker_open"
|
799
|
+
|
800
|
+
log_circuit_breaker_event(provider_name, "opened")
|
801
|
+
end
|
802
|
+
|
803
|
+
# Reset circuit breaker for provider
|
804
|
+
def reset_circuit_breaker(provider_name)
|
805
|
+
health = @provider_health[provider_name]
|
806
|
+
return unless health
|
807
|
+
|
808
|
+
was_open = health[:circuit_breaker_open]
|
809
|
+
health[:circuit_breaker_open] = false
|
810
|
+
health[:circuit_breaker_opened_at] = nil
|
811
|
+
health[:error_count] = 0
|
812
|
+
health[:status] = "healthy"
|
813
|
+
|
814
|
+
log_circuit_breaker_event(provider_name, "reset") if was_open
|
815
|
+
end
|
816
|
+
|
817
|
+
# Mark provider as rate limited
|
818
|
+
def mark_rate_limited(provider_name, reset_time = nil)
|
819
|
+
@rate_limit_info[provider_name] = {
|
820
|
+
rate_limited_at: Time.now,
|
821
|
+
reset_time: reset_time || calculate_reset_time(provider_name),
|
822
|
+
error_count: (@rate_limit_info[provider_name]&.dig(:error_count) || 0) + 1
|
823
|
+
}
|
824
|
+
|
825
|
+
# Update provider health
|
826
|
+
update_provider_health(provider_name, "rate_limited")
|
827
|
+
|
828
|
+
# Switch to next provider if current one is rate limited
|
829
|
+
if provider_name == current_provider
|
830
|
+
switch_provider("rate_limit", {provider: provider_name})
|
831
|
+
end
|
832
|
+
end
|
833
|
+
|
834
|
+
# Get next reset time for any provider
|
835
|
+
def next_reset_time
|
836
|
+
reset_times = @rate_limit_info.values
|
837
|
+
.map { |info| info[:reset_time] }
|
838
|
+
.compact
|
839
|
+
.select { |time| time > Time.now }
|
840
|
+
|
841
|
+
reset_times.min
|
842
|
+
end
|
843
|
+
|
844
|
+
# Clear rate limit for provider
|
845
|
+
def clear_rate_limit(provider_name)
|
846
|
+
@rate_limit_info.delete(provider_name)
|
847
|
+
end
|
848
|
+
|
849
|
+
# Get provider configuration
|
850
|
+
def provider_config(provider_name)
|
851
|
+
@configuration.provider_config(provider_name)
|
852
|
+
end
|
853
|
+
|
854
|
+
# Get provider type
|
855
|
+
def provider_type(provider_name)
|
856
|
+
@configuration.provider_type(provider_name)
|
857
|
+
end
|
858
|
+
|
859
|
+
# Get default flags for provider
|
860
|
+
def default_flags(provider_name)
|
861
|
+
@configuration.default_flags(provider_name)
|
862
|
+
end
|
863
|
+
|
864
|
+
# Record provider metrics
|
865
|
+
def record_metrics(provider_name, success:, duration:, tokens_used: nil, error: nil)
|
866
|
+
@provider_metrics[provider_name] ||= {
|
867
|
+
total_requests: 0,
|
868
|
+
successful_requests: 0,
|
869
|
+
failed_requests: 0,
|
870
|
+
total_duration: 0.0,
|
871
|
+
total_tokens: 0,
|
872
|
+
last_used: nil,
|
873
|
+
last_error: nil,
|
874
|
+
last_error_time: nil
|
875
|
+
}
|
876
|
+
|
877
|
+
metrics = @provider_metrics[provider_name]
|
878
|
+
metrics[:total_requests] += 1
|
879
|
+
metrics[:last_used] = Time.now
|
880
|
+
|
881
|
+
if success
|
882
|
+
metrics[:successful_requests] += 1
|
883
|
+
metrics[:total_duration] += duration
|
884
|
+
metrics[:total_tokens] += tokens_used if tokens_used
|
885
|
+
update_provider_health(provider_name, "success")
|
886
|
+
else
|
887
|
+
metrics[:failed_requests] += 1
|
888
|
+
metrics[:last_error] = error&.message || "Unknown error"
|
889
|
+
metrics[:last_error_time] = Time.now
|
890
|
+
update_provider_health(provider_name, "error", {error: error})
|
891
|
+
end
|
892
|
+
end
|
893
|
+
|
894
|
+
# Record model metrics
|
895
|
+
def record_model_metrics(provider_name, model_name, success:, duration:, tokens_used: nil, error: nil)
|
896
|
+
@model_metrics[provider_name] ||= {}
|
897
|
+
@model_metrics[provider_name][model_name] ||= {
|
898
|
+
total_requests: 0,
|
899
|
+
successful_requests: 0,
|
900
|
+
failed_requests: 0,
|
901
|
+
total_duration: 0.0,
|
902
|
+
total_tokens: 0,
|
903
|
+
last_used: nil,
|
904
|
+
last_error: nil,
|
905
|
+
last_error_time: nil
|
906
|
+
}
|
907
|
+
|
908
|
+
metrics = @model_metrics[provider_name][model_name]
|
909
|
+
metrics[:total_requests] += 1
|
910
|
+
metrics[:last_used] = Time.now
|
911
|
+
|
912
|
+
if success
|
913
|
+
metrics[:successful_requests] += 1
|
914
|
+
metrics[:total_duration] += duration
|
915
|
+
metrics[:total_tokens] += tokens_used if tokens_used
|
916
|
+
update_model_health(provider_name, model_name, "success")
|
917
|
+
else
|
918
|
+
metrics[:failed_requests] += 1
|
919
|
+
metrics[:last_error] = error&.message || "Unknown error"
|
920
|
+
metrics[:last_error_time] = Time.now
|
921
|
+
update_model_health(provider_name, model_name, "error", {error: error})
|
922
|
+
end
|
923
|
+
end
|
924
|
+
|
925
|
+
# Get model metrics
|
926
|
+
def get_model_metrics(provider_name, model_name)
|
927
|
+
@model_metrics[provider_name]&.dig(model_name) || {}
|
928
|
+
end
|
929
|
+
|
930
|
+
# Get all model metrics for provider
|
931
|
+
def get_all_model_metrics(provider_name)
|
932
|
+
@model_metrics[provider_name] || {}
|
933
|
+
end
|
934
|
+
|
935
|
+
# Get model history
|
936
|
+
def model_history
|
937
|
+
@model_history ||= []
|
938
|
+
@model_history.dup
|
939
|
+
end
|
940
|
+
|
941
|
+
# Get provider metrics
|
942
|
+
def get_metrics(provider_name)
|
943
|
+
@provider_metrics[provider_name] || {}
|
944
|
+
end
|
945
|
+
|
946
|
+
# Get all provider metrics
|
947
|
+
def all_metrics
|
948
|
+
@provider_metrics.dup
|
949
|
+
end
|
950
|
+
|
951
|
+
# Get provider history
|
952
|
+
def provider_history
|
953
|
+
@provider_history.dup
|
954
|
+
end
|
955
|
+
|
956
|
+
# Reset all provider state
|
957
|
+
def reset
|
958
|
+
@current_provider = nil
|
959
|
+
@current_model = nil
|
960
|
+
@provider_history.clear
|
961
|
+
@rate_limit_info.clear
|
962
|
+
@provider_metrics.clear
|
963
|
+
@provider_health.clear
|
964
|
+
@retry_counts.clear
|
965
|
+
@sticky_sessions.clear
|
966
|
+
@model_configs.clear
|
967
|
+
@model_health.clear
|
968
|
+
@model_metrics.clear
|
969
|
+
@model_fallback_chains.clear
|
970
|
+
@model_rate_limit_info&.clear
|
971
|
+
@model_history&.clear
|
972
|
+
initialize_fallback_chains
|
973
|
+
initialize_provider_health
|
974
|
+
initialize_model_configs
|
975
|
+
initialize_model_health
|
976
|
+
end
|
977
|
+
|
978
|
+
# Get status summary
|
979
|
+
def status
|
980
|
+
{
|
981
|
+
current_provider: current_provider,
|
982
|
+
current_model: current_model,
|
983
|
+
current_provider_model: current_provider_model,
|
984
|
+
available_providers: get_available_providers,
|
985
|
+
rate_limited_providers: @rate_limit_info.keys,
|
986
|
+
unhealthy_providers: @provider_health.select { |_, health| health[:status] != "healthy" }.keys,
|
987
|
+
circuit_breaker_open: @provider_health.select { |_, health| health[:circuit_breaker_open] }.keys,
|
988
|
+
next_reset_time: next_reset_time,
|
989
|
+
total_switches: @provider_history.size,
|
990
|
+
load_balancing_enabled: @load_balancing_enabled,
|
991
|
+
provider_weights: @provider_weights,
|
992
|
+
model_switching_enabled: @model_switching_enabled,
|
993
|
+
model_weights: @model_weights
|
994
|
+
}
|
995
|
+
end
|
996
|
+
|
997
|
+
# Get detailed provider health status
|
998
|
+
def get_provider_health_status
|
999
|
+
@provider_health.transform_values do |health|
|
1000
|
+
{
|
1001
|
+
status: health[:status],
|
1002
|
+
error_count: health[:error_count],
|
1003
|
+
success_count: health[:success_count],
|
1004
|
+
circuit_breaker_open: health[:circuit_breaker_open],
|
1005
|
+
last_updated: health[:last_updated],
|
1006
|
+
last_used: health[:last_used],
|
1007
|
+
last_rate_limited: health[:last_rate_limited]
|
1008
|
+
}
|
1009
|
+
end
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
# Get detailed model health status
|
1013
|
+
def get_model_health_status(provider_name)
|
1014
|
+
@model_health[provider_name]&.transform_values do |health|
|
1015
|
+
{
|
1016
|
+
status: health[:status],
|
1017
|
+
error_count: health[:error_count],
|
1018
|
+
success_count: health[:success_count],
|
1019
|
+
circuit_breaker_open: health[:circuit_breaker_open],
|
1020
|
+
last_updated: health[:last_updated],
|
1021
|
+
last_used: health[:last_used],
|
1022
|
+
last_rate_limited: health[:last_rate_limited]
|
1023
|
+
}
|
1024
|
+
end || {}
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
# Get all model health status
|
1028
|
+
def get_all_model_health_status
|
1029
|
+
@model_health.transform_values do |provider_models|
|
1030
|
+
provider_models.transform_values do |health|
|
1031
|
+
{
|
1032
|
+
status: health[:status],
|
1033
|
+
error_count: health[:error_count],
|
1034
|
+
success_count: health[:success_count],
|
1035
|
+
circuit_breaker_open: health[:circuit_breaker_open],
|
1036
|
+
last_updated: health[:last_updated],
|
1037
|
+
last_used: health[:last_used],
|
1038
|
+
last_rate_limited: health[:last_rate_limited]
|
1039
|
+
}
|
1040
|
+
end
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
# Configure provider weights for load balancing
|
1045
|
+
def configure_provider_weights(weights)
|
1046
|
+
@provider_weights = weights.dup
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
# Configure model weights for load balancing
|
1050
|
+
def configure_model_weights(provider_name, weights)
|
1051
|
+
@model_weights[provider_name] = weights.dup
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
# Enable/disable load balancing
|
1055
|
+
def set_load_balancing(enabled)
|
1056
|
+
@load_balancing_enabled = enabled
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
# Enable/disable model switching
|
1060
|
+
def set_model_switching(enabled)
|
1061
|
+
@model_switching_enabled = enabled
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
# Update sticky session
|
1065
|
+
def update_sticky_session(provider_name)
|
1066
|
+
@sticky_sessions[provider_name] = Time.now
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
# Get sticky session provider
|
1070
|
+
def get_sticky_session_provider(session_id)
|
1071
|
+
return nil unless session_id
|
1072
|
+
|
1073
|
+
# Find provider with recent session activity
|
1074
|
+
recent_sessions = @sticky_sessions.select do |_, time|
|
1075
|
+
Time.now - time < @session_timeout
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
recent_sessions.max_by { |_, time| time }&.first
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
# Execute a prompt with a specific provider
|
1082
|
+
def execute_with_provider(provider_type, prompt, options = {})
|
1083
|
+
# Create provider factory instance
|
1084
|
+
provider_factory = ProviderFactory.new
|
1085
|
+
|
1086
|
+
# Create provider instance
|
1087
|
+
provider = provider_factory.create_provider(provider_type, options)
|
1088
|
+
|
1089
|
+
# Set current provider
|
1090
|
+
@current_provider = provider_type
|
1091
|
+
|
1092
|
+
# Execute the prompt with the provider
|
1093
|
+
result = provider.send(prompt: prompt, session: nil)
|
1094
|
+
|
1095
|
+
# Return structured result
|
1096
|
+
{
|
1097
|
+
status: "completed",
|
1098
|
+
provider: provider_type,
|
1099
|
+
output: result,
|
1100
|
+
metadata: {
|
1101
|
+
provider_type: provider_type,
|
1102
|
+
prompt_length: prompt.length,
|
1103
|
+
timestamp: Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N%z")
|
1104
|
+
}
|
1105
|
+
}
|
1106
|
+
rescue => e
|
1107
|
+
# Return error result
|
1108
|
+
{
|
1109
|
+
status: "error",
|
1110
|
+
provider: provider_type,
|
1111
|
+
error: e.message,
|
1112
|
+
metadata: {
|
1113
|
+
provider_type: provider_type,
|
1114
|
+
error_class: e.class.name,
|
1115
|
+
timestamp: Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N%z")
|
1116
|
+
}
|
1117
|
+
}
|
1118
|
+
end
|
1119
|
+
|
1120
|
+
private
|
1121
|
+
|
1122
|
+
# Initialize fallback chains
|
1123
|
+
def initialize_fallback_chains
|
1124
|
+
@fallback_chains.clear
|
1125
|
+
all_providers = @configuration.configured_providers
|
1126
|
+
|
1127
|
+
all_providers.each do |provider|
|
1128
|
+
build_default_fallback_chain(provider)
|
1129
|
+
end
|
1130
|
+
end
|
1131
|
+
|
1132
|
+
# Initialize provider health
|
1133
|
+
def initialize_provider_health
|
1134
|
+
@provider_health.clear
|
1135
|
+
all_providers = @configuration.configured_providers
|
1136
|
+
|
1137
|
+
all_providers.each do |provider|
|
1138
|
+
@provider_health[provider] = {
|
1139
|
+
status: "healthy",
|
1140
|
+
last_updated: Time.now,
|
1141
|
+
error_count: 0,
|
1142
|
+
success_count: 0,
|
1143
|
+
circuit_breaker_open: false,
|
1144
|
+
circuit_breaker_opened_at: nil
|
1145
|
+
}
|
1146
|
+
end
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
# Initialize model configurations
|
1150
|
+
def initialize_model_configs
|
1151
|
+
@model_configs.clear
|
1152
|
+
all_providers = @configuration.configured_providers
|
1153
|
+
|
1154
|
+
all_providers.each do |provider|
|
1155
|
+
@model_configs[provider] = get_default_models_for_provider(provider)
|
1156
|
+
end
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
# Initialize model health
|
1160
|
+
def initialize_model_health
|
1161
|
+
@model_health.clear
|
1162
|
+
all_providers = @configuration.configured_providers
|
1163
|
+
|
1164
|
+
all_providers.each do |provider|
|
1165
|
+
@model_health[provider] = {}
|
1166
|
+
models = get_default_models_for_provider(provider)
|
1167
|
+
|
1168
|
+
models.each do |model|
|
1169
|
+
@model_health[provider][model] = {
|
1170
|
+
status: "healthy",
|
1171
|
+
last_updated: Time.now,
|
1172
|
+
error_count: 0,
|
1173
|
+
success_count: 0,
|
1174
|
+
circuit_breaker_open: false,
|
1175
|
+
circuit_breaker_opened_at: nil
|
1176
|
+
}
|
1177
|
+
end
|
1178
|
+
end
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
# Get default models for provider
|
1182
|
+
def get_default_models_for_provider(provider_name)
|
1183
|
+
case provider_name
|
1184
|
+
when "claude"
|
1185
|
+
["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"]
|
1186
|
+
when "gemini"
|
1187
|
+
["gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.0-pro"]
|
1188
|
+
when "cursor"
|
1189
|
+
["cursor-default", "cursor-fast", "cursor-precise"]
|
1190
|
+
else
|
1191
|
+
["default"]
|
1192
|
+
end
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
# Calculate retry delay with exponential backoff
|
1196
|
+
def calculate_retry_delay(retry_count)
|
1197
|
+
# Exponential backoff: 1s, 2s, 4s, 8s, etc.
|
1198
|
+
delay = 2**retry_count
|
1199
|
+
[delay, 60].min # Cap at 60 seconds
|
1200
|
+
end
|
1201
|
+
|
1202
|
+
# Log provider switch
|
1203
|
+
def log_provider_switch(from_provider, to_provider, reason, context)
|
1204
|
+
puts "🔄 Provider switch: #{from_provider} → #{to_provider} (#{reason})"
|
1205
|
+
if context.any?
|
1206
|
+
puts " Context: #{context.inspect}"
|
1207
|
+
end
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
# Log no providers available
|
1211
|
+
def log_no_providers_available(reason, context)
|
1212
|
+
puts "❌ No providers available for switching (#{reason})"
|
1213
|
+
puts " All providers are rate limited, unhealthy, or circuit breaker open"
|
1214
|
+
if context.any?
|
1215
|
+
puts " Context: #{context.inspect}"
|
1216
|
+
end
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
# Log circuit breaker event
|
1220
|
+
def log_circuit_breaker_event(provider_name, event)
|
1221
|
+
case event
|
1222
|
+
when "opened"
|
1223
|
+
puts "🔴 Circuit breaker opened for provider: #{provider_name}"
|
1224
|
+
when "reset"
|
1225
|
+
puts "🟢 Circuit breaker reset for provider: #{provider_name}"
|
1226
|
+
end
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
# Log model switch
|
1230
|
+
def log_model_switch(from_model, to_model, reason, context)
|
1231
|
+
puts "🔄 Model switch: #{from_model} → #{to_model} (#{reason})"
|
1232
|
+
if context.any?
|
1233
|
+
puts " Context: #{context.inspect}"
|
1234
|
+
end
|
1235
|
+
end
|
1236
|
+
|
1237
|
+
# Log no models available
|
1238
|
+
def log_no_models_available(provider_name, reason, context)
|
1239
|
+
puts "❌ No models available for provider #{provider_name} (#{reason})"
|
1240
|
+
puts " All models are rate limited, unhealthy, or circuit breaker open"
|
1241
|
+
if context.any?
|
1242
|
+
puts " Context: #{context.inspect}"
|
1243
|
+
end
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
# Log model circuit breaker event
|
1247
|
+
def log_model_circuit_breaker_event(provider_name, model_name, event)
|
1248
|
+
case event
|
1249
|
+
when "opened"
|
1250
|
+
puts "🔴 Circuit breaker opened for model: #{provider_name}:#{model_name}"
|
1251
|
+
when "reset"
|
1252
|
+
puts "🟢 Circuit breaker reset for model: #{provider_name}:#{model_name}"
|
1253
|
+
end
|
1254
|
+
end
|
1255
|
+
|
1256
|
+
def calculate_reset_time(_provider_name)
|
1257
|
+
# Default reset time calculation
|
1258
|
+
# Most providers reset rate limits every hour
|
1259
|
+
Time.now + (60 * 60)
|
1260
|
+
end
|
1261
|
+
|
1262
|
+
def calculate_model_reset_time(_provider_name, _model_name)
|
1263
|
+
# Default reset time calculation for models
|
1264
|
+
# Most models reset rate limits every hour
|
1265
|
+
Time.now + (60 * 60)
|
1266
|
+
end
|
1267
|
+
end
|
1268
|
+
end
|
1269
|
+
end
|