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的功能實現。
寫作不易,轉載請註明博文地址,否則禁轉!!!