唠唠闲话

由于 OpenAI 的 API 会限制访问 IP,除了在本地访问时开启代理外,还可以使用代理链接的方式。

教程需要一台国外服务器,个人主要用 Azure,内容一览:

  • 将用户所有请求转发给 OpenAI,不做任何处理
  • 将用户请求发送给 OpenAI,在这过程中记录请求日志(包括请求密钥和请求内容)
  • 越过 ChatGPT 的 Cloudflare 限制

注:本篇配合 openai_api_call 工具使用更佳。

直接转发请求

参考知乎:Nginx 反向代理 OpenAI API

设置 http 转发

配置 Nginx 信息如下,vim /etc/nginx/sites-enabled/default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
server_name {your_domain_name};
location /prefix_real {
proxy_pass https://api.openai.com; # 端口号与 listen 保持一致
proxy_ssl_server_name on;
proxy_set_header Host api.openai.com;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Forwarded-For $server_addr;
proxy_set_header X-Forwarded-Proto https; # 或者用 $scheme 保持协议
}
}

将第一处的 {your_domain_name} 替换为你的域名,如果没有域名,这行可以注释掉。这里的 prefix_real 根据需要修改,起类似密码的作用。

参数说明:

  • proxy_pass:转发的上游服务器地址
  • proxy_ssl_server_name on 和 SSL/TLS 握手相关(待了解)
  • proxy_set_header Host,默认将上游地址作为 Host,这里手动设置为 api.openai.com
  • proxy_set_header X-Forwarded-For,建议填写代理所在的 IP 地址(可以通过 curl ifconfig.me 获取),这里通常有两个可用变量:
    • $server_addr 为 Nginx 的地址,但可能为内网 IP 而非实际 IP
    • $remote_addr 为客户端地址,不建议使用,否则可能被 OpenAI 限制
  • proxy_set_header X-Forwarded-Proto,默认将客户端协议作为 X-Forwarded-Proto

设置 ssl 证书

申请 ssl 证书后,将其放在 your_cert_pathyour_cert_key_path 处,并修改 Nginx 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 443 ssl;
server_name {your_domain_name};
ssl_certificate {your_cert_path};
ssl_certificate_key {your_cert_key_path};
location / {
proxy_pass https://api.openai.com/;
proxy_ssl_server_name on;
proxy_set_header Host api.openai.com;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Forwarded-For $server_addr;
proxy_set_header X-Forwarded-Proto https; # 或者用 $scheme 保持协议
}
}

监测数据并转发

比起直接用 nginx 转发,我们通过 Python 脚本转发请求,这样可以监测和处理交互的数据。

  • 使用 Python 的 flask 包编写脚本文件,将请求转发到 OpenAI
  • 使用 Nginx 配置域名

flask 转发请求

编写 main.py 文件,下载链接:main.py

下边是对文件内容的解释。

加载头部

导入必要的 Python 包

1
2
3
4
5
#!/usr/bin/env python
from flask import Flask, request, Response
import requests, random

app = Flask(__name__)

设置 API 请求日志的保存路径

1
2
3
4
5
6
7
8
9
# file for saving API requests
home = os.path.expanduser("~")
# api path
api_path = os.path.join(home, '.chatlog/apilist.log')
# request log
logfile = 'chatdata-' + datetime.datetime.now().strftime("%Y-%m-%d") + '.log'
log_path = os.path.join(home, '.chatlog', logfile)
os.makedirs(os.path.dirname(log_path), exist_ok=True)
os.makedirs(os.path.dirname(api_path), exist_ok=True)

替换密钥

API_LIST 中填写实际的 API 密钥,当请求密钥为 fake_keys 中的值时,会随机返回 API_LIST 中的一个密钥。

1
2
3
4
# real API keys
API_LIST = [ 'sk-real-1', 'sk-real-2', 'sk-real-3']
# fake API keys
fake_keys = [ 'sk-fake-1', 'sk-fake-2', 'sk-fake-3']

转发 chat 请求

将请求转发到 OpenAI,将返回结果保存到日志文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# wrap chat requests
@app.route('/v1/chat/completions', methods=['POST'])
def forward_request():
try:
# get request headers, body and query parameters
api_key = request.headers.get('Authorization').strip("Bearer ").strip()
url = 'https://api.openai.com/v1/chat/completions'
data = request.get_json()
params = request.args
# If the API key starts with fake-, randomly select one key
if api_key in fake_keys:
api_key = random.choice(API_LIST)

