Repository: microsoft/kiota (csharp)

Issue:
Kiota should support x-openai-isConsequential OpenAPI extension
PR opened to include x-openapi-isConsequential as part of the OpenAPI extensions supported by Kiota: https://github.com/microsoft/kiota/pull/4961

We should not remove the x-openapi-isConsequential as part of the slicing logic and no additional step is needed, just keep the OAS as part of the sliced OpenAPI.

Pull request:
Fixes https://github.com/microsoft/kiota/issues/4962

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fdb78e754..087d30fe2a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed a bug where the copilot teams toolkit integration would serialize empty declarative copilots. [#4974](https://github.com/microsoft/kiota/issues/4974)
 - Fixed a bug for the docker image where the volume path would not match the expected configuration for the description.
 - Fixed a bug in Go where certain namespaces were escaped unexpectedly. [#5012](https://github.com/microsoft/kiota/issues/5012)
+- Exclude the `x-openai-isConsequential` extension from cleanup. [#4962](https://github.com/microsoft/kiota/issues/4962)
 - Fixed file name and namespace sanitization when generating plugins. [#5019](https://github.com/microsoft/kiota/issues/5019)
 
 ## [1.16.0] - 2024-07-05
diff --git a/src/Kiota.Builder/Extensions/OpenApiSettingsExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSettingsExtensions.cs
index eb097ce999..118ac47698 100644
--- a/src/Kiota.Builder/Extensions/OpenApiSettingsExtensions.cs
+++ b/src/Kiota.Builder/Extensions/OpenApiSettingsExtensions.cs
@@ -1,4 +1,6 @@
 ﻿using System;
+using System.Collections.Generic;
+using System.Linq;
 using Kiota.Builder.OpenApiExtensions;
 using Microsoft.OpenApi.Readers;
 
@@ -18,4 +20,24 @@ public static void AddPluginsExtensions(this OpenApiReaderSettings settings)
         settings.ExtensionParsers.TryAdd(OpenApiAiReasoningInstructionsExtension.Name, static (i, _) => OpenApiAiReasoningInstructionsExtension.Parse(i));
         settings.ExtensionParsers.TryAdd(OpenApiAiRespondingInstructionsExtension.Name, static (i, _) => OpenApiAiRespondingInstructionsExtension.Parse(i));
     }
+
+    public static void AddGenerationExtensions(this OpenApiReaderSettings settings)
+    {
+        ArgumentNullException.ThrowIfNull(settings);
+        settings.AddMicrosoftExtensionParsers();
+        settings.ExtensionParsers.TryAdd(OpenApiKiotaExtension.Name, static (i, _) => OpenApiKiotaExtension.Parse(i));
+    }
+
+    public static HashSet<string> KiotaSupportedExtensions()
+    {
+        var dummySettings = new OpenApiReaderSettings();
+        dummySettings.AddGenerationExtensions();
+        dummySettings.AddPluginsExtensions();
+
+        var supportedExtensions = dummySettings.ExtensionParsers.Keys.ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+        supportedExtensions.Add("x-openai-isConsequential");// add extension we don't parse to the list 
+
+        return supportedExtensions;
+    }
 }
diff --git a/src/Kiota.Builder/OpenApiDocumentDownloadService.cs b/src/Kiota.Builder/OpenApiDocumentDownloadService.cs
index b62c438439..ae586c86a3 100644
--- a/src/Kiota.Builder/OpenApiDocumentDownloadService.cs
+++ b/src/Kiota.Builder/OpenApiDocumentDownloadService.cs
@@ -112,10 +112,12 @@ ex is SecurityException ||
         {
             RuleSet = ruleSet,
         };
-        settings.AddMicrosoftExtensionParsers();
+
+        // Add all extensions for generation
+        settings.AddGenerationExtensions();
         if (config.IsPluginConfiguration)
-            settings.AddPluginsExtensions();
-        settings.ExtensionParsers.TryAdd(OpenApiKiotaExtension.Name, static (i, _) => OpenApiKiotaExtension.Parse(i));
+            settings.AddPluginsExtensions();// Add all extensions for plugins
+
         try
         {
             var rawUri = config.OpenAPIFilePath.TrimEnd(KiotaBuilder.ForwardSlash);
diff --git a/src/Kiota.Builder/Plugins/OpenApiPluginWalker.cs b/src/Kiota.Builder/Plugins/OpenApiPluginWalker.cs
new file mode 100644
index 0000000000..b89d65d3e9
--- /dev/null
+++ b/src/Kiota.Builder/Plugins/OpenApiPluginWalker.cs
@@ -0,0 +1,63 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using Kiota.Builder.Extensions;
+using Microsoft.OpenApi.Interfaces;
+using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.Services;
+
+namespace Kiota.Builder.Plugins;
+
+public class OpenApiPluginWalker : OpenApiVisitorBase
+{
+    private static readonly HashSet<string> SupportedExtensions = OpenApiSettingsExtensions.KiotaSupportedExtensions();
+
+    /// <summary>
+    /// Visits <see cref="OpenApiSchema"/>
+    /// </summary>
+    public override void Visit(IOpenApiExtensible openApiExtensible)
+    {
+        ArgumentNullException.ThrowIfNull(openApiExtensible);
+        // remove any extensions we do not support
+        foreach (var extension in openApiExtensible.Extensions.Where(static extension => !SupportedExtensions.Contains(extension.Key)))
+        {
+            openApiExtensible.Extensions.Remove(extension.Key);
+        }
+    }
+    /// <summary>
+    /// Visits the operations.
+    /// </summary>
+    public override void Visit(IDictionary<OperationType, OpenApiOperation> operations)
+    {
+        ArgumentNullException.ThrowIfNull(operations);
+
+        // Cleanup responses for the operation
+        foreach (var operation in operations.Values)
+        {
+            var responseDescription = operation.Responses.Values.Select(static response => response.Description)
+                .FirstOrDefault(static desc => !string.IsNullOrEmpty(desc)) ?? "Api Response";
+
+            operation.Responses = new OpenApiResponses()
+            {
+                {
+                    "2XX",new OpenApiResponse
+                    {
+                        Description = responseDescription,
+                        Content = new Dictionary<string, OpenApiMediaType>
+                        {
+                            {
+                                "text/plain", new OpenApiMediaType
+                                {
+                                    Schema = new OpenApiSchema
+                                    {
+                                        Type = "string"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            };
+        }
+    }
+}
diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
index 48f4132beb..eeea94082f 100644
--- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
+++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
@@ -192,35 +192,11 @@ private OpenApiDocument GetDocumentWithTrimmedComponentsAndResponses(OpenApiDocu
         if (string.IsNullOrEmpty(doc.Info?.Version)) // filtering fails if there's no version.
             doc.Info!.Version = "1.0";
 
-        //empty out all the responses with a single empty 2XX
-        foreach (var operation in doc.Paths.SelectMany(static item => item.Value.Operations.Values))
-        {
-            var responseDescription = operation.Responses.Values.Select(static response => response.Description)
-                                                                      .FirstOrDefault(static desc => !string.IsNullOrEmpty(desc)) ?? "Api Response";
-            operation.Responses = new OpenApiResponses()
-            {
-                {
-                    "2XX",new OpenApiResponse
-                    {
-                        Description = responseDescription,
-                        Content = new Dictionary<string, OpenApiMediaType>
-                        {
-                            {
-                                "text/plain", new OpenApiMediaType
-                                {
-                                    Schema = new OpenApiSchema
-                                    {
-                                        Type = "string"
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            };
-        }
+        //empty out all the responses with a single empty 2XX and cleanup the extensions
+        var openApiWalker = new OpenApiWalker(new OpenApiPluginWalker());
+        openApiWalker.Walk(doc);
 
-        // remove unused components using the OpenApi.Net
+        // remove unused components using the OpenApi.Net library
         var requestUrls = new Dictionary<string, List<string>>();
         var basePath = doc.GetAPIRootUrl(Configuration.OpenAPIFilePath);
         foreach (var path in doc.Paths.Where(static path => path.Value.Operations.Count > 0))


Test patch:
diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
index bb39cb9396..75e7f6cff2 100644
--- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
+++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
@@ -685,6 +685,7 @@ public async Task GeneratesManifestAndCleansUpInputDescription()
   /test:
     get:
       description: description for test path
+      x-random-extension: true
       responses:
         '200':
           description: test
@@ -694,6 +695,7 @@ public async Task GeneratesManifestAndCleansUpInputDescription()
     get:
       description: description for test path with id
       operationId: test.WithId
+      x-openai-isConsequential: true
       parameters:
       - name: id
         in: path
@@ -764,7 +766,9 @@ public async Task GeneratesManifestAndCleansUpInputDescription()
         Assert.Single(originalDocument.Extensions); // single unsupported extension at root
         Assert.Equal(2, originalDocument.Paths.Count); // document has only two paths
         Assert.Equal(2, originalDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count); // 2 responses originally
+        Assert.Single(originalDocument.Paths["/test"].Operations[OperationType.Get].Extensions); // 1 UNsupported extension
         Assert.Equal(2, originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count); // 2 responses originally
+        Assert.Single(originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Extensions); // 1 supported extension
 
         // Validate the output open api file
         var resultOpenApiFile = File.OpenRead(Path.Combine(outputDirectory, OpenApiFileName));
@@ -777,7 +781,9 @@ public async Task GeneratesManifestAndCleansUpInputDescription()
         Assert.Equal(2, resultDocument.Paths.Count); // document has only two paths
         Assert.Single(resultDocument.Paths["/test"].Operations[OperationType.Get].Responses); // other responses are removed from the document
         Assert.NotEmpty(resultDocument.Paths["/test"].Operations[OperationType.Get].Responses["2XX"].Description); // response description string is not empty
+        Assert.Empty(resultDocument.Paths["/test"].Operations[OperationType.Get].Extensions); // NO UNsupported extension
         Assert.Single(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses); // 2 responses originally
         Assert.NotEmpty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["2XX"].Description);// response description string is not empty
+        Assert.Single(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Extensions); // 1 supported extension still present in operation
     }
 }