2
\$\begingroup\$

I have written a .Net Client wrapper for Google Gemini (Generative AI) REST API and published it as a Nuget package. I am seeking a review of the code.

I have spent quite some time in designing how it is right now, so kind of out of ideas but I am certain that it needs lots of improvements. I aim to incorporate best coding practices that should be a part of any open-source library, be it, consumer-driven design, code readability and efficiency etc.

Link to repo: https://github.com/jaslam94/Junaid.GoogleGemini.Net

There are multiple services. Each service has an interface. The service instance is obtained by using its interface from the DI container. Each service offers methods that can be used to generate content.

AddGemini extension method is called to configure the library.

    public static IServiceCollection AddGemini(this IServiceCollection services)
    {
        services.AddTransient<GeminiAuthHandler<GeminiHttpClientOptions>>();

        services.AddHttpClient<GeminiClient>((sp, client) =>
        {
            var options = sp.GetRequiredService<IOptions<GeminiHttpClientOptions>>().Value;
            client.BaseAddress = options.Url;
        })
        .AddHttpMessageHandler<GeminiAuthHandler<GeminiHttpClientOptions>>();

        services.AddTransient<ITextService, TextService>();
        services.AddTransient<IVisionService, VisionService>();
        services.AddTransient<IChatService, ChatService>();
        services.AddTransient<IEmbeddingService, EmbeddingService>();
        services.AddTransient<IModelInfoService, ModelInfoService>();

        return services;
    }

GeminiAuthHandler is being used to set the default HTTP headers.

public class GeminiAuthHandler<TOptions> : DelegatingHandler
    where TOptions : class, IGeminiAuthHttpClientOptions
{
    private readonly TOptions _options;

    public GeminiAuthHandler(IOptions<TOptions> options)
    {
        _options = options.Value;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (_options.Credentials != null)
        {
            request.Headers.Add(GeminiConfiguration.Scheme, _options.Credentials.ApiKey);
        }
        else
        {
            var apiKey = Environment.GetEnvironmentVariable("GeminiApiKey");
            if (!string.IsNullOrEmpty(apiKey))
            {
                request.Headers.Add(GeminiConfiguration.Scheme, apiKey);
            }
            else
            {
                throw new ArgumentNullException($"Your API key is invalid, as it is an empty string. You can double-check your API key from the Google Cloud API Credentials page (https://console.cloud.google.com/apis/credentials).");
            }
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

public interface IGeminiAuthHttpClientOptions
{
    Uri Url { get; }
    GeminiConfiguration? Credentials { get; set; }
}

public class GeminiHttpClientOptions : IGeminiAuthHttpClientOptions
{
    private const string DefaultBaseAddress = "https://generativelanguage.googleapis.com";

    private readonly Uri uri = new Uri(DefaultBaseAddress);

    public Uri Url
    {
        get
        {
            return uri;
        }
    }

    public GeminiConfiguration? Credentials { get; set; }
}


public class GeminiConfiguration
{
    public const string Scheme = "x-goog-api-key";

    private string apiKey;

    public GeminiConfiguration()
    {
        apiKey = string.Empty;
    }

    public string ApiKey
    {
        get
        {
            return apiKey;
        }

        set
        {
            if (string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value))
            {
                throw new ArgumentNullException(nameof(ApiKey), $"Your API key is invalid, as it is an empty string. You can double-check your API key from the Google Cloud API Credentials page (https://console.cloud.google.com/apis/credentials).");
            }
            apiKey = value;
        }
    }
}

GeminiClient is the typed HttpClient and that is registered with the HttpClient factory in the AddGemini method.

public class GeminiClient
{
    private readonly HttpClient _httpClient;

    public GeminiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<TResponse> GetAsync<TResponse>(string endpoint)
    {
        var response = await _httpClient.GetAsync(endpoint);
        return await HandleResponse<TResponse>(response)
               ?? throw new GeminiException("The API has returned a null response.");
    }

    public async Task<TResponse> PostAsync<TRequest, TResponse>(string endpoint, TRequest data)
    {
        var serializedContent = JsonSerializer.Serialize(data, options: new JsonSerializerOptions
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        });
        var jsonContent = new StringContent(serializedContent, Encoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync(endpoint, jsonContent);
        return await HandleResponse<TResponse>(response)
               ?? throw new GeminiException("The API has returned a null response.");
    }

    private static async Task<T> HandleResponse<T>(HttpResponseMessage response)
    {
        var content = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode)
        {
            return JsonSerializer.Deserialize<T>(content)
                   ?? throw new GeminiException("The API has returned a null response.");
        }
        else
        {
            var geminiError = JsonSerializer.Deserialize<ApiErrorResponse>(content)
                              ?? throw new GeminiException("The API has returned a null response.");
            throw new GeminiException(geminiError, geminiError.error.message);
        }
    }

    public async IAsyncEnumerable<string> SendAsync<TRequest>(string endpoint, TRequest data)
    {
        var ms = new MemoryStream();
        await JsonSerializer.SerializeAsync(ms, data, options: new JsonSerializerOptions
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        });
        ms.Seek(0, SeekOrigin.Begin);

        var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        using (var requestContent = new StreamContent(ms))
        {
            request.Content = requestContent;
            requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
            {
                if (response.IsSuccessStatusCode)
                {
                    using (var responseStream = await response.Content.ReadAsStreamAsync())
                    using (var streamReader = new StreamReader(responseStream))
                    {
                        string line = string.Empty;
                        while ((line = await streamReader.ReadLineAsync()) != null)
                        {
                            if (line.Contains(@"""text"""))
                            {
                                var jsonString = "{" + line + "}";
                                var jsonObject = JsonSerializer.Deserialize<JsonObject>(jsonString);
                                yield return jsonObject?["text"]?.ToString();
                            }
                        }
                    }
                }
                else
                {
                    var content = await response.Content.ReadAsStringAsync();
                    var geminiError = JsonSerializer.Deserialize<ApiErrorResponse>(content)
                                      ?? throw new GeminiException("The API has returned a null response."); ;
                    throw new GeminiException(geminiError, geminiError.error.message);
                }
            }
        }
    }
}

