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

Class Method Details

.api_type(provider_id) ⇒ String?

Get the API type for a provider

Parameters:

  • provider_id (String)

    The provider identifier

Returns:

  • (String, nil)

    The API type or nil if provider not found



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

Parameters:

  • provider_id (String)

    The provider identifier

Returns:

  • (String, nil)

    The base URL or nil if provider not found



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).

Parameters:

  • provider_id (String)

    The provider identifier

  • model_name (String, nil) (defaults to: nil)

    Optional specific model for override lookup

Returns:

  • (Hash)

    capabilities mapping (e.g. { “vision” => true })



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

Parameters:

  • provider_id (String)

    The provider identifier

Returns:

  • (String, nil)

    The default model name or nil if provider not found



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

Parameters:

  • provider_id (String)

    The provider identifier (e.g., “anthropic”, “openrouter”)

Returns:

  • (Boolean)

    True if the 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.

Parameters:

  • provider_id (String)

    The provider identifier

  • model (String)

    The primary model name

Returns:

  • (String, nil)

    The fallback model name or nil



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.

Parameters:

  • base_url (String)

    The base URL to look up

Returns:

  • (String, nil)

    The provider ID or nil if not found



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

Parameters:

  • provider_id (String)

    The provider identifier

Returns:

  • (Hash, nil)

    The preset configuration or nil if not found



205
206
207
# File 'lib/clacky/providers.rb', line 205

def get(provider_id)
  PRESETS[provider_id]
end

.listArray<Array(String, String)>

List all available providers with their names

Returns:

  • (Array<Array(String, String)>)

    Array of [id, name] pairs



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.

Parameters:

  • provider_id (String)

    The provider identifier

  • primary_model (String, nil) (defaults to: nil)

    The currently-selected primary model name. When given, look it up in the provider’s ‘lite_models` table first (so one provider can host multiple model families, each with its own lite sidekick — e.g. Claude Opus/Sonnet → Haiku, DeepSeek Pro → Flash). Falls back to the global `lite_model` field for old-style presets (e.g. deepseekv4) that declare a single provider-wide lite.

Returns:

  • (String, nil)

    The lite model name, or nil when the primary is already lite-class (no entry) and no global ‘lite_model` is defined.



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

Parameters:

  • provider_id (String)

    The provider identifier

Returns:

  • (Array<String>)

    List of model names (empty if dynamic)



248
249
250
251
# File 'lib/clacky/providers.rb', line 248

def models(provider_id)
  preset = PRESETS[provider_id]
  preset&.dig("models") || []
end

.provider_idsArray<String>

List all available provider IDs

Returns:

  • (Array<String>)

    List of provider identifiers



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.

Parameters:

  • base_url (String, nil) (defaults to: nil)

    the configured base_url

  • api_key (String, nil) (defaults to: nil)

    the configured api_key

Returns:

  • (String, nil)

    provider id or nil if unresolvable



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.

Parameters:

  • provider_id (String)

    The provider identifier

  • capability (String, Symbol)

    The capability name (e.g. :vision, “vision”)

  • model_name (String, nil) (defaults to: nil)

    Optional specific model name

Returns:

  • (Boolean)

    true unless the preset explicitly says false



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