aidp 0.29.0 → 0.30.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/lib/aidp/harness/deprecation_cache.rb +177 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +93 -5
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -5
- data/lib/aidp/providers/anthropic.rb +183 -9
- data/lib/aidp/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcf4f30f93195b349c527dd5531e056ad566a34be1ed079ced4b5002f07a93f5
|
|
4
|
+
data.tar.gz: 22a7ef871ab691e0072a1bc8f1254784e1605f46d3edfc9585e91c3a21c5180f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5630eb8255464816006bb091b02666b29ac58b4a1e3e9136c17f79dff54762d6ebe16a4ec45df3f26c22047959121db04ef4e94904be5a4fb2e03320c6a6113
|
|
7
|
+
data.tar.gz: 5bdae7edfb7aad8ef03dd2274c4a8095bd927097cbd882a28856f0f753b46c4f1f0d8f756801c6df239bd13540355cbf9e53805b3bc815e5ac1cfc407d61c1c9
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Harness
|
|
8
|
+
# Manages a dynamic cache of deprecated models detected at runtime
|
|
9
|
+
# When deprecation errors are detected from provider APIs, models are
|
|
10
|
+
# added to this cache with metadata (replacement, detected date, etc.)
|
|
11
|
+
class DeprecationCache
|
|
12
|
+
class CacheError < StandardError; end
|
|
13
|
+
|
|
14
|
+
attr_reader :cache_path
|
|
15
|
+
|
|
16
|
+
def initialize(cache_path: nil, root_dir: nil)
|
|
17
|
+
@root_dir = root_dir || safe_root_dir
|
|
18
|
+
@cache_path = cache_path || default_cache_path
|
|
19
|
+
@cache_data = nil
|
|
20
|
+
ensure_cache_directory
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Add a deprecated model to the cache
|
|
24
|
+
# @param provider [String] Provider name (e.g., "anthropic")
|
|
25
|
+
# @param model_id [String] Deprecated model ID
|
|
26
|
+
# @param replacement [String, nil] Replacement model ID (if known)
|
|
27
|
+
# @param reason [String, nil] Deprecation reason/message
|
|
28
|
+
def add_deprecated_model(provider:, model_id:, replacement: nil, reason: nil)
|
|
29
|
+
load_cache unless @cache_data
|
|
30
|
+
|
|
31
|
+
@cache_data["providers"][provider] ||= {}
|
|
32
|
+
@cache_data["providers"][provider][model_id] = {
|
|
33
|
+
"deprecated_at" => Time.now.iso8601,
|
|
34
|
+
"replacement" => replacement,
|
|
35
|
+
"reason" => reason
|
|
36
|
+
}.compact
|
|
37
|
+
|
|
38
|
+
save_cache
|
|
39
|
+
Aidp.log_info("deprecation_cache", "Added deprecated model",
|
|
40
|
+
provider: provider, model: model_id, replacement: replacement)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if a model is deprecated
|
|
44
|
+
# @param provider [String] Provider name
|
|
45
|
+
# @param model_id [String] Model ID to check
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def deprecated?(provider:, model_id:)
|
|
48
|
+
load_cache unless @cache_data
|
|
49
|
+
@cache_data.dig("providers", provider, model_id) != nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get replacement model for a deprecated model
|
|
53
|
+
# @param provider [String] Provider name
|
|
54
|
+
# @param model_id [String] Deprecated model ID
|
|
55
|
+
# @return [String, nil] Replacement model ID or nil
|
|
56
|
+
def replacement_for(provider:, model_id:)
|
|
57
|
+
load_cache unless @cache_data
|
|
58
|
+
@cache_data.dig("providers", provider, model_id, "replacement")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get all deprecated models for a provider
|
|
62
|
+
# @param provider [String] Provider name
|
|
63
|
+
# @return [Array<String>] List of deprecated model IDs
|
|
64
|
+
def deprecated_models(provider:)
|
|
65
|
+
load_cache unless @cache_data
|
|
66
|
+
(@cache_data.dig("providers", provider) || {}).keys
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Remove a model from the deprecated cache
|
|
70
|
+
# Useful if a model comes back or was incorrectly marked
|
|
71
|
+
# @param provider [String] Provider name
|
|
72
|
+
# @param model_id [String] Model ID to remove
|
|
73
|
+
def remove_deprecated_model(provider:, model_id:)
|
|
74
|
+
load_cache unless @cache_data
|
|
75
|
+
return unless @cache_data.dig("providers", provider, model_id)
|
|
76
|
+
|
|
77
|
+
@cache_data["providers"][provider].delete(model_id)
|
|
78
|
+
@cache_data["providers"].delete(provider) if @cache_data["providers"][provider].empty?
|
|
79
|
+
|
|
80
|
+
save_cache
|
|
81
|
+
Aidp.log_info("deprecation_cache", "Removed deprecated model",
|
|
82
|
+
provider: provider, model: model_id)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get full deprecation info for a model
|
|
86
|
+
# @param provider [String] Provider name
|
|
87
|
+
# @param model_id [String] Model ID
|
|
88
|
+
# @return [Hash, nil] Deprecation metadata or nil
|
|
89
|
+
def info(provider:, model_id:)
|
|
90
|
+
load_cache unless @cache_data
|
|
91
|
+
@cache_data.dig("providers", provider, model_id)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Clear all cached deprecations
|
|
95
|
+
def clear!
|
|
96
|
+
@cache_data = default_cache_structure
|
|
97
|
+
save_cache
|
|
98
|
+
Aidp.log_info("deprecation_cache", "Cleared all deprecations")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get cache statistics
|
|
102
|
+
# @return [Hash] Statistics about cached deprecations
|
|
103
|
+
def stats
|
|
104
|
+
load_cache unless @cache_data
|
|
105
|
+
{
|
|
106
|
+
providers: @cache_data["providers"].keys.sort,
|
|
107
|
+
total_deprecated: @cache_data["providers"].sum { |_, models| models.size },
|
|
108
|
+
by_provider: @cache_data["providers"].transform_values(&:size)
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Get a safe root directory for the cache
|
|
115
|
+
# Uses Dir.pwd if writable, otherwise falls back to tmpdir
|
|
116
|
+
def safe_root_dir
|
|
117
|
+
pwd = Dir.pwd
|
|
118
|
+
aidp_dir = File.join(pwd, ".aidp")
|
|
119
|
+
|
|
120
|
+
# Try to create the directory to test writability
|
|
121
|
+
begin
|
|
122
|
+
FileUtils.mkdir_p(aidp_dir) unless File.exist?(aidp_dir)
|
|
123
|
+
pwd
|
|
124
|
+
rescue Errno::EACCES, Errno::EROFS, Errno::EPERM
|
|
125
|
+
# Permission denied or read-only filesystem - use temp directory
|
|
126
|
+
require "tmpdir"
|
|
127
|
+
Dir.tmpdir
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def default_cache_path
|
|
132
|
+
File.join(@root_dir, ".aidp", "deprecated_models.json")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def ensure_cache_directory
|
|
136
|
+
dir = File.dirname(@cache_path)
|
|
137
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def load_cache
|
|
141
|
+
if File.exist?(@cache_path)
|
|
142
|
+
@cache_data = JSON.parse(File.read(@cache_path))
|
|
143
|
+
validate_cache_structure
|
|
144
|
+
else
|
|
145
|
+
@cache_data = default_cache_structure
|
|
146
|
+
end
|
|
147
|
+
rescue JSON::ParserError => e
|
|
148
|
+
Aidp.log_warn("deprecation_cache", "Invalid cache file, resetting",
|
|
149
|
+
error: e.message, path: @cache_path)
|
|
150
|
+
@cache_data = default_cache_structure
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def save_cache
|
|
154
|
+
File.write(@cache_path, JSON.pretty_generate(@cache_data))
|
|
155
|
+
rescue => e
|
|
156
|
+
Aidp.log_error("deprecation_cache", "Failed to save cache",
|
|
157
|
+
error: e.message, path: @cache_path)
|
|
158
|
+
raise CacheError, "Failed to save deprecation cache: #{e.message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def default_cache_structure
|
|
162
|
+
{
|
|
163
|
+
"version" => "1.0",
|
|
164
|
+
"updated_at" => Time.now.iso8601,
|
|
165
|
+
"providers" => {}
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def validate_cache_structure
|
|
170
|
+
unless @cache_data.is_a?(Hash) && @cache_data["providers"].is_a?(Hash)
|
|
171
|
+
Aidp.log_warn("deprecation_cache", "Invalid cache structure, resetting")
|
|
172
|
+
@cache_data = default_cache_structure
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ruby_llm"
|
|
4
|
+
require_relative "deprecation_cache"
|
|
4
5
|
|
|
5
6
|
module Aidp
|
|
6
7
|
module Harness
|
|
@@ -26,6 +27,11 @@ module Aidp
|
|
|
26
27
|
"openrouter" => "openrouter"
|
|
27
28
|
}.freeze
|
|
28
29
|
|
|
30
|
+
# Get deprecation cache instance (lazy loaded)
|
|
31
|
+
def deprecation_cache
|
|
32
|
+
@deprecation_cache ||= Aidp::Harness::DeprecationCache.new
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
# Tier classification based on model characteristics
|
|
30
36
|
# These are heuristics since ruby_llm doesn't classify tiers
|
|
31
37
|
TIER_CLASSIFICATION = {
|
|
@@ -59,7 +65,8 @@ module Aidp
|
|
|
59
65
|
standard: ->(model) { true }
|
|
60
66
|
}.freeze
|
|
61
67
|
|
|
62
|
-
def initialize
|
|
68
|
+
def initialize(deprecation_cache: nil)
|
|
69
|
+
@deprecation_cache = deprecation_cache
|
|
63
70
|
@models = RubyLLM::Models.instance.instance_variable_get(:@models)
|
|
64
71
|
@index_by_id = @models.to_h { |m| [m.id, m] }
|
|
65
72
|
|
|
@@ -73,11 +80,18 @@ module Aidp
|
|
|
73
80
|
#
|
|
74
81
|
# @param model_name [String] Model name (e.g., "claude-3-5-haiku" or "claude-3-5-haiku-20241022")
|
|
75
82
|
# @param provider [String, nil] Optional AIDP provider filter
|
|
83
|
+
# @param skip_deprecated [Boolean] Skip deprecated models (default: true)
|
|
76
84
|
# @return [String, nil] Canonical model ID for API calls, or nil if not found
|
|
77
|
-
def resolve_model(model_name, provider: nil)
|
|
85
|
+
def resolve_model(model_name, provider: nil, skip_deprecated: true)
|
|
78
86
|
# Map AIDP provider to registry provider if filtering
|
|
79
87
|
registry_provider = provider ? PROVIDER_NAME_MAPPING[provider] : nil
|
|
80
88
|
|
|
89
|
+
# Check if model is deprecated
|
|
90
|
+
if skip_deprecated && model_deprecated?(model_name, registry_provider)
|
|
91
|
+
Aidp.log_warn("ruby_llm_registry", "skipping deprecated model", model: model_name, provider: provider)
|
|
92
|
+
return nil
|
|
93
|
+
end
|
|
94
|
+
|
|
81
95
|
# Try exact match first
|
|
82
96
|
model = @index_by_id[model_name]
|
|
83
97
|
return model.id if model && (registry_provider.nil? || model.provider.to_s == registry_provider)
|
|
@@ -88,13 +102,20 @@ module Aidp
|
|
|
88
102
|
# Filter by provider if specified
|
|
89
103
|
family_models = family_models.select { |m| m.provider.to_s == registry_provider } if registry_provider
|
|
90
104
|
|
|
105
|
+
# Filter out deprecated models if requested
|
|
106
|
+
if skip_deprecated
|
|
107
|
+
family_models = family_models.reject do |m|
|
|
108
|
+
deprecation_cache.deprecated?(provider: registry_provider, model_id: m.id.to_s)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
91
112
|
# Return the latest version (first non-"latest" model, or the latest one)
|
|
92
113
|
model = family_models.reject { |m| m.id.to_s.include?("-latest") }.first || family_models.first
|
|
93
114
|
return model.id if model
|
|
94
115
|
end
|
|
95
116
|
|
|
96
117
|
# Try fuzzy matching for common patterns
|
|
97
|
-
fuzzy_match = find_fuzzy_match(model_name, registry_provider)
|
|
118
|
+
fuzzy_match = find_fuzzy_match(model_name, registry_provider, skip_deprecated: skip_deprecated)
|
|
98
119
|
return fuzzy_match.id if fuzzy_match
|
|
99
120
|
|
|
100
121
|
Aidp.log_warn("ruby_llm_registry", "model not found", model: model_name, provider: provider)
|
|
@@ -124,8 +145,9 @@ module Aidp
|
|
|
124
145
|
#
|
|
125
146
|
# @param tier [String, Symbol] The tier name (mini, standard, advanced)
|
|
126
147
|
# @param provider [String, nil] Optional AIDP provider filter
|
|
148
|
+
# @param skip_deprecated [Boolean] Skip deprecated models (default: true)
|
|
127
149
|
# @return [Array<String>] List of model IDs for the tier
|
|
128
|
-
def models_for_tier(tier, provider: nil)
|
|
150
|
+
def models_for_tier(tier, provider: nil, skip_deprecated: true)
|
|
129
151
|
tier_sym = tier.to_sym
|
|
130
152
|
classifier = TIER_CLASSIFICATION[tier_sym]
|
|
131
153
|
|
|
@@ -152,6 +174,11 @@ module Aidp
|
|
|
152
174
|
end
|
|
153
175
|
end
|
|
154
176
|
|
|
177
|
+
# Filter out deprecated models if requested
|
|
178
|
+
if skip_deprecated
|
|
179
|
+
models.reject! { |m| deprecation_cache.deprecated?(provider: registry_provider, model_id: m.id.to_s) }
|
|
180
|
+
end
|
|
181
|
+
|
|
155
182
|
model_ids = models.map(&:id).uniq
|
|
156
183
|
Aidp.log_debug("ruby_llm_registry", "found models for tier",
|
|
157
184
|
tier: tier, provider: provider, count: model_ids.size)
|
|
@@ -191,6 +218,51 @@ module Aidp
|
|
|
191
218
|
Aidp.log_info("ruby_llm_registry", "refreshed", models: @models.size)
|
|
192
219
|
end
|
|
193
220
|
|
|
221
|
+
# Check if a model is deprecated
|
|
222
|
+
# @param model_id [String] The model ID to check
|
|
223
|
+
# @param provider [String, nil] The provider name (registry format)
|
|
224
|
+
# @return [Boolean] True if model is deprecated
|
|
225
|
+
def model_deprecated?(model_id, provider = nil)
|
|
226
|
+
return false unless provider
|
|
227
|
+
|
|
228
|
+
deprecation_cache.deprecated?(provider: provider, model_id: model_id.to_s)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Find replacement for a deprecated model
|
|
232
|
+
# Returns the latest non-deprecated model in the same family/tier
|
|
233
|
+
# @param deprecated_model [String] The deprecated model ID
|
|
234
|
+
# @param provider [String, nil] The provider name (AIDP format)
|
|
235
|
+
# @return [String, nil] Replacement model ID or nil
|
|
236
|
+
def find_replacement_model(deprecated_model, provider: nil)
|
|
237
|
+
registry_provider = provider ? PROVIDER_NAME_MAPPING[provider] : nil
|
|
238
|
+
return nil unless registry_provider
|
|
239
|
+
|
|
240
|
+
# Determine tier of deprecated model
|
|
241
|
+
deprecated_info = @index_by_id[deprecated_model]
|
|
242
|
+
return nil unless deprecated_info
|
|
243
|
+
|
|
244
|
+
tier = classify_tier(deprecated_info)
|
|
245
|
+
|
|
246
|
+
# Get all non-deprecated models for this tier and provider
|
|
247
|
+
candidates = models_for_tier(tier, provider: provider, skip_deprecated: true)
|
|
248
|
+
|
|
249
|
+
# Prefer models in the same family (e.g., both "sonnet")
|
|
250
|
+
family_keyword = extract_family_keyword(deprecated_model)
|
|
251
|
+
same_family = candidates.select { |m| m.to_s.include?(family_keyword) } if family_keyword
|
|
252
|
+
|
|
253
|
+
# Return first match from same family, or first candidate overall
|
|
254
|
+
replacement = same_family&.first || candidates.first
|
|
255
|
+
|
|
256
|
+
if replacement
|
|
257
|
+
Aidp.log_info("ruby_llm_registry", "found replacement",
|
|
258
|
+
deprecated: deprecated_model,
|
|
259
|
+
replacement: replacement,
|
|
260
|
+
tier: tier)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
replacement
|
|
264
|
+
end
|
|
265
|
+
|
|
194
266
|
private
|
|
195
267
|
|
|
196
268
|
# Build an index mapping family names to model objects
|
|
@@ -208,13 +280,18 @@ module Aidp
|
|
|
208
280
|
end
|
|
209
281
|
|
|
210
282
|
# Find a model by fuzzy matching
|
|
211
|
-
def find_fuzzy_match(model_name, provider)
|
|
283
|
+
def find_fuzzy_match(model_name, provider, skip_deprecated: true)
|
|
212
284
|
# Normalize the search term
|
|
213
285
|
normalized = model_name.downcase.gsub(/[^a-z0-9]/, "")
|
|
214
286
|
|
|
215
287
|
candidates = @models.select do |m|
|
|
216
288
|
next false if provider && m.provider.to_s != provider
|
|
217
289
|
|
|
290
|
+
# Skip deprecated if requested
|
|
291
|
+
if skip_deprecated
|
|
292
|
+
next false if deprecation_cache.deprecated?(provider: provider, model_id: m.id.to_s)
|
|
293
|
+
end
|
|
294
|
+
|
|
218
295
|
# Check if model ID contains the search term
|
|
219
296
|
m.id.to_s.downcase.gsub(/[^a-z0-9]/, "").include?(normalized) ||
|
|
220
297
|
m.name.to_s.downcase.gsub(/[^a-z0-9]/, "").include?(normalized)
|
|
@@ -224,6 +301,17 @@ module Aidp
|
|
|
224
301
|
candidates.min_by { |m| m.id.to_s.length }
|
|
225
302
|
end
|
|
226
303
|
|
|
304
|
+
# Extract family keyword from model ID (e.g., "sonnet", "haiku", "opus")
|
|
305
|
+
def extract_family_keyword(model_id)
|
|
306
|
+
case model_id.to_s
|
|
307
|
+
when /sonnet/i then "sonnet"
|
|
308
|
+
when /haiku/i then "haiku"
|
|
309
|
+
when /opus/i then "opus"
|
|
310
|
+
when /gpt-4/i then "gpt-4"
|
|
311
|
+
when /gpt-3/i then "gpt-3"
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
227
315
|
# Extract capabilities from model info
|
|
228
316
|
def extract_capabilities(model)
|
|
229
317
|
caps = []
|
|
@@ -160,11 +160,53 @@ module Aidp
|
|
|
160
160
|
if configured_models.any?
|
|
161
161
|
# Use first configured model for this provider and tier
|
|
162
162
|
model_name = configured_models.first
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
|
|
164
|
+
# Check if model is deprecated and try to upgrade
|
|
165
|
+
require_relative "ruby_llm_registry" unless defined?(Aidp::Harness::RubyLLMRegistry)
|
|
166
|
+
llm_registry = Aidp::Harness::RubyLLMRegistry.new
|
|
167
|
+
|
|
168
|
+
if llm_registry.model_deprecated?(model_name, provider)
|
|
169
|
+
Aidp.log_warn("thinking_depth_manager", "Configured model is deprecated",
|
|
170
|
+
tier: tier,
|
|
171
|
+
provider: provider,
|
|
172
|
+
model: model_name)
|
|
173
|
+
|
|
174
|
+
# Try to find replacement
|
|
175
|
+
replacement = llm_registry.find_replacement_model(model_name, provider: provider)
|
|
176
|
+
if replacement
|
|
177
|
+
Aidp.log_info("thinking_depth_manager", "Auto-upgrading to non-deprecated model",
|
|
178
|
+
tier: tier,
|
|
179
|
+
provider: provider,
|
|
180
|
+
old_model: model_name,
|
|
181
|
+
new_model: replacement)
|
|
182
|
+
model_name = replacement
|
|
183
|
+
else
|
|
184
|
+
# Try next model in config list
|
|
185
|
+
non_deprecated = configured_models.find { |m| !llm_registry.model_deprecated?(m, provider) }
|
|
186
|
+
if non_deprecated
|
|
187
|
+
Aidp.log_info("thinking_depth_manager", "Using alternate configured model",
|
|
188
|
+
tier: tier,
|
|
189
|
+
provider: provider,
|
|
190
|
+
skipped: model_name,
|
|
191
|
+
selected: non_deprecated)
|
|
192
|
+
model_name = non_deprecated
|
|
193
|
+
else
|
|
194
|
+
Aidp.log_warn("thinking_depth_manager", "All configured models deprecated, falling back to catalog",
|
|
195
|
+
tier: tier,
|
|
196
|
+
provider: provider)
|
|
197
|
+
# Fall through to catalog selection
|
|
198
|
+
model_name = nil
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if model_name
|
|
204
|
+
Aidp.log_debug("thinking_depth_manager", "Selected model from user config",
|
|
205
|
+
tier: tier,
|
|
206
|
+
provider: provider,
|
|
207
|
+
model: model_name)
|
|
208
|
+
return [provider, model_name, {}]
|
|
209
|
+
end
|
|
168
210
|
end
|
|
169
211
|
|
|
170
212
|
# Provider specified but has no models for this tier in config
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "base"
|
|
5
5
|
require_relative "../debug_mixin"
|
|
6
|
+
require_relative "../harness/deprecation_cache"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
module Providers
|
|
@@ -14,6 +15,11 @@ module Aidp
|
|
|
14
15
|
# Model name pattern for Anthropic Claude models
|
|
15
16
|
MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
|
|
16
17
|
|
|
18
|
+
# Get deprecation cache instance (lazy loaded)
|
|
19
|
+
def self.deprecation_cache
|
|
20
|
+
@deprecation_cache ||= Aidp::Harness::DeprecationCache.new
|
|
21
|
+
end
|
|
22
|
+
|
|
17
23
|
def self.available?
|
|
18
24
|
!!Aidp::Util.which("claude")
|
|
19
25
|
end
|
|
@@ -212,6 +218,56 @@ module Aidp
|
|
|
212
218
|
["--dangerously-skip-permissions"]
|
|
213
219
|
end
|
|
214
220
|
|
|
221
|
+
# Classify provider error using string matching
|
|
222
|
+
#
|
|
223
|
+
# ZFC EXCEPTION: Cannot use AI to classify provider errors because:
|
|
224
|
+
# 1. The failing provider IS the AI we'd use for classification (circular dependency)
|
|
225
|
+
# 2. Provider may be rate-limited, down, or misconfigured
|
|
226
|
+
# 3. Error classification must work even when AI unavailable
|
|
227
|
+
#
|
|
228
|
+
# This is a legitimate exception to ZFC principles per LLM_STYLE_GUIDE:
|
|
229
|
+
# "Structural safety checks" are allowed in code when AI cannot be used.
|
|
230
|
+
#
|
|
231
|
+
# @param error_message [String] The error message to classify
|
|
232
|
+
# @return [Hash] Classification result with :type, :is_deprecation, :is_rate_limit, :confidence
|
|
233
|
+
def self.classify_provider_error(error_message)
|
|
234
|
+
msg_lower = error_message.downcase
|
|
235
|
+
|
|
236
|
+
# Use simple string.include? checks (not regex) to avoid ReDoS vulnerabilities
|
|
237
|
+
is_rate_limit = msg_lower.include?("rate limit") || msg_lower.include?("session limit")
|
|
238
|
+
is_deprecation = msg_lower.include?("deprecat") || msg_lower.include?("end-of-life")
|
|
239
|
+
is_auth_error = msg_lower.include?("auth") && (msg_lower.include?("expired") || msg_lower.include?("invalid"))
|
|
240
|
+
|
|
241
|
+
# Determine primary type
|
|
242
|
+
type = if is_rate_limit
|
|
243
|
+
"rate_limit"
|
|
244
|
+
elsif is_deprecation
|
|
245
|
+
"deprecation"
|
|
246
|
+
elsif is_auth_error
|
|
247
|
+
"auth_error"
|
|
248
|
+
else
|
|
249
|
+
"other"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
Aidp.log_debug("anthropic", "Provider error classification",
|
|
253
|
+
type: type,
|
|
254
|
+
is_rate_limit: is_rate_limit,
|
|
255
|
+
is_deprecation: is_deprecation,
|
|
256
|
+
is_auth_error: is_auth_error)
|
|
257
|
+
|
|
258
|
+
{
|
|
259
|
+
type: type,
|
|
260
|
+
is_rate_limit: is_rate_limit,
|
|
261
|
+
is_deprecation: is_deprecation,
|
|
262
|
+
is_auth_error: is_auth_error,
|
|
263
|
+
confidence: 0.85, # Good confidence for clear error messages
|
|
264
|
+
reasoning: "Pattern-based classification (ZFC exception: circular dependency)"
|
|
265
|
+
}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Error patterns for classify_error method (legacy support)
|
|
269
|
+
# NOTE: ZFC-based classification preferred - see classify_error_with_zfc
|
|
270
|
+
# These patterns serve as fallback when ZFC is unavailable
|
|
215
271
|
def error_patterns
|
|
216
272
|
{
|
|
217
273
|
rate_limited: [
|
|
@@ -247,14 +303,72 @@ module Aidp
|
|
|
247
303
|
/not.*found/i,
|
|
248
304
|
/404/,
|
|
249
305
|
/bad.*request/i,
|
|
250
|
-
/400
|
|
306
|
+
/400/,
|
|
307
|
+
/model.*deprecated/i,
|
|
308
|
+
/end-of-life/i
|
|
251
309
|
]
|
|
252
310
|
}
|
|
253
311
|
end
|
|
254
312
|
|
|
313
|
+
# Check if a model is deprecated and return replacement
|
|
314
|
+
# @param model_name [String] The model name to check
|
|
315
|
+
# @return [String, nil] Replacement model name if deprecated, nil otherwise
|
|
316
|
+
def self.check_model_deprecation(model_name)
|
|
317
|
+
deprecation_cache.replacement_for(provider: "anthropic", model_id: model_name)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Find replacement model for deprecated one using RubyLLM registry
|
|
321
|
+
# @param deprecated_model [String] The deprecated model name
|
|
322
|
+
# @param provider_name [String] Provider name for registry lookup
|
|
323
|
+
# @return [String, nil] Latest model in the same family, or configured replacement
|
|
324
|
+
def self.find_replacement_model(deprecated_model, provider_name: "anthropic")
|
|
325
|
+
# First check the deprecation cache for explicit replacement
|
|
326
|
+
replacement = deprecation_cache.replacement_for(provider: provider_name, model_id: deprecated_model)
|
|
327
|
+
return replacement if replacement
|
|
328
|
+
|
|
329
|
+
# Try to find latest model in same family using registry
|
|
330
|
+
require_relative "../harness/ruby_llm_registry" unless defined?(Aidp::Harness::RubyLLMRegistry)
|
|
331
|
+
|
|
332
|
+
begin
|
|
333
|
+
registry = Aidp::Harness::RubyLLMRegistry.new
|
|
334
|
+
|
|
335
|
+
# Search for non-deprecated models in the same family
|
|
336
|
+
# Prefer models without "latest" suffix, sorted by ID (newer dates first)
|
|
337
|
+
models = registry.models_for_provider(provider_name)
|
|
338
|
+
candidates = models.select { |m| m.include?("sonnet") && !deprecation_cache.deprecated?(provider: provider_name, model_id: m) }
|
|
339
|
+
|
|
340
|
+
# Prioritize: versioned models over -latest, newer versions first
|
|
341
|
+
versioned = candidates.reject { |m| m.end_with?("-latest") }.sort.reverse
|
|
342
|
+
latest_models = candidates.select { |m| m.end_with?("-latest") }
|
|
343
|
+
|
|
344
|
+
replacement = versioned.first || latest_models.first
|
|
345
|
+
|
|
346
|
+
if replacement
|
|
347
|
+
Aidp.log_info("anthropic", "Found replacement model",
|
|
348
|
+
deprecated: deprecated_model,
|
|
349
|
+
replacement: replacement)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
replacement
|
|
353
|
+
rescue => e
|
|
354
|
+
Aidp.log_error("anthropic", "Failed to find replacement model",
|
|
355
|
+
deprecated: deprecated_model,
|
|
356
|
+
error: e.message)
|
|
357
|
+
nil
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
255
361
|
def send_message(prompt:, session: nil, options: {})
|
|
256
362
|
raise "claude CLI not available" unless self.class.available?
|
|
257
363
|
|
|
364
|
+
# Check if current model is deprecated and warn
|
|
365
|
+
if @model && (replacement = self.class.check_model_deprecation(@model))
|
|
366
|
+
Aidp.log_warn("anthropic", "Using deprecated model",
|
|
367
|
+
current: @model,
|
|
368
|
+
replacement: replacement)
|
|
369
|
+
debug_log("⚠️ Model #{@model} is deprecated. Consider upgrading to #{replacement}", level: :warn)
|
|
370
|
+
end
|
|
371
|
+
|
|
258
372
|
# Smart timeout calculation with tier awareness
|
|
259
373
|
timeout_seconds = calculate_timeout(options)
|
|
260
374
|
|
|
@@ -287,15 +401,72 @@ module Aidp
|
|
|
287
401
|
# Detect issues in stdout/stderr (Claude sometimes prints to stdout)
|
|
288
402
|
combined = [result.out, result.err].compact.join("\n")
|
|
289
403
|
|
|
290
|
-
#
|
|
291
|
-
|
|
404
|
+
# Classify provider error using pattern matching
|
|
405
|
+
# ZFC EXCEPTION: Cannot use AI to classify provider's own errors (circular dependency)
|
|
406
|
+
error_classification = self.class.classify_provider_error(combined)
|
|
407
|
+
|
|
408
|
+
Aidp.log_debug("anthropic_provider", "error_classified",
|
|
409
|
+
exit_code: result.exit_status,
|
|
410
|
+
type: error_classification[:type],
|
|
411
|
+
confidence: error_classification[:confidence]) # Check for rate limit
|
|
412
|
+
if error_classification[:is_rate_limit]
|
|
292
413
|
Aidp.log_debug("anthropic_provider", "rate_limit_detected",
|
|
293
414
|
exit_code: result.exit_status,
|
|
415
|
+
confidence: error_classification[:confidence],
|
|
294
416
|
message: combined)
|
|
295
417
|
notify_rate_limit(combined)
|
|
296
418
|
error_message = "Rate limit reached for Claude CLI.\n#{combined}"
|
|
297
|
-
|
|
298
|
-
|
|
419
|
+
error = RuntimeError.new(error_message)
|
|
420
|
+
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
421
|
+
raise error
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Check for model deprecation
|
|
425
|
+
if error_classification[:is_deprecation]
|
|
426
|
+
deprecated_model = @model
|
|
427
|
+
Aidp.log_error("anthropic", "Model deprecation detected",
|
|
428
|
+
model: deprecated_model,
|
|
429
|
+
message: combined)
|
|
430
|
+
|
|
431
|
+
# Try to find replacement
|
|
432
|
+
replacement = deprecated_model ? self.class.find_replacement_model(deprecated_model) : nil
|
|
433
|
+
|
|
434
|
+
# Record deprecation in cache for future runs
|
|
435
|
+
if replacement
|
|
436
|
+
self.class.deprecation_cache.add_deprecated_model(
|
|
437
|
+
provider: "anthropic",
|
|
438
|
+
model_id: deprecated_model,
|
|
439
|
+
replacement: replacement,
|
|
440
|
+
reason: combined.lines.first&.strip || "Model deprecated"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
Aidp.log_info("anthropic", "Auto-upgrading to non-deprecated model",
|
|
444
|
+
old_model: deprecated_model,
|
|
445
|
+
new_model: replacement)
|
|
446
|
+
debug_log("🔄 Upgrading from deprecated model #{deprecated_model} to #{replacement}", level: :info)
|
|
447
|
+
|
|
448
|
+
# Update model and retry
|
|
449
|
+
@model = replacement
|
|
450
|
+
|
|
451
|
+
# Retry with new model
|
|
452
|
+
debug_log("🔄 Retrying with upgraded model: #{replacement}", level: :info)
|
|
453
|
+
return send_message(prompt: prompt, session: session, options: options)
|
|
454
|
+
else
|
|
455
|
+
# Record deprecation even without replacement
|
|
456
|
+
if deprecated_model
|
|
457
|
+
self.class.deprecation_cache.add_deprecated_model(
|
|
458
|
+
provider: "anthropic",
|
|
459
|
+
model_id: deprecated_model,
|
|
460
|
+
replacement: nil,
|
|
461
|
+
reason: combined.lines.first&.strip || "Model deprecated"
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
error_message = "Model '#{deprecated_model}' is deprecated and no replacement found.\n#{combined}"
|
|
466
|
+
error = RuntimeError.new(error_message)
|
|
467
|
+
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
468
|
+
raise error
|
|
469
|
+
end
|
|
299
470
|
end
|
|
300
471
|
|
|
301
472
|
# Check for auth issues
|
|
@@ -303,12 +474,15 @@ module Aidp
|
|
|
303
474
|
error_message = "Authentication error from Claude CLI: token expired or invalid.\n" \
|
|
304
475
|
"Run 'claude /login' or refresh credentials.\n" \
|
|
305
476
|
"Note: Model discovery requires valid authentication."
|
|
306
|
-
|
|
307
|
-
|
|
477
|
+
error = RuntimeError.new(error_message)
|
|
478
|
+
debug_error(error, {exit_code: result.exit_status, stdout: result.out, stderr: result.err})
|
|
479
|
+
raise error
|
|
308
480
|
end
|
|
309
481
|
|
|
310
|
-
|
|
311
|
-
|
|
482
|
+
error_message = "claude failed with exit code #{result.exit_status}: #{result.err}"
|
|
483
|
+
error = RuntimeError.new(error_message)
|
|
484
|
+
debug_error(error, {exit_code: result.exit_status, stderr: result.err})
|
|
485
|
+
raise error
|
|
312
486
|
end
|
|
313
487
|
rescue => e
|
|
314
488
|
debug_error(e, {provider: "claude", prompt_length: prompt.length})
|
data/lib/aidp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aidp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -324,6 +324,7 @@ files:
|
|
|
324
324
|
- lib/aidp/harness/config_schema.rb
|
|
325
325
|
- lib/aidp/harness/config_validator.rb
|
|
326
326
|
- lib/aidp/harness/configuration.rb
|
|
327
|
+
- lib/aidp/harness/deprecation_cache.rb
|
|
327
328
|
- lib/aidp/harness/enhanced_runner.rb
|
|
328
329
|
- lib/aidp/harness/error_handler.rb
|
|
329
330
|
- lib/aidp/harness/filter_strategy.rb
|