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

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

  1. S5PV210开发系列四_uCGUI的移植

    S5PV210开发系列四 uCGUI的移植 象棋小子          1048272975 GUI(图形用户界面)极大地方便了非专业用户的使用,用户无需记忆大量的命令,取而代之的是能够通过窗体.菜单 ...

  2. 转:arcgis api for js入门开发系列四地图查询

    原文地址:arcgis api for js入门开发系列四地图查询 arcgis for js的地图查询方式,一般来说,总共有三种查询方式:FindTask.IdentifyTask.QueryTas ...

  3. 【Qt编程】基于Qt的词典开发系列<四>--无边框窗口的缩放与拖动

    在现在,绝大多数软件都向着简洁,时尚发展.就拿有道的单词本和我做的单词本来说,绝大多数用户肯定喜欢我所做的单词本(就单单界面,关于颜色搭配和布局问题,大家就不要在意了). 有道的单词本: 我所做的单词 ...

  4. BizTalk 开发系列(四十一) BizTalk 2010 BAM 安装手记

    使用64位系统可以支持更大的内存,现在服务器基本上都使用64位系统.微软从Windows Server 2008 R2开始服务器版的操作系统也只支持64位了,不过对于像BizTalk这种“繁杂的东西” ...

  5. 微信小程序开发系列四:微信小程序之控制器的初始化逻辑

    微信小程序开发系列教程 微信小程序开发系列一:微信小程序的申请和开发环境的搭建 微信小程序开发系列二:微信小程序的视图设计 微信小程序开发系列三:微信小程序的调试方法 这个教程的前两篇文章,介绍了如何 ...

  6. leaflet-webpack 入门开发系列四图层控件样式优化篇(附源码下载)

    前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...

  7. BizTalk 开发系列(四十) BizTalk WCF-SQL Adapter读取SQL Service Broker消息

    SQL Service Broker 是在SQL Server 2005中新增的功能.Service Broker 为 SQL Server 提供队列和可靠的消息传递,可以可用来建立以异步消息为基础的 ...

  8. 10分钟学会web通讯的四种方式,短轮询、长轮询(comet)、长连接(SSE)、WebSocket

    一般看到标题我们一般会产生下面几个问题??? 什么是短轮询? 什么是长轮询? 长连接又是什么? wensocket怎么实现呢? 他们都能实现web通讯,区别在哪呢,哪个好用呢? 接下来我们就一个个来了 ...

  9. BizTalk 开发系列(四十二) 为BizTalk应用程序打包不同的环境Binding

    我们在使用微软或者其他公司提供的BizTalk应用程序MSI包的时候经常会有一个目标环境的选择选项.该选项可以在不同的环境下使用不同的绑定(BizTalk应用程序配置)感觉很高级. 其实这个非常的简单 ...

  10. BizTalk开发系列(四) 深入Map测试

    在BizTalk的开发过程中XML消息间的映射是一个很重要的内容.如果只是一般的从源节点的值复制到目标节点的话,BizTalk项目提供的 MAP测试和验证就已经可以满足需求了.但是很多时候需要在映射的 ...

随机推荐

  1. 用 Go 剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

    输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分. 示例: 输入:nums = [1,2,3,4]输出:[1,3,2,4] 注:[3,1, ...

  2. 10.CAS实现单点登录

    1.总结: 昨天主要是了解和编写了CAS实现单点登录的代码: CAS实现单点登录的流程:用户访问资源服务器,先跳转到验证服务器验证身份通过后,认证服务器发送一个ticket给用户,用户拿着ticket ...

  3. 【Note】矩阵加速

    感谢 \(\text{tidongCrazy}\) 倾情授课. 目录 基本形式 基础习题 P1962 斐波那契数列(例题) P4838 P哥破解密码(矩阵加速) 稍微up P1397 [NOI2013 ...

  4. .NET Core反射获取带有自定义特性的类,通过依赖注入根据Attribute元数据信息调用对应的方法

    前言 前段时间有朋友问道一个这样的问题,.NET Core中如何通过Attribute的元数据信息来调用标记的对应方法.我第一时间想到的就是通过C#反射获取带有Custom Attribute标记的类 ...

  5. 飞桨paddlespeech语音唤醒推理C实现

    上篇(飞桨paddlespeech 语音唤醒初探)初探了paddlespeech下的语音唤醒方案,通过调试也搞清楚了里面的细节.因为是python 下的,不能直接部署,要想在嵌入式上部署需要有C下的推 ...

  6. 基于Java实现数据脱敏

    用法 Jdk版本 大于等于1.8 maven依赖 <dependency> <groupId>red.zyc</groupId> <artifactId> ...

  7. linux下live555编译和调试

    linux下live555编译和调试 live555 支持 h.264 初步告捷,可以播放,尽管不是很稳定,或者说暂时只能播放 1 帧(主要是我现在还不了解 帧的概念),同时还有 Mal SDP 的传 ...

  8. js对象方法大全

    JavaScript中Object构造函数的方法 Object构造函数的方法节 Object.assign() 通过复制一个或多个对象来创建一个新的对象. Object.create() 使用指定的原 ...

  9. 是时候,升级你的 Windows 了「GitHub 热点速览」

    不知道多少小伙伴用着 Windows 操作系统,可能会有一个烦恼是有时候操作系统过慢,因为众多拖慢 Windows 系统的组件.Atlas 作为一个修改版的 Windows 系统,能极大提高操作系统运 ...

  10. abp(net core)+easyui+efcore实现仓储管理系统——供应商管理升级之上(六十三)

    abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统--ABP总体介绍(一) abp(net core)+ ...