diff --git a/ee/lib/gitlab/llm/ai_gateway/completions/base.rb b/ee/lib/gitlab/llm/ai_gateway/completions/base.rb
index ab869ed71e8e9090dff1e07bcd37941b86e839f1..ba33a26784e628ad4dcfc4251fa90000d4654f1d 100644
--- a/ee/lib/gitlab/llm/ai_gateway/completions/base.rb
+++ b/ee/lib/gitlab/llm/ai_gateway/completions/base.rb
@@ -5,28 +5,53 @@ module Llm
     module AiGateway
       module Completions
         class Base < Llm::Completions::Base
+          DEFAULT_ERROR = 'An unexpected error has occurred.'
+          RESPONSE_MODIFIER = ResponseModifiers::Base
+
           def execute
+            return unless valid?
+
             response = request!
-            response_modifier = ResponseModifiers::Base.new(response)
+            response_modifier = self.class::RESPONSE_MODIFIER.new(post_process(response))
 
             ::Gitlab::Llm::GraphqlSubscriptionResponseService.new(
               user, resource, response_modifier, options: response_options
             ).execute
           end
 
+          # Subclasses must implement this method returning a Hash with all the needed input.
+          # An `ArgumentError` can be emitted to signal an error extracting data from the `prompt_message`
           def inputs
             raise NotImplementedError
           end
 
           private
 
+          # Can be overwritten by child classes to perform additional validations
+          def valid?
+            true
+          end
+
+          # Can be used by subclasses to perform additional steps or transformations before returning the response data
+          def post_process(response)
+            response
+          end
+
           def request!
             ai_client = ::Gitlab::Llm::AiGateway::Client.new(user, service_name: prompt_message.ai_action.to_sym,
               tracking_context: tracking_context)
