【.Net/C#之ChatGPT开发系列】四、ChatGPT多KEY动态轮询,自动删除无效KEY

时间:2024-10-05 07:05:14

ChatGPT是一种基于Token数量计费的语言模型,它可以生成高质量的文本。然而,每个新账号只有一个有限的初始配额,用完后就需要付费才能继续使用。为此,我们可能存在使用多KEY的情况,并在每个KEY达到额度上限后,自动将其删除。那么,我们应该如何实现这个功能呢?还请大家扫个小关。????

ChatGPT多KEY轮询

为了实现多KEY管理,我们通常需要把所有密钥保存在数据库中,但为了简化演示,这里我使用Redis来进行存储和管理多个KEY。同样,我将重新创建一个名为ChatGPT.Demo4的项目,代码和ChatGPT.Demo3相同。

一、Redis密钥管理

1、定义IChatGPTKeyService接口

在根目录下,创建一个名为Extensions的文件夹,然后右键点击它,新建一个接口文件,并写入以下代码:

  1. public interface IChatGPTKeyService
  2. {
  3. //初始话密钥
  4. public Task InitAsync();
  5. //随机获取密钥KEY
  6. public Task<string> GetRandomAsync();
  7. //获取所有密钥
  8. Task<string[]> GetAllAsync();
  9. //移除密钥
  10. Task RemoveAsync(string apiKey);
  11. }

InitAsync方法用以初始化密钥,GetRandomAsync方法用于随机读取一个密钥,GetAllAsync方法用于读取所有密钥,RemoveAsync方法用于删除指定密钥。

2、实现IChatGPTKeyService服务

安装库,这是一个用于访问和操作Redis数据库的.NET客户端。

PM> Install-Package 

右键点击Extensions文件夹,新建一个文件,并在文件中写入以下代码:

  1. using ;
  2. public class ChatGPTKeyService : IChatGPTKeyService
  3. {
  4. private ConnectionMultiplexer? _connection;
  5. private IDatabase? _cache;
  6. private readonly string _configuration;
  7. private const string _redisKey = "ChatGPTKey";
  8. public ChatGPTKeyService(string configuration)
  9. {
  10. _configuration = configuration;
  11. }
  12. private async Task ConnectAsync()
  13. {
  14. if (_cache != null) return;
  15. _connection = await (_configuration);
  16. _cache = _connection.GetDatabase();
  17. }
  18. public async Task InitAsync()
  19. {
  20. await ConnectAsync();
  21. //使用Set对象存储密钥
  22. await _cache!.SetAddAsync(_redisKey, new RedisValue[] {
  23. "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1",
  24. "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2",
  25. });
  26. }
  27. public async Task<string> GetRandomAsync()
  28. {
  29. await ConnectAsync();
  30. //使用Set随机返回一个密钥
  31. var redisValue = await _cache!.SetRandomMemberAsync(_redisKey);
  32. return ();
  33. }
  34. public async Task<string[]> GetAllAsync()
  35. {
  36. await ConnectAsync();
  37. //读取所有密钥
  38. var redisValues = await _cache!.SetMembersAsync(_redisKey);
  39. return (m => ()).ToArray();
  40. }
  41. public async Task RemoveAsync(string apiKey)
  42. {
  43. await ConnectAsync();
  44. await _cache!.SetRemoveAsync(_redisKey, apiKey);
  45. }
  46. }

为了保存KEY,我们选择使用Redis的Set数据结构,它可以存储不重复的元素,并且可以随机返回一个元素。这样,我们就可以实现密钥的随机轮换功能。ConnectAsync方法是用来建立和Redis数据库的连接。

接下来,我们打开文件注册ChatGPTKeyService服务。另外,为了演示效果,我们需要在项目启动的时候,调用InitAsync方法来初始化数据:

  1. using 4.Extensions;
  2. //注册IChatGPTKeyService单例服务
  3. <IChatGPTKeyService>(
  4. new ChatGPTKeyService("localhost"));
  5. var app = ();
  6. //初始化redis数据库
  7. var _chatGPTKeyService = <IChatGPTKeyService>();
  8. _chatGPTKeyService.InitAsync().Wait();

提供了两种使用方式,一种是依赖注入,一种是非依赖注入。之前我们采用的是依赖注入方式,大家会发现,依赖注入并不支持多KEY的设置,为此,我们先来看看如何使用非依赖注入的方式实现。

//地址:/betalgo/openai

二、 非依赖注入实现密钥轮换

1、取消IOpenAIService服务注册

我们先打开文件,把IOpenAIService服务的注册代码注释掉。

2、取消IOpenAIService依赖注入

打开Controllers/文件,在文件开头添加IChatGPTKeyService服务的命名空间,然后在构造函数中注入该服务。同时,我们把IOpenAIService服务的注入也注释掉。

  1. using 4.Extensions;
  2. //private readonly IOpenAIService _openAiService;
  3. private readonly IChatGPTKeyService _chatGPTKeyService;
  4. public ChatController(/*IOpenAIService openAiService,*/ IChatGPTKeyService chatGPTKeyService)
  5. {
  6. //_openAiService = openAiService;
  7. _chatGPTKeyService = chatGPTKeyService;
  8. }