This is the default AddGemini and related implementation that works out of the box. The user just has to call this method and everything gets to the services container.

builder.Services.Configure<GeminiHttpClientOptions>(builder.Configuration.GetSection("Gemini"));

Then call AddGemini extension method (which configures a typed HttpClient named GeminiClient and all of the library services).

builder.Services.AddGemini();

However, when a custom-typed HttpClient is needed, then the entire extension method kind of needs to be rewritten, which doesn't seem like the right way of doing it.

  1. Created a new Typed HttpClient:

    public class CustomClient : GeminiClient
    {
        public CustomClient(HttpClient httpClient) : base(httpClient)
        {
        }
    }
    
  2. Add relevant configuration to the Typed HttpClient and register it with the DI container:

    builder.Services.AddHttpClient<GeminiClient, CustomClient>((sp, client) =>
    {
        var options = sp.GetRequiredService<IOptions<GeminiHttpClientOptions>>().Value;
        client.BaseAddress = options.Url;
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var proxy = new WebProxy
        {
            Address = new Uri("http://localhost:1080/")
        };
    
        var httpClientHandler = new HttpClientHandler { Proxy = proxy, UseProxy = true };
    
        //Not recommended for production
        httpClientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
    
        return httpClientHandler;
    })
    .AddHttpMessageHandler<GeminiAuthHandler<GeminiHttpClientOptions>>();
    
  3. Register the required service:

    builder.Services.AddTransient<ITextService, TextService>();
    

If the user calls AddGemini and then goes ahead with registering a new custom-typed client, the previous one stays in the pipeline and I think it becomes unnecessary. There has to be a way to replace the existing registered client from the services container if the user wants.

