Helix Core is now P4 and Helix Swarm is now P4 Code Review. Product user interfaces will be updated soon. Learn more >>

Documentation | 2025.5

# How to write the Custom AI Adapter in P4 Code Review to support an in-house AI model

Use this guide to write and integrate a Custom AI Adapter for your in-house or on-premise AI vendor with P4 Code Review.

Currently, the Custom AI Adapter has been tested with the following AI vendors and models:

- `lmstudio-community/gemma-3-1B-it-qat-GGUF (/v1/completions)`

- `granite-embedding-278m-multilingual-GGUF (/v1/embeddings)`

If you are developing a Custom AI Adapter for a different vendor or model, additional configuration may be required. Refer to the vendor's documentation for specific configuration details.

We strongly recommend implementing and testing it first in a sandbox environment, test account, or a new instance of P4 Code Review before integrating it into your production environment.

To write a Custom AI Adapter in P4 Code Review, complete these three stages:

- Copy and modify GenericAIAdapter

- Validate the response

- Add custom AI adapter to config files

To review the terms for using your own API keys, download the API Key Usage Agreement for Perforce P4 Code Review.

## Copy and modify GenericAIAdapter

1. Go to the folder `module/AiAnalysis/src/Service/` and create the copy of file `GenericAIAdapter.php` within the same folder.

2. Name the copied file `CustomAIAdapter.php`. Below is an example of `GenericAIAdapter.php`.

   Copy

       <?php
       /**
        * Perforce Swarm
        *
        * @copyright   2024 Perforce Software. All rights reserved.
        * @license     Please see LICENSE.txt in top-level folder of this distribution.
        * @version     <release>/<patch>
        */

       namespace AiAnalysis\Service;

       use AiAnalysis\Filter\IAiAnalysis;
       use AiAnalysis\Helper\IAiAnalysisHelper;
       use Application\Config\ConfigException;
       use Application\Config\ConfigManager;
       use Application\Config\IConfigDefinition;
       use Application\Log\SwarmLogger;
       use Laminas\Http\Response;
       use OpenAI;

       class GenericAIAdapter extends AbstractAiAdapter implements AiServiceInterface
       {

           const LOG_PREFIX = GenericAIAdapter::class;

           /**
            * performAI, note this uses the content body and ignores any data provided
            * Login to Swarm using the credentials provided
            *
            * @param mixed $data
            * @return array
            */
           public function executeAIRequest(mixed $data): array
           {
               $logger = $this->services->get(SwarmLogger::SERVICE);
               try {
                   $fileName        = ($data[IAiAnalysis::FILE_NAME]) ?
                       'of the file: ' . $data[IAiAnalysis::FILE_NAME] . ' ' : ' ';
                   $content         = $data[IConfigDefinition::AI_PACKAGE_TYPE] . $fileName .
                       $data[IAiAnalysisHelper::CONTENT_TO_ANALYZE];
                   $args['content'] = $content;
                   $result          = $this->request($this->getApiUrl(), false, $args);

                   if ($this->validateResult($result)) {
                       $logger->trace(sprintf("[%s]: Content generation successful ", self::LOG_PREFIX));
                       return [IAiAnalysisHelper::CONTENT_GENERATED => $result->choices[0]->message->content,
                           self::ERROR => null,
                           self::CODE => Response::STATUS_CODE_200];
                   } else {
                       $logger->trace(sprintf("[%s]: Failed at content generation ", self::LOG_PREFIX));
                       return [IAiAnalysisHelper::CONTENT_GENERATED => '',
                           self::ERROR => is_object($result) && property_exists($result, 'error') ? $result->error :
                               'Response not received from AI Vendor',
                           self::CODE => is_object($result) && property_exists($result, 'code') ? $result->code :
                               Response::STATUS_CODE_500];
                   }
               } catch (\Exception | \Throwable $exception) {
                   $logger->debug(
                       sprintf(
                           "[%s]: Error occurred at Generic AI Adapter %s",
                           self::LOG_PREFIX,
                           $exception->getMessage()
                       )
                   );
                   $statusCode = $exception->getCode();
                   return [
                       IAiAnalysisHelper::CONTENT_GENERATED => null,
                       'error' => $exception->getMessage(),
                       'code' => $statusCode === 0 ? Response::STATUS_CODE_500 : $statusCode,
                   ];
               }
           }

           /**
            * This method will fetch the API key from config, required to execute the generic AI request
            * @throws ConfigException
            * @return string
            */
           private function getApiUrl() : string
           {
               $config = $this->services->get(IConfigDefinition::CONFIG);
               return ConfigManager::getValue($config, IConfigDefinition::AI_REVIEW_AI_VENDORS_AI_MODEL1_API_END_POINT);
           }
       }

