Class: OpenRouter::ModelRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/open_router/model_registry.rb

Constant Summary collapse

API_BASE =
"https://openrouter.ai/api/v1"
CACHE_DIR =
File.join(Dir.tmpdir, "openrouter_cache")
CACHE_DATA_FILE =
File.join(CACHE_DIR, "models_data.json")
CACHE_METADATA_FILE =
File.join(CACHE_DIR, "cache_metadata.json")
MAX_CACHE_SIZE_MB =

Maximum cache size in megabytes

50
REGISTRY_MUTEX =
Mutex.new

Class Method Summary collapse

Class Method Details

.all_modelsObject

Get all registered models (fetch from API if needed)



252
253
254
255
256
257
258
# File 'lib/open_router/model_registry.rb', line 252

def all_models
  return @all_models if @all_models # fast path without lock

  REGISTRY_MUTEX.synchronize do
    @all_models ||= fetch_and_cache_models
  end
end

.cache_stale?Boolean

Check if cache is stale based on TTL

Returns:

  • (Boolean)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/open_router/model_registry.rb', line 44

def cache_stale?
  return true unless File.exist?(CACHE_METADATA_FILE)

  begin
     = JSON.parse(File.read(CACHE_METADATA_FILE))
    cache_time = ["cached_at"]
    ttl = OpenRouter.configuration.cache_ttl

    return true unless cache_time

    Time.now.to_i - cache_time.to_i > ttl
  rescue JSON::ParserError, StandardError
    true # If we can't read metadata, consider cache stale
  end
end

.calculate_estimated_cost(model, input_tokens: 0, output_tokens: 0) ⇒ Object

Calculate estimated cost for a request



261
262
263
264
265
266
267
268
269
# File 'lib/open_router/model_registry.rb', line 261

def calculate_estimated_cost(model, input_tokens: 0, output_tokens: 0)
  model_info = get_model_info(model)
  return 0 unless model_info

  input_cost = input_tokens * model_info[:cost_per_token][:input]
  output_cost = output_tokens * model_info[:cost_per_token][:output]

  input_cost + output_cost
end

.clear_cache!Object

Clear local cache (both files and memory)



87
88
89
90
91
92
93
# File 'lib/open_router/model_registry.rb', line 87

def clear_cache!
  REGISTRY_MUTEX.synchronize do
    FileUtils.rm_rf(CACHE_DIR) if Dir.exist?(CACHE_DIR)
    @processed_models = nil
    @all_models = nil
  end
end

.cost_per_million(model) ⇒ Object

Cost per 1,000,000 tokens — { input: Float, output: Float } or nil



281
282
283
284
285
286
287
# File 'lib/open_router/model_registry.rb', line 281

def cost_per_million(model)
  info = get_model_info(model)
  return nil unless info

  { input: info[:cost_per_token][:input] * 1_000_000,
    output: info[:cost_per_token][:output] * 1_000_000 }
end

.cost_per_thousand(model) ⇒ Object

Cost per 1,000 tokens — { input: Float, output: Float } or nil



272
273
274
275
276
277
278
# File 'lib/open_router/model_registry.rb', line 272

def cost_per_thousand(model)
  info = get_model_info(model)
  return nil unless info

  { input: info[:cost_per_token][:input] * 1_000,
    output: info[:cost_per_token][:output] * 1_000 }
end

.determine_fallbacks(_model_id, _model_data) ⇒ Object

Determine fallback models (simplified logic)



201
202
203
204
# File 'lib/open_router/model_registry.rb', line 201

def determine_fallbacks(_model_id, _model_data)
  # For now, return empty array - could be enhanced with smart fallback logic
  []
end

.determine_performance_tier(model_data) ⇒ Object

Determine performance tier based on pricing and capabilities



188
189
190
191
192
193
194
195
196
197
198
# File 'lib/open_router/model_registry.rb', line 188

def determine_performance_tier(model_data)
  input_cost = model_data.dig("pricing", "prompt").to_f

  # Higher cost generally indicates premium models
  # Note: pricing is per token, not per 1k tokens
  if input_cost > 0.000001 # > $0.001 per 1k tokens (converted from per-token)
    :premium
  else
    :standard
  end
end

.ensure_cache_dirObject

Ensure cache directory exists and set up cleanup



38
39
40
41
# File 'lib/open_router/model_registry.rb', line 38

def ensure_cache_dir
  FileUtils.mkdir_p(CACHE_DIR) unless Dir.exist?(CACHE_DIR)
  setup_cleanup_hook
end

.extract_capabilities(model_data) ⇒ Object

Extract capabilities from model data



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/open_router/model_registry.rb', line 161

def extract_capabilities(model_data)
  capabilities = [:chat] # All models support basic chat

  # Check for function calling support
  supported_params = model_data["supported_parameters"] || []
  if supported_params.include?("tools") && supported_params.include?("tool_choice")
    capabilities << :function_calling
  end

  # Check for structured output support
  if supported_params.include?("structured_outputs") || supported_params.include?("response_format")
    capabilities << :structured_outputs
  end

  # Check for vision support
  architecture = model_data["architecture"] || {}
  input_modalities = architecture["input_modalities"] || []
  capabilities << :vision if input_modalities.include?("image")

  # Check for large context support
  context_length = model_data["context_length"] || 0
  capabilities << :long_context if context_length > 100_000

  capabilities
end

.fetch_and_cache_modelsObject

Get processed models (fetch if needed)



102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/open_router/model_registry.rb', line 102