Second, each service is derived from the Service class. Could there be a way to improve the way services call the third-party API as each service is calling the same Get/Post method from the typed HttpClient?

    public class Service
    {
        protected readonly GeminiClient GeminiClient;

        public Service(GeminiClient geminiClient)
        {
            GeminiClient = geminiClient;
        }
    }


    public class TextService : Service, ITextService
{
    public TextService(GeminiClient geminiClient) : base(geminiClient)
    {
    }

    public async Task<GenerateContentResponse> GenereateContentAsync(string text,
                                                                     GenerateContentConfiguration? configuration)
    {
        GenerateContentRequest model = CreateRequestModel(text);
        if (configuration != null)
        {
            model.ApplyConfiguration(configuration);
        }
        return await GeminiClient.PostAsync<GenerateContentRequest, GenerateContentResponse>($"/v1beta/models/gemini-pro:generateContent", model);
    }

    public async Task StreamGenereateContentAsync(string text,
                                                  Action<string> handleStreamResponse,
                                                  GenerateContentConfiguration? configuration)
    {
        GenerateContentRequest model = CreateRequestModel(text);
        if (configuration != null)
        {
            model.ApplyConfiguration(configuration);
        }
        await foreach (var data in GeminiClient.SendAsync($"/v1beta/models/gemini-pro:streamGenerateContent", model))
        {
            handleStreamResponse(data);
        }
    }

    public async Task<CountTokensResponse> CountTokensAsync(string text)
    {
        GenerateContentRequest model = CreateRequestModel(text);
        return await GeminiClient.PostAsync<GenerateContentRequest, CountTokensResponse>($"/v1beta/models/gemini-pro:countTokens", model);
    }

    private static GenerateContentRequest CreateRequestModel(string text)
    {
        var contents = new Content[]
        {
            new Content
            {
                parts = new[]
                {
                    new Part
                    {
                        text = text
                    }
                }
            }
        };
        return new GenerateContentRequest(contents);
    }
}

ChatService:

public class ChatService : Service, IChatService
{
    public ChatService(GeminiClient geminiClient) : base(geminiClient)
    {
    }

    public async Task<GenerateContentResponse> GenereateContentAsync(MessageObject[] chat,
                                                                     GenerateContentConfiguration? configuration)
    {
        GenerateContentRequest model = CreateRequestModel(chat);
        if (configuration != null)
        {
            model.ApplyConfiguration(configuration);
        }
        return await GeminiClient.PostAsync<GenerateContentRequest, GenerateContentResponse>($"/v1beta/models/gemini-pro:generateContent", model);
    }

    public async Task StreamGenereateContentAsync(MessageObject[] chat,
                                                  Action<string> handleStreamResponse,
                                                  GenerateContentConfiguration? configuration)
    {
        GenerateContentRequest model = CreateRequestModel(chat);
        if (configuration != null)
        {
            model.ApplyConfiguration(configuration);
        }
        await foreach (var data in GeminiClient.SendAsync($"/v1beta/models/gemini-pro:streamGenerateContent", model))
        {
            handleStreamResponse(data);
        }
    }

    public async Task<CountTokensResponse> CountTokensAsync(MessageObject[] chat)
    {
        GenerateContentRequest model = CreateRequestModel(chat);
        return await GeminiClient.PostAsync<GenerateContentRequest, CountTokensResponse>($"/v1beta/models/gemini-pro:countTokens", model);
    }

    private static GenerateContentRequest CreateRequestModel(MessageObject[] chat)
    {
        var contents = new List<Content>();

        foreach (var message in chat)
        {
            contents.Add(new Content
            {
                role = message.Role,
                parts = new[]
                {
                    new Part
                    {
                        text = message.Text
                    }
                }
            });
        }

        return new GenerateContentRequest(contents.ToArray());
    }
}

VisionService:

public class VisionService : Service, IVisionService
{
    public VisionService(GeminiClient geminiClient) : base(geminiClient)
    {
    }

