aidp 0.7.0 → 0.8.1

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -214
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +38 -23
  5. data/lib/aidp/analysis/seams.rb +2 -31
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +1 -13
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +3 -20
  8. data/lib/aidp/analyze/error_handler.rb +2 -75
  9. data/lib/aidp/analyze/json_file_storage.rb +292 -0
  10. data/lib/aidp/analyze/progress.rb +12 -0
  11. data/lib/aidp/analyze/progress_visualizer.rb +12 -17
  12. data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
  13. data/lib/aidp/analyze/runner.rb +256 -87
  14. data/lib/aidp/cli/jobs_command.rb +100 -432
  15. data/lib/aidp/cli.rb +309 -239
  16. data/lib/aidp/config.rb +298 -10
  17. data/lib/aidp/debug_logger.rb +195 -0
  18. data/lib/aidp/debug_mixin.rb +187 -0
  19. data/lib/aidp/execute/progress.rb +9 -0
  20. data/lib/aidp/execute/runner.rb +221 -40
  21. data/lib/aidp/execute/steps.rb +17 -7
  22. data/lib/aidp/execute/workflow_selector.rb +211 -0
  23. data/lib/aidp/harness/completion_checker.rb +268 -0
  24. data/lib/aidp/harness/condition_detector.rb +1526 -0
  25. data/lib/aidp/harness/config_loader.rb +373 -0
  26. data/lib/aidp/harness/config_manager.rb +382 -0
  27. data/lib/aidp/harness/config_schema.rb +1006 -0
  28. data/lib/aidp/harness/config_validator.rb +355 -0
  29. data/lib/aidp/harness/configuration.rb +477 -0
  30. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  31. data/lib/aidp/harness/error_handler.rb +616 -0
  32. data/lib/aidp/harness/provider_config.rb +423 -0
  33. data/lib/aidp/harness/provider_factory.rb +306 -0
  34. data/lib/aidp/harness/provider_manager.rb +1269 -0
  35. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  36. data/lib/aidp/harness/runner.rb +411 -0
  37. data/lib/aidp/harness/state/errors.rb +28 -0
  38. data/lib/aidp/harness/state/metrics.rb +219 -0
  39. data/lib/aidp/harness/state/persistence.rb +128 -0
  40. data/lib/aidp/harness/state/provider_state.rb +132 -0
  41. data/lib/aidp/harness/state/ui_state.rb +68 -0
  42. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  43. data/lib/aidp/harness/state_manager.rb +586 -0
  44. data/lib/aidp/harness/status_display.rb +888 -0
  45. data/lib/aidp/harness/ui/base.rb +16 -0
  46. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  47. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  48. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  49. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  50. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  51. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  52. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  53. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  54. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  55. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  56. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  57. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  58. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  59. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  60. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  61. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  62. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  63. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  64. data/lib/aidp/harness/user_interface.rb +2381 -0
  65. data/lib/aidp/provider_manager.rb +131 -7
  66. data/lib/aidp/providers/anthropic.rb +28 -103
  67. data/lib/aidp/providers/base.rb +170 -0
  68. data/lib/aidp/providers/cursor.rb +52 -181
  69. data/lib/aidp/providers/gemini.rb +24 -107
  70. data/lib/aidp/providers/macos_ui.rb +99 -5
  71. data/lib/aidp/providers/opencode.rb +194 -0
  72. data/lib/aidp/storage/csv_storage.rb +172 -0
  73. data/lib/aidp/storage/file_manager.rb +214 -0
  74. data/lib/aidp/storage/json_storage.rb +140 -0
  75. data/lib/aidp/version.rb +1 -1
  76. data/lib/aidp.rb +54 -39
  77. data/templates/COMMON/AGENT_BASE.md +11 -0
  78. data/templates/EXECUTE/00_PRD.md +4 -4
  79. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  80. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  81. data/templates/EXECUTE/08_TASKS.md +4 -4
  82. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  83. data/templates/README.md +279 -0
  84. data/templates/aidp-development.yml.example +373 -0
  85. data/templates/aidp-minimal.yml.example +48 -0
  86. data/templates/aidp-production.yml.example +475 -0
  87. data/templates/aidp.yml.example +598 -0
  88. metadata +93 -69
  89. data/lib/aidp/analyze/agent_personas.rb +0 -71
  90. data/lib/aidp/analyze/agent_tool_executor.rb +0 -439
  91. data/lib/aidp/analyze/data_retention_manager.rb +0 -421
  92. data/lib/aidp/analyze/database.rb +0 -260
  93. data/lib/aidp/analyze/dependencies.rb +0 -335
  94. data/lib/aidp/analyze/export_manager.rb +0 -418
  95. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  96. data/lib/aidp/analyze/incremental_analyzer.rb +0 -533
  97. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  98. data/lib/aidp/analyze/large_analysis_progress.rb +0 -499
  99. data/lib/aidp/analyze/memory_manager.rb +0 -339
  100. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  101. data/lib/aidp/analyze/parallel_processor.rb +0 -454
  102. data/lib/aidp/analyze/performance_optimizer.rb +0 -691
  103. data/lib/aidp/analyze/repository_chunker.rb +0 -697
  104. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  105. data/lib/aidp/analyze/storage.rb +0 -655
  106. data/lib/aidp/analyze/tool_configuration.rb +0 -441
  107. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  108. data/lib/aidp/database/pg_adapter.rb +0 -148
  109. data/lib/aidp/database_config.rb +0 -69
  110. data/lib/aidp/database_connection.rb +0 -72
  111. data/lib/aidp/job_manager.rb +0 -41
  112. data/lib/aidp/jobs/base_job.rb +0 -45
  113. data/lib/aidp/jobs/provider_execution_job.rb +0 -83
  114. data/lib/aidp/project_detector.rb +0 -117
  115. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  116. data/lib/aidp/providers/supervised_base.rb +0 -317
  117. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  118. data/lib/aidp/sync.rb +0 -13
  119. 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