Module: Clacky::Providers
- Defined in:
- lib/clacky/providers.rb
Overview
Built-in model provider presets Provides default configurations for supported AI model providers
Constant Summary collapse
- PRESETS =
Provider preset definitions Each preset includes:
-
name: Human-readable provider name
-
base_url: Default API endpoint
-
api: API type (anthropic-messages, openai-responses, openai-completions)
-
default_model: Recommended default model
-
capabilities (optional): provider-level capability hash (e.g. { “vision” => false }). Applies to all models under this provider unless overridden by model_capabilities below.
-
model_capabilities (optional): per-model capability override map, { “<model_name>” => { “<cap>” => bool, … } }. Use this when a single provider hosts models with different capabilities (e.g. openclacky hosts both vision-capable Claude and text-only DeepSeek).
-
{ "openclacky" => { "name" => "OpenClacky", "base_url" => "https://api.openclacky.com", "api" => "bedrock", "default_model" => "abs-claude-sonnet-4-5", "models" => [ "abs-claude-opus-4-7", "abs-claude-opus-4-6", "abs-claude-sonnet-4-6", "abs-claude-sonnet-4-5", "abs-claude-haiku-4-5", "dsk-deepseek-v4-pro", "dsk-deepseek-v4-flash" ], # Provider-level default: the Claude family served here is vision-capable. "capabilities" => { "vision" => true }.freeze, # Model-level overrides: DeepSeek models routed through this provider # are text-only; images uploaded for them must be downgraded to disk refs. "model_capabilities" => { "dsk-deepseek-v4-pro" => { "vision" => false }.freeze, "dsk-deepseek-v4-flash" => { "vision" => false }.freeze }.freeze, # Per-primary lite pairing: keys are "strong" primary models, values # are the lite sidekick to auto-inject when that primary is the # default. Lite is consumed by some subagents for cheap/fast work; # weak models (haiku / v4-flash) ARE the lite tier themselves, so # they're intentionally not listed here — no injection happens when # the default model is already lite-class. "lite_models" => { "abs-claude-opus-4-7" => "abs-claude-haiku-4-5", "abs-claude-opus-4-6" => "abs-claude-haiku-4-5", "abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5", "abs-claude-sonnet-4-5" => "abs-claude-haiku-4-5", "dsk-deepseek-v4-pro" => "dsk-deepseek-v4-flash" }, # Fallback chain: if a model is unavailable, try the next one in order. # Keys are primary model names; values are the fallback model to use instead. "fallback_models" => { "abs-claude-sonnet-4-6" => "abs-claude-sonnet-4-5" }, "website_url" => "https://www.openclacky.com/ai-keys" }.freeze, "openrouter" => { "name" => "OpenRouter", "base_url" => "https://openrouter.ai/api/v1", "api" => "openai-responses", "default_model" => "anthropic/claude-sonnet-4-6", "models" => [], # Dynamic - fetched from API "website_url" => "https://openrouter.ai/keys" }.freeze, "deepseekv4" => { "name" => "DeepSeek V4", # DeepSeek API is compatible with both OpenAI and Anthropic formats. # We use the OpenAI-compatible endpoint here (matches kimi/minimax/glm style). # For Anthropic-format usage, point base_url at https://api.deepseek.com/anthropic # and change "api" to "anthropic-messages". "base_url" => "https://api.deepseek.com", "api" => "openai-completions", "default_model" => "deepseek-v4-pro", "lite_model" => "deepseek-v4-flash", # Note: deepseek-chat and deepseek-reasoner are legacy aliases being # deprecated on 2026-07-24; they map to deepseek-v4-flash's non-thinking # and thinking modes respectively. Prefer deepseek-v4-flash / deepseek-v4-pro. "models" => [ "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat", "deepseek-reasoner" ], # DeepSeek V4 API does not accept image inputs — text-only across all models. "capabilities" => { "vision" => false }.freeze, "website_url" => "https://platform.deepseek.com/api_keys" }.freeze, "minimax" => { "name" => "Minimax", "base_url" => "https://api.minimaxi.com/v1", "api" => "openai-completions", "default_model" => "MiniMax-M2.7", "models" => ["MiniMax-M2.5", "MiniMax-M2.7"], # MiniMax M2.x does not support multimodal/vision input on this endpoint. "capabilities" => { "vision" => false }.freeze, "website_url" => "https://www.minimaxi.com/user-center/basic-information/interface-key" }.freeze, "kimi" => { "name" => "Kimi (Moonshot)", "base_url" => "https://api.moonshot.cn/v1", "api" => "openai-completions", "default_model" => "kimi-k2.5", "models" => ["kimi-k2.5"], # Kimi k2.5 (text family) does not accept image inputs. "capabilities" => { "vision" => false }.freeze, "website_url" => "https://platform.moonshot.cn/console/api-keys" }.freeze, "anthropic" => { "name" => "Anthropic (Claude)", "base_url" => "https://api.anthropic.com", "api" => "anthropic-messages", "default_model" => "claude-sonnet-4.6", "models" => ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"], "website_url" => "https://console.anthropic.com/settings/keys" }.freeze, "clackyai-sea" => { "name" => "ClackyAI( Sea )", "base_url" => "https://api.clacky.ai", "api" => "bedrock", "default_model" => "abs-claude-sonnet-4-5", "models" => [ "abs-claude-opus-4-7", "abs-claude-opus-4-6", "abs-claude-sonnet-4-6", "abs-claude-sonnet-4-5", "abs-claude-haiku-4-5", "dsk-deepseek-v4-pro", "dsk-deepseek-v4-flash" ], # Same lineup as openclacky — Claude is vision, DeepSeek is text-only. "capabilities" => { "vision" => true }.freeze, "model_capabilities" => { "dsk-deepseek-v4-pro" => { "vision" => false }.freeze, "dsk-deepseek-v4-flash" => { "vision" => false }.freeze }.freeze, # Per-primary lite pairing — see openclacky preset for rationale. "lite_models" => { "abs-claude-opus-4-7" => "abs-claude-haiku-4-5", "abs-claude-opus-4-6" => "abs-claude-haiku-4-5", "abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5", "abs-claude-sonnet-4-5" => "abs-claude-haiku-4-5", "dsk-deepseek-v4-pro" => "dsk-deepseek-v4-flash" }, # Fallback chain: if a model is unavailable, try the next one in order. # Keys are primary model names; values are the fallback model to use instead. "fallback_models" => { "abs-claude-sonnet-4-6" => "abs-claude-sonnet-4-5" }, "website_url" => "https://clacky.ai" }.freeze, "mimo" => { "name" => "MiMo (Xiaomi)", "base_url" => "https://api.xiaomimimo.com/v1", "api" => "openai-completions", "default_model" => "mimo-v2-pro", "models" => ["mimo-v2-pro", "mimo-v2-omni"], # MiMo-V2-Pro is text-only; MiMo-V2-Omni supports vision (omni = multimodal). "capabilities" => { "vision" => false }.freeze, "model_capabilities" => { "mimo-v2-omni" => { "vision" => true }.freeze }.freeze, "website_url" => "https://platform.xiaomimimo.com/" }.freeze, "glm" => { "name" => "GLM (ZhipuAI)", "base_url" => "https://open.bigmodel.cn/api/paas/v4", "api" => "openai-completions", "default_model" => "glm-5.1", "models" => ["glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7"], # GLM models are text-only except glm-5v-turbo which is vision-capable ("v" = visual). "capabilities" => { "vision" => false }.freeze, "model_capabilities" => { "glm-5v-turbo" => { "vision" => true }.freeze }.freeze, "website_url" => "https://open.bigmodel.cn/usercenter/apikeys" }.freeze }.freeze
Class Method Summary collapse
-
.api_type(provider_id) ⇒ String?
Get the API type for a provider.
-
.base_url(provider_id) ⇒ String?
Get the base URL for a provider.
-
.capabilities(provider_id, model_name: nil) ⇒ Hash
Resolve the capabilities hash for a given provider+model.
-
.default_model(provider_id) ⇒ String?
Get the default model for a provider.
-
.exists?(provider_id) ⇒ Boolean
Check if a provider preset exists.
-
.fallback_model(provider_id, model) ⇒ String?
Get the fallback model for a given model within a provider.
-
.find_by_base_url(base_url) ⇒ String?
Find provider ID by base URL.
-
.get(provider_id) ⇒ Hash?
Get a provider preset by ID.
-
.list ⇒ Array<Array(String, String)>
List all available providers with their names.
-
.lite_model(provider_id, primary_model = nil) ⇒ String?
Get the lite model for a provider.
-
.models(provider_id) ⇒ Array<String>
Get available models for a provider.
-
.provider_ids ⇒ Array<String>
List all available provider IDs.
-
.resolve_provider(base_url: nil, api_key: nil) ⇒ String?
Resolve the provider id for a model entry, trying base_url first and then falling back to an api_key hint for the openclacky family.
-
.supports?(provider_id, capability, model_name: nil) ⇒ Boolean
Check if a provider+model supports a capability.
Class Method Details
.api_type(provider_id) ⇒ String?
Get the API type for a provider
228 229 230 231 |
# File 'lib/clacky/providers.rb', line 228 def api_type(provider_id) preset = PRESETS[provider_id] preset&.dig("api") end |
.base_url(provider_id) ⇒ String?
Get the base URL for a provider
220 221 222 223 |
# File 'lib/clacky/providers.rb', line 220 def base_url(provider_id) preset = PRESETS[provider_id] preset&.dig("base_url") end |
.capabilities(provider_id, model_name: nil) ⇒ Hash
Resolve the capabilities hash for a given provider+model.
Resolution order (most specific wins):
1. PRESETS[provider_id]["model_capabilities"][model_name] — per-model
override, used when a single provider hosts a mix of capabilities
(e.g. openclacky serves both Claude [vision] and DeepSeek [text]).
2. PRESETS[provider_id]["capabilities"] — provider-wide defaults,
used when the whole lineup shares the same capabilities.
3. {} — no declaration; callers get the conservative default (true)
via `supports?`.
Returns a plain Hash (always safe to inspect; never nil).
355 356 357 358 359 360 361 362 363 364 |
# File 'lib/clacky/providers.rb', line 355 def capabilities(provider_id, model_name: nil) preset = PRESETS[provider_id] return {} unless preset provider_caps = preset["capabilities"] || {} return provider_caps.dup unless model_name model_caps = preset.dig("model_capabilities", model_name) || {} provider_caps.merge(model_caps) end |
.default_model(provider_id) ⇒ String?
Get the default model for a provider
212 213 214 215 |
# File 'lib/clacky/providers.rb', line 212 def default_model(provider_id) preset = PRESETS[provider_id] preset&.dig("default_model") end |
.exists?(provider_id) ⇒ Boolean
Check if a provider preset exists
198 199 200 |
# File 'lib/clacky/providers.rb', line 198 def exists?(provider_id) PRESETS.key?(provider_id) end |
.fallback_model(provider_id, model) ⇒ String?
Get the fallback model for a given model within a provider. Returns nil if no fallback is defined for that model.
286 287 288 289 |
# File 'lib/clacky/providers.rb', line 286 def fallback_model(provider_id, model) preset = PRESETS[provider_id] preset&.dig("fallback_models", model) end |
.find_by_base_url(base_url) ⇒ String?
Find provider ID by base URL. Matches if the given URL starts with the provider’s base_url (after normalisation), so both exact matches and sub-path variants (e.g. “/v1”) are recognised.
296 297 298 299 300 301 302 303 |
# File 'lib/clacky/providers.rb', line 296 def find_by_base_url(base_url) return nil if base_url.nil? || base_url.empty? normalized = base_url.to_s.chomp("/") PRESETS.find do |_id, preset| preset_base = preset["base_url"].to_s.chomp("/") normalized == preset_base || normalized.start_with?("#{preset_base}/") end&.first end |
.get(provider_id) ⇒ Hash?
Get a provider preset by ID
205 206 207 |
# File 'lib/clacky/providers.rb', line 205 def get(provider_id) PRESETS[provider_id] end |
.list ⇒ Array<Array(String, String)>
List all available providers with their names
241 242 243 |
# File 'lib/clacky/providers.rb', line 241 def list PRESETS.map { |id, config| [id, config["name"]] } end |
.lite_model(provider_id, primary_model = nil) ⇒ String?
Get the lite model for a provider.
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/clacky/providers.rb', line 263 def lite_model(provider_id, primary_model = nil) preset = PRESETS[provider_id] return nil unless preset if primary_model && preset["lite_models"].is_a?(Hash) mapped = preset["lite_models"][primary_model] return mapped if mapped # When a `lite_models` table is defined but the current primary # isn't listed, it means the primary is already a lite-class model # (e.g. haiku / v4-flash) — do NOT fall back to the legacy single # field, because that would incorrectly inject a lite for a model # that doesn't need one. return nil if preset["lite_models"].any? end preset["lite_model"] end |
.models(provider_id) ⇒ Array<String>
Get available models for a provider
248 249 250 251 |
# File 'lib/clacky/providers.rb', line 248 def models(provider_id) preset = PRESETS[provider_id] preset&.dig("models") || [] end |
.provider_ids ⇒ Array<String>
List all available provider IDs
235 236 237 |
# File 'lib/clacky/providers.rb', line 235 def provider_ids PRESETS.keys end |
.resolve_provider(base_url: nil, api_key: nil) ⇒ String?
Resolve the provider id for a model entry, trying base_url first and then falling back to an api_key hint for the openclacky family.
Why the api_key fallback exists:
For local-debug / self-hosted proxy setups, users sometimes point
an "abs-claude-*" or "dsk-deepseek-*" model at http://localhost:XXXX
while still using a real `clacky-...` api key. Pure base_url matching
would report "unknown provider" and downstream logic (lite pairing,
fallback_models, capability detection) silently degrades. Recognising
the `clacky-` key prefix keeps those flows working without forcing
the user to edit base_url.
Not generalised to other providers: the ‘sk-…` prefix is used by OpenAI, DeepSeek, Moonshot, and many others, so it can’t uniquely identify a provider. We only special-case ‘clacky-` because it’s unique to us and the debug-proxy scenario is specifically ours.
325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/clacky/providers.rb', line 325 def resolve_provider(base_url: nil, api_key: nil) id = find_by_base_url(base_url) return id if id # Local-debug fallback: clacky-* api keys belong to the openclacky # family. Both "openclacky" and "clackyai-sea" share the same key # namespace and an identical model lineup/lite mapping, so picking # "openclacky" is equivalent for downstream lookups. if api_key.is_a?(String) && api_key.start_with?("clacky-") return "openclacky" end nil end |
.supports?(provider_id, capability, model_name: nil) ⇒ Boolean
Check if a provider+model supports a capability. Unknown provider / missing capability declaration → returns true (conservative default: assume supported unless we explicitly say otherwise). This keeps custom base_urls working and avoids over-aggressive downgrades.
375 376 377 378 379 380 381 382 383 384 |
# File 'lib/clacky/providers.rb', line 375 def supports?(provider_id, capability, model_name: nil) preset = PRESETS[provider_id] return true unless preset key = capability.to_s caps = capabilities(provider_id, model_name: model_name) # When the capability is not declared at either level, default to true. return true unless caps.key?(key) caps[key] != false end |