    public async Task<GenerateContentResponse> GenereateContentAsync(string text,
                                                                     FileObject fileObject,
                                                                     GenerateContentConfiguration? configuration)
    {
        GenerateContentRequest model = CreateRequestModel(text, fileObject);
        if (configuration != null)
        {
            model.ApplyConfiguration(configuration);
        }
        return await GeminiClient.PostAsync<GenerateContentRequest, GenerateContentResponse>($"/v1beta/models/gemini-pro-vision:generateContent", model);
    }

    public async Task StreamGenereateContentAsync(string text,
                                                  FileObject fileObject,
                                                  Action<string> handleStreamResponse,
                                                  GenerateContentConfiguration? configuration)
    {
        GenerateContentRequest model = CreateRequestModel(text, fileObject);
        if (configuration != null)
        {
            model.ApplyConfiguration(configuration);
        }
        await foreach (var data in GeminiClient.SendAsync($"/v1beta/models/gemini-pro-vision:streamGenerateContent", model))
        {
            handleStreamResponse(data);
        }
    }

    public async Task<CountTokensResponse> CountTokensAsync(string text,
                                                            FileObject fileObject)
    {
        GenerateContentRequest model = CreateRequestModel(text, fileObject);
        return await GeminiClient.PostAsync<GenerateContentRequest, CountTokensResponse>($"/v1beta/models/gemini-pro-vision:countTokens", model);
    }

    private static GenerateContentRequest CreateRequestModel(string text, FileObject fileObject)
    {
        var contents = new[]
        {
            new Content
            {
                parts = new[]
                {
                    new Part
                    {
                        text = text
                    },
                    new Part
                    {
                        inline_data = new Inline_Data
                        {
                            mime_type = MimeTypeHelper.GetMimeType(fileObject.FileName),
                            data = Convert.ToBase64String(fileObject.FileContent)
                        }
                    }
                }
            }
        };
        return new GenerateContentRequest(contents);
    }
}

EDIT: Added all of the relevant code.

\$\endgroup\$
7
  • \$\begingroup\$ The current title of your question is too generic to be helpful. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How do I ask a good question?. \$\endgroup\$
    – BCdotWEB
    Commented Jan 23 at 11:57
  • \$\begingroup\$ @BCdotWEB I've updated the question title. \$\endgroup\$
    – Junaid
    Commented Jan 23 at 15:03
  • \$\begingroup\$ Please follow the instructions in the link. There's no need to put things like "C#" in the title, that's what the tags are for. Your title is still too generic, and doesn't even mention "Google Gemini". Look at the titles of other questions, look at the guidance for titles: "The norm is to summarize the goal of the code in the title." \$\endgroup\$
    – BCdotWEB
    Commented Jan 23 at 15:42
  • \$\begingroup\$ Which C# version are you using? It seems like a lot of things could be simplified / make it more concise by utilizing newer C# features. \$\endgroup\$ Commented Jan 24 at 16:14
  • \$\begingroup\$ @PeterCsala I am using C# 10. \$\endgroup\$
    – Junaid
    Commented Jan 25 at 5:59

2 Answers 2

3
\$\begingroup\$
  • You must separate the provider and the consumer as you're the man in the middle, so you can maintain both without any breakage.
  • BaseAddress should include /v1beta/models/gemini-pro: as well.
  • GeminiAuthHandler can be omitted as you can use HttpClient.DefaultRequestHeaders either from the instance or from the factory.
  • string.IsNullOrWhiteSpace covers string.IsNullOrEmpty so it can be omitted.
  • Requirements should be enforced at startup phase, this would ensure of the required values to be present, and prevent checks redundancy.
  • Exceptions should be defined once and reused instead of redefining them on every class.
  • No API versioning plan for the client. You should consider versioning your client based on its dependency (in your case Google Gemini version), provide the latest stable version throughout your library, but give an option for the consumer to use different version.
  • GeminiClient and Service should be one instance with some changes (more on that later).
  • No CancellationToken were provided. all asynchronous methods should have an optional CancellationToken to handle the process cancellation.
  • [Opinion]: all public classes related to the main service would be better with a prefix Gemini for better readability and to prevent from confusion with other services that already used at the consumer side.

