Module: Llm::ProviderRegistry

Defined in:
app/services/llm/provider_registry.rb

Constant Summary collapse

PROVIDERS =
{
  "anthropic" => {
    label: "Anthropic",
    base_uri: nil, # handled by the Anthropic gem directly, not the generic OpenAI-compatible adapter
    models: {
      "claude-sonnet-5" => { vision: true, input_price_per_million: 3.00, output_price_per_million: 15.00 },
      "claude-opus-4-8" => { vision: true, input_price_per_million: 15.00, output_price_per_million: 75.00 },
      "claude-haiku-4-5-20251001" => { vision: true, input_price_per_million: 1.00, output_price_per_million: 5.00 }
    }
  },
  "openai" => {
    label: "OpenAI",
    base_uri: "https://api.openai.com/v1",
    models: {
      "gpt-4o" => { vision: true, input_price_per_million: 2.50, output_price_per_million: 10.00 },
      "gpt-4o-mini" => { vision: true, input_price_per_million: 0.15, output_price_per_million: 0.60 },
      "gpt-4.1" => { vision: true, input_price_per_million: 2.00, output_price_per_million: 8.00 },
      "o3-mini" => { vision: false, input_price_per_million: 1.10, output_price_per_million: 4.40 }
    }
  },
  "deepseek" => {
    label: "DeepSeek",
    base_uri: "https://api.deepseek.com/v1",
    # No vision-capable model today — never eligible for vision-only
    # calls, but fine for text-only calls.
    models: {
      "deepseek-chat" => { vision: false, input_price_per_million: 0.27, output_price_per_million: 1.10 },
      "deepseek-reasoner" => { vision: false, input_price_per_million: 0.55, output_price_per_million: 2.19 }
    }
  },
  "mistral" => {
    label: "Mistral",
    base_uri: "https://api.mistral.ai/v1",
    models: {
      "pixtral-large-latest" => { vision: true, input_price_per_million: 2.00, output_price_per_million: 6.00 },
      "pixtral-12b-latest" => { vision: true, input_price_per_million: 0.15, output_price_per_million: 0.15 }
    }
  },
  "kimi" => {
    label: "Kimi (Moonshot AI)",
    base_uri: "https://api.moonshot.ai/v1",
    # Pricing here is the standard (cache-miss) input rate — this gem
    # doesn't model cache-hit discounts for any provider.
    models: {
      "kimi-k3" => { vision: true, input_price_per_million: 3.00, output_price_per_million: 15.00 },
      "kimi-k2.7-code" => { vision: false, input_price_per_million: 0.95, output_price_per_million: 4.00 },
      "kimi-k2.6" => { vision: true, input_price_per_million: 0.95, output_price_per_million: 4.00 }
    }
  },
  "openrouter" => {
    label: "OpenRouter",
    base_uri: "https://openrouter.ai/api/v1",
    models: {
      "anthropic/claude-3.5-sonnet" => { vision: true, input_price_per_million: 3.00, output_price_per_million: 15.00 },
      "google/gemini-2.0-flash-001" => { vision: true, input_price_per_million: 0.10, output_price_per_million: 0.40 },
      "openai/gpt-4o" => { vision: true, input_price_per_million: 2.50, output_price_per_million: 10.00 },
      # Lets OpenRouter's own router pick the best underlying model per
      # request instead of pinning one — convenient, but it's a
      # meta-router rather than a specific model, so treat it as
      # non-vision-guaranteed (which model it picks varies) and
      # unpriceable (cost depends on whatever it picks).
      "openrouter/auto" => { vision: false, input_price_per_million: nil, output_price_per_million: nil }
    }
  }
}.freeze

Class Method Summary collapse

Class Method Details

.base_uri_for(provider) ⇒ Object



92
93
94
# File 'app/services/llm/provider_registry.rb', line 92

def self.base_uri_for(provider)
  PROVIDERS.fetch(provider).fetch(:base_uri)
end

.cost_for(provider, model, input_tokens:, output_tokens:) ⇒ Object

Returns nil (rather than 0) when a model has no known/fixed pricing — callers must not silently treat "unknown" as "free".



107
108
109
110
111
112
# File 'app/services/llm/provider_registry.rb', line 107

def self.cost_for(provider, model, input_tokens:, output_tokens:)
  pricing = pricing_for(provider, model)
  return nil unless pricing

  (input_tokens * pricing[:input_price_per_million] + output_tokens * pricing[:output_price_per_million]) / 1_000_000.0
end

.grouped_optionsObject



114
115
116
# File 'app/services/llm/provider_registry.rb', line 114

def self.grouped_options
  PROVIDERS.map { |key, cfg| [ cfg[:label], cfg[:models].keys.map { |m| [ m, "#{key}:#{m}" ] } ] }
end

.pricing_for(provider, model) ⇒ Object



96
97
98
99
100
101
102
103
# File 'app/services/llm/provider_registry.rb', line 96

def self.pricing_for(provider, model)
  return nil unless valid?(provider, model)

  cfg = PROVIDERS[provider][:models][model]
  return nil if cfg[:input_price_per_million].nil? || cfg[:output_price_per_million].nil?

  { input_price_per_million: cfg[:input_price_per_million], output_price_per_million: cfg[:output_price_per_million] }
end

.valid?(provider, model) ⇒ Boolean

Returns:

  • (Boolean)


84
85
86
# File 'app/services/llm/provider_registry.rb', line 84

def self.valid?(provider, model)
  PROVIDERS.key?(provider) && PROVIDERS[provider][:models].key?(model)
end

.vision_capable?(provider, model) ⇒ Boolean

Returns:

  • (Boolean)


88
89
90
# File 'app/services/llm/provider_registry.rb', line 88

def self.vision_capable?(provider, model)
  valid?(provider, model) && PROVIDERS[provider][:models][model][:vision]
end