【.Net/C#之ChatGPT開發系列】四、ChatGPT多KEY動態輪詢,自動刪除無效KEY

ChatGPT是一種基於Token數量計費的語言模型,它可以生成高質量的文本。然而,每個新賬號只有一個有限的初始配額,用完後就需要付費才能繼續使用。爲此,我們可能存在使用多KEY的情況,並在每個KEY達到額度上限後,自動將其刪除。那麼,我們應該如何實現這個功能呢?還請大家掃個小關。👇

ChatGPT多KEY輪詢

爲了實現多KEY管理,我們通常需要把所有密鑰保存在數據庫中,但爲了簡化演示,這裏我使用Redis來進行存儲和管理多個KEY。同樣,我將重新創建一個名爲ChatGPT.Demo4的項目,代碼和ChatGPT.Demo3相同。

一、Redis密鑰管理

1、定義IChatGPTKeyService接口

在根目錄下,創建一個名爲Extensions的文件夾,然後右鍵點擊它,新建一個IChatGPTKeyService.cs接口文件,並寫入以下代碼:

public interface IChatGPTKeyService
{
    //初始話密鑰
    public Task InitAsync();

    //隨機獲取密鑰KEY
    public Task<string> GetRandomAsync();

    //獲取所有密鑰
    Task<string[]> GetAllAsync();

    //移除密鑰
    Task RemoveAsync(string apiKey);
}

InitAsync方法用以初始化密鑰,GetRandomAsync方法用於隨機讀取一個密鑰,GetAllAsync方法用於讀取所有密鑰,RemoveAsync方法用於刪除指定密鑰。

2、實現IChatGPTKeyService服務

安裝StackExchange.Redis庫,這是一個用於訪問和操作Redis數據庫的.NET客戶端。

PM> Install-Package StackExchange.Redis

右鍵點擊Extensions文件夾,新建一個ChatGPTKeyService.cs文件,並在文件中寫入以下代碼:

using StackExchange.Redis;

public class ChatGPTKeyService : IChatGPTKeyService
{
    private ConnectionMultiplexer? _connection;
    private IDatabase? _cache;
    private readonly string _configuration;
    private const string _redisKey = "ChatGPTKey";

    public ChatGPTKeyService(string configuration)
    {
        _configuration = configuration;
    }

    private async Task ConnectAsync()
    {
        if (_cache != null) return;
        _connection = await ConnectionMultiplexer.ConnectAsync(_configuration);
        _cache = _connection.GetDatabase();
    }
    public async Task InitAsync()
    {
        await ConnectAsync();
        //使用Set對象存儲密鑰
        await _cache!.SetAddAsync(_redisKey, new RedisValue[] {
        "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1",
        "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2",
        });
    }
    public async Task<string> GetRandomAsync()
    {
        await ConnectAsync();
        //使用Set隨機返回一個密鑰
        var redisValue = await _cache!.SetRandomMemberAsync(_redisKey);
        return redisValue.ToString();
    }

    public async Task<string[]> GetAllAsync()
    {
        await ConnectAsync();
        //讀取所有密鑰
        var redisValues = await _cache!.SetMembersAsync(_redisKey);
        return redisValues.Select(m => m.ToString()).ToArray();
    }

    public async Task RemoveAsync(string apiKey)
    {
        await ConnectAsync();
        await _cache!.SetRemoveAsync(_redisKey, apiKey);
    }
}

爲了保存KEY,我們選擇使用Redis的Set數據結構,它可以存儲不重複的元素,並且可以隨機返回一個元素。這樣,我們就可以實現密鑰的隨機輪換功能。ConnectAsync方法是用來建立和Redis數據庫的連接。

接下來,我們打開Program.cs文件註冊ChatGPTKeyService服務。另外,爲了演示效果,我們需要在項目啓動的時候,調用InitAsync方法來初始化數據:

using ChatGPT.Demo4.Extensions;
//註冊IChatGPTKeyService單例服務
builder.Services.AddSingleton<IChatGPTKeyService>(
    new ChatGPTKeyService("localhost"));
    
var app = builder.Build();
//初始化redis數據庫
var _chatGPTKeyService = app.Services.GetRequiredService<IChatGPTKeyService>();
_chatGPTKeyService.InitAsync().Wait();