Let's start with the separation part because it's very crucial with public libraries. Your library acts as a middleware, it should handles the service provider requirements and dependencies internally, and have a thin public layer that would be exposed to the consumer.

When you do that separation, you will gain more control on both sides, and would be easier to maintain for any future changes.

Example:

Google <-> Gemini Service (Internal) <-> Gemini Service (Public) <-> Consumer

With this, we can ensure that internal changes would be always maintained and will not break the consumer experience.

For sake of simplicity, I would assume that we are working on the public part of the code where the consumer will interact with.

As a consumer, I would expect to add the service, then configure it at startup with DI, then inject it into my own code. Something like this should be decent enough :

builder.Services.AddGemini(config => {
    //Required
    ApiKey = "SomeApiKey",
    
    //Optional with default value that is configured by the library
    Version = GeminiVersion.Version1, 
    
    //Optional Global Configuration with default configured values.
    GeminiModelConfiguration = new GeminiModelConfiguration {
        x.SafetyFilter = GeminiSafetyFilter.HateSpeech,
        x.SafetyFilterLevel = SafetyFilterLevel.Moderate, 
        /// etc..
    });
});

now all I need is to inject IGeminiService into my class, and use it from there, something like this would be effecient :

public sealed CustomGeminiService 
{
    private readonly IGeminiService _geminiService; 
    
    public CustomGeminiService(IGeminiService geminiService)
    {
        // optional to override some global configuration at instance level
        geminiService.SafetyFilter = GeminiSafetyFilter.DangerousContent;
        
        _geminiService = geminiService;
    }
    
    public async Task DoSomething(string text, CancellationToken cancellationToken) {
        
        // a static helper class that can be used to handle the request body
        var model = GeminiContent.Create(text);

        // the public method, where it will transform the model 
        // into a request body, and decides which service to use 
        // based on `enum GeminiServiceType`
        var result = await _geminiService.GenerateAsync(model, GeminiServiceType.TextService, cancellationToken); 
        
        if(result.IsSuccess) return result.Value;
        
        // something went wrong 
        // deal with it.
        return null;
    }
    
}

The main service example:

public enum GeminiServiceType { Text, Chat, Vision }

// Basic contract
public interface IGeminiService
{ 
    IGeminiServiceOption Options { get; }
    Task<IGeminiServiceResult<TResult>> GenerateAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, CancellationToken cancellationToken);
    Task<IGeminiServiceResult<TResult>> StreamAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, Action<string> handleStreamResponse, CancellationToken cancellationToken);
    Task<IGeminiServiceResult<TResult>> CountTokensAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, CancellationToken cancellationToken);
}


public sealed class GeminiService : IGeminiService
{
    private readonly HttpClient _httpClient;
    private readonly IGeminiServiceOption _options;

    public IGeminiServiceOption Options => _options;
    
    public GeminiService(HttpClient httpClient, IGeminiServiceOption options)
    {
        _httpClient = httpClient;
        _options = options;
        _httpClient.BaseAddress= _options.BaseAddress.Value;
        _httpClient.DefaultRequestHeaders.Add(_options.Credentials.Scheme, _options.Credentials.ApiKey);
    }
    
