7

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

 1 year ago
source link: https://www.cnblogs.com/anech/p/17520977.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

49399-20230702165056627-1110807339.png

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服务的注册代码注释掉。

49399-20230702170125913-1459860304.png

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
});
49399-20230702170126089-1935924512.png

这样,通过非依赖注入方式,我们已经实现了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服务的注释。

49399-20230702170126066-1415387514.png

4、恢复IOpenAIService依赖注入

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

49399-20230702170126105-2002955109.png

动态删除无效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>();
49399-20230702170126109-1936714270.png

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK