# 【记录】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反代

- Nginx Proxy Manager

## 前端测试

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

#docker #ngpm #golang #openai
【记录】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
【记录】Warp 与 Tor 共存 Previous
【设定】生命变革计划 Next

#### Search

Keyword