Betalgo.OpenAI提供了兩種使用方式,一種是依賴注入,一種是非依賴注入。之前我們採用的是依賴注入方式,大家會發現,依賴注入並不支持多KEY的設置,爲此,我們先來看看如何使用非依賴注入的方式實現。

//Betalgo.OpenAI地址:https://github.com/betalgo/openai

二、 非依賴注入實現密鑰輪換

1、取消IOpenAIService服務註冊

我們先打開Program.cs文件,把IOpenAIService服務的註冊代碼註釋掉。

2、取消IOpenAIService依賴注入

打開Controllers/ChatController.cs文件,在文件開頭添加IChatGPTKeyService服務的命名空間,然後在構造函數中注入該服務。同時,我們把IOpenAIService服務的注入也註釋掉。

using ChatGPT.Demo4.Extensions;

//private readonly IOpenAIService _openAiService;
private readonly IChatGPTKeyService _chatGPTKeyService;

public ChatController(/*IOpenAIService openAiService,*/ IChatGPTKeyService chatGPTKeyService)
{
    //_openAiService = openAiService;
    _chatGPTKeyService = chatGPTKeyService;
}

3、手動實例化IOpenAIService

接着修改Input方法,先調用IChatGPTKeyService中的GetRandomAsync方法,獲取一個隨機的密鑰。然後,使用這個密鑰來手動創建一個IOpenAIService服務的實例。

string apiKey = await _chatGPTKeyService.GetRandomAsync();
IOpenAIService _openAiService = new OpenAIService(new OpenAiOptions
{
    ApiKey = apiKey
});

這樣,通過非依賴注入方式,我們已經實現了ChatGPT的多KEY動態輪詢功能,但是這種方式沒有利用.Net Core的依賴注入機制,無法發揮它的優勢。那麼,有沒有可能用依賴注入的方式來達到同樣的效果呢?答案是肯定的,讓我們繼續。

三、 依賴注入實現密鑰輪換

Betalgo.OpenAI請求是基於HttpClient來實現的,這給我們實現多KEY切換帶來了希望。

DelegatingHandler是一個抽象類,它繼承自HttpMessageHandler,用於處理HTTP請求和響應。它的特點是可以將請求和響應的處理委託給另一個處理程序,稱爲內部處理程序。通常,一系列的DelegatingHandler被鏈接在一起,形成一個處理程序鏈。第一個處理程序接收一個HTTP請求,做一些處理,然後將請求傳遞給下一個處理程序,這種模式被稱爲委託處理程序模式。

HttpClient默認使用HttpClientHandler處理程序來處理請求,HttpClientHandler繼承自HttpMessageHandler,它重寫了HttpMessageHandler的Send方法,負責將請求通過網絡發送到服務器並獲取服務器的響應。因此,我們可以在管道中插入自定義的DelegatingHandler,來攔截修改請求頭中的密鑰,實現多KEY輪換的功能。

1、創建DelegatingHandler

要編寫一個自定義的DelegatingHandler,我們需要繼承System.Net.Http.DelegatingHandler類,並重寫它的Send方法。

我們在Extensions文件夾中創建一個名爲ChatGPTHttpMessageHandler.cs的文件,然後在其中添加以下代碼:

    public class ChatGPTHttpMessageHandler : DelegatingHandler
{
        private readonly IChatGPTKeyService _chatGPTKeyService;

        public ChatGPTHttpMessageHandler(IChatGPTKeyService  chatGPTKeyService)
        {
            _chatGPTKeyService = chatGPTKeyService;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var apiKey = await _chatGPTKeyService.GetRandomAsync();

            request.Headers.Remove("Authorization");
            request.Headers.Add("Authorization", $"Bearer {apiKey}");
            return await base.SendAsync(request, cancellationToken);
        }

        protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var apiKey = _chatGPTKeyService.GetRandomAsync().Result;
            request.Headers.Remove("Authorization");
            request.Headers.Add("Authorization", $"Bearer {apiKey}");
            return base.Send(request, cancellationToken);
        }
    }