def fetch_and_cache_models
  # Try cache first (only if fresh)
  cached_data = read_cache_if_fresh

  if cached_data
    api_data = cached_data
  else
    # Cache is stale or doesn't exist, fetch from API
    api_data = fetch_models_from_api
    write_cache_with_timestamp(api_data)
  end

  @processed_models = process_api_models(api_data["data"])
end

.fetch_models_from_apiObject

Fetch models from OpenRouter API using Faraday for consistent SSL handling



22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/open_router/model_registry.rb', line 22

def fetch_models_from_api
  conn = Faraday.new(url: API_BASE) do |f|
    f.options[:timeout] = OpenRouter.configuration.model_registry_timeout
    f.options[:open_timeout] = OpenRouter.configuration.model_registry_timeout
    f.response :raise_error
  end

  response = conn.get("models")
  JSON.parse(response.body)
rescue Faraday::Error => e
  raise ModelRegistryError, "Failed to fetch models from OpenRouter API: #{e.message}"
rescue JSON::ParserError => e
  raise ModelRegistryError, "Failed to parse OpenRouter API response: #{e.message}"
end

.find_best_model(requirements = {}) ⇒ Object

Find the best model matching given requirements



207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/open_router/model_registry.rb', line 207

def find_best_model(requirements = {})
  candidates = models_meeting_requirements(requirements)
  return nil if candidates.empty?

  # If pick_newer is true, prefer newer models over cost
  if requirements[:pick_newer]
    candidates.max_by { |_, specs| specs[:created_at] }
  else
    # Sort by cost (cheapest first) as default strategy
    candidates.min_by { |_, specs| calculate_model_cost(specs, requirements) }
  end
end

.find_original_model_data(model_id) ⇒ Object

Find original API model data by model ID



118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/open_router/model_registry.rb', line 118

def find_original_model_data(model_id)
  # Get raw models data (not processed)
  cached_data = read_cache_if_fresh

  if cached_data
    api_data = cached_data
  else
    api_data = fetch_models_from_api
    write_cache_with_timestamp(api_data)
  end

  raw_models = api_data["data"] || []
  raw_models.find { |model| model["id"] == model_id }
end

.get_fallbacks(model) ⇒ Object

Get fallback models for a given model



228
229
230
231
# File 'lib/open_router/model_registry.rb', line 228

def get_fallbacks(model)
  model_info = get_model_info(model)
  model_info ? model_info[:fallbacks] || [] : []
end

.get_model_info(model) ⇒ Object

Get detailed information about a model



247
248
249
# File 'lib/open_router/model_registry.rb', line 247

def get_model_info(model)
  all_models[model]
end

.has_capability?(model, capability) ⇒ Boolean

Check if a model has a specific capability

Returns:

  • (Boolean)


239
240
241
242
243
244
# File 'lib/open_router/model_registry.rb', line 239

def has_capability?(model, capability)
  model_info = get_model_info(model)
  return false unless model_info

  model_info[:capabilities].include?(capability)
end

.model_exists?(model) ⇒ Boolean

Check if a model exists in the registry

Returns:

  • (Boolean)


234
235
236
# File 'lib/open_router/model_registry.rb', line 234

def model_exists?(model)
  all_models.key?(model)
end

.models_meeting_requirements(requirements = {}) ⇒ Object

Get all models that meet requirements (without sorting)



221
222
223
224
225
# File 'lib/open_router/model_registry.rb', line 221

def models_meeting_requirements(requirements = {})
  all_models.select do |_model, specs|
    meets_requirements?(specs, requirements)
  end
end

.process_api_models(api_models) ⇒ Object

Convert API model data to our internal format



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/open_router/model_registry.rb', line 134

def process_api_models(api_models)
  models = {}

  api_models.each do |model_data|
    model_id = model_data["id"]

    models[model_id] = {
      name: model_data["name"],
      cost_per_token: {
        input: model_data.dig("pricing", "prompt").to_f,
        output: model_data.dig("pricing", "completion").to_f
      },
      context_length: model_data["context_length"],
      capabilities: extract_capabilities(model_data),
      description: model_data["description"],
      supported_parameters: model_data["supported_parameters"] || [],
      architecture: model_data["architecture"],
      performance_tier: determine_performance_tier(model_data),
      fallbacks: determine_fallbacks(model_id, model_data),
      created_at: model_data["created"]
    }
  end

  models
end

.read_cache_if_freshObject

Read cache only if it’s fresh



77
78
79
80
81
82
83
84
# File 'lib/open_router/model_registry.rb', line 77

def read_cache_if_fresh
  return nil if cache_stale?
  return nil unless File.exist?(CACHE_DATA_FILE)

  JSON.parse(File.read(CACHE_DATA_FILE))
rescue JSON::ParserError
  nil
end

.refresh!Object

Refresh models data from API



96
97
98
99
# File 'lib/open_router/model_registry.rb', line 96

def refresh!
  clear_cache!
  fetch_and_cache_models
end

.write_cache_with_timestamp(models_data) ⇒ Object

Write cache with timestamp metadata



61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/open_router/model_registry.rb', line 61

def write_cache_with_timestamp(models_data)
  ensure_cache_dir

  # Write the actual models data
  File.write(CACHE_DATA_FILE, JSON.pretty_generate(models_data))

  # Write metadata with timestamp
   = {
    "cached_at" => Time.now.to_i,
    "version" => "1.0",
    "source" => "openrouter_api"
  }
  File.write(CACHE_METADATA_FILE, JSON.pretty_generate())
end