# Forward the data to the target API and get the response
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
response = requests.post(url, json=data, params=params, headers=headers)

# Save API request log
if not api_key in API_LIST:
with open(api_path, 'a') as f:
f.write(f'{api_key}\n\n')

# Save request data
with open(log_path, 'a', encoding="utf-8") as f:
f.write(f'#Request\n{data}\n#Request\n')
f.write(f'#Response\n{response.content}\n#Response\n\n')

# return the response
return Response(response.content, status=response.status_code)
except Exception as e: # hide the error message
return Response(f"invalid request!\n", status=400)

转发其他请求

这部分用于获取 API 的使用信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# wrap dashboard requests
@app.route('/<path:path>', methods=['GET'])
def handle_all(path):
try:
api_key = request.headers.get('Authorization').strip("Bearer ").strip()
if api_key in fake_keys:
api_key = random.choice(API_LIST)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"}
# get recent usage
if path == 'v1/dashboard/billing/usage':
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
url = f'https://api.openai.com/v1/dashboard/billing/usage?start_date={start_date}&end_date={end_date}'
response = requests.get(url, headers=headers)
return Response(response.content, status=response.status_code)
# get subscription info
elif path == 'v1/dashboard/billing/subscription':
url = 'https://api.openai.com/v1/dashboard/billing/subscription'
response = requests.get(url, headers=headers)
return Response(response.content, status=response.status_code)
else:
return Response(f"Unsupported url!\n", status=400)
except Exception as e:
return Response(f"invalid request!\n", status=400)

启动服务

最后,加上启动服务的代码,端口按需修改。

1
2
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5000)

执行 python main.py 启动服务。

开机自启动(可选)

编写启动脚本 chatapi.service,其中 WorkingDirectory 填写为 main.py 所在的目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=chatapi service
After=network.target syslog.target
Wants=network.target

[Service]
User=azureuser
WorkingDirectory=/path/to/chatapi/directory
ExecStart=/usr/bin/python3 main.py
Restart=always

[Install]
WantedBy=multi-user.target

将脚本文件复制到 /etc/systemd/system 目录下,并启动

1
2
3
sudo cp chatapi.service /etc/systemd/system
sudo systemctl enable chatapi
sudo systemctl start chatapi

检查服务状态

1
sudo systemctl status chatapi

也可以检查端口是否正在使用

1
sudo lsof -i:5000

修改 Nginx 配置文件

这一步需要域名,根据监听的端口号编写 nginx 配置文件,当访问该域名时,自动转发到相应的端口。

/etc/nginx/sites-available/default 文件中,添加如下内容:

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name <域名地址>;

location / {
proxy_pass http://localhost:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

设置 ssl 证书

  1. 在域名所在服务商申请证书后,下载 nginx 证书文件,如 example.pemexample.key

  2. 将证书文件上传到服务器,比如到 /etc/nginx/cert 目录下

  3. 修改 nginx 配置文件,追加下边内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server {
    listen 443 ssl;
    server_name <域名地址>;

    ssl_certificate /etc/nginx/cert/example.pem;
    ssl_certificate_key /etc/nginx/cert/example.key;

    location / {
    proxy_pass http://localhost:5000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    }
    }

    注意服务器需要开启 443 入站端口。

越过 Cloudflare 限制

借助 go-chatgpt-api 项目,只需要编写 docker-compose 文件:

1
2
3
4
5
6
7
8
9
10
version: "3"
services:
go-chatgpt-api:
container_name: go-chatgpt-api
image: linweiyuan/go-chatgpt-api:latest # 最新
ports:
- 8080:8080 # 宿主机8080端口可按需改为其它端口
environment:
- GIN_MODE=release
restart: unless-stopped

然后编写 nginx 配置文件,设置反向代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
server_name <域名地址>;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_ssl_server_name on;
proxy_set_header Host api.openai.com;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

443 端口类似配置。为避免代理被他人使用,这里也可以修改参数,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
server_name <域名地址>;
location /password {
proxy_pass http://127.0.0.1:8080;
proxy_ssl_server_name on;
proxy_set_header Host api.openai.com;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

另外,该项目不仅支持 API 代理,也支持 Access Token 代理,接口示例参看:

https://github.com/linweiyuan/go-chatgpt-api/blob/main/example/vscode