コードは GO-OPENAI-PROXY を参考にしており、GPT-3.5 によって修正されています。
利点#
- 複数のキーのラウンドロビンをサポートし、キーはフロントエンドに対して透明です
- messages をカスタマイズして変更できます
- ネットワーク環境の影響を受けません
逆プロキシプログラムのコンパイル#
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
}
// 与えられたリクエストボディのための JSON デコーダを取得します
func requestBodyDecoder(request *http.Request) *json.Decoder {
// ボディをバッファに読み取る
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("ボディの読み取りエラー: %v", err)
panic(err)
}
// Go 言語ではボディを読み取ると、その後の呼び出しでボディを再度読み取ることができなくなります....
// 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("ボディのデコードエラー: %v", err)
panic(err)
}
// "messages" リストを取得する
messages, ok := requestData["messages"].([]interface{})
if !ok {
// "messages" フィールドが存在しないか、タイプが正しくありません
log.Printf("メッセージの読み取りエラー: %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("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("system role を ", thirdToLastIndex に移動)
}
}
// 更新されたデータを JSON にエンコードする
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
err = encoder.Encode(requestData)
if err != nil {
// JSON データエンコードの例外を処理する
log.Printf("ボディのエンコードエラー: %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("URL の解析エラー: ", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// 環境プレフィックスを削除する(Tencent Cloud 用、含まれている場合、現在は 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("プロキシリクエストの作成エラー: ", err.Error())
http.Error(w, "プロキシリクエストの作成エラー", 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 {
// User-Agent を明示的に無効にして、デフォルト値に設定されないようにする
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")},
}
// デフォルトのタイムアウト時間を60秒に設定
client := &http.Client{
Timeout: 60 * time.Second,
}
// OpenAI にプロキシリクエストを送信する
resp, err := client.Do(proxyReq)
if err != nil {
log.Println("プロキシリクエストの送信エラー: ", 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("respbody の読み取り中のエラー: ", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else {
if _, err = w.Write(buf[:n]); err != nil {
log.Println("レスポンスの書き込み中のエラー: ", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.(http.Flusher).Flush()
}
}
}
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 デプロイ#
mkdir -p ~/app/apio && cd ~/app/apio && nano docker-compose.yml
chmod 777 openai
sudo docker-compose up -d && sudo docker-compose logs
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 キーは適当に入力