【记录】Go 实现 OpenAI API HTTP 代理

Last updated on March 19, 2024 pm

代码参考自 GO-OPENAI-PROXY,由 GPT-3.5 辅助修改。

优点

  • 支持多key轮询,且key对前端透明
  • 可以对messages进行自定义修改
  • 不受网络环境影响

编译反代程序

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package main

import (
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"bytes"
"encoding/json"
"time"
"sync"
)

var (
target = "https://api.openai.com" // 目标域名
mu sync.Mutex
count int
)

func main() {
http.HandleFunc("/", handleRequest)
http.ListenAndServe(":9000", nil)
}

func get1Key(key string) string {
mu.Lock()
defer mu.Unlock()

arr := strings.Split(key, "|")
randomIndex := count % len(arr)
count++
if count > 999999 {
count = 0
}
randomSubstr := arr[randomIndex]
log.Println("Authorization", randomSubstr)
return randomSubstr
}

// Get a json decoder for a given requests body
func requestBodyDecoder(request *http.Request) *json.Decoder {
// Read body to buffer
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
panic(err)
}

// Because go lang is a pain in the ass if you read the body then any susequent calls
// are unable to read the body again....
// request.Body = ioutil.NopCloser(bytes.NewBuffer(body))

return json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
}

// 在需要设置响应头的地方调用setResponseHeader函数即可
func setResponseHeader(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Max-Age", "86400")
w.Header().Set("Access-Control-Allow-Headers", "authorization,content-type")
}

func handleBody(r *http.Request) io.ReadCloser {
// 读取请求体
decoder := requestBodyDecoder(r)
// 解析JSON数据
var requestData map[string]interface{}
err := decoder.Decode(&requestData)
if err != nil {
// 处理解析JSON数据异常
log.Printf("Error decoding body: %v", err)
panic(err)
}
// 获取"messages"列表
messages, ok := requestData["messages"].([]interface{})
if !ok {
// "messages"字段不存在或类型不正确
log.Printf("Error reading messages: %v", ok)
panic(ok)
}
// 按需修改"messages"列表
// log.Println("debug 5", len(messages), len(messages) > 4)
if len(messages) > 4 {
firstMessage, ok := messages[0].(map[string]interface{})
if !ok {
// 第一个消息类型不正确
log.Printf("Error reading firstMessage: %v", ok)
panic(ok)
}

role, roleOk := firstMessage["role"].(string)
// log.Println("debug 6", roleOk, role, role == "system", strings.EqualFold(role, "system"))
if roleOk && strings.EqualFold(role, "system") {
// 将第一个消息移动到倒数第三个位置
thirdToLastIndex := len(messages) - 3
messages_copy := make([]interface{}, 0)
messages_copy = append(messages_copy, messages[0])
messages_copy = append(messages_copy, messages[thirdToLastIndex:]...)
messages = append(messages[1:thirdToLastIndex], messages_copy...)

// 更新"messages"列表
requestData["messages"] = messages
log.Println("move system role to ", thirdToLastIndex)
}
}
// 将更新后的数据编码为JSON
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
err = encoder.Encode(requestData)
if err != nil {
// 处理编码JSON数据异常
log.Printf("Error encoding body: %v", err)
panic(err)
}

return ioutil.NopCloser(&buf)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
// 过滤无效URL
_, err := url.Parse(r.URL.String())
if err != nil {
log.Println("Error parsing URL: ", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

// 去掉环境前缀(针对腾讯云,如果包含的话,目前我只用到了test和release)
newPath := strings.Replace(r.URL.Path, "/release", "", 1)
newPath = strings.Replace(newPath, "/test", "", 1)

// 拼接目标URL
targetURL := target + newPath

// 创建代理HTTP请求
var proxyReq *http.Request
// log.Println("debug 1", targetURL, r.Method)
// log.Println("debug 2", strings.Contains(targetURL, "chat/completions"))
// log.Println("debug 3", strings.EqualFold(r.Method, "POST"))
if strings.Contains(targetURL, "chat/completions") && strings.EqualFold(r.Method, "POST") {
// log.Println("debug 4-0")
proxyReq, err = http.NewRequest(r.Method, targetURL, handleBody(r))
} else {
// log.Println("debug 4-1")
proxyReq, err = http.NewRequest(r.Method, targetURL, r.Body)
}

if err != nil {
log.Println("Error creating proxy request: ", err.Error())
http.Error(w, "Error creating proxy request", http.StatusInternalServerError)
return
}

// 将原始请求头复制到新请求中
keys := strings.Split(r.Header.Get("Authorization"), " ")
if len(keys) == 2 {
r.Header.Set("Authorization", "Bearer " + get1Key(keys[1]))
}
if _, ok := r.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
r.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36")
}
proxyReq.Header = http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{r.Header.Get("Authorization")},
"User-Agent": []string{r.Header.Get("User-Agent")},
}


// 默认超时时间设置为60s
client := &http.Client{
Timeout: 60 * time.Second,
}

// 向 OpenAI 发起代理请求
resp, err := client.Do(proxyReq)
if err != nil {
log.Println("Error sending proxy request: ", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()

// 设置响应头
setResponseHeader(w)

// 将响应状态码设置为原始响应状态码
w.WriteHeader(resp.StatusCode)

// 将响应实体写入到响应流中(支持流式响应)
buf := make([]byte, 1024)
for {
if n, err := resp.Body.Read(buf); err == io.EOF || n == 0 {
return
} else if err != nil {
log.Println("error while reading respbody: ", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else {
if _, err = w.Write(buf[:n]); err != nil {
log.Println("error while writing resp: ", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.(http.Flusher).Flush()
}
}
}
1
CC=musl-gcc /home/jovyan/go/bin/go1.20.1 build -tags musl -o openai -trimpath -ldflags '-linkmode "external" -extldflags "-static" -s -w -buildid=' ./openai-proxy.go

docker部署

1
2
3
mkdir -p ~/app/apio && cd ~/app/apio && nano docker-compose.yml
chmod 777 openai
sudo docker-compose up -d && sudo docker-compose logs
1
2
3
4
5
6
7
8
9
10
11
12
13
version: '3.3'
services:
apio:
restart: always
image: alpine:latest
volumes:
- ./openai:/bin/openai
entrypoint: ["/bin/openai"]

networks:
default:
external: true
name: ngpm

Nginx Proxy Manager反代

前端测试

  • BetterChatGPT
  • API端点 填入https://yourdomain/token/v1/chat/completions
  • API密钥 随便填

【记录】Go 实现 OpenAI API HTTP 代理
https://hexo.limour.top/-ji-lu-Go-shi-xian-OpenAI-API-HTTP-dai-li
Author
Limour
Posted on
June 28, 2023
Updated on
March 19, 2024
Licensed under