aidp 0.28.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/cli/models_command.rb +75 -117
- data/lib/aidp/cli/tools_command.rb +333 -0
- data/lib/aidp/cli.rb +8 -1
- data/lib/aidp/config.rb +9 -0
- data/lib/aidp/execute/work_loop_runner.rb +6 -3
- data/lib/aidp/harness/capability_registry.rb +4 -4
- data/lib/aidp/harness/deprecation_cache.rb +177 -0
- data/lib/aidp/harness/provider_manager.rb +5 -3
- data/lib/aidp/harness/ruby_llm_registry.rb +327 -0
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -5
- data/lib/aidp/metadata/cache.rb +201 -0
- data/lib/aidp/metadata/compiler.rb +229 -0
- data/lib/aidp/metadata/parser.rb +204 -0
- data/lib/aidp/metadata/query.rb +237 -0
- data/lib/aidp/metadata/scanner.rb +191 -0
- data/lib/aidp/metadata/tool_metadata.rb +245 -0
- data/lib/aidp/metadata/validator.rb +187 -0
- data/lib/aidp/providers/aider.rb +1 -1
- data/lib/aidp/providers/anthropic.rb +189 -13
- data/lib/aidp/providers/base.rb +105 -35
- data/lib/aidp/providers/codex.rb +1 -1
- data/lib/aidp/providers/cursor.rb +1 -1
- data/lib/aidp/providers/gemini.rb +1 -1
- data/lib/aidp/providers/github_copilot.rb +1 -1
- data/lib/aidp/providers/kilocode.rb +1 -1
- data/lib/aidp/providers/opencode.rb +1 -1
- data/lib/aidp/setup/wizard.rb +35 -107
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +11 -0
- data/templates/implementation/implement_features.md +4 -1
- metadata +25 -2
- data/lib/aidp/harness/model_discovery_service.rb +0 -259
data/lib/aidp/cli.rb
CHANGED
|
@@ -256,7 +256,7 @@ module Aidp
|
|
|
256
256
|
# Determine if the invocation is a subcommand style call
|
|
257
257
|
def subcommand?(args)
|
|
258
258
|
return false if args.nil? || args.empty?
|
|
259
|
-
%w[status jobs kb harness providers checkpoint mcp issue config init watch ws work skill settings models].include?(args.first)
|
|
259
|
+
%w[status jobs kb harness providers checkpoint mcp issue config init watch ws work skill settings models tools].include?(args.first)
|
|
260
260
|
end
|
|
261
261
|
|
|
262
262
|
def run_subcommand(args)
|
|
@@ -279,6 +279,7 @@ module Aidp
|
|
|
279
279
|
when "skill" then run_skill_command(args)
|
|
280
280
|
when "settings" then run_settings_command(args)
|
|
281
281
|
when "models" then run_models_command(args)
|
|
282
|
+
when "tools" then run_tools_command(args)
|
|
282
283
|
else
|
|
283
284
|
display_message("Unknown command: #{cmd}", type: :info)
|
|
284
285
|
return 1
|
|
@@ -625,6 +626,12 @@ module Aidp
|
|
|
625
626
|
models_cmd.run(args)
|
|
626
627
|
end
|
|
627
628
|
|
|
629
|
+
def run_tools_command(args)
|
|
630
|
+
require_relative "cli/tools_command"
|
|
631
|
+
tools_cmd = Aidp::CLI::ToolsCommand.new(project_dir: Dir.pwd, prompt: create_prompt)
|
|
632
|
+
tools_cmd.run(args)
|
|
633
|
+
end
|
|
634
|
+
|
|
628
635
|
def run_issue_command(args)
|
|
629
636
|
require_relative "cli/issue_importer"
|
|
630
637
|
|
data/lib/aidp/config.rb
CHANGED
|
@@ -285,6 +285,15 @@ module Aidp
|
|
|
285
285
|
symbolize_keys(agile_section)
|
|
286
286
|
end
|
|
287
287
|
|
|
288
|
+
# Get tool metadata configuration
|
|
289
|
+
def self.tool_metadata_config(project_dir = Dir.pwd)
|
|
290
|
+
config = load_harness_config(project_dir)
|
|
291
|
+
tool_metadata_section = config[:tool_metadata] || config["tool_metadata"] || {}
|
|
292
|
+
|
|
293
|
+
# Convert string keys to symbols for consistency
|
|
294
|
+
symbolize_keys(tool_metadata_section)
|
|
295
|
+
end
|
|
296
|
+
|
|
288
297
|
# Check if configuration file exists
|
|
289
298
|
def self.config_exists?(project_dir = Dir.pwd)
|
|
290
299
|
ConfigPaths.config_exists?(project_dir)
|
|
@@ -308,7 +308,8 @@ module Aidp
|
|
|
308
308
|
iteration: @iteration_count,
|
|
309
309
|
project_dir: @project_dir,
|
|
310
310
|
mode: :decide_whats_next,
|
|
311
|
-
model: model_name
|
|
311
|
+
model: model_name,
|
|
312
|
+
tier: @thinking_depth_manager.current_tier
|
|
312
313
|
}
|
|
313
314
|
)
|
|
314
315
|
|
|
@@ -340,7 +341,8 @@ module Aidp
|
|
|
340
341
|
iteration: @iteration_count,
|
|
341
342
|
project_dir: @project_dir,
|
|
342
343
|
mode: :diagnose_failures,
|
|
343
|
-
model: model_name
|
|
344
|
+
model: model_name,
|
|
345
|
+
tier: @thinking_depth_manager.current_tier
|
|
344
346
|
}
|
|
345
347
|
)
|
|
346
348
|
|
|
@@ -761,7 +763,8 @@ module Aidp
|
|
|
761
763
|
step_name: @step_name,
|
|
762
764
|
iteration: @iteration_count,
|
|
763
765
|
project_dir: @project_dir,
|
|
764
|
-
model: model_name
|
|
766
|
+
model: model_name,
|
|
767
|
+
tier: @thinking_depth_manager.current_tier
|
|
765
768
|
}
|
|
766
769
|
)
|
|
767
770
|
end
|
|
@@ -87,7 +87,7 @@ module Aidp
|
|
|
87
87
|
providers_to_search.each do |provider_name|
|
|
88
88
|
matching_models = []
|
|
89
89
|
models_for_provider(provider_name).each do |model_name, model_data|
|
|
90
|
-
matching_models << model_name if model_data["tier"] == tier
|
|
90
|
+
matching_models << model_name if model_data["tier"] == tier.to_s
|
|
91
91
|
end
|
|
92
92
|
results[provider_name] = matching_models unless matching_models.empty?
|
|
93
93
|
end
|
|
@@ -114,12 +114,12 @@ module Aidp
|
|
|
114
114
|
|
|
115
115
|
# Check if a tier is valid
|
|
116
116
|
def valid_tier?(tier)
|
|
117
|
-
VALID_TIERS.include?(tier)
|
|
117
|
+
VALID_TIERS.include?(tier.to_s)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Get tier priority (0 = lowest, 4 = highest)
|
|
121
121
|
def tier_priority(tier)
|
|
122
|
-
TIER_PRIORITY[tier]
|
|
122
|
+
TIER_PRIORITY[tier.to_s]
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
# Compare two tiers (returns -1, 0, 1 like <=>)
|
|
@@ -154,7 +154,7 @@ module Aidp
|
|
|
154
154
|
models = models_for_provider(provider_name)
|
|
155
155
|
|
|
156
156
|
# Find all models matching tier
|
|
157
|
-
tier_models = models.select { |_name, data| data["tier"] == tier }
|
|
157
|
+
tier_models = models.select { |_name, data| data["tier"] == tier.to_s }
|
|
158
158
|
return nil if tier_models.empty?
|
|
159
159
|
|
|
160
160
|
# Prefer newer models (higher in the list)
|
|
@@ -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
|
|
@@ -1394,8 +1394,9 @@ module Aidp
|
|
|
1394
1394
|
|
|
1395
1395
|
# Execute a prompt with a specific provider
|
|
1396
1396
|
def execute_with_provider(provider_type, prompt, options = {})
|
|
1397
|
-
# Extract model from options if provided
|
|
1397
|
+
# Extract model and tier from options if provided
|
|
1398
1398
|
model_name = options.delete(:model)
|
|
1399
|
+
tier = options[:tier] # Keep tier in options for provider
|
|
1399
1400
|
|
|
1400
1401
|
# Create provider factory instance
|
|
1401
1402
|
provider_factory = ProviderFactory.new
|
|
@@ -1414,10 +1415,11 @@ module Aidp
|
|
|
1414
1415
|
Aidp.logger.debug("provider_manager", "Executing with provider",
|
|
1415
1416
|
provider: provider_type,
|
|
1416
1417
|
model: model_name,
|
|
1418
|
+
tier: tier,
|
|
1417
1419
|
prompt_length: prompt.length)
|
|
1418
1420
|
|
|
1419
|
-
# Execute the prompt with the provider
|
|
1420
|
-
result = provider.send_message(prompt: prompt, session: nil)
|
|
1421
|
+
# Execute the prompt with the provider (pass options including tier)
|
|
1422
|
+
result = provider.send_message(prompt: prompt, session: nil, options: options)
|
|
1421
1423
|
|
|
1422
1424
|
# Return structured result
|
|
1423
1425
|
{
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require_relative "deprecation_cache"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Harness
|
|
8
|
+
# RubyLLMRegistry wraps the ruby_llm gem's model registry
|
|
9
|
+
# to provide AIDP-specific functionality while leveraging
|
|
10
|
+
# ruby_llm's comprehensive and actively maintained model database
|
|
11
|
+
class RubyLLMRegistry
|
|
12
|
+
class RegistryError < StandardError; end
|
|
13
|
+
class ModelNotFound < RegistryError; end
|
|
14
|
+
|
|
15
|
+
# Map AIDP provider names to RubyLLM provider names
|
|
16
|
+
# Some AIDP providers use different names than the upstream APIs
|
|
17
|
+
PROVIDER_NAME_MAPPING = {
|
|
18
|
+
"codex" => "openai", # Codex is AIDP's OpenAI adapter
|
|
19
|
+
"anthropic" => "anthropic",
|
|
20
|
+
"gemini" => "gemini", # Gemini provider name matches
|
|
21
|
+
"aider" => nil, # Aider aggregates multiple providers
|
|
22
|
+
"cursor" => nil, # Cursor has its own models
|
|
23
|
+
"openai" => "openai",
|
|
24
|
+
"google" => "gemini", # Google's API uses gemini provider name
|
|
25
|
+
"azure" => "bedrock", # Azure OpenAI uses bedrock in registry
|
|
26
|
+
"bedrock" => "bedrock",
|
|
27
|
+
"openrouter" => "openrouter"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Get deprecation cache instance (lazy loaded)
|
|
31
|
+
def deprecation_cache
|
|
32
|
+
@deprecation_cache ||= Aidp::Harness::DeprecationCache.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Tier classification based on model characteristics
|
|
36
|
+
# These are heuristics since ruby_llm doesn't classify tiers
|
|
37
|
+
TIER_CLASSIFICATION = {
|
|
38
|
+
# Mini tier: fast, cost-effective models
|
|
39
|
+
mini: ->(model) {
|
|
40
|
+
return true if model.id.to_s.match?(/haiku|mini|flash|small/i)
|
|
41
|
+
|
|
42
|
+
# Check pricing if available
|
|
43
|
+
if model.pricing
|
|
44
|
+
pricing_hash = model.pricing.to_h
|
|
45
|
+
input_cost = pricing_hash.dig(:text_tokens, :standard, :input_per_million)
|
|
46
|
+
return true if input_cost && input_cost < 1.0
|
|
47
|
+
end
|
|
48
|
+
false
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
# Advanced tier: high-capability, expensive models
|
|
52
|
+
advanced: ->(model) {
|
|
53
|
+
return true if model.id.to_s.match?(/opus|turbo|pro|preview|o1/i)
|
|
54
|
+
|
|
55
|
+
# Check pricing if available
|
|
56
|
+
if model.pricing
|
|
57
|
+
pricing_hash = model.pricing.to_h
|
|
58
|
+
input_cost = pricing_hash.dig(:text_tokens, :standard, :input_per_million)
|
|
59
|
+
return true if input_cost && input_cost > 10.0
|
|
60
|
+
end
|
|
61
|
+
false
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
# Standard tier: everything else (default)
|
|
65
|
+
standard: ->(model) { true }
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
def initialize(deprecation_cache: nil)
|
|
69
|
+
@deprecation_cache = deprecation_cache
|
|
70
|
+
@models = RubyLLM::Models.instance.instance_variable_get(:@models)
|
|
71
|
+
@index_by_id = @models.to_h { |m| [m.id, m] }
|
|
72
|
+
|
|
73
|
+
# Build family index for mapping versioned names to families
|
|
74
|
+
@family_index = build_family_index
|
|
75
|
+
|
|
76
|
+
Aidp.log_info("ruby_llm_registry", "initialized", models: @models.size)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Resolve a model name (family or versioned) to the canonical API model
|
|
80
|
+
#
|
|
81
|
+
# @param model_name [String] Model name (e.g., "claude-3-5-haiku" or "claude-3-5-haiku-20241022")
|
|
82
|
+
# @param provider [String, nil] Optional AIDP provider filter
|
|
83
|
+
# @param skip_deprecated [Boolean] Skip deprecated models (default: true)
|
|
84
|
+
# @return [String, nil] Canonical model ID for API calls, or nil if not found
|
|
85
|
+
def resolve_model(model_name, provider: nil, skip_deprecated: true)
|
|
86
|
+
# Map AIDP provider to registry provider if filtering
|
|
87
|
+
registry_provider = provider ? PROVIDER_NAME_MAPPING[provider] : nil
|
|
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
|
+
|
|
95
|
+
# Try exact match first
|
|
96
|
+
model = @index_by_id[model_name]
|
|
97
|
+
return model.id if model && (registry_provider.nil? || model.provider.to_s == registry_provider)
|
|
98
|
+
|
|
99
|
+
# Try family mapping
|
|
100
|
+
family_models = @family_index[model_name]
|
|
101
|
+
if family_models
|
|
102
|
+
# Filter by provider if specified
|
|
103
|
+
family_models = family_models.select { |m| m.provider.to_s == registry_provider } if registry_provider
|
|
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
|
+
|
|
112
|
+
# Return the latest version (first non-"latest" model, or the latest one)
|
|
113
|
+
model = family_models.reject { |m| m.id.to_s.include?("-latest") }.first || family_models.first
|
|
114
|
+
return model.id if model
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Try fuzzy matching for common patterns
|
|
118
|
+
fuzzy_match = find_fuzzy_match(model_name, registry_provider, skip_deprecated: skip_deprecated)
|
|
119
|
+
return fuzzy_match.id if fuzzy_match
|
|
120
|
+
|
|
121
|
+
Aidp.log_warn("ruby_llm_registry", "model not found", model: model_name, provider: provider)
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get model information
|
|
126
|
+
#
|
|
127
|
+
# @param model_id [String] The model ID
|
|
128
|
+
# @return [Hash, nil] Model information hash or nil if not found
|
|
129
|
+
def get_model_info(model_id)
|
|
130
|
+
model = @index_by_id[model_id]
|
|
131
|
+
return nil unless model
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
id: model.id,
|
|
135
|
+
name: model.name || model.display_name,
|
|
136
|
+
provider: model.provider.to_s,
|
|
137
|
+
tier: classify_tier(model),
|
|
138
|
+
context_window: model.context_window,
|
|
139
|
+
capabilities: extract_capabilities(model),
|
|
140
|
+
pricing: model.pricing
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get all models for a specific tier
|
|
145
|
+
#
|
|
146
|
+
# @param tier [String, Symbol] The tier name (mini, standard, advanced)
|
|
147
|
+
# @param provider [String, nil] Optional AIDP provider filter
|
|
148
|
+
# @param skip_deprecated [Boolean] Skip deprecated models (default: true)
|
|
149
|
+
# @return [Array<String>] List of model IDs for the tier
|
|
150
|
+
def models_for_tier(tier, provider: nil, skip_deprecated: true)
|
|
151
|
+
tier_sym = tier.to_sym
|
|
152
|
+
classifier = TIER_CLASSIFICATION[tier_sym]
|
|
153
|
+
|
|
154
|
+
unless classifier
|
|
155
|
+
Aidp.log_warn("ruby_llm_registry", "invalid tier", tier: tier)
|
|
156
|
+
return []
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Map AIDP provider to registry provider if filtering
|
|
160
|
+
registry_provider = provider ? PROVIDER_NAME_MAPPING[provider] : nil
|
|
161
|
+
return [] if provider && registry_provider.nil?
|
|
162
|
+
|
|
163
|
+
models = @models.select do |model|
|
|
164
|
+
(registry_provider.nil? || model.provider.to_s == registry_provider) &&
|
|
165
|
+
classifier.call(model)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# For mini and standard tiers, exclude if advanced classification matches
|
|
169
|
+
if tier_sym == :mini
|
|
170
|
+
models.reject! { |m| TIER_CLASSIFICATION[:advanced].call(m) }
|
|
171
|
+
elsif tier_sym == :standard
|
|
172
|
+
models.reject! do |m|
|
|
173
|
+
TIER_CLASSIFICATION[:mini].call(m) || TIER_CLASSIFICATION[:advanced].call(m)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
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
|
+
|
|
182
|
+
model_ids = models.map(&:id).uniq
|
|
183
|
+
Aidp.log_debug("ruby_llm_registry", "found models for tier",
|
|
184
|
+
tier: tier, provider: provider, count: model_ids.size)
|
|
185
|
+
model_ids
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Get all models for a provider
|
|
189
|
+
#
|
|
190
|
+
# @param provider [String] The AIDP provider name
|
|
191
|
+
# @return [Array<String>] List of model IDs
|
|
192
|
+
def models_for_provider(provider)
|
|
193
|
+
# Map AIDP provider name to RubyLLM provider name
|
|
194
|
+
registry_provider = PROVIDER_NAME_MAPPING[provider]
|
|
195
|
+
|
|
196
|
+
# Return empty if provider doesn't map to a registry provider
|
|
197
|
+
return [] if registry_provider.nil?
|
|
198
|
+
|
|
199
|
+
@models.select { |m| m.provider.to_s == registry_provider }.map(&:id)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Classify a model's tier
|
|
203
|
+
#
|
|
204
|
+
# @param model [RubyLLM::Model::Info] The model info object
|
|
205
|
+
# @return [String] The tier name (mini, standard, advanced)
|
|
206
|
+
def classify_tier(model)
|
|
207
|
+
return "advanced" if TIER_CLASSIFICATION[:advanced].call(model)
|
|
208
|
+
return "mini" if TIER_CLASSIFICATION[:mini].call(model)
|
|
209
|
+
"standard"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Refresh the model registry from ruby_llm
|
|
213
|
+
def refresh!
|
|
214
|
+
RubyLLM::Models.refresh!
|
|
215
|
+
@models = RubyLLM::Models.instance.instance_variable_get(:@models)
|
|
216
|
+
@index_by_id = @models.to_h { |m| [m.id, m] }
|
|
217
|
+
@family_index = build_family_index
|
|
218
|
+
Aidp.log_info("ruby_llm_registry", "refreshed", models: @models.size)
|
|
219
|
+
end
|
|
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
|
+
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
# Build an index mapping family names to model objects
|
|
269
|
+
# Family name is model ID with version suffix removed
|
|
270
|
+
def build_family_index
|
|
271
|
+
index = Hash.new { |h, k| h[k] = [] }
|
|
272
|
+
|
|
273
|
+
@models.each do |model|
|
|
274
|
+
# Remove date suffix (e.g., "claude-3-5-haiku-20241022" -> "claude-3-5-haiku")
|
|
275
|
+
family = model.id.to_s.sub(/-\d{8}$/, "").sub(/-latest$/, "")
|
|
276
|
+
index[family] << model unless family == model.id.to_s
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
index
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Find a model by fuzzy matching
|
|
283
|
+
def find_fuzzy_match(model_name, provider, skip_deprecated: true)
|
|
284
|
+
# Normalize the search term
|
|
285
|
+
normalized = model_name.downcase.gsub(/[^a-z0-9]/, "")
|
|
286
|
+
|
|
287
|
+
candidates = @models.select do |m|
|
|
288
|
+
next false if provider && m.provider.to_s != provider
|
|
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
|
+
|
|
295
|
+
# Check if model ID contains the search term
|
|
296
|
+
m.id.to_s.downcase.gsub(/[^a-z0-9]/, "").include?(normalized) ||
|
|
297
|
+
m.name.to_s.downcase.gsub(/[^a-z0-9]/, "").include?(normalized)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Prefer shorter matches (more specific)
|
|
301
|
+
candidates.min_by { |m| m.id.to_s.length }
|
|
302
|
+
end
|
|
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
|
+
|
|
315
|
+
# Extract capabilities from model info
|
|
316
|
+
def extract_capabilities(model)
|
|
317
|
+
caps = []
|
|
318
|
+
caps << "chat" if model.capabilities.include?(:chat)
|
|
319
|
+
caps << "code" if model.capabilities.include?(:code) || model.id.to_s.include?("code")
|
|
320
|
+
caps << "vision" if model.capabilities.include?(:vision)
|
|
321
|
+
caps << "tool_use" if model.capabilities.include?(:function_calling) || model.capabilities.include?(:tools)
|
|
322
|
+
caps << "streaming" if model.capabilities.include?(:streaming)
|
|
323
|
+
caps
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|