diff --git a/Google.GenAI.Tests/ClientTest.cs b/Google.GenAI.Tests/ClientTest.cs index 0475351e..a4822ca2 100644 --- a/Google.GenAI.Tests/ClientTest.cs +++ b/Google.GenAI.Tests/ClientTest.cs @@ -15,6 +15,7 @@ */ using System; +using System.Net.Http; using Google.Apis.Auth.OAuth2; using Google.GenAI; @@ -666,6 +667,16 @@ public void Constructor_HttpOptionsProvided_Timeout() { Assert.AreEqual(1000, client._apiClient.HttpOptions.Timeout); } + [TestMethod] + public void Constructor_HttpOptionsProvided_HttpClientFactory() { + Func factory = () => new HttpClient(); + var options = new HttpOptions { HttpClientFactory = factory }; + + var client = new Client(vertexAI: false, apiKey: "key", httpOptions: options); + + Assert.AreSame(factory, client._apiClient.HttpOptions.HttpClientFactory); + } + #endregion #region Successful Instantiation(all modules) diff --git a/Google.GenAI.Tests/HttpApiClientTest.cs b/Google.GenAI.Tests/HttpApiClientTest.cs index a49dcda7..f50506cd 100644 --- a/Google.GenAI.Tests/HttpApiClientTest.cs +++ b/Google.GenAI.Tests/HttpApiClientTest.cs @@ -54,6 +54,7 @@ public void GeminiConstructor_WithApiKey_SetsPropertiesCorrectly() { Assert.AreEqual("https://generativelanguage.googleapis.com", client.HttpOptions.BaseUrl); Assert.AreEqual("v1beta", client.HttpOptions.ApiVersion); Assert.IsNull(client.HttpOptions.Timeout); + Assert.IsNull(client.HttpOptions.HttpClientFactory); } [TestMethod] @@ -70,13 +71,16 @@ public void GeminiConstructor_WithApiKeyFromEnvironment_SetsPropertiesCorrectly( Assert.AreEqual("https://generativelanguage.googleapis.com", client.HttpOptions.BaseUrl); Assert.AreEqual("v1beta", client.HttpOptions.ApiVersion); Assert.IsNull(client.HttpOptions.Timeout); + Assert.IsNull(client.HttpOptions.HttpClientFactory); } [TestMethod] public void GeminiConstructor_WithHttpOptionsProvided_SetsPropertiesCorrectly() { System.Environment.SetEnvironmentVariable(EnvApiKeyName, TestApiKey); + var httpClientFactory = () => new HttpClient(); var customHttpOptions = new Types.HttpOptions { BaseUrl = "https://custom-url.com", - ApiVersion = "v2", Timeout = 6000 }; + ApiVersion = "v2", Timeout = 6000, + HttpClientFactory = httpClientFactory }; var client = new HttpApiClient(vertexAI: false, httpOptions: customHttpOptions); @@ -88,6 +92,7 @@ public void GeminiConstructor_WithHttpOptionsProvided_SetsPropertiesCorrectly() Assert.AreEqual("https://custom-url.com", client.HttpOptions.BaseUrl); Assert.AreEqual("v2", client.HttpOptions.ApiVersion); Assert.AreEqual(6000, client.HttpOptions.Timeout); + Assert.AreSame(httpClientFactory, client.HttpOptions.HttpClientFactory); } [TestMethod] @@ -127,6 +132,7 @@ public void VertexConstructor_WithProjectLocationCredentials_SetsPropertiesCorre client.HttpOptions.BaseUrl); Assert.AreEqual("v1beta1", client.HttpOptions.ApiVersion); Assert.IsNull(client.HttpOptions.Timeout); + Assert.IsNull(client.HttpOptions.HttpClientFactory); } [TestMethod] @@ -150,6 +156,7 @@ public void VertexConstructor_WithProjectLocationFromEnvironment_SetsPropertiesC client.HttpOptions.BaseUrl); Assert.AreEqual("v1beta1", client.HttpOptions.ApiVersion); Assert.IsNull(client.HttpOptions.Timeout); + Assert.IsNull(client.HttpOptions.HttpClientFactory); } [TestMethod] @@ -213,8 +220,10 @@ public void VertexConstructor_WithCustomHttpOptions_OverridesDefaults() { .Setup(c => c.GetAccessTokenForRequestAsync( It.IsAny(), It.IsAny())) .ReturnsAsync("mock-access-token"); + var httpClientFactory = () => new HttpClient(); var customOptions = new Types.HttpOptions { BaseUrl = "https://custom.vertex.ai", - ApiVersion = "v2alpha", Timeout = 8000 }; + ApiVersion = "v2alpha", Timeout = 8000, + HttpClientFactory = httpClientFactory }; var client = new HttpApiClient(vertexAI: true, project: TestProject, location: TestLocation, credentials: mockCredential.Object, httpOptions: customOptions); @@ -227,6 +236,7 @@ public void VertexConstructor_WithCustomHttpOptions_OverridesDefaults() { Assert.AreEqual("https://custom.vertex.ai", client.HttpOptions.BaseUrl); Assert.AreEqual("v2alpha", client.HttpOptions.ApiVersion); Assert.AreEqual(8000, client.HttpOptions.Timeout); + Assert.AreSame(httpClientFactory, client.HttpOptions.HttpClientFactory); } [TestMethod] diff --git a/Google.GenAI/ApiClient.cs b/Google.GenAI/ApiClient.cs index 8be3c6a0..0a6d0539 100644 --- a/Google.GenAI/ApiClient.cs +++ b/Google.GenAI/ApiClient.cs @@ -184,7 +184,7 @@ protected ApiClient( private static HttpClient CreateHttpClient(HttpOptions httpOptions) { - var client = new HttpClient(); + var client = httpOptions.HttpClientFactory?.Invoke() ?? new HttpClient(); if (httpOptions.Timeout != null) { client.Timeout = System.TimeSpan.FromMilliseconds(httpOptions.Timeout.Value); @@ -263,6 +263,10 @@ protected HttpOptions MergeHttpOptions(HttpOptions? optionsToApply) { mergedOptions.BaseUrlResourceScope = optionsToApply?.BaseUrlResourceScope; } + if (optionsToApply?.HttpClientFactory != null) + { + mergedOptions.HttpClientFactory = optionsToApply?.HttpClientFactory; + } var currentHeaders = this.HttpOptions.Headers ?? new Dictionary(); var newHeaders = optionsToApply?.Headers ?? new Dictionary(); diff --git a/Google.GenAI/UploadClient.cs b/Google.GenAI/UploadClient.cs index 0909f37a..793351f1 100644 --- a/Google.GenAI/UploadClient.cs +++ b/Google.GenAI/UploadClient.cs @@ -86,7 +86,8 @@ public static HttpOptions BuildResumableUploadHttpOptions( ApiVersion = "", Headers = mergedHeaders, BaseUrl = userOptions?.BaseUrl, - Timeout = userOptions?.Timeout + Timeout = userOptions?.Timeout, + HttpClientFactory = userOptions?.HttpClientFactory }; } diff --git a/Google.GenAI/types/HttpOptions.cs b/Google.GenAI/types/HttpOptions.cs index 5a8b3187..c721b167 100644 --- a/Google.GenAI/types/HttpOptions.cs +++ b/Google.GenAI/types/HttpOptions.cs @@ -74,6 +74,16 @@ public int get; set; } + /// + /// A factory function to create HttpClient instances. + /// This allows for custom configuration of HttpClient, such as setting default headers, timeouts, or using a custom message handler. + /// + [JsonIgnore] + public Func + ? HttpClientFactory { + get; set; + } + /// /// Deserializes a JSON string to a HttpOptions object. ///