    // content configurations will be read from Options
    public async Task<IGeminiServiceResult<TResult>> GenerateAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, CancellationToken cancellationToken = default) { ... }
    
    // content configurations will be read from Options
    public async Task<IGeminiServiceResult<TResult>> StreamAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, Action<string> handleStreamResponse, CancellationToken cancellationToken = default) { ... }
    
    // content configurations will be read from Options
    public async Task<IGeminiServiceResult<TResult>> CountTokensAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, CancellationToken cancellationToken = default) { ... }

    // an optional overload method to pass a custom configuration
    public async Task<IGeminiServiceResult<TResult>> GenerateAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, IGeminiContentConfiguration configuration, CancellationToken cancellationToken = default) { ... }
    
    // an optional overload method to pass a custom configuration
    public async Task<IGeminiServiceResult<TResult>> StreamAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, Action<string> handleStreamResponse, IGeminiContentConfiguration configuration, CancellationToken cancellationToken = default) { ... }
    
    // an optional overload method to pass a custom configuration
    public async Task<IGeminiServiceResult<TResult>> CountTokensAsync<TContent, TResult>(IGeminiContent<TContent> content, GeminiServiceType type, IGeminiContentConfiguration configuration, CancellationToken cancellationToken = default) { ... }
    
    
    private async Task<TResponse> GetAsync<TResponse>(string endpoint) { ... }
    private async Task<TResponse> PostAsync<TRequest, TResponse>(string endpoint, TRequest data) { ... }
    private static async Task<T> HandleResponse<T>(HttpResponseMessage response) { ... }
    private async IAsyncEnumerable<string> SendAsync<TRequest>(string endpoint, TRequest data){ ... }
}

At the end, your consumer would only interact with IGeminiService and it's requirements only.

You will also need to transform all possible values and objects into a safe-typed objects for the consumer. For instance, ObjectMessage.Role is string, and only accepts two values user or model in this case, expose enum to the consumer to represent that, and transform it into the original form internally.

So, you will need to implement IGeminiContent<T> as a content proxy between the consumer and the provider, in which you can just hookup some internal extensions to it to map the value into the internal Google models and vise versa.

Those public interfaces that needs to be tranformed (such as IGeminiContent) it needs to be handled internally (at Gemini Service (Internal) layer (your actual domain layer)) which then the doamin will pass it to the Google and get the result and reverse the process to return it to the consumer. There is no direct access to the Google service from the consumer nor the Gemini Service (Public)

UPDATE : (from the comments).

How do we make it type-safe? How should I use IGeminiContent with any input classes, e.g. FileObject or MessageObject?

The IGeminiContent<TContent> can be implemented in a such :

public enum RoleEnum { Model, User }

public interface IGeminiObject { }

public interface IGeminiTextObject : IGeminiObject
{
    string Text { get; }
}

public interface IGeminiFileObject : IGeminiObject
{
    byte[] FileContent { get; }
    string FileName { get; }
}

public interface IGeminiMessageObject : IGeminiObject
{
    RoleEnum Role { get; }
    IGeminiTextObject Text { get; }
}

public interface IGeminiContent<T> where T : IGeminiObject
{
    T Value { get; }
}

public sealed class GeminiMessageObject : IGeminiMessageObject
{
    public RoleEnum Role { get; set; }
    public IGeminiTextObject Text { get; set; }
}

internal sealed class RequestMessageObject
{
    private static readonly string[] _acceptedRoles = ["user", "model"];
    public string? Role { get; private set; }
    public string? Text { get; private set; }

    private RequestMessageObject()
    {
    }
    internal RequestMessageObject(string? role, string? text)
    {
        if (string.IsNullOrWhiteSpace(role))
            throw new ArgumentNullException(nameof(role));

        if (!_acceptedRoles.Any(s => s.Equals(role, StringComparison.OrdinalIgnoreCase)))
            throw new ArgumentException("Value cannot be other than 'model' or 'user'.");

        if (string.IsNullOrWhiteSpace(text))
            throw new ArgumentNullException("Text cannot be empty.");
    }
}

The RequestMessageObject will be only used internally, where you convert IGeminiMessageObject into RequestMessageObject and serialize it internally into the request body.