3. In `CustomAIAdapter.php`, change the `class` from `GenericAIAdapter` to `CustomAIAdapter`.

4. In `CustomAIAdapter.php`, within the `executeAIRequest` function, replace `$arg` with the request format used by your chosen AI model. By default, this is `$args['content'] = $content;` however, below is an example of a different `$arg` request format.

   Copy

       $args = [
            'messages' => [
               [
                   'role' => 'user',
                   'content' => $content
               ],
           ],
       ];
       //Note here $content is prompt ( $data[IConfigDefinition::AI_PACKAGE_TYPE] ) + filename with its extension + diff from review

5. Save the changes to in `CustomAIAdapter.php` and build the request. The following line of code in `CustomAIAdapter.php` sends the request to the API endpoint (`api_end_point`) specified in `config.php` file.

   Copy

       $result = $this->request($this->getApiUrl(), false, $args);

## Validate the response

1. Go to the folder `module/AiAnalysis/src/Service/`, open the file `AbstractAiAdapter.php` and copy the function `validateResult`.

2. Open `CustomAIAdapter.php` and overwrite `validateResult` with the copied function.

3. Modify the copied function in `CustomAIAdapter.php` with your AI model’s response format. An example of an unmodified `validateResult` function is as follows:

   Copy

       protected function validateResult(object $result): bool
       {
           if (!property_exists($result, 'error') &&
               property_exists($result, 'choices') &&
               isset($result->choices[0]) && is_object($result->choices[0]) &&
               property_exists($result->choices[0], 'message') &&
               is_object($result->choices[0]->message) &&
               property_exists($result->choices[0]->message, 'content')) {
               return true;
           }
           return false;
       }

   If your AI model response is not a object, you need to modify the `validateResult` function in `CustomAIAdapter.php` so that is can handle the response.

   For example, if the AI vendor returns the response as an array, update the `validateResult` function to parse the response. An example of an updated `validateResult` function that can handle the response as an array is shown below.

   Copy

       protected function validateAIResponse(array $result): bool
       {
           if (is_array($result) && isset($result[0]) && is_object($result[0]) &&
               property_exists($result[0], 'embedding'))
           {
               return true;
           }
           return false;
       }

4. If your AI model’s response format is different from the example above, modify the format accordingly. You will also need to update `IAiAnalysisHelper` within the `CustomAIAdapter.php` file. An example of different AI response format is shown below:

   Copy

       {
         "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT",
         "choices": [
           {
             "index": 0,
             "message": {
               "role": "assistant",
               "content": "Hello! How can I assist you today?",
               "refusal": null,
               "annotations": []
             },
           }
         ],
       }

5. Update the `Services.php` file located in `module/Application/src/Config/` to include an entry for `CustomAIAdapter` alongside the other AI adapters. Below is an example for the AI adapter entries in this file:

   Copy

       const LM_STUDIO_AI = 'lmStudioAI';
       const CUSTOM_AI = 'customAI';

6. Update the `AiServiceFactory.php` file located in `module/AiAnalysis/src/Factory/` to include an entry for `CustomAIAdapter` alongside the other AI adapters. Below is an example for the AI adapter entries in this file:

   Copy

           const OPENAIADAPTER     = 'openAI';
           const GENERICAIADAPTER  = 'genericAI';
           const LMSTUDIOAIADAPTER = 'lmStudioAI';
           const CUSTOMAIADAPTER = 'customAI';

## Add custom AI adapter to config files

Two config files require updating with entries for the custom AI adapter. These files are:

- The `module.config.php` file found in module/AiAnalysis/config/.

- The `config.php` file found in `SWARM_ROOT/data/`.

To update the files, follow these steps:

1. Open the `module.config.php` file found in module/AiAnalysis/config/.

2. At the top of the `module.config.php` file, add a `use` statement for the `CustomAIAdapter` above the current `use` statement for the `GenericAIAdapter`. Below is an example of these `use` statements:

   Copy

       use AiAnalysis\Service\CustomAIAdapter;
       use AiAnalysis\Service\GenericAIAdapter;

3. Inside the same `module.config.php` file, find the `service_manager` array and add the following entries.

   In the `aliases` array, add:

   - `AiServiceFactory::CUSTOMAIADAPTER => CustomAIAdapter::class`

   In the `factories` array, add:

   - `CustomAIAdapter::class => AiServiceFactory::class`
   - `Services::CUSTOM_AI => CustomAIAdapter::class`

   Below is an example of the updated `service_manager` array in the `module.config.php` file.

   Copy

       'service_manager' => [
           'aliases' => [
               IDao::AI_ANALYSIS_DAO  => AiAnalysisDAO::class,
               AiServiceFactory::OPENAIADAPTER => OpenAIAdapter::class,
               AiServiceFactory::GENERICAIADAPTER => GenericAIAdapter::class,
               AiServiceFactory::LMSTUDIOAIADAPTER => LMStudioAIAdapter::class,
               IAiAnalysis::NAME => AiAnalysis::class,
               IAiAnalysis::DISCARD_ANALYSIS_FILTER => DiscardAnalysis::class,
               AiServiceFactory::CUSTOMAIADAPTER => CustomAIAdapter::class
           ],
           'factories' => [
                   AiAnalysisDAO::class => InvokableServiceFactory::class,
                   OpenAIAdapter::class => AiServiceFactory::class,
                   Services::OPEN_AI => OpenAIAdapter::class,
                   GenericAIAdapter::class => AiServiceFactory::class,
                   Services::GENERIC_AI => GenericAIAdapter::class,
                   LMStudioAIAdapter::class => AiServiceFactory::class,
                   Services::LM_STUDIO_AI => LMStudioAIAdapter::class,
                   AiAnalysis::class => InvokableServiceFactory::class,
                   AiAnalysisChecker::class => InvokableServiceFactory::class,
                   AiAnalysisCharLimitChecker::class => InvokableServiceFactory::class,
                   AiAnalysisDataRetentionLifetimeChecker::class => InvokableServiceFactory::class,
                   DiscardAnalysis::class => InvokableServiceFactory::class,
                   CustomAIAdapter::class => AiServiceFactory::class,
                   Services::CUSTOM_AI => CustomAIAdapter::class,
           ],
       ],

4. Open `config.php` file found in `SWARM_ROOT/data/`.

5. Update the `ai_review` module with the values for the custom AI Adapter.

   Make sure you update `api_end_point` with your AI vendor end point .

   Below is an example of a modified `ai_review` module:

   Copy

       'ai_review' => array(
           // Please read through the https://www.perforce.com/generative-ai-policy before you enable this feature
           'enabled'    => true,
           'data_retention_lifetime' => '150 days',
           'timeout' => 500,
           'ai_vendors' => array(
               'ai_model1' => array(
                   'ai_vendor' => 'customAI',
                   'ai_package_id' => '1', // id should be one & should not modify it
                   'ai_package_key' => 'CustomAIPackage', // This is for future purposes when we will be supporting multiple models & will have an AI configuration page on UI
                   'ai_package_value' => 'Custom AI', // This is used for displaying the model type on the AI vendor response summary
                   'ai_package_type' => "Explain the following code ", // This is prompt type
                   'api_key' => '$SECRET_KEY', // Add in the API-key for you AI vendor
                   'api_end_point' => 'http://myAIVendorAPI/chat/completion', //Add an optional API endpoint
                   'ai_min_char_limit' => 10,
                   'ai_max_char_limit' => 70000
               ),
           ),
       ),

After completing these steps, a custom AI adapter has been added to P4 Code Review that supports your in-house AI model.

Your search for returned results