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. [Linux]常用命令之【hostname】

    1: 个人的片面理解: hostname是主机名(的"昵称"),而非域名.一般设置hostname,来标识当前机器的主要用途.以区别与其它机器 2: hostname的严格定义: ...

  2. day07 字符串和列表

    day07字符串与列表 字符串的内置方法 lower upper startswitch endwhich 格式化输出 format join的用法 replace替换字符串 isdigit判断 字符 ...

  3. must be reducible node 错误

    "must be reducible node"错误通常是由于使用了无法转换为表达式树的代码或表达式. 场景再现:在项目中使用GroupBy的时候,对字段进行了类型转换,接下来正常 ...

  4. day91:luffy:基于vue+drf的路飞学城项目后端部署

    目录 1.安装mysql镜像 2.把本地的数据导入到容器的mysql数据库中 3.安装redis容器 4.把后端项目部署前的处理 5.修改项目的配置文件:prod.py 6.从后端项目中收集静态文件 ...

  5. YII2.0的文件上传, 并把文件名称重新编译

    /** *@Action 文件上传示例 *@这里我们演示的是一个YII2.0的文件上传, 并把文件名称重新编译 *@我们上传的是一个叫 photo 的jpg和png格式的文件 */ Controlle ...

  6. 特性介绍 | MySQL 测试框架 MTR 系列教程(一):入门篇

    作者:卢文双 资深数据库内核研发 去年年底通过微信公众号[数据库内核]设定了一个目标--2023 年要写一系列 特性介绍+内核解析 的文章(现阶段还是以 MySQL 为主). 虽然关注者很少,但本着& ...

  7. Qt开发技术:Q3D图表开发笔记(三):Q3DSurface三维曲面图介绍、Demo以及代码详解

    前言   qt提供了q3d进行三维开发,虽然这个框架没有得到大量运用也不是那么成功,性能上也有很大的欠缺,但是普通的点到为止的应用展示还是可以的.  其中就包括华丽绚烂的三维图表,数据量不大的时候是可 ...

  8. 互异关系容斥&集合幂级数小记

    最近碰见了一些互异关系容斥的题目,而这类题目往往要配合集合幂级数的一些技术使用,所以简单记记. 内容很杂,行文很乱,作者水平很低,酌情观看. 互异关系容斥 思想其实很基本,应用范围其实很广. 原论文. ...

  9. 2022-09-15:Range模块是跟踪数字范围的模块。 设计一个数据结构来跟踪表示为 半开区间 的范围并查询它们。 半开区间 [left, right) 表示所有 left <= x < righ

    2022-09-15:Range模块是跟踪数字范围的模块. 设计一个数据结构来跟踪表示为 半开区间 的范围并查询它们. 半开区间 [left, right) 表示所有 left <= x < ...

  10. 2021-09-20:给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O

    2021-09-20:给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度.不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O ...