From ae3687364dcee755f73d759a213dc667984e4529 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 23 Dec 2025 19:38:01 +0530 Subject: [PATCH] feat: abstract models --- .../v1/accounts/captain/config_controller.rb | 14 ++-- app/models/account.rb | 39 --------- app/models/concerns/captain_featurable.rb | 44 +++++++++- lib/llm/config_service.rb | 81 ------------------- lib/llm/models.rb | 43 ++++++++++ .../concerns/captain_featurable_spec.rb | 26 +++--- 6 files changed, 106 insertions(+), 141 deletions(-) delete mode 100644 lib/llm/config_service.rb create mode 100644 lib/llm/models.rb diff --git a/app/controllers/api/v1/accounts/captain/config_controller.rb b/app/controllers/api/v1/accounts/captain/config_controller.rb index c028c89c5..3fcda5d72 100644 --- a/app/controllers/api/v1/accounts/captain/config_controller.rb +++ b/app/controllers/api/v1/accounts/captain/config_controller.rb @@ -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 diff --git a/app/models/account.rb b/app/models/account.rb index 4744843c2..dad5bc18d 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -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 diff --git a/app/models/concerns/captain_featurable.rb b/app/models/concerns/captain_featurable.rb index 23795c09b..af73fded3 100644 --- a/app/models/concerns/captain_featurable.rb +++ b/app/models/concerns/captain_featurable.rb @@ -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 diff --git a/lib/llm/config_service.rb b/lib/llm/config_service.rb deleted file mode 100644 index 73ee53370..000000000 --- a/lib/llm/config_service.rb +++ /dev/null @@ -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 diff --git a/lib/llm/models.rb b/lib/llm/models.rb new file mode 100644 index 000000000..a5db741ea --- /dev/null +++ b/lib/llm/models.rb @@ -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 diff --git a/spec/models/concerns/captain_featurable_spec.rb b/spec/models/concerns/captain_featurable_spec.rb index 5b12c257d..7221ad055 100644 --- a/spec/models/concerns/captain_featurable_spec.rb +++ b/spec/models/concerns/captain_featurable_spec.rb @@ -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