Module: Kward::ModelInfo
- Defined in:
- lib/kward/model/model_info.rb
Overview
Static and configured model metadata helpers.
Constant Summary collapse
- DEFAULT_OPENAI_MODEL =
"gpt-5.5"- DEFAULT_COPILOT_MODEL =
"gpt-5-mini"- DEFAULT_ANTHROPIC_MODEL =
"claude-sonnet-4-6"- DEFAULT_REASONING_EFFORT =
"medium"- OPENAI_MODEL_CHOICES =
%w[gpt-5.5 gpt-5.4 gpt-5.4-mini gpt-5.3-codex-spark].freeze
- ANTHROPIC_MODEL_CHOICES =
%w[ claude-opus-4-8 claude-sonnet-4-6 claude-haiku-4-5 claude-opus-4-7 claude-opus-4-6 claude-opus-4-5 claude-sonnet-4-5 ].freeze
- COPILOT_MODEL_CHOICES =
%w[ gpt-5-mini gpt-5.3-codex gpt-5.4 gpt-5.4-mini gpt-5.5 claude-haiku-4.5 claude-opus-4.5 claude-opus-4.7 claude-opus-4.8 claude-sonnet-4.5 claude-sonnet-4.6 gemini-2.5-pro gemini-3-flash-preview gemini-3.1-pro-preview gemini-3.5-flash oswe-vscode-prime ].freeze
- REASONING_EFFORT_CHOICES =
[ ["low", "Low"], ["medium", "Medium"], ["high", "High"], ["xhigh", "Extra High"] ].freeze
- OPENAI_REASONING_EFFORT_CHOICES =
[ ["none", "None"], *REASONING_EFFORT_CHOICES ].freeze
- ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES =
[ ["low", "Low"], ["medium", "Medium"], ["high", "High"], ["xhigh", "Extra High"], ["max", "Max"] ].freeze
- ANTHROPIC_STANDARD_REASONING_EFFORT_CHOICES =
[ ["low", "Low"], ["medium", "Medium"], ["high", "High"], ["max", "Max"] ].freeze
- ANTHROPIC_OPUS_4_5_REASONING_EFFORT_CHOICES =
[ ["low", "Low"], ["medium", "Medium"], ["high", "High"] ].freeze
- IMAGE_UNSUPPORTED_MODELS =
[ /(?:\A|\/)gpt-5\.3-codex-spark\z/ ].freeze
- CODEX_CONTEXT_WINDOWS =
[ [/\Agpt-5\.5/, 400_000], [/\Agpt-5\.4-mini/, 400_000], [/\Agpt-5\.4/, 1_050_000], [/\Agpt-5-mini/, 400_000], [/\Agpt-5-codex/, 400_000], [/\Agpt-5\.3-codex-spark/, 128_000], [/\Agpt-5\.3-codex/, 400_000], [/\Agpt-5\.2-codex/, 400_000], [/\Agpt-5/, 400_000], [/\Agpt-4\.1/, 1_047_576], [/\Agpt-4o/, 128_000], [/\Ao3/, 200_000], [/\Ao4/, 200_000], [/\Agpt-4/, 128_000], [/\Agpt-3\.5-turbo/, 16_385] ].freeze
- OPENAI_CONTEXT_WINDOWS =
[ [/\Agpt-5\.5/, 1_050_000], [/\Agpt-5\.4-mini/, 400_000], [/\Agpt-5\.4/, 1_050_000], [/\Agpt-5-mini/, 400_000], [/\Agpt-5-codex/, 400_000], [/\Agpt-5\.3-codex-spark/, 128_000], [/\Agpt-5\.3-codex/, 400_000], [/\Agpt-5\.2-codex/, 400_000], [/\Agpt-5/, 400_000], [/\Agpt-4\.1/, 1_047_576], [/\Agpt-4o/, 128_000], [/\Ao3/, 200_000], [/\Ao4/, 200_000], [/\Agpt-4/, 128_000], [/\Agpt-3\.5-turbo/, 16_385] ].freeze
- ANTHROPIC_CONTEXT_WINDOWS =
[ [/\Aclaude-(?:fable|mythos)-5(?:\z|-)/, 1_000_000], [/\Aclaude-opus-4-(?:6|7|8)(?:\z|-)/, 1_000_000], [/\Aclaude-sonnet-4-6(?:\z|-)/, 1_000_000], [/\Aclaude-(?:haiku|opus|sonnet)-4-5(?:\z|-)/, 200_000], [/\Aclaude-(?:haiku|opus|sonnet)-4(?:\z|-)/, 200_000] ].freeze
- GEMINI_CONTEXT_WINDOWS =
[ [/\Agemini-(?:2\.5-pro|3(?:\.1)?-pro|3(?:\.5)?-flash)/, 1_048_576] ].freeze
Class Method Summary collapse
- .anthropic_context_window(id) ⇒ Object
- .anthropic_reasoning_effort_choices(id) ⇒ Object
- .boolean_value(value, default: false) ⇒ Object
- .config_key_for_provider(provider) ⇒ Object
- .config_provider_for_provider(provider) ⇒ Object
- .config_values_for_selection(provider, model) ⇒ Object
- .conservative_anthropic_context_window(id) ⇒ Object
- .conservative_context_window(id) ⇒ Object
- .conservative_openrouter_context_window(id) ⇒ Object
- .conservative_unknown_context_window(id) ⇒ Object
- .context_window(provider, id, openrouter_models: nil) ⇒ Object
- .copilot_context_window(id) ⇒ Object
- .copilot_openrouter_equivalent_ids(id) ⇒ Object
- .model_for(provider, config:, override_model: nil, env: ENV) ⇒ Object
- .normalize(model, current_provider: nil, current_model: nil, current_reasoning_effort: nil) ⇒ Object
- .normalize_anthropic_model(id) ⇒ Object
- .normalize_copilot_model(id) ⇒ Object
- .openai_reasoning_effort_choices(id) ⇒ Object
- .openrouter_cached_context_window(models, id) ⇒ Object
- .openrouter_context_window(id) ⇒ Object
- .openrouter_equivalent_ids(provider, id) ⇒ Object
- .openrouter_inferred_context_window(provider, id, openrouter_models: nil) ⇒ Object
- .pattern_context_window(patterns, id) ⇒ Object
- .positive_integer(value) ⇒ Object
- .provider_config_value(provider) ⇒ Object
- .provider_label(provider) ⇒ Object
- .reasoning_config_key_for_provider(provider) ⇒ Object
- .reasoning_effort(config:, env: ENV, provider: nil) ⇒ Object
- .reasoning_effort_choices(provider, id) ⇒ Object
- .reasoning_supported?(provider, id) ⇒ Boolean
- .stringify_keys(value) ⇒ Object
- .supports_images?(_provider, id) ⇒ Boolean
Class Method Details
.anthropic_context_window(id) ⇒ Object
348 349 350 |
# File 'lib/kward/model/model_info.rb', line 348 def anthropic_context_window(id) pattern_context_window(ANTHROPIC_CONTEXT_WINDOWS, normalize_anthropic_model(id)) end |
.anthropic_reasoning_effort_choices(id) ⇒ Object
389 390 391 392 393 394 395 396 |
# File 'lib/kward/model/model_info.rb', line 389 def anthropic_reasoning_effort_choices(id) text = normalize_anthropic_model(id) return ANTHROPIC_HIGH_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:fable|mythos)-5(?:\z|-)|\Aclaude-opus-4-(?:7|8)(?:\z|-)/) return ANTHROPIC_STANDARD_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-(?:opus-4-6|sonnet-4-6)(?:\z|-)/) return ANTHROPIC_OPUS_4_5_REASONING_EFFORT_CHOICES if text.match?(/\Aclaude-opus-4-5(?:\z|-)/) [] end |
.boolean_value(value, default: false) ⇒ Object
415 416 417 418 419 420 |
# File 'lib/kward/model/model_info.rb', line 415 def boolean_value(value, default: false) return default if value.nil? return value if value == true || value == false value.to_s == "true" end |
.config_key_for_provider(provider) ⇒ Object
200 201 202 203 204 205 206 207 |
# File 'lib/kward/model/model_info.rb', line 200 def config_key_for_provider(provider) case provider.to_s.downcase when "openrouter" then "openrouter_model" when "copilot" then "copilot_model" when "anthropic", "claude" then "anthropic_model" else "openai_model" end end |
.config_provider_for_provider(provider) ⇒ Object
218 219 220 |
# File 'lib/kward/model/model_info.rb', line 218 def config_provider_for_provider(provider) provider_config_value(provider) end |
.config_values_for_selection(provider, model) ⇒ Object
222 223 224 225 226 227 |
# File 'lib/kward/model/model_info.rb', line 222 def config_values_for_selection(provider, model) { config_key_for_provider(provider) => model, "provider" => config_provider_for_provider(provider) } end |
.conservative_anthropic_context_window(id) ⇒ Object
319 320 321 |
# File 'lib/kward/model/model_info.rb', line 319 def conservative_anthropic_context_window(id) id.to_s.strip.empty? ? nil : 200_000 end |
.conservative_context_window(id) ⇒ Object
312 313 314 315 316 317 |
# File 'lib/kward/model/model_info.rb', line 312 def conservative_context_window(id) text = id.to_s return 128_000 if text.match?(/\A(?:gpt-|o\d)/) return 200_000 if text.start_with?("claude-") return 128_000 if text.start_with?("gemini-") end |
.conservative_openrouter_context_window(id) ⇒ Object
334 335 336 337 338 339 |
# File 'lib/kward/model/model_info.rb', line 334 def conservative_openrouter_context_window(id) text = id.to_s return conservative_context_window(text.delete_prefix("openai/")) if text.start_with?("openai/") return conservative_context_window(normalize_anthropic_model(text.delete_prefix("anthropic/"))) if text.start_with?("anthropic/") return conservative_context_window(text.delete_prefix("google/")) if text.start_with?("google/") end |
.conservative_unknown_context_window(id) ⇒ Object
323 324 325 |
# File 'lib/kward/model/model_info.rb', line 323 def conservative_unknown_context_window(id) id.to_s.strip.empty? ? nil : 128_000 end |
.context_window(provider, id, openrouter_models: nil) ⇒ Object
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/kward/model/model_info.rb', line 229 def context_window(provider, id, openrouter_models: nil) case provider when "Codex" pattern_context_window(CODEX_CONTEXT_WINDOWS, id) || openrouter_inferred_context_window(provider, id, openrouter_models: openrouter_models) || conservative_context_window(id) when "OpenRouter" openrouter_cached_context_window(openrouter_models, id) || openrouter_context_window(id) || conservative_openrouter_context_window(id) || conservative_unknown_context_window(id) when "Copilot" copilot_context_window(id) || openrouter_inferred_context_window(provider, id, openrouter_models: openrouter_models) || conservative_context_window(id) || conservative_unknown_context_window(id) when "Anthropic" anthropic_context_window(id) || openrouter_inferred_context_window(provider, id, openrouter_models: openrouter_models) || conservative_context_window(normalize_anthropic_model(id)) || conservative_anthropic_context_window(id) end end |
.copilot_context_window(id) ⇒ Object
341 342 343 344 345 346 |
# File 'lib/kward/model/model_info.rb', line 341 def copilot_context_window(id) text = id.to_s pattern_context_window(OPENAI_CONTEXT_WINDOWS, text) || anthropic_context_window(normalize_anthropic_model(text)) || pattern_context_window(GEMINI_CONTEXT_WINDOWS, text) end |
.copilot_openrouter_equivalent_ids(id) ⇒ Object
292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/kward/model/model_info.rb', line 292 def copilot_openrouter_equivalent_ids(id) return ["openai/#{id.delete_prefix("openai/")}"] if id.start_with?("openai/") || id.match?(/\A(?:gpt-|o\d)/) return ["google/#{id.delete_prefix("google/")}"] if id.start_with?("google/") || id.start_with?("gemini-") if id.start_with?("anthropic/") || id.start_with?("claude-") raw = id.delete_prefix("anthropic/") normalized = normalize_anthropic_model(raw) return ["anthropic/#{raw}", "anthropic/#{normalized}"].uniq end [] end |
.model_for(provider, config:, override_model: nil, env: ENV) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/kward/model/model_info.rb', line 119 def model_for(provider, config:, override_model: nil, env: ENV) return override_model if override_model case provider when "OpenRouter" env["OPENROUTER_MODEL"] || ConfigFiles.config_value(config, "openrouter_model", "model") when "Copilot" normalize_copilot_model(env["COPILOT_MODEL"] || ConfigFiles.config_value(config, "copilot_model", "model") || DEFAULT_COPILOT_MODEL) when "Anthropic" normalize_anthropic_model(env["ANTHROPIC_MODEL"] || ConfigFiles.config_value(config, "anthropic_model", "model") || DEFAULT_ANTHROPIC_MODEL) else env["OPENAI_MODEL"] || ConfigFiles.config_value(config, "openai_model", "model") || DEFAULT_OPENAI_MODEL end end |
.normalize(model, current_provider: nil, current_model: nil, current_reasoning_effort: nil) ⇒ Object
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 |
# File 'lib/kward/model/model_info.rb', line 398 def normalize(model, current_provider: nil, current_model: nil, current_reasoning_effort: nil) model = stringify_keys(model || {}) provider = model["provider"] id = model["id"] || model["model"] reasoning = boolean_value(model["reasoning"], default: reasoning_supported?(provider, id)) reasoning_effort = model["reasoningEffort"] || model["reasoning_effort"] || (current_reasoning_effort if reasoning) { provider: provider, id: id, name: model["name"] || id, reasoning: reasoning, reasoningEffort: reasoning_effort, contextWindow: model["contextWindow"] || model["context_window"] || context_window(provider, id), current: boolean_value(model["current"], default: provider == current_provider && id == current_model) }.compact end |
.normalize_anthropic_model(id) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/kward/model/model_info.rb', line 134 def normalize_anthropic_model(id) text = id.to_s.strip return DEFAULT_ANTHROPIC_MODEL if text.empty? text = text.delete_prefix("anthropic/").delete_prefix("claude/") { "claude-sonnet-4.6" => "claude-sonnet-4-6", "claude-sonnet-4.5" => "claude-sonnet-4-5", "claude-opus-4.8" => "claude-opus-4-8", "claude-opus-4.7" => "claude-opus-4-7", "claude-opus-4.6" => "claude-opus-4-6", "claude-opus-4.5" => "claude-opus-4-5", "claude-haiku-4.5" => "claude-haiku-4-5" }.fetch(text, text) end |
.normalize_copilot_model(id) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/kward/model/model_info.rb', line 149 def normalize_copilot_model(id) text = id.to_s.strip return DEFAULT_COPILOT_MODEL if text.empty? text = text.delete_prefix("copilot/") { "claude-haiku-4-5" => "claude-haiku-4.5", "claude-opus-4-5" => "claude-opus-4.5", "claude-opus-4-6" => "claude-opus-4.6", "claude-opus-4-6-fast" => "claude-opus-4.6-fast", "claude-opus-4-7" => "claude-opus-4.7", "claude-opus-4-8" => "claude-opus-4.8", "claude-sonnet-4-5" => "claude-sonnet-4.5", "claude-sonnet-4-6" => "claude-sonnet-4.6", "gemini-3.1-pro" => "gemini-3.1-pro-preview", "gemini-3-flash" => "gemini-3-flash-preview" }.fetch(text, text) end |
.openai_reasoning_effort_choices(id) ⇒ Object
382 383 384 385 386 387 |
# File 'lib/kward/model/model_info.rb', line 382 def openai_reasoning_effort_choices(id) text = id.to_s.delete_prefix("openai/") return REASONING_EFFORT_CHOICES if text.match?(/\Agpt-5\.[23]-codex/) OPENAI_REASONING_EFFORT_CHOICES end |
.openrouter_cached_context_window(models, id) ⇒ Object
258 259 260 261 262 263 264 265 266 |
# File 'lib/kward/model/model_info.rb', line 258 def openrouter_cached_context_window(models, id) model = Array(models).find do |entry| next false unless entry.respond_to?(:key?) entry["id"].to_s == id.to_s || entry[:id].to_s == id.to_s end value = model && (model["contextWindow"] || model[:contextWindow] || model["context_window"] || model[:context_window]) positive_integer(value) end |
.openrouter_context_window(id) ⇒ Object
327 328 329 330 331 332 |
# File 'lib/kward/model/model_info.rb', line 327 def openrouter_context_window(id) text = id.to_s return pattern_context_window(OPENAI_CONTEXT_WINDOWS, text.delete_prefix("openai/")) if text.start_with?("openai/") return anthropic_context_window(text.delete_prefix("anthropic/")) if text.start_with?("anthropic/") return pattern_context_window(GEMINI_CONTEXT_WINDOWS, text.delete_prefix("google/")) if text.start_with?("google/") end |
.openrouter_equivalent_ids(provider, id) ⇒ Object
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/kward/model/model_info.rb', line 274 def openrouter_equivalent_ids(provider, id) text = id.to_s.strip return [] if text.empty? case provider when "Codex" ["openai/#{text.delete_prefix("openai/")}"] when "Anthropic" raw = text.delete_prefix("anthropic/") normalized = normalize_anthropic_model(raw) ["anthropic/#{raw}", "anthropic/#{normalized}"].uniq when "Copilot" copilot_openrouter_equivalent_ids(text) else [] end end |
.openrouter_inferred_context_window(provider, id, openrouter_models: nil) ⇒ Object
268 269 270 271 272 |
# File 'lib/kward/model/model_info.rb', line 268 def openrouter_inferred_context_window(provider, id, openrouter_models: nil) openrouter_equivalent_ids(provider, id).filter_map do |candidate| openrouter_cached_context_window(openrouter_models, candidate) end.min end |
.pattern_context_window(patterns, id) ⇒ Object
253 254 255 256 |
# File 'lib/kward/model/model_info.rb', line 253 def pattern_context_window(patterns, id) match = patterns.find { |pattern, _window| id.to_s.match?(pattern) } match&.last end |
.positive_integer(value) ⇒ Object
305 306 307 308 309 310 |
# File 'lib/kward/model/model_info.rb', line 305 def positive_integer(value) integer = Integer(value) integer.positive? ? integer : nil rescue ArgumentError, TypeError nil end |
.provider_config_value(provider) ⇒ Object
191 192 193 194 195 196 197 198 |
# File 'lib/kward/model/model_info.rb', line 191 def provider_config_value(provider) case provider.to_s.downcase when "openrouter" then "openrouter" when "copilot" then "copilot" when "anthropic", "claude" then "anthropic" else "codex" end end |
.provider_label(provider) ⇒ Object
181 182 183 184 185 186 187 188 189 |
# File 'lib/kward/model/model_info.rb', line 181 def provider_label(provider) case provider.to_s.downcase when "openrouter" then "OpenRouter" when "copilot" then "Copilot" when "anthropic", "claude" then "Anthropic" when "codex", "openai" then "Codex" else provider.to_s end end |
.reasoning_config_key_for_provider(provider) ⇒ Object
209 210 211 212 213 214 215 216 |
# File 'lib/kward/model/model_info.rb', line 209 def reasoning_config_key_for_provider(provider) case provider.to_s.downcase when "openrouter" then "openrouter_reasoning_effort" when "copilot" then "copilot_reasoning_effort" when "anthropic", "claude" then "anthropic_reasoning_effort" else "openai_reasoning_effort" end end |
.reasoning_effort(config:, env: ENV, provider: nil) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/kward/model/model_info.rb', line 168 def reasoning_effort(config:, env: ENV, provider: nil) case provider.to_s when "OpenRouter" env["OPENROUTER_REASONING_EFFORT"] || ConfigFiles.config_value(config, "openrouter_reasoning_effort", "reasoning_effort", "thinking_level") || DEFAULT_REASONING_EFFORT when "Copilot" env["COPILOT_REASONING_EFFORT"] || ConfigFiles.config_value(config, "copilot_reasoning_effort", "reasoning_effort", "thinking_level") || DEFAULT_REASONING_EFFORT when "Anthropic" env["ANTHROPIC_REASONING_EFFORT"] || ConfigFiles.config_value(config, "anthropic_reasoning_effort", "reasoning_effort", "thinking_level") || DEFAULT_REASONING_EFFORT else env["OPENAI_REASONING_EFFORT"] || ConfigFiles.config_value(config, "openai_reasoning_effort", "reasoning_effort", "thinking_level") || DEFAULT_REASONING_EFFORT end end |
.reasoning_effort_choices(provider, id) ⇒ Object
369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/kward/model/model_info.rb', line 369 def reasoning_effort_choices(provider, id) case provider when "Codex", "OpenRouter" openai_reasoning_effort_choices(id) when "Anthropic" anthropic_reasoning_effort_choices(id) when "Copilot" id.to_s.match?(/\Agpt-5(?:\.|-|\z)/) ? openai_reasoning_effort_choices(id) : [] else [] end end |
.reasoning_supported?(provider, id) ⇒ Boolean
356 357 358 359 360 361 362 363 364 365 366 367 |
# File 'lib/kward/model/model_info.rb', line 356 def reasoning_supported?(provider, id) case provider when "Codex", "OpenRouter" true when "Anthropic" !reasoning_effort_choices(provider, id).empty? when "Copilot" id.to_s.match?(/\Agpt-5(?:\.|-|\z)/) else false end end |
.stringify_keys(value) ⇒ Object
422 423 424 |
# File 'lib/kward/model/model_info.rb', line 422 def stringify_keys(value) value.each_with_object({}) { |(key, item), result| result[key.to_s] = item } end |
.supports_images?(_provider, id) ⇒ Boolean
352 353 354 |
# File 'lib/kward/model/model_info.rb', line 352 def supports_images?(_provider, id) IMAGE_UNSUPPORTED_MODELS.none? { |pattern| id.to_s.match?(pattern) } end |