在ChatGPTHttpMessageHandler中,我們通過依賴注入的方式獲取IChatGPTKeyService密鑰服務的實例,然後重寫了Send方法,調用IChatGPTKeyService的GetRandomAsync方法隨機獲取一個KEY,接着使用HttpHeaders的Remove方法移除默認的KEY,再使用HttpHeaders的Add方法添加獲取的KEY,最後我們調用base.SendAsync方法將請求傳遞給內部處理程序進行後續的處理。這樣我們就完成了KEY的切換。

2、註冊DelegatingHandler

接下來,我們需要在Program.cs文件中,將ChatGPTHttpMessageHandler處理程序註冊到OpenAIService的請求管道中。

builder.Services.AddTransient<ChatGPTHttpMessageHandler>();
builder.Services.AddHttpClient<IOpenAIService, OpenAIService>().AddHttpMessageHandler<ChatGPTHttpMessageHandler>();

3、重新註冊IOpenAIService服務

同時取消Program.cs文件中OpenAIService服務的註釋。

4、恢復IOpenAIService依賴注入

最後在Controllers/ChatController.cs中,我們重新使用依賴注入的方式獲取OpenAIService服務的實例,同時註釋掉手動創建OpenAIService的代碼。

動態刪除無效KEY

當ChatGPT賬號使用達到額度上限時,KEY將會失效,爲此,我們需要及時刪除無效的KEY,避免影響請求的正常發送。但比較遺憾,OpenAI官方並沒有提供直接的API來查詢額度,那麼,我們怎麼知道KEY是否還有效呢?

幸運的是,有大神通過抓包分析發現了兩個可用的接口,可以用來查詢KEY的相關信息,一個是賬單查詢API,用來查詢KEY的過期時間和剩餘額度,它接受GET請求,在Header中帶上授權Token(API KEY)即可。

//賬單查詢API:https://api.openai.com/v1/dashboard/billing/subscription

另一個是賬單明細查詢,用來查詢已使用的額度和具體的請求記錄,它也是一個GET請求,在Header中同樣需要攜帶授權Token(API KEY),另外還可以通過參數指定要查詢的日期範圍。

//賬單明細:https://api.openai.com/v1/v1/dashboard/billing/usage?start_date=2023-07-01&end_date=2023-07-02

1、創建ChatGPT賬單查詢服務

我們在Extensions文件夾中創建IChatGPTBillService.cs接口和ChatGPTBillService.cs服務兩個文件,IChatGPTBillService接口聲明瞭賬單及明細查詢兩個方法,代碼如下:

public interface IChatGPTBillService
{
    /// <summary>
    /// 查詢賬單
    /// </summary>
    /// <param name="apiKey">api密鑰</param>
    /// <returns></returns>
    Task<ChatGPTBillModel?> QueryAsync(string apiKey);

    /// <summary>
    /// 賬單明細
    /// </summary>
    /// <param name="apiKey">api密鑰</param>
    /// <param name="startTime">開始日期</param>
    /// <param name="endTime">結束日期</param>
    /// <returns></returns>
    Task<ChatGPTBillDetailsModel?> QueryDetailsAsync(string apiKey, DateTimeOffset startTime, DateTimeOffset endTime);
}

ChatGPTBillService服務是IChatGPTBillService接口的實現,代碼如下所示:

public class ChatGPTBillService : IChatGPTBillService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public ChatGPTBillService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<ChatGPTBillModel?> QueryAsync(string apiKey)
    {
        string url = "https://api.openai.com/v1/dashboard/billing/subscription";
        var client = _httpClientFactory.CreateClient();
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
        var response = await client.GetFromJsonAsync<ChatGPTBillModel>(url);
        return response;
    }

    public async Task<ChatGPTBillDetailsModel?> QueryDetailsAsync(string apiKey, DateTimeOffset startTime, DateTimeOffset endTime)
    {
        string url = $"https://api.openai.com/dashboard/billing/usage?start_date={startTime:yyyy-MM-dd}&end_date={endTime:yyyy-MM-dd}";
        var client = _httpClientFactory.CreateClient();
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
        var response = await client.GetFromJsonAsync<ChatGPTBillDetailsModel>(url);
        return response;
    }
}