3、手动实例化IOpenAIService

接着修改Input方法,先调用IChatGPTKeyService中的GetRandomAsync方法,获取一个随机的密钥。然后,使用这个密钥来手动创建一个IOpenAIService服务的实例。

  1. string apiKey = await _chatGPTKeyService.GetRandomAsync();
  2. IOpenAIService _openAiService = new OpenAIService(new OpenAiOptions
  3. {
  4. ApiKey = apiKey
  5. });

这样,通过非依赖注入方式,我们已经实现了ChatGPT的多KEY动态轮询功能,但是这种方式没有利用.Net Core的依赖注入机制,无法发挥它的优势。那么,有没有可能用依赖注入的方式来达到同样的效果呢?答案是肯定的,让我们继续。

三、 依赖注入实现密钥轮换

请求是基于HttpClient来实现的,这给我们实现多KEY切换带来了希望。

DelegatingHandler是一个抽象类,它继承自HttpMessageHandler,用于处理HTTP请求和响应。它的特点是可以将请求和响应的处理委托给另一个处理程序,称为内部处理程序。通常,一系列的DelegatingHandler被链接在一起,形成一个处理程序链。第一个处理程序接收一个HTTP请求,做一些处理,然后将请求传递给下一个处理程序,这种模式被称为委托处理程序模式。

HttpClient默认使用HttpClientHandler处理程序来处理请求,HttpClientHandler继承自HttpMessageHandler,它重写了HttpMessageHandler的Send方法,负责将请求通过网络发送到服务器并获取服务器的响应。因此,我们可以在管道中插入自定义的DelegatingHandler,来拦截修改请求头中的密钥,实现多KEY轮换的功能。

1、创建DelegatingHandler

要编写一个自定义的DelegatingHandler,我们需要继承类,并重写它的Send方法。

我们在Extensions文件夹中创建一个名为的文件,然后在其中添加以下代码:

  1. public class ChatGPTHttpMessageHandler : DelegatingHandler
  2. {
  3. private readonly IChatGPTKeyService _chatGPTKeyService;
  4. public ChatGPTHttpMessageHandler(IChatGPTKeyService chatGPTKeyService)
  5. {
  6. _chatGPTKeyService = chatGPTKeyService;
  7. }
  8. protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  9. {
  10. var apiKey = await _chatGPTKeyService.GetRandomAsync();
  11. ("Authorization");
  12. ("Authorization", $"Bearer {apiKey}");
  13. return await base.SendAsync(request, cancellationToken);
  14. }
  15. protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
  16. {
  17. var apiKey = _chatGPTKeyService.GetRandomAsync().Result;
  18. ("Authorization");
  19. ("Authorization", $"Bearer {apiKey}");
  20. return base.Send(request, cancellationToken);
  21. }
  22. }

在ChatGPTHttpMessageHandler中,我们通过依赖注入的方式获取IChatGPTKeyService密钥服务的实例,然后重写了Send方法,调用IChatGPTKeyService的GetRandomAsync方法随机获取一个KEY,接着使用HttpHeaders的Remove方法移除默认的KEY,再使用HttpHeaders的Add方法添加获取的KEY,最后我们调用方法将请求传递给内部处理程序进行后续的处理。这样我们就完成了KEY的切换。

2、注册DelegatingHandler

接下来,我们需要在文件中,将ChatGPTHttpMessageHandler处理程序注册到OpenAIService的请求管道中。

  1. <ChatGPTHttpMessageHandler>();
  2. <IOpenAIService, OpenAIService>().AddHttpMessageHandler<ChatGPTHttpMessageHandler>();
3、重新注册IOpenAIService服务

同时取消文件中OpenAIService服务的注释。

4、恢复IOpenAIService依赖注入

最后在Controllers/中,我们重新使用依赖注入的方式获取OpenAIService服务的实例,同时注释掉手动创建OpenAIService的代码。

动态删除无效KEY

当ChatGPT账号使用达到额度上限时,KEY将会失效,为此,我们需要及时删除无效的KEY,避免影响请求的正常发送。但比较遗憾,OpenAI官方并没有提供直接的API来查询额度,那么,我们怎么知道KEY是否还有效呢?

幸运的是,有大神通过抓包分析发现了两个可用的接口,可以用来查询KEY的相关信息,一个是账单查询API,用来查询KEY的过期时间和剩余额度,它接受GET请求,在Header中带上授权Token(API KEY)即可。

//账单查询API:/v1/dashboard/billing/subscription

另一个是账单明细查询,用来查询已使用的额度和具体的请求记录,它也是一个GET请求,在Header中同样需要携带授权Token(API KEY),另外还可以通过参数指定要查询的日期范围。

//账单明细:/v1/v1/dashboard/billing/usage?start_date=2023-07-01&end_date=2023-07-02

1、创建ChatGPT账单查询服务

