feat: abstract models
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user