Introduce AI review chat integration with ChatGPT

Change-Id: I585b7d75b55ba51686dd2d86f932e599d3ed1efd
diff --git a/ai/ai-review-agent-chatgpt-1.0.groovy b/ai/ai-review-agent-chatgpt-1.0.groovy
new file mode 100644
index 0000000..6de8861
--- /dev/null
+++ b/ai/ai-review-agent-chatgpt-1.0.groovy
@@ -0,0 +1,135 @@
+// Copyright (C) 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import com.gerritforge.gerrit.plugins.ai.provider.api.*
+
+import com.google.common.flogger.FluentLogger
+import com.google.gerrit.extensions.registration.DynamicSet
+import com.google.inject.*
+
+import org.apache.http.*
+import org.apache.http.message.*
+import org.apache.http.entity.StringEntity
+
+import java.nio.charset.StandardCharsets
+import org.apache.http.HttpHeaders
+
+import groovy.json.*
+
+@Singleton
+class AiChatGptReviewProvider implements AiReviewProvider {
+    private static final FluentLogger logger = FluentLogger.forEnclosingClass()
+    private static final String OPENAI_API_URL = 'https://api.openai.com/v1'
+    private static final int MAX_ERROR_LEN = 500
+    private static final float CREATIVITY_TEMPERATURE = 0.7
+
+    final String displayName = 'ChatGPT'
+
+    @Inject
+    private AiHttpClient http
+
+    @Override
+    Set<String> getModels(String apiKey) {
+        try {
+            http.get("${OPENAI_API_URL}/models",
+                    [authHeader(apiKey)] as Header[],
+                    { extractErrorMessage(it) },
+                    { extractModelsList(it) }) as Set
+        } catch (IOException | JsonException e) {
+            logger.atWarning().withCause(e).log("Failed to fetch OpenAI models")
+            [] as Set
+        }
+    }
+
+    @Override
+    String review(String apiKey, String model, String prompt) {
+        try {
+            def requestPayload = [
+                    model: model,
+                    input: prompt
+            ]
+            if (!isReasoningModel(model)) {
+                requestPayload.temperature = CREATIVITY_TEMPERATURE
+            }
+            http.post("${OPENAI_API_URL}/responses",
+                    [http.contentTypeApplicationJson(), authHeader(apiKey)] as Header[],
+                    new StringEntity(new JsonBuilder(requestPayload).toString(), StandardCharsets.UTF_8),
+                    { extractErrorMessage(it) },
+                    { extractResponseText(it) })
+        } catch (IOException | JsonException e) {
+            logger.atWarning().withCause(e).log('Failed to call OpenAI API (model=%s)', model)
+            throw new IllegalStateException('Failed to call OpenAI API', e)
+        }
+    }
+
+    private static boolean isReasoningModel(String model) {
+        model.startsWith('o1') || model.startsWith('o3')
+    }
+
+    private static Header authHeader(String apiKey) {
+        new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer $apiKey")
+    }
+
+    private static Set<String> extractModelsList(String body) {
+        def json = new JsonSlurper().parseText(body)
+
+        if (!json.data) {
+            throw new IOException('OpenAI API response contains no data array for models')
+        }
+
+        return json.data.collect { it.id as String }
+                .findAll { it.startsWith('gpt-') } as Set
+    }
+
+    private static String extractResponseText(String body) {
+        def json = new JsonSlurper().parseText(body)
+
+        def textContent = new StringBuilder()
+
+        json.output?.each { item ->
+            if (item.type == 'message' && item.role == 'assistant') {
+                item.content?.each { contentBlock ->
+                    if (contentBlock.type == 'output_text') {
+                        textContent.append(contentBlock.text)
+                    }
+                }
+            }
+        }
+
+        if (textContent.length() == 0) {
+            throw new IOException('OpenAI API response contains no valid text output')
+        }
+
+        return textContent.toString().trim()
+    }
+
+    private static String extractErrorMessage(String body) {
+        try {
+            def json = new JsonSlurper().parseText(body)
+            if (json?.error) return "[${json.error.code ?: 'Error'}] ${json.error.message}"
+        } catch (Exception e) {
+            logger.atWarning().withCause(e).log('Failed to parse error response')
+        }
+        return body.length() > MAX_ERROR_LEN ? "${body.take(MAX_ERROR_LEN)}..." : body
+    }
+}
+
+class AiChatGptModule extends AbstractModule {
+    @Override
+    protected void configure() {
+        DynamicSet.bind(binder(), AiReviewProvider).to(AiChatGptReviewProvider)
+    }
+}
+
+module = AiChatGptModule
\ No newline at end of file
diff --git a/ai/ai-review-agent-chatgpt.md b/ai/ai-review-agent-chatgpt.md
new file mode 100644
index 0000000..da06166
--- /dev/null
+++ b/ai/ai-review-agent-chatgpt.md
@@ -0,0 +1,35 @@
+# Gerrit AI Review Agent for ChatGPT
+
+Implementation of Gerrit's AI Code Review Agent API on top of OpenAI's ChatGPT.
+
+[Install](#install-in-gerrit) this plugin and enable the Gerrit AI chat to enjoy
+a side-by-side collaboration with ChatGPT on the Change screen.
+
+## License
+
+This script is licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+## How to use
+
+### Prerequisites
+
+Gerrit v3.14 or later with the following additional plugins:
+
+- [Groovy scripting provider](https://github.com/GerritForge/groovy-provider/)
+- [GerritForge's AI Review Agent Provider](https://github.com/GerritForge/ai-review-agent-provider)
+
+### [Install in Gerrit](#install-in-gerrit)
+
+Copy the `ai-review-agent-chatgpt-1.0.groovy` script into your Gerrit site (`$GERRIT_SITE`)
+plugins' directory.
+
+```bash
+cp ai-review-agent-chatgpt-1.0.groovy "$GERRIT_SITE/plugins/"
+```
\ No newline at end of file