ChatGPTBillService通過使用IHttpClientFactory工廠創建HttpClient來發送請求,並在請求頭中添加ChatGPT的授權Token,即API KEY,從而實現對ChatGPT的賬單和明細的查詢功能。考慮到篇幅長度,這裏不再給出賬單類ChatGPTBillModel和賬單明細類ChatGPTBillDetailsModel的具體定義。

2、創建後臺任務過濾無效KEY

我們使用BackgroundService來實現自動過濾任務,BackgroundService是.NET Core中的一個抽象基類,它實現了IHostedService接口,用於執行後臺任務或長時間運行的服務。BackgroundService類提供了以下方法:

  • StartAsync (CancellationToken):在服務啓動時調用,可以用於執行一些初始化操作。
  • StopAsync (CancellationToken):在服務停止時調用,可以用於執行一些清理操作。
  • ExecuteAsync (CancellationToken):在服務運行時調用,包含了後臺任務的主要邏輯,必須被重寫

我們創建一個後臺定時任務,在ExecuteAsync方法中執行ChatGPT的密鑰過濾。在Extensions文件夾中新建一個名爲ChatGPTBillBackgroundService.cs的文件,並在其中添加如下代碼:

public class ChatGPTBillBackgroundService : BackgroundService
{
    private readonly IChatGPTKeyService _chatGPTKeyService;
    private readonly IChatGPTBillService _chatGPTBillService;

    public ChatGPTBillBackgroundService(IChatGPTKeyService chatGPTKeyService, IChatGPTBillService chatGPTBillService)
    {
        _chatGPTKeyService = chatGPTKeyService;
        _chatGPTBillService = chatGPTBillService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var apiKeys = await _chatGPTKeyService.GetAllAsync();
            foreach (var apiKey in apiKeys)
            {
                var bill = await _chatGPTBillService.QueryAsync(apiKey);
                if (bill == null) continue;

                var dt = DateTimeOffset.Now;
                //判斷key是否到期或是否有額度
                if (bill.AccessUntil < dt.ToUnixTimeSeconds() || bill.HardLimitUsd == 0)
                {
                    await _chatGPTKeyService.RemoveAsync(apiKey);
                    continue;
                }
                //查詢99天以內的賬單明細
                var billDetails = await _chatGPTBillService.QueryDetailsAsync(
                    apiKey, dt.AddDays(-99), dt.AddDays(1));

                if (billDetails == null) continue;

                //判斷已使用額度大於等於總額度
                if (billDetails.TotalUsage >= bill.HardLimitUsd)
                {
                    await _chatGPTKeyService.RemoveAsync(apiKey);
                    continue;
                }
            }

            // 創建一個異步的任務,該任務在指定1分鐘間隔後完成
            await Task.Delay(1 * 60 * 1000, stoppingToken);
        }

    }
}

ChatGPTBillBackgroundService類繼承自BackgroundService,並通過構造函數注入了IChatGPTKeyService密鑰服務和IChatGPTBillService賬單服務,然後重寫了ExecuteAsync方法,通過使用while循環和Task.Delay方法間接實現每分鐘執行一次的定時任務,任務的邏輯是:從緩存中獲取所有密鑰,然後對每個密鑰進行以下操作:

  • 調用IChatGPTBillService服務,查詢密鑰的有效期和總額度。
  • 如果密鑰已過期或總額度爲零,就從緩存中移除該密鑰。
  • 如果密鑰仍有效,就繼續調用IChatGPTBillService服務,查詢密鑰的已使用額度。
  • 如果已使用額度大於或等於總額度,就從緩存中移除該密鑰。

爲了讓這個後臺服務能夠在系統啓動時運行,我們還需要在Program.cs文件中註冊它。打Program.cs文件,加入下面的代碼:

//註冊賬單服務
builder.Services.AddSingleton<IChatGPTBillService, ChatGPTBillService>();
//註冊後臺任務
builder.Services.AddHostedService<ChatGPTBillBackgroundService>();

至此,我們完成了ChatGPT的多KEY動態輪詢,和自動刪除無效KEY的功能實現。

寫作不易,轉載請註明博文地址,否則禁轉!!!

//源碼地址:https://github.com/ynanech/ChatGPT.Demo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章