-            ai_client.complete(
+            response = ai_client.complete(
               url: "#{::Gitlab::AiGateway.url}/v1/prompts/#{prompt_message.ai_action}",
               body: { 'inputs' => inputs }
             )
+
+            Gitlab::Json.parse(response.body)
+          rescue ArgumentError => e
+            { 'detail' => e.message }
+          rescue StandardError => e
+            Gitlab::ErrorTracking.track_exception(e, ai_action: prompt_message.ai_action)
+
+            { 'detail' => DEFAULT_ERROR }
           end
         end
       end
diff --git a/ee/lib/gitlab/llm/ai_gateway/response_modifiers/base.rb b/ee/lib/gitlab/llm/ai_gateway/response_modifiers/base.rb
index 6e7ff67aa72cf5b41ab55b5a01a8a09b3a8b69aa..6166a4029821e30f546772975242cb15ad4aafc0 100644
--- a/ee/lib/gitlab/llm/ai_gateway/response_modifiers/base.rb
+++ b/ee/lib/gitlab/llm/ai_gateway/response_modifiers/base.rb
@@ -5,23 +5,29 @@ module Llm
     module AiGateway
       module ResponseModifiers
         class Base < Gitlab::Llm::BaseResponseModifier
+          extend ::Gitlab::Utils::Override
+
           def initialize(ai_response)
-            @ai_response = Gitlab::Json.parse(ai_response.body)
+            @ai_response = ai_response
           end
 
+          override :response_body
           def response_body
             ai_response
           end
 
+          override :errors
           def errors
             # On success, the response is just a plain JSON string
-            @errors ||= if ai_response.is_a?(String)
-                          []
-                        else
-                          detail = ai_response&.dig('detail')
+            @errors ||= ai_response.is_a?(String) ? [] : error_from_response
+          end
+
+          private
+
+          def error_from_response
+            detail = ai_response['detail']
 
-                          [detail.is_a?(String) ? detail : detail&.dig(0, 'msg')].compact
-                        end
+            [detail.is_a?(String) ? detail : detail&.dig(0, 'msg')].compact
           end
         end
       end
diff --git a/ee/spec/lib/gitlab/llm/ai_gateway/completions/base_spec.rb b/ee/spec/lib/gitlab/llm/ai_gateway/completions/base_spec.rb
index 875442a5dc597e30fe9f7f6c6fc79e8be0ffae88..c05a386c5bed84061660cd7b1d54606b6806a734 100644
--- a/ee/spec/lib/gitlab/llm/ai_gateway/completions/base_spec.rb
+++ b/ee/spec/lib/gitlab/llm/ai_gateway/completions/base_spec.rb
@@ -3,13 +3,15 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::Llm::AiGateway::Completions::Base, feature_category: :ai_abstraction_layer do
-  let(:subclass) { Class.new(described_class) }
   let(:user) { build(:user) }
   let(:resource) { build(:issue) }
   let(:ai_action) { 'test_action' }
   let(:prompt_message) { build(:ai_message, ai_action: ai_action, user: user, resource: resource) }
   let(:inputs) { { prompt: "What's your name?" } }
-  let(:response) { instance_double(HTTParty::Response, body: "I'm Duo!") }
+  let(:response) { "I'm Duo" }
+  let(:http_response) { instance_double(HTTParty::Response, body: %("#{response}")) }
+  let(:processed_repsonse) { response }
+  let(:response_modifier_class) { Gitlab::Llm::AiGateway::ResponseModifiers::Base }
   let(:response_modifier) { instance_double(Gitlab::Llm::AiGateway::ResponseModifiers::Base) }
   let(:response_service) { instance_double(Gitlab::Llm::GraphqlSubscriptionResponseService) }
   let(:tracking_context) { { action: ai_action, request_id: prompt_message.request_id } }
@@ -18,35 +20,107 @@
     prompt_message.to_h.slice(:request_id, :client_subscription_id, :ai_action, :agent_version_id)
   end
 
+  let(:subclass) do
+    prompt_inputs = inputs
+
+    Class.new(described_class) do
+      define_method :inputs do
+        prompt_inputs
+      end
+    end
+  end
+
   subject(:completion) { subclass.new(prompt_message, nil) }
 
   describe 'required methods' do
+    let(:subclass) { Class.new(described_class) }
+
     it { expect { completion.inputs }.to raise_error(NotImplementedError) }
   end
 
   describe '#execute' do
     before do
-      allow(completion).to receive(:inputs).and_return(inputs)
-
       allow(Gitlab::Llm::AiGateway::Client).to receive(:new)
         .with(user, service_name: ai_action.to_sym, tracking_context: tracking_context).and_return(client)
-      allow(client).to receive(:complete).with(url: "#{Gitlab::AiGateway.url}/v1/prompts/#{ai_action}",
-        body: { 'inputs' => inputs })
-        .and_return(response)
-      allow(Gitlab::Llm::AiGateway::ResponseModifiers::Base).to receive(:new).with(response)
-        .and_return(response_modifier)
-      allow(Gitlab::Llm::GraphqlSubscriptionResponseService).to receive(:new)
-        .with(user, resource, response_modifier, options: response_options).and_return(response_service)
     end
 
     let(:result) { { status: :success } }
 
     subject(:execute) { completion.execute }
 
-    it 'executes the response service and returns its result' do
-      expect(response_service).to receive(:execute).and_return(result)
+    shared_examples 'executing successfully' do
+      it 'executes the response service and returns its result' do
+        if http_response
+          expect(client).to receive(:complete).with(url: "#{Gitlab::AiGateway.url}/v1/prompts/#{ai_action}",
+            body: { 'inputs' => inputs })
+            .and_return(http_response)
+        end
+
+        expect(response_modifier_class).to receive(:new).with(processed_repsonse)
+          .and_return(response_modifier)
+        expect(Gitlab::Llm::GraphqlSubscriptionResponseService).to receive(:new)
+          .with(user, resource, response_modifier, options: response_options).and_return(response_service)
+        expect(response_service).to receive(:execute).and_return(result)
+
+        is_expected.to be(result)
+      end
+    end
+
+    it_behaves_like 'executing successfully'
+
+    context 'when the completion is not valid' do
+      before do
+        subclass.define_method(:valid?) { false }
+      end
+
+      it 'returns nil without making a request' do
+        expect(client).not_to receive(:complete)
+
+        expect(execute).to be_nil
+      end
+    end
+
+    context 'when the subclass raises an ArgumentError when gathering inputs' do
+      let(:http_response) { nil }
+      let(:processed_repsonse) { { 'detail' => 'Something went wrong.' } }
+
+      before do
+        subclass.define_method(:inputs) { raise ArgumentError, 'Something went wrong.' }
+      end
+
+      # Note: The completion "executes successfully" in that it relays the error to the user via GraphQL, which we check
+      # by changing the `let(:processed_repsonse)` in this context
+      it_behaves_like 'executing successfully'
+    end
+
+    context 'when an unexpected error is raised' do
+      let(:processed_repsonse) { { 'detail' => 'An unexpected error has occurred.' } }
+
+      before do
+        allow(Gitlab::Json).to receive(:parse).and_raise(StandardError)
+      end
+
+      it_behaves_like 'executing successfully'
+    end
+
+    context 'when the subclass overrides the post_process method' do
+      let(:processed_repsonse) { response.upcase }
+
+      before do
+        subclass.define_method(:post_process) { |response| response.upcase }
+      end
+
+      it_behaves_like 'executing successfully'
+    end
+
+    context 'when the subclass overrides the response modifier' do
+      let(:response_modifier_class) { Class.new }
+
+      before do
+        subclass.const_set(:RESPONSE_MODIFIER, response_modifier_class)
+      end
 
-      expect(execute).to be(result)
+      it_behaves_like 'executing successfully'
     end
   end
 end
diff --git a/ee/spec/lib/gitlab/llm/ai_gateway/response_modifiers/base_spec.rb b/ee/spec/lib/gitlab/llm/ai_gateway/response_modifiers/base_spec.rb
index 4e0c3700edfb97b0a26e72d2341abaa907378de3..135614f51dfaf7d7b166f0228131e521cb709053 100644
--- a/ee/spec/lib/gitlab/llm/ai_gateway/response_modifiers/base_spec.rb
+++ b/ee/spec/lib/gitlab/llm/ai_gateway/response_modifiers/base_spec.rb
@@ -3,14 +3,12 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::Llm::AiGateway::ResponseModifiers::Base, feature_category: :ai_abstraction_layer do
-  let(:response) { "I'm GitLab Duo!" }
-  let(:response_body) { %("#{response}") }
-  let(:ai_response) { instance_double(HTTParty::Response, body: response_body) }
+  let(:ai_response) { %("I'm GitLab Duo") }
   let(:base_modifier) { described_class.new(ai_response) }
 
   describe '#response_body' do
-    it 'returns the parsed response body' do
-      expect(base_modifier.response_body).to eq(response)
+    it 'returns the response body' do
+      expect(base_modifier.response_body).to eq(ai_response)
     end
   end
 
@@ -25,7 +23,7 @@
       let(:error) { 'Error message' }
 
       context 'when the detail is an string' do
-        let(:response_body) { %({"detail": "#{error}"}) }
+        let(:ai_response) { { 'detail' => error } }
 
         it 'returns an array with the error message' do
           expect(base_modifier.errors).to eq([error])
@@ -33,7 +31,7 @@
       end
 
       context 'when the detail is an array' do
-        let(:response_body) { %({"detail": [{"msg": "#{error}"}]}) }
+        let(:ai_response) { { 'detail' => [{ 'msg' => error }] } }
 
         it 'returns an array with the error message' do
           expect(base_modifier.errors).to eq([error])