\$\endgroup\$
15
  • \$\begingroup\$ Thanks for your review and suggestions. Wouldn't setting the default header inside the constructor violate SRP? I think using GeminiAuthHandler is the right way to set the header. There is an official MS guide on it as well I think. \$\endgroup\$
    – Junaid
    Commented Feb 2 at 19:42
  • \$\begingroup\$ both are valid choices. If you want to check for API-Key on every request, then use a handler, but if you want to just pass it with every request, then just add it to the default headers once, and reuse that instead. handlers are best for time-based tokens or tokens where needs to be checked on every request. but for public API keys that has no expiry (unless it's revoked)., adding it once and reuse it would speed things up a bit. \$\endgroup\$
    – iSR5
    Commented Feb 2 at 22:29
  • \$\begingroup\$ yeah, that's not a public key.. one more question, I am not sure about how to implement the GeminiContent method.. can you please explain its design? \$\endgroup\$
    – Junaid
    Commented Feb 3 at 7:17
  • \$\begingroup\$ public interface IGeminiContent<T> { BatchEmbedContentRequest CreateBatchRequestModel(T input); GenerateContentRequest CreateContentRequestModel(T input); } this is what I am thinking about it.. \$\endgroup\$
    – Junaid
    Commented Feb 3 at 9:31
  • \$\begingroup\$ The idea of IGeminiContent<T> is to have a base interface that you can use to control your Gemini contents. You can treat it as StringContent HttpClient, but we don't want to make it wide-open to send any StringContent, so we use IGeminiContent<T>. \$\endgroup\$
    – iSR5
    Commented Feb 3 at 12:24
1
\$\begingroup\$

Which C# version are you using? It seems like a lot of things could be simplified / make it more concise by utilizing newer C# features. – Peter Csala

I am using C# 10. – Junaid

@PeterCsala can you please mention those areas? – Junaid

GeminiAuthHandler

if (!string.IsNullOrEmpty(apiKey))
{
   request.Headers.Add(GeminiConfiguration.Scheme, apiKey);
}
else
{
   throw new ArgumentNullException($"Your API key is invalid ...");
}

to

ArgumentException.ThrowIfNullOrEmpty(apiKey, "Your API key is invalid ...");

request.Headers.Add(GeminiConfiguration.Scheme, apiKey);

Reference

GeminiHttpClientOptions

private const string DefaultBaseAddress = "https://generativelanguage.googleapis.com";

private readonly Uri uri = new Uri(DefaultBaseAddress);

public Uri Url
{
    get
    {
        return uri;
    }
}

to

private const string DefaultBaseAddress = "https://generativelanguage.googleapis.com";

public Uri Url { get; } = new Uri(DefaultBaseAddress);

Reference

GeminiConfiguration

private string apiKey;

public GeminiConfiguration()
{
    apiKey = string.Empty;
}

public string ApiKey
{
    get
    {
        return apiKey;
    }

    set
    {
        if (string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentNullException(nameof(ApiKey), $"Your API key is invalid, ...");
        }
        apiKey = value;
    }
}

to

private string? apiKey;
public string ApiKey
{
    get => apiKey ?? string.Empty;
    set
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(value?.Trim(), "Your API key is invalid, ...");
        apiKey = value;
    }
}

GeminiClient

You might consider to take advantage of the JsonContent instead of using StringContent and StreamContent.

SendAsync inside GeminiClient

You might consider to prefer using declaration over using statement to reduce/eliminate much of the indentations.


var jsonString = "{" + line + "}";
var jsonObject = JsonSerializer.Deserialize<JsonObject>(jsonString);
yield return jsonObject?["text"]?.ToString();

to

yield return JsonNode.Parse($"{{ {line} }}")["text"];

Reference

CreateModelRequest inside TextService

var contents = new Content[]
{
    new Content
    {
        parts = new[]
        {
            new Part
            {
                text = text
            }
        }
    }
};

to

Content[] contents = [ new() { parts = [ new() { text = text } ] } ];

Reference

CreateRequestModel inside ChatService

var contents = new List<Content>();

foreach (var message in chat)
{
    contents.Add(new Content
    {
        role = message.Role,
        parts = new[]
        {
            new Part
            {
                text = message.Text
            }
        }
    });
}

to

var contents = chat
    .Select(message => new Content() { role = message.Role, parts = [ new() { text = message.Text } ] })
    .ToList();

Reference

And the same sort of transformation could be applied for the CreateRequestModel inside the VisionService.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.