feat: abstract models

This commit is contained in:
Shivam Mishra
2025-12-23 19:38:01 +05:30
parent 4eed216560
commit ae3687364d
6 changed files with 106 additions and 141 deletions
@@ -3,8 +3,8 @@ class Api::V1::Accounts::Captain::ConfigController < Api::V1::Accounts::BaseCont
def show
render json: {
providers: Llm::ConfigService.providers,
models: Llm::ConfigService.models,
providers: Llm::Models.providers,
models: Llm::Models.models,
features: features_with_account_preferences
}
end
@@ -16,12 +16,12 @@ class Api::V1::Accounts::Captain::ConfigController < Api::V1::Accounts::BaseCont
account_features = preferences[:features] || {}
account_models = preferences[:models] || {}
Llm::ConfigService.all_features_config.transform_keys(&:to_s).to_h do |feature_key, feature_config|
selected_model = account_models[feature_key] || feature_config[:default]
[feature_key, feature_config.merge(
Llm::Models.feature_keys.index_with do |feature_key|
config = Llm::Models.feature_config(feature_key)
config.merge(
enabled: account_features[feature_key] == true,
selected: selected_model
)]
selected: account_models[feature_key] || config[:default]
)
end
end
end
-39
View File
@@ -82,7 +82,6 @@ class Account < ApplicationRecord
validates_with JsonSchemaValidator,
schema: SETTINGS_PARAMS_SCHEMA,
attribute_resolver: ->(record) { record.settings }
validate :validate_captain_models
store_accessor :settings, :auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting
@@ -192,46 +191,8 @@ class Account < ApplicationRecord
ISO_639.find(account_locale)&.english_name&.downcase || 'english'
end
def captain_preferences
{
models: captain_models_with_defaults,
features: captain_features_with_defaults
}.with_indifferent_access
end
private
def captain_models_with_defaults
stored_models = captain_models || {}
Llm::ConfigService.feature_keys.each_with_object({}) do |feature_key, result|
stored_value = stored_models[feature_key]
result[feature_key] = if stored_value.present? && Llm::ConfigService.valid_model_for_feature?(feature_key, stored_value)
stored_value
else
Llm::ConfigService.default_model_for_feature(feature_key)
end
end
end
def captain_features_with_defaults
stored_features = captain_features || {}
Llm::ConfigService.feature_keys.index_with do |feature_key|
stored_features[feature_key] == true
end
end
def validate_captain_models
return if captain_models.blank?
captain_models.each do |feature_key, model_name|
next if model_name.blank?
next if Llm::ConfigService.valid_model_for_feature?(feature_key, model_name)
allowed_models = Llm::ConfigService.models_for_feature(feature_key)
errors.add(:captain_models, "'#{model_name}' is not a valid model for #{feature_key}. Allowed: #{allowed_models.join(', ')}")
end
end
def notify_creation
Rails.configuration.dispatcher.dispatch(ACCOUNT_CREATED, Time.zone.now, account: self)
end
+43 -1
View File
@@ -4,8 +4,10 @@ module CaptainFeaturable
extend ActiveSupport::Concern
included do
validate :validate_captain_models
# Dynamically define accessor methods for each captain feature
Llm::ConfigService.feature_keys.each do |feature_key|
Llm::Models.feature_keys.each do |feature_key|
# Define enabled? methods (e.g., captain_editor_enabled?)
define_method("captain_#{feature_key}_enabled?") do
captain_features_with_defaults[feature_key]
@@ -17,4 +19,44 @@ module CaptainFeaturable
end
end
end
def captain_preferences
{
models: captain_models_with_defaults,
features: captain_features_with_defaults
}.with_indifferent_access
end
private
def captain_models_with_defaults
stored_models = captain_models || {}
Llm::Models.feature_keys.each_with_object({}) do |feature_key, result|
stored_value = stored_models[feature_key]
result[feature_key] = if stored_value.present? && Llm::Models.valid_model_for?(feature_key, stored_value)
stored_value
else
Llm::Models.default_model_for(feature_key)
end
end
end
def captain_features_with_defaults
stored_features = captain_features || {}
Llm::Models.feature_keys.index_with do |feature_key|
stored_features[feature_key] == true
end
end
def validate_captain_models
return if captain_models.blank?
captain_models.each do |feature_key, model_name|
next if model_name.blank?
next if Llm::Models.valid_model_for?(feature_key, model_name)
allowed_models = Llm::Models.models_for(feature_key)
errors.add(:captain_models, "'#{model_name}' is not a valid model for #{feature_key}. Allowed: #{allowed_models.join(', ')}")
end
end
end
-81
View File
@@ -1,81 +0,0 @@
# frozen_string_literal: true
module Llm::ConfigService
CONFIG_PATH = Rails.root.join('config/llm.yml').freeze
class << self
def config
@config ||= load_config
end
def reload!
@config = load_config
end
%w[providers models features].each do |key|
define_method(key) do
config[key] || {}
end
end
def models_for_feature(feature_key)
features.dig(feature_key.to_s, 'models') || []
end
def default_model_for_feature(feature_key)
features.dig(feature_key.to_s, 'default')
end
def model_info(model_name)
models[model_name.to_s]
end
def provider_info(provider_name)
providers[provider_name.to_s]
end
def feature_keys
features.keys
end
def valid_model_for_feature?(feature_key, model_name)
allowed_models = models_for_feature(feature_key)
allowed_models.include?(model_name.to_s)
end
def feature_config(feature_key)
feature = features[feature_key.to_s]
return nil unless feature
{
models: feature['models'].filter_map do |model_name|
model = models[model_name]
next nil unless model
{
id: model_name,
display_name: model['display_name'],
provider: model['provider'],
coming_soon: model['coming_soon'],
credit_multiplier: model['credit_multiplier']
}
end,
default: feature['default']
}
end
def all_features_config
features.keys.index_with do |feature_key|
feature_config(feature_key)
end
end
private
def load_config
return {} unless File.exist?(CONFIG_PATH)
YAML.load_file(CONFIG_PATH) || {}
end
end
end
+43
View File
@@ -0,0 +1,43 @@
# frozen_string_literal: true
module Llm::Models
CONFIG = YAML.load_file(Rails.root.join('config/llm.yml')).freeze
class << self
def providers = CONFIG['providers']
def models = CONFIG['models']
def features = CONFIG['features']
def feature_keys = CONFIG['features'].keys
def default_model_for(feature)
CONFIG.dig('features', feature.to_s, 'default')
end
def models_for(feature)
CONFIG.dig('features', feature.to_s, 'models') || []
end
def valid_model_for?(feature, model_name)
models_for(feature).include?(model_name.to_s)
end
def feature_config(feature_key)
feature = features[feature_key.to_s]
return nil unless feature
{
models: feature['models'].map do |model_name|
model = models[model_name]
{
id: model_name,
display_name: model['display_name'],
provider: model['provider'],
coming_soon: model['coming_soon'],
credit_multiplier: model['credit_multiplier']
}
end,
default: feature['default']
}
end
end
end
+13 -13
View File
@@ -7,13 +7,13 @@ RSpec.describe CaptainFeaturable do
describe 'dynamic method generation' do
it 'generates enabled? methods for all features' do
Llm::ConfigService.feature_keys.each do |feature_key|
Llm::Models.feature_keys.each do |feature_key|
expect(account).to respond_to("captain_#{feature_key}_enabled?")
end
end
it 'generates model accessor methods for all features' do
Llm::ConfigService.feature_keys.each do |feature_key|
Llm::Models.feature_keys.each do |feature_key|
expect(account).to respond_to("captain_#{feature_key}_model")
end
end
@@ -22,7 +22,7 @@ RSpec.describe CaptainFeaturable do
describe 'feature enabled methods' do
context 'when no features are explicitly enabled' do
it 'returns false for all features' do
Llm::ConfigService.feature_keys.each do |feature_key|
Llm::Models.feature_keys.each do |feature_key|
expect(account.send("captain_#{feature_key}_enabled?")).to be false
end
end
@@ -50,7 +50,7 @@ RSpec.describe CaptainFeaturable do
end
it 'returns false for all features' do
Llm::ConfigService.feature_keys.each do |feature_key|
Llm::Models.feature_keys.each do |feature_key|
expect(account.send("captain_#{feature_key}_enabled?")).to be false
end
end
@@ -60,8 +60,8 @@ RSpec.describe CaptainFeaturable do
describe 'model accessor methods' do
context 'when no models are explicitly configured' do
it 'returns default models for all features' do
Llm::ConfigService.feature_keys.each do |feature_key|
expected_default = Llm::ConfigService.default_model_for_feature(feature_key)
Llm::Models.feature_keys.each do |feature_key|
expected_default = Llm::Models.default_model_for(feature_key)
expect(account.send("captain_#{feature_key}_model")).to eq(expected_default)
end
end
@@ -83,8 +83,8 @@ RSpec.describe CaptainFeaturable do
end
it 'returns default models for unconfigured features' do
expect(account.captain_copilot_model).to eq(Llm::ConfigService.default_model_for_feature('copilot'))
expect(account.captain_audio_transcription_model).to eq(Llm::ConfigService.default_model_for_feature('audio_transcription'))
expect(account.captain_copilot_model).to eq(Llm::Models.default_model_for('copilot'))
expect(account.captain_audio_transcription_model).to eq(Llm::Models.default_model_for('audio_transcription'))
end
end
@@ -94,7 +94,7 @@ RSpec.describe CaptainFeaturable do
end
it 'falls back to default model' do
expect(account.captain_editor_model).to eq(Llm::ConfigService.default_model_for_feature('editor'))
expect(account.captain_editor_model).to eq(Llm::Models.default_model_for('editor'))
end
end
@@ -104,8 +104,8 @@ RSpec.describe CaptainFeaturable do
end
it 'returns default models for all features' do
Llm::ConfigService.feature_keys.each do |feature_key|
expected_default = Llm::ConfigService.default_model_for_feature(feature_key)
Llm::Models.feature_keys.each do |feature_key|
expected_default = Llm::Models.default_model_for(feature_key)
expect(account.send("captain_#{feature_key}_model")).to eq(expected_default)
end
end
@@ -117,7 +117,7 @@ RSpec.describe CaptainFeaturable do
account.update!(captain_features: { 'editor' => true, 'copilot' => true })
prefs = account.captain_preferences
Llm::ConfigService.feature_keys.each do |feature_key|
Llm::Models.feature_keys.each do |feature_key|
expect(account.send("captain_#{feature_key}_enabled?")).to eq(prefs[:features][feature_key])
end
end
@@ -126,7 +126,7 @@ RSpec.describe CaptainFeaturable do
account.update!(captain_models: { 'editor' => 'gpt-4.1-mini', 'assistant' => 'gpt-5.2' })
prefs = account.captain_preferences
Llm::ConfigService.feature_keys.each do |feature_key|
Llm::Models.feature_keys.each do |feature_key|
expect(account.send("captain_#{feature_key}_model")).to eq(prefs[:models][feature_key])
end
end