├── go.mod ├── config └── config.json ├── main.go ├── config.go ├── README.md └── proxy_handler.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/surenkid/openai-api-proxy-key-pool 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": { 3 | "ai-yourkey1": [ 4 | "sk-openaikey1", 5 | "sk-openaikey2", 6 | "sk-openaikey3" 7 | ], 8 | "ai-yourkey2": [ 9 | "sk-openaikey4" 10 | ] 11 | }, 12 | "helicone": "sk-heliconekey", 13 | "baseurl": "https://api.openai.com" 14 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | config, err := LoadConfig("config/config.json") 10 | if err != nil { 11 | log.Fatalf("Error loading configuration: %v", err) 12 | } 13 | 14 | http.HandleFunc("/", ProxyHandler(config)) 15 | log.Fatal(http.ListenAndServe(":8124", nil)) 16 | } 17 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Config struct { 11 | Keys map[string][]string `json:"keys"` 12 | Helicone string `json:"helicone"` 13 | BaseURL string `json:"baseurl"` 14 | } 15 | 16 | func LoadConfig(configPath string) (config Config, err error) { 17 | file, err := os.Open(configPath) 18 | if err != nil { 19 | cwd, _ := os.Getwd() 20 | fullPath := filepath.Join(cwd, configPath) 21 | log.Fatalf("Error opening config file at path %s: %v", fullPath, err) 22 | } 23 | defer file.Close() 24 | 25 | decoder := json.NewDecoder(file) 26 | err = decoder.Decode(&config) 27 | if err != nil { 28 | log.Fatal("Error decoding config file:", err) 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI API Proxy with Key Pool 2 | 3 | `openai-api-proxy-key-pool` 是一个反向代理服务,用于代理 OpenAI API 的请求。同时,它还提供了轮询池 key 调用功能,使您能够在访问 OpenAI API 时轮询使用不同的 API 密钥。 4 | 5 | ## 功能 6 | 7 | - 反向代理 OpenAI API 请求 8 | - 轮询池 key 调用,实现对不同 API 密钥的轮询使用 9 | - 配置文件支持,方便管理 API 密钥 10 | 11 | ## 安装 12 | 13 | ### 方法 1:使用 Go 编译 14 | 15 | 1. 克隆项目到本地: 16 | 17 | ```bash 18 | git clone https://github.com/surenkid/openai-api-proxy-key-pool.git 19 | ``` 20 | 21 | 2. 进入项目目录: 22 | 23 | ```bash 24 | cd openai-api-proxy-key-pool 25 | ``` 26 | 27 | 3. 使用 Go 编译项目(确保已安装 [Go](https://golang.org/doc/install)): 28 | 29 | ```bash 30 | go build 31 | ``` 32 | 33 | ### 方法 2:使用 Docker 部署 34 | 35 | 确保已安装 [Docker](https://docs.docker.com/engine/install/)。 36 | 37 | 运行以下命令,将代理服务部署为 Docker 容器: 38 | 39 | ```bash 40 | docker run -p 8124:8124 -v /home/volume/openai-api-proxy-key-pool/config.json:/config/config.json surenkid/openai-api-proxy-key-pool:latest 41 | ``` 42 | 43 | ### 方法 3:使用 Docker Compose 部署 44 | 45 | 确保已安装 [Docker Compose](https://docs.docker.com/compose/install/)。 46 | 47 | 1. 在项目根目录创建一个名为 `docker-compose.yml` 的文件,并填充以下内容: 48 | 49 | ```yaml 50 | version: '3' 51 | 52 | services: 53 | openai-api-proxy-key-pool: 54 | image: surenkid/openai-api-proxy-key-pool:latest 55 | ports: 56 | - "8124:8124" 57 | volumes: 58 | - ./config/config.json:/config/config.json 59 | ``` 60 | 61 | 2. 运行以下命令,使用 Docker Compose 启动代理服务: 62 | 63 | ```bash 64 | docker-compose up 65 | ``` 66 | 67 | ## 使用 68 | 69 | 1. 更新 `config/config.json` 文件,添加您的 API 密钥。参考配置文件示例: 70 | 71 | ```json 72 | { 73 | "keys": { 74 | "ai-yourkey1": [ 75 | "sk-openaikey1", 76 | "sk-openaikey2", 77 | "sk-openaikey3" 78 | ], 79 | "ai-yourkey2": [ 80 | "sk-openaikey4" 81 | ] 82 | }, 83 | "helicone": "sk-heliconekey", 84 | "baseurl": "https://api.openai.com" 85 | } 86 | ``` 87 | 88 | 2. 如果使用 Go 编译方法,运行编译好的二进制文件: 89 | 90 | ```bash 91 | ./openai-api-proxy-key-pool 92 | ``` 93 | 94 | 如果使用 Docker 部署或 Docker Compose 部署方法,容器已在上一步启动。 95 | 96 | 3. 代理服务将在端口 `8124` 上启动,您可以将您的请求发送到 `http://localhost:8124`。 97 | -------------------------------------------------------------------------------- /proxy_handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "log" 7 | "net/http" 8 | "sync" 9 | "unicode/utf8" 10 | ) 11 | 12 | var keyIndex sync.Map 13 | 14 | func getClientIP(r *http.Request) string { 15 | ip := r.Header.Get("X-Forwarded-For") 16 | if ip == "" { 17 | ip = r.Header.Get("X-Real-Ip") 18 | } 19 | if ip == "" { 20 | ip = r.RemoteAddr 21 | } 22 | return ip 23 | } 24 | 25 | func writeCharByChar(w http.ResponseWriter, r io.Reader) { 26 | reader := bufio.NewReader(r) 27 | for { 28 | char, _, err := reader.ReadRune() 29 | if err != nil { 30 | if err != io.EOF { 31 | http.Error(w, err.Error(), http.StatusInternalServerError) 32 | } 33 | break 34 | } 35 | 36 | buf := make([]byte, utf8.RuneLen(char)) 37 | utf8.EncodeRune(buf, char) 38 | 39 | w.Write(buf) 40 | w.(http.Flusher).Flush() 41 | } 42 | } 43 | 44 | func ProxyHandler(config Config) http.HandlerFunc { 45 | return func(w http.ResponseWriter, r *http.Request) { 46 | authorization := r.Header.Get("Authorization") 47 | if len(authorization) == 0 { 48 | errorMessage := "Authorization header is missing" 49 | log.Printf("[Error] %s", errorMessage) 50 | http.Error(w, errorMessage, http.StatusBadRequest) 51 | return 52 | } 53 | 54 | if len(authorization) < 9 || authorization[:7] != "Bearer " { 55 | errorMessage := "Invalid Authorization header format" 56 | log.Printf("[Error] %s", errorMessage) 57 | http.Error(w, errorMessage, http.StatusBadRequest) 58 | return 59 | } 60 | 61 | token := authorization[7:] 62 | log.Printf("Parsed token: %s", token) 63 | 64 | if token[:3] == "ai-" { 65 | keys, ok := config.Keys[token] 66 | if !ok { 67 | errorMessage := `{"error":{"message":"Invalid Token","code":403}}` 68 | log.Printf("[Error] %s", errorMessage) 69 | http.Error(w, errorMessage, http.StatusForbidden) 70 | return 71 | } 72 | 73 | index, _ := keyIndex.LoadOrStore(token, 0) 74 | r.Header.Set("Authorization", "Bearer "+keys[index.(int)]) 75 | 76 | nextIndex := (index.(int) + 1) % len(keys) 77 | keyIndex.Store(token, nextIndex) 78 | log.Printf("Used key: %s, Updated index: %d", keys[index.(int)], nextIndex) 79 | } else { 80 | r.Header.Set("Authorization", "Bearer "+token) 81 | } 82 | 83 | var baseURL string 84 | if config.BaseURL != "" { 85 | baseURL = config.BaseURL 86 | } else { 87 | if config.Helicone != "" { 88 | baseURL = "https://oai.hconeai.com" 89 | } else { 90 | baseURL = "https://api.openai.com" 91 | } 92 | } 93 | log.Printf("Using baseURL: %s", baseURL) 94 | 95 | r.Header.Del("CF-Connecting-IP") 96 | r.Header.Del("X-Forwarded-For") 97 | r.Header.Del("X-Real-IP") 98 | r.Header.Del("X-Envoy-External-Address") 99 | r.Header.Del("X-Forwarded-Host") 100 | r.Header.Del("X-Forwarded-Proto") 101 | r.Header.Del("Cf-Ray") 102 | r.Header.Del("Cf-Visitor") 103 | r.Header.Del("Cf-Ipcountry") 104 | r.Header.Del("Cf-Request-Id") 105 | r.Host = baseURL 106 | 107 | proxyURL := baseURL + r.RequestURI 108 | req, err := http.NewRequest(r.Method, proxyURL, r.Body) 109 | if err != nil { 110 | errorMessage := "Error creating proxy request" 111 | log.Printf("[Error] %s: %v", errorMessage, err) 112 | http.Error(w, errorMessage, http.StatusInternalServerError) 113 | return 114 | } 115 | 116 | req.URL.RawQuery = r.URL.RawQuery 117 | 118 | req.Header = r.Header 119 | req.Header.Set("Transfer-Encoding", r.Header.Get("Transfer-Encoding")) 120 | req.Header.Set("Content-Type", r.Header.Get("Content-Type")) 121 | if config.Helicone != "" { 122 | clientIP := getClientIP(r) 123 | req.Header.Set("Helicone-Auth", "Bearer "+config.Helicone) 124 | req.Header.Set("helicone-user-id", clientIP) 125 | } 126 | 127 | resp, err := http.DefaultClient.Do(req) 128 | if err != nil { 129 | errorMessage := "Error sending proxy request" 130 | log.Printf("[Error] %s: %v", errorMessage, err) 131 | http.Error(w, errorMessage, http.StatusInternalServerError) 132 | return 133 | } 134 | defer resp.Body.Close() 135 | 136 | for k, v := range resp.Header { 137 | w.Header()[k] = v 138 | } 139 | w.Header().Set("Transfer-Encoding", resp.Header.Get("Transfer-Encoding")) 140 | w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) 141 | w.WriteHeader(resp.StatusCode) 142 | 143 | writeCharByChar(w, resp.Body) 144 | } 145 | } 146 | --------------------------------------------------------------------------------