diff --git a/enterprise/lib/enterprise/captain/reply_suggestion_service.rb b/enterprise/lib/enterprise/captain/reply_suggestion_service.rb index 503dd095a..31f52ff1e 100644 --- a/enterprise/lib/enterprise/captain/reply_suggestion_service.rb +++ b/enterprise/lib/enterprise/captain/reply_suggestion_service.rb @@ -1,8 +1,8 @@ module Enterprise::Captain::ReplySuggestionService - def make_api_call(model:, messages:, tools: []) + def make_api_call(messages:, model: nil, feature: nil, schema: nil, tools: []) return super unless use_search_tool? - super(model: model, messages: messages, tools: [build_search_tool]) + super(messages: messages, model: model, feature: feature, schema: schema, tools: [build_search_tool]) end private diff --git a/lib/captain/base_task_service.rb b/lib/captain/base_task_service.rb index d382204a5..d098b24af 100644 --- a/lib/captain/base_task_service.rb +++ b/lib/captain/base_task_service.rb @@ -37,12 +37,13 @@ class Captain::BaseTaskService "#{endpoint}/v1" end - def make_api_call(model:, messages:, schema: nil, tools: []) + def make_api_call(messages:, model: nil, feature: nil, schema: nil, tools: []) # Community edition prerequisite checks # Enterprise module handles these with more specific error messages (cloud vs self-hosted) return { error: I18n.t('captain.disabled'), error_code: 403 } unless captain_tasks_enabled? return { error: I18n.t('captain.api_key_missing'), error_code: 401 } unless api_key_configured? + model = resolved_model(model: model, feature: feature) instrumentation_params = build_instrumentation_params(model, messages) instrumentation_method = tools.any? ? :instrument_tool_session : :instrument_llm_call @@ -55,6 +56,12 @@ class Captain::BaseTaskService response.merge(follow_up_context: build_follow_up_context(messages, response)) end + def resolved_model(model:, feature:) + return model if feature.blank? + + Llm::FeatureRouter.resolve(feature: feature, account: account)[:model] + end + def execute_ruby_llm_request(model:, messages:, schema: nil, tools: []) credential = llm_credential diff --git a/lib/captain/csat_utility_analysis_service.rb b/lib/captain/csat_utility_analysis_service.rb index 7aab18e6c..a29c52a1c 100644 --- a/lib/captain/csat_utility_analysis_service.rb +++ b/lib/captain/csat_utility_analysis_service.rb @@ -3,7 +3,7 @@ class Captain::CsatUtilityAnalysisService < Captain::BaseTaskService def perform api_response = make_api_call( - model: GPT_MODEL, + feature: 'editor', messages: [ { role: 'system', content: system_prompt }, { role: 'user', content: message } diff --git a/lib/captain/follow_up_service.rb b/lib/captain/follow_up_service.rb index c4c1225be..60b8e63b2 100644 --- a/lib/captain/follow_up_service.rb +++ b/lib/captain/follow_up_service.rb @@ -33,7 +33,7 @@ class Captain::FollowUpService < Captain::BaseTaskService { role: 'user', content: user_message } ] - response = make_api_call(model: GPT_MODEL, messages: messages) + response = make_api_call(feature: 'editor', messages: messages) return response if response[:error] response.merge(follow_up_context: update_follow_up_context(user_message, response[:message])) diff --git a/lib/captain/label_suggestion_service.rb b/lib/captain/label_suggestion_service.rb index a0e030963..6487fdca4 100644 --- a/lib/captain/label_suggestion_service.rb +++ b/lib/captain/label_suggestion_service.rb @@ -12,7 +12,7 @@ class Captain::LabelSuggestionService < Captain::BaseTaskService # Make API call response = make_api_call( - model: GPT_MODEL, # TODO: Use separate model for label suggestion + feature: 'label_suggestion', messages: [ { role: 'system', content: prompt_from_file('label_suggestion') }, { role: 'user', content: content } diff --git a/lib/captain/reply_suggestion_service.rb b/lib/captain/reply_suggestion_service.rb index 039bdcf26..7af014879 100644 --- a/lib/captain/reply_suggestion_service.rb +++ b/lib/captain/reply_suggestion_service.rb @@ -3,7 +3,7 @@ class Captain::ReplySuggestionService < Captain::BaseTaskService def perform make_api_call( - model: GPT_MODEL, + feature: 'editor', messages: [ { role: 'system', content: system_prompt }, { role: 'user', content: formatted_conversation } diff --git a/lib/captain/rewrite_service.rb b/lib/captain/rewrite_service.rb index 6f880e775..0d16613c1 100644 --- a/lib/captain/rewrite_service.rb +++ b/lib/captain/rewrite_service.rb @@ -36,7 +36,7 @@ class Captain::RewriteService < Captain::BaseTaskService def call_llm_with_prompt(system_content, user_content = content) make_api_call( - model: GPT_MODEL, + feature: 'editor', messages: [ { role: 'system', content: system_content }, { role: 'user', content: user_content } diff --git a/lib/captain/summary_service.rb b/lib/captain/summary_service.rb index f06aa42ca..fad60c0f5 100644 --- a/lib/captain/summary_service.rb +++ b/lib/captain/summary_service.rb @@ -3,7 +3,7 @@ class Captain::SummaryService < Captain::BaseTaskService def perform make_api_call( - model: GPT_MODEL, + feature: 'editor', messages: [ { role: 'system', content: system_prompt }, { role: 'user', content: conversation.to_llm_text(include_contact_details: false) } diff --git a/spec/lib/captain/base_task_service_spec.rb b/spec/lib/captain/base_task_service_spec.rb index 34c889967..7b67ab94a 100644 --- a/spec/lib/captain/base_task_service_spec.rb +++ b/spec/lib/captain/base_task_service_spec.rb @@ -167,6 +167,17 @@ RSpec.describe Captain::BaseTaskService do service.send(:make_api_call, model: model, messages: messages) end + it 'uses the resolved feature model for the request and instrumentation' do + account.update!(captain_models: { 'editor' => 'gpt-4.1' }) + + expect(mock_context).to receive(:chat).with(model: 'gpt-4.1').and_return(mock_chat) + expect(service).to receive(:instrument_llm_call).with( + hash_including(model: 'gpt-4.1', feature_name: 'test_event') + ).and_call_original + + service.send(:make_api_call, feature: 'editor', messages: messages) + end + it 'returns formatted response with tokens' do result = service.send(:make_api_call, model: model, messages: messages) diff --git a/spec/lib/captain/csat_utility_analysis_service_spec.rb b/spec/lib/captain/csat_utility_analysis_service_spec.rb index 34e0c9ece..70c07cdb2 100644 --- a/spec/lib/captain/csat_utility_analysis_service_spec.rb +++ b/spec/lib/captain/csat_utility_analysis_service_spec.rb @@ -25,6 +25,14 @@ RSpec.describe Captain::CsatUtilityAnalysisService do expect(result[:optimized_message]).to eq('Utility-safe message') expect(result[:message]).to eq('{"classification":"LIKELY_UTILITY","optimized_message":"Utility-safe message"}') end + + it 'routes through the editor feature' do + expect(service).to receive(:make_api_call).with( + hash_including(feature: 'editor') + ).and_return({ message: '{"classification":"LIKELY_UTILITY"}' }) + + service.perform + end end describe '#api_key' do diff --git a/spec/lib/captain/follow_up_service_spec.rb b/spec/lib/captain/follow_up_service_spec.rb index 9e330efdc..45535d574 100644 --- a/spec/lib/captain/follow_up_service_spec.rb +++ b/spec/lib/captain/follow_up_service_spec.rb @@ -42,6 +42,7 @@ RSpec.describe Captain::FollowUpService do context 'when follow-up context exists' do it 'constructs messages array with full conversation history' do expect(service).to receive(:make_api_call) do |args| + expect(args[:feature]).to eq('editor') messages = args[:messages] expect(messages).to match( diff --git a/spec/lib/captain/label_suggestion_service_spec.rb b/spec/lib/captain/label_suggestion_service_spec.rb index 0c40b103c..c8d9ed6c7 100644 --- a/spec/lib/captain/label_suggestion_service_spec.rb +++ b/spec/lib/captain/label_suggestion_service_spec.rb @@ -58,6 +58,7 @@ RSpec.describe Captain::LabelSuggestionService do it 'builds labels_with_messages format correctly' do expect(service).to receive(:make_api_call) do |args| + expect(args[:feature]).to eq('label_suggestion') user_message = args[:messages].find { |m| m[:role] == 'user' }[:content] expect(user_message).to include('Messages:') diff --git a/spec/lib/captain/reply_suggestion_service_spec.rb b/spec/lib/captain/reply_suggestion_service_spec.rb index a53825ee4..608db43a2 100644 --- a/spec/lib/captain/reply_suggestion_service_spec.rb +++ b/spec/lib/captain/reply_suggestion_service_spec.rb @@ -30,6 +30,12 @@ RSpec.describe Captain::ReplySuggestionService do end describe '#perform' do + it 'routes through the editor feature' do + expect(Llm::FeatureRouter).to receive(:resolve).with(feature: 'editor', account: account).and_call_original + + service.perform + end + it 'returns the suggested reply' do result = service.perform diff --git a/spec/lib/captain/rewrite_service_spec.rb b/spec/lib/captain/rewrite_service_spec.rb index 3c1d7997a..e4ef7efbf 100644 --- a/spec/lib/captain/rewrite_service_spec.rb +++ b/spec/lib/captain/rewrite_service_spec.rb @@ -29,6 +29,7 @@ RSpec.describe Captain::RewriteService do expect(service).to receive(:prompt_from_file).with('fix_spelling_grammar').and_return('Fix errors') expect(service).to receive(:make_api_call) do |args| + expect(args[:feature]).to eq('editor') expect(args[:messages][0][:content]).to eq('Fix errors') expect(args[:messages][1][:content]).to eq(content) { message: 'Fixed' } @@ -122,6 +123,7 @@ RSpec.describe Captain::RewriteService do it 'uses conversation context and draft message with Liquid template' do expect(service).to receive(:make_api_call) do |args| + expect(args[:feature]).to eq('editor') system_content = args[:messages][0][:content] expect(system_content).to include('Context:') diff --git a/spec/lib/captain/summary_service_spec.rb b/spec/lib/captain/summary_service_spec.rb index c5ec50687..def6daefe 100644 --- a/spec/lib/captain/summary_service_spec.rb +++ b/spec/lib/captain/summary_service_spec.rb @@ -21,9 +21,9 @@ RSpec.describe Captain::SummaryService do end describe '#perform' do - it 'passes correct model to API' do + it 'routes through the editor feature' do expect(service).to receive(:make_api_call).with( - hash_including(model: Captain::BaseTaskService::GPT_MODEL) + hash_including(feature: 'editor') ).and_call_original service.perform