我们在Extensions文件夹中创建接口和服务两个文件,IChatGPTBillService接口声明了账单及明细查询两个方法,代码如下:

  1. public interface IChatGPTBillService
  2. {
  3. /// <summary>
  4. /// 查询账单
  5. /// </summary>
  6. /// <param name="apiKey">api密钥</param>
  7. /// <returns></returns>
  8. Task<ChatGPTBillModel?> QueryAsync(string apiKey);
  9. /// <summary>
  10. /// 账单明细
  11. /// </summary>
  12. /// <param name="apiKey">api密钥</param>
  13. /// <param name="startTime">开始日期</param>
  14. /// <param name="endTime">结束日期</param>
  15. /// <returns></returns>
  16. Task<ChatGPTBillDetailsModel?> QueryDetailsAsync(string apiKey, DateTimeOffset startTime, DateTimeOffset endTime);
  17. }

ChatGPTBillService服务是IChatGPTBillService接口的实现,代码如下所示:

  1. public class ChatGPTBillService : IChatGPTBillService
  2. {
  3. private readonly IHttpClientFactory _httpClientFactory;
  4. public ChatGPTBillService(IHttpClientFactory httpClientFactory)
  5. {
  6. _httpClientFactory = httpClientFactory;
  7. }
  8. public async Task<ChatGPTBillModel?> QueryAsync(string apiKey)
  9. {
  10. string url = "/v1/dashboard/billing/subscription";
  11. var client = _httpClientFactory.CreateClient();
  12. ("Authorization", $"Bearer {apiKey}");
  13. var response = await <ChatGPTBillModel>(url);
  14. return response;
  15. }
  16. public async Task<ChatGPTBillDetailsModel?> QueryDetailsAsync(string apiKey, DateTimeOffset startTime, DateTimeOffset endTime)
  17. {
  18. string url = $"/dashboard/billing/usage?start_date={startTime:yyyy-MM-dd}&end_date={endTime:yyyy-MM-dd}";
  19. var client = _httpClientFactory.CreateClient();
  20. ("Authorization", $"Bearer {apiKey}");
  21. var response = await <ChatGPTBillDetailsModel>(url);
  22. return response;
  23. }
  24. }

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文件夹中新建一个名为的文件,并在其中添加如下代码:

  1. public class ChatGPTBillBackgroundService : BackgroundService
  2. {
  3. private readonly IChatGPTKeyService _chatGPTKeyService;
  4. private readonly IChatGPTBillService _chatGPTBillService;
  5. public ChatGPTBillBackgroundService(IChatGPTKeyService chatGPTKeyService, IChatGPTBillService chatGPTBillService)
  6. {
  7. _chatGPTKeyService = chatGPTKeyService;
  8. _chatGPTBillService = chatGPTBillService;
  9. }
  10. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  11. {
  12. while (!)
  13. {
  14. var apiKeys = await _chatGPTKeyService.GetAllAsync();
  15. foreach (var apiKey in apiKeys)
  16. {
  17. var bill = await _chatGPTBillService.QueryAsync(apiKey);
  18. if (bill == null) continue;
  19. var dt = ;
  20. //判断key是否到期或是否有额度
  21. if ( < () || == 0)
  22. {
  23. await _chatGPTKeyService.RemoveAsync(apiKey);
  24. continue;
  25. }
  26. //查询99天以内的账单明细
  27. var billDetails = await _chatGPTBillService.QueryDetailsAsync(
  28. apiKey, (-99), (1));
  29. if (billDetails == null) continue;
  30. //判断已使用额度大于等于总额度
  31. if ( >= )
  32. {
  33. await _chatGPTKeyService.RemoveAsync(apiKey);
  34. continue;
  35. }
  36. }
  37. // 创建一个异步的任务,该任务在指定1分钟间隔后完成
  38. await (1 * 60 * 1000, stoppingToken);
  39. }
  40. }
  41. }

ChatGPTBillBackgroundService类继承自BackgroundService,并通过构造函数注入了IChatGPTKeyService密钥服务和IChatGPTBillService账单服务,然后重写了ExecuteAsync方法,通过使用while循环和方法间接实现每分钟执行一次的定时任务,任务的逻辑是:从缓存中获取所有密钥,然后对每个密钥进行以下操作:

  • 调用IChatGPTBillService服务,查询密钥的有效期和总额度。
  • 如果密钥已过期或总额度为零,就从缓存中移除该密钥。
  • 如果密钥仍有效,就继续调用IChatGPTBillService服务,查询密钥的已使用额度。
  • 如果已使用额度大于或等于总额度,就从缓存中移除该密钥。

为了让这个后台服务能够在系统启动时运行,我们还需要在文件中注册它。打文件,加入下面的代码:

  1. //注册账单服务
  2. <IChatGPTBillService, ChatGPTBillService>();
  3. //注册后台任务
  4. <ChatGPTBillBackgroundService>();

至此,我们完成了ChatGPT的多KEY动态轮询,和自动删除无效KEY的功能实现。

写作不易,转载请注明博文地址,否则禁转!!!

//源码地址:/ynanech/