├── .gitignore ├── Howtouse.md ├── README.md ├── apireq └── apireq.go ├── apirespstream └── apirespstream.go ├── config ├── config.go └── config.yaml ├── docker-compose.yml ├── go.mod ├── go.sum ├── images ├── telegram.jpg └── wx.jpg ├── main.go ├── manifest └── docker │ ├── Dockerfile │ └── docker.sh ├── release.sh ├── stream.sh ├── test.sh └── v1 └── chat ├── completions.go ├── dalle3.go ├── gpt4v.go └── token.go /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | test.sh 3 | temp 4 | config.yaml 5 | xytest.sh 6 | -------------------------------------------------------------------------------- /Howtouse.md: -------------------------------------------------------------------------------- 1 | ### 如何使用 以及 config.yaml参数作用 2 | 3 | 4 | ## 配置config文件 5 | 6 | 默认config在config/config.yaml中 7 | 8 | ```config/config.yaml 9 | # PORT: 8089 10 | APISERVER: https://freechat.xyhelper.cn/backend-api/conversation 11 | PASSMODE: true 12 | MAXTIME: 60 13 | NOPLUGINS: true 14 | KEEPHISTORY: true 15 | 16 | 17 | sk-api-xyhelper-cn-free-token-for-everyone-xyhelper: "xyhelper.cn" 18 | 19 | ``` 20 | 21 | 其中在不提供config.yaml时默认值为 22 | 23 | ```config/config.go 24 | var ( 25 | PORT = 8080 26 | APISERVER = "http://chatproxy/backend-api/conversation" 27 | APIHOST = "http://chatproxy" 28 | PASSMODE = false 29 | MAXTIME = 0 30 | NOPLUGINS = false 31 | KEEPHISTORY = false 32 | AUTHKEY = "" 33 | ) 34 | 35 | ``` 36 | 默认值是在有容器chatproxy提供代理服务的前提下。如果自己部署的话不进行修改/部署chatproxy,将无法使用。 37 | - PORT:提供服务的端口 38 | - APISERVER:对应的网页*聊天*提供服务的后端api网址,如果服务器处在openai服务范围内可以直接使用 https://chat.openai.com/backend-api/conversation 39 | - APIHOST:就是去掉尾巴的APISERVER,为DALLE.3或者上传文件时使用,若服务器可以直接访问可填写 https://chat.openai.com 如果不需要使用除了聊天以外功能则可不填。 40 | - PASSMODE:BOOL类型变量,决定是否直接把bearer参数传出而非通过SK2TOKEN(config/config.go)中转为token 41 | - MAXTIME:最大等待响应时间 42 | - KEEPHISTORY:chatgpt网页是否保存历史记录设置为true时,前往chat.openai.com登录对应账号会查看到之前对话。**对话如果选择保存会遵循相关政策,目前应该时允许使用数据作为训练集,详细查看openai官网** 43 | 44 | 45 | 所以一个可以直接访问chat.openai.com的服务器的配置文件可能如下 46 | ```config 47 | # PORT: 8089 48 | APISERVER: https://chat.openai.com/backend-api/conversation 49 | APIHOST: https://chat.openai.com 50 | PASSMODE: true 51 | MAXTIME: 60 52 | NOPLUGINS: true 53 | KEEPHISTORY: false 54 | ``` 55 | 56 | 在这个情况下传入的请求原本openai api key应该变为accesstoken。 57 | 58 | 59 | ## 获取accesstoken 60 | - EDGE/CHROME浏览器 登录chat.openai.com时输入完账号和密码。按F12-Network会查看到session 61 | 其中会有如下格式内容 62 | ``` 63 | { 64 | "user": { 65 | "id": "user-id", 66 | "name": "yourname", 67 | "email": "someemail", 68 | "image": "xxxxxx", 69 | "picture": "zzzz", 70 | "idp": "auth0", 71 | "iat": xxxx, 72 | "mfa": false, 73 | "groups": [], 74 | "intercom_hash": "xxxx" 75 | }, 76 | "expires": "2024-04-01T09:33:07.368Z", 77 | "accessToken": "some access token", 78 | "authProvider": "auth0" 79 | } 80 | ``` 81 | 其中acesstoken之后便是需要的 82 | - PandoraNext自建站可以获取accesstoken,详情参考[pandora-next/deploy](https://github.com/pandora-next/deploy) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CHAT2API 2 | 3 | 4 | ## 项目简介 5 | 6 | CHAT2API 是一个开源项目,旨在将OPENAI官网接口转换为API格式,以兼容针对API开发的应用 7 | 8 | **请注意:这个代码库不经常维护。** 9 | 10 | 由于资源有限,我们不能保证及时回应问题或合并拉取请求。我们鼓励社区成员互相帮助和合作,但请理解我们可能无法立即处理问题或合并更改。 11 | 12 | ## 特点 13 | 14 | - 将OPENAI官网接口转换为API格式 15 | 16 | ## 如何贡献 17 | 18 | 尽管这个代码库不经常维护,但我们仍然欢迎社区的贡献。如果您想为项目做出贡献,您可以遵循以下步骤: 19 | 20 | 1. 选择一个任务,或者创建一个新的任务,开始编写代码。 21 | 2. 提交拉取请求(Pull Request)并等待审核。 22 | 23 | 虽然我们不能保证及时处理,但我们仍然感谢每一个对项目做出贡献的人! 24 | 25 | ## 安装和使用 26 | 27 | ```bash 28 | curl https://api.xyhelper.cn/v1/chat/completions \ 29 | -H "Content-Type: application/json" \ 30 | -H "Authorization: Bearer sk-api-xyhelper-cn-free-token-for-everyone-xyhelper" \ 31 | -d '{ 32 | "model": "gpt-3.5-turbo", 33 | "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}], 34 | "stream": true 35 | }' 36 | ``` 37 | 38 | ## 社区支持 39 | 40 | 如果您有任何问题、建议或反馈,请随时联系我们或者在 [GitHub Issues](https://github.com/xyhelper/chat2api/issues) 上提交一个新的问题。 41 | 或者联系微信客户,或telegram客服 42 | | ![wx](./images/wx.jpg) | ![telegram](./images/telegram.jpg) | 43 | | ---------------------- | ---------------------------------- | 44 | | 企业微信 | telegram | 45 | 46 | 47 | **请注意:问题的响应时间可能较长。** 48 | 49 | 50 | ## 鸣谢 51 | 52 | 感谢您对 CHAT2API 的兴趣和支持! 53 | 54 | 如果您想了解更多关于项目的信息,请访问我们的 [xyhelper](https://www.xyhelper.com.cn/) 。 55 | -------------------------------------------------------------------------------- /apireq/apireq.go: -------------------------------------------------------------------------------- 1 | package apireq 2 | 3 | type Message struct { 4 | Role string `json:"role"` 5 | Content string `json:"content"` 6 | } 7 | 8 | type Req struct { 9 | Messages []Message `json:"messages"` 10 | Model string `json:"model"` 11 | Stream bool `json:"stream"` 12 | PluginIds []string `json:"plugin_ids"` 13 | } 14 | -------------------------------------------------------------------------------- /apirespstream/apirespstream.go: -------------------------------------------------------------------------------- 1 | package apirespstream 2 | 3 | // ApiRespStream represents the JSON structure 4 | type ApiRespStreamStruct struct { 5 | ID string `json:"id"` 6 | Object string `json:"object"` 7 | Created int64 `json:"created"` 8 | Model string `json:"model"` 9 | Choices []ChoiceObj `json:"choices"` 10 | } 11 | 12 | // ChoiceObj represents the nested "choices" object in the JSON 13 | type ChoiceObj struct { 14 | Delta DeltaObj `json:"delta"` 15 | Index int `json:"index"` 16 | FinishReason *string `json:"finish_reason"` 17 | } 18 | 19 | // DeltaObj represents the nested "delta" object in the JSON 20 | type DeltaObj struct { 21 | Content string `json:"content"` 22 | } 23 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/gogf/gf/v2/frame/g" 7 | "github.com/gogf/gf/v2/os/gctx" 8 | "github.com/gogf/gf/v2/text/gstr" 9 | ) 10 | 11 | var ( 12 | PORT = 8080 13 | APISERVER = "http://chatproxy/backend-api/conversation" 14 | APIHOST = "http://chatproxy" 15 | PASSMODE = false 16 | MAXTIME = 0 17 | NOPLUGINS = false 18 | KEEPHISTORY = false 19 | AUTHKEY = "" 20 | ) 21 | 22 | func init() { 23 | ctx := gctx.GetInitCtx() 24 | port := g.Cfg().MustGetWithEnv(ctx, "PORT").Int() 25 | if port != 0 { 26 | PORT = port 27 | } 28 | apiServer := g.Cfg().MustGetWithEnv(ctx, "APISERVER").String() 29 | if apiServer != "" { 30 | APISERVER = apiServer 31 | } 32 | // 从apiServer中获取APIHOST 33 | apihost := gstr.SubStr(apiServer, 0, gstr.PosR(apiServer, "/backend-api/conversation")) 34 | if apihost != "" { 35 | APIHOST = apihost 36 | } 37 | passMode := g.Cfg().MustGetWithEnv(ctx, "PASSMODE").Bool() 38 | if passMode { 39 | PASSMODE = passMode 40 | } 41 | maxtime := g.Cfg().MustGetWithEnv(ctx, "MAXTIME").Int() 42 | if maxtime > 0 { 43 | MAXTIME = maxtime 44 | } 45 | noplugins := g.Cfg().MustGetWithEnv(ctx, "NOPLUGINS").Bool() 46 | if noplugins { 47 | NOPLUGINS = noplugins 48 | } 49 | keepHistory := g.Cfg().MustGetWithEnv(ctx, "KEEPHISTORY").Bool() 50 | if keepHistory { 51 | KEEPHISTORY = keepHistory 52 | } 53 | authKey := g.Cfg().MustGetWithEnv(ctx, "AUTHKEY").String() 54 | if authKey != "" { 55 | AUTHKEY = authKey 56 | } 57 | 58 | g.Log().Info(ctx, "PORT:", PORT) 59 | g.Log().Info(ctx, "APISERVER:", APISERVER) 60 | g.Log().Info(ctx, "PASSMODE:", PASSMODE) 61 | g.Log().Info(ctx, "APIHOST:", APIHOST) 62 | g.Log().Info(ctx, "MAXTIME:", MAXTIME) 63 | g.Log().Info(ctx, "NOPLUGINS:", NOPLUGINS) 64 | g.Log().Info(ctx, "KEEPHISTORY:", KEEPHISTORY) 65 | g.Log().Info(ctx, "AUTHKEY:", AUTHKEY) 66 | } 67 | 68 | func SK2TOKEN(ctx g.Ctx, SK string) (token string) { 69 | // 检查SK是否有效格式 如 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 70 | // 如果有效则返回token 71 | // 如果无效则返回空字符串 72 | sampleKey := "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" 73 | if len(SK) != len(sampleKey) { 74 | return "" 75 | } 76 | 77 | return g.Cfg().MustGetWithEnv(ctx, SK).String() 78 | } 79 | 80 | func GenerateID(length int) string { 81 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 82 | // rand.Seed(time.Now().UnixNano()) 83 | 84 | id := "chatcmpl-" 85 | for i := 0; i < length; i++ { 86 | id += string(charset[rand.Intn(len(charset))]) 87 | } 88 | return id 89 | } 90 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | # PORT: 8089 2 | APISERVER: https://freechat.xyhelper.cn/backend-api/conversation 3 | PASSMODE: true 4 | MAXTIME: 60 5 | NOPLUGINS: true 6 | KEEPHISTORY: true 7 | 8 | 9 | sk-api-xyhelper-cn-free-token-for-everyone-xyhelper: "xyhelper.cn" 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | chat2api: 4 | image: xyhelper/chat2api 5 | ports: 6 | - "8080:8080" 7 | restart: always 8 | volumes: 9 | - ./config:/app/config 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module chat2api 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gogf/gf/v2 v2.5.7 7 | github.com/launchdarkly/eventsource v1.7.1 8 | ) 9 | 10 | require ( 11 | github.com/google/uuid v1.4.0 12 | github.com/json-iterator/go v1.1.12 13 | github.com/pkoukk/tiktoken-go v0.1.6 14 | ) 15 | 16 | require ( 17 | github.com/BurntSushi/toml v1.3.2 // indirect 18 | github.com/clbanning/mxj/v2 v2.7.0 // indirect 19 | github.com/dlclark/regexp2 v1.10.0 // indirect 20 | github.com/fatih/color v1.16.0 // indirect 21 | github.com/fsnotify/fsnotify v1.7.0 // indirect 22 | github.com/go-logr/logr v1.3.0 // indirect 23 | github.com/go-logr/stdr v1.2.2 // indirect 24 | github.com/gorilla/websocket v1.5.1 // indirect 25 | github.com/grokify/html-strip-tags-go v0.1.0 // indirect 26 | github.com/magiconair/properties v1.8.7 // indirect 27 | github.com/mattn/go-colorable v0.1.13 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/mattn/go-runewidth v0.0.15 // indirect 30 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 31 | github.com/modern-go/reflect2 v1.0.2 // indirect 32 | github.com/olekukonko/tablewriter v0.0.5 // indirect 33 | github.com/rivo/uniseg v0.4.4 // indirect 34 | go.opentelemetry.io/otel v1.21.0 // indirect 35 | go.opentelemetry.io/otel/metric v1.21.0 // indirect 36 | go.opentelemetry.io/otel/sdk v1.21.0 // indirect 37 | go.opentelemetry.io/otel/trace v1.21.0 // indirect 38 | golang.org/x/net v0.18.0 // indirect 39 | golang.org/x/sys v0.14.0 // indirect 40 | golang.org/x/text v0.14.0 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= 4 | github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= 9 | github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 10 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 11 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 12 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 13 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 14 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 15 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 16 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 17 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 18 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 19 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 20 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 21 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 22 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 23 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 24 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 25 | github.com/gogf/gf/v2 v2.5.4 h1:UBCSw8mInkHmEqL0E1LYc6QhSpaNFY/wHcFrTI/rzTk= 26 | github.com/gogf/gf/v2 v2.5.4/go.mod h1:7yf5qp0BznfsYx7Sw49m3mQvBsHpwAjJk3Q9ZnKoUEc= 27 | github.com/gogf/gf/v2 v2.5.7 h1:h+JSoD6z3d2q0uGszvtahrSm4DiM2ECyNjyTwKIo8wE= 28 | github.com/gogf/gf/v2 v2.5.7/go.mod h1:x2XONYcI4hRQ/4gMNbWHmZrNzSEIg20s2NULbzom5k0= 29 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 32 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 33 | github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 34 | github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 35 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 36 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 37 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 38 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 39 | github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= 40 | github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= 41 | github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= 42 | github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= 43 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 44 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 45 | github.com/launchdarkly/eventsource v1.7.1 h1:StoRQeiPyrcQIXjlQ7b5jWMzHW4p+GGczN2r2oBhujg= 46 | github.com/launchdarkly/eventsource v1.7.1/go.mod h1:LHxSeb4OnqznNZxCSXbFghxS/CjIQfzHovNoAqbO/Wk= 47 | github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs= 48 | github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw= 49 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 50 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 51 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 52 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 53 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 54 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 55 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 56 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 57 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 58 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 59 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 60 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 61 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 65 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 66 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 67 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 68 | github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= 69 | github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= 70 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 71 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 72 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 73 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 74 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 77 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 78 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 79 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 80 | go.opentelemetry.io/otel v1.18.0 h1:TgVozPGZ01nHyDZxK5WGPFB9QexeTMXEH7+tIClWfzs= 81 | go.opentelemetry.io/otel v1.18.0/go.mod h1:9lWqYO0Db579XzVuCKFNPDl4s73Voa+zEck3wHaAYQI= 82 | go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= 83 | go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 84 | go.opentelemetry.io/otel/metric v1.18.0 h1:JwVzw94UYmbx3ej++CwLUQZxEODDj/pOuTCvzhtRrSQ= 85 | go.opentelemetry.io/otel/metric v1.18.0/go.mod h1:nNSpsVDjWGfb7chbRLUNW+PBNdcSTHD4Uu5pfFMOI0k= 86 | go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= 87 | go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 88 | go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY= 89 | go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M= 90 | go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= 91 | go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= 92 | go.opentelemetry.io/otel/trace v1.18.0 h1:NY+czwbHbmndxojTEKiSMHkG2ClNH2PwmcHrdo0JY10= 93 | go.opentelemetry.io/otel/trace v1.18.0/go.mod h1:T2+SGJGuYZY3bjj5rgh/hN7KIrlpWC5nS8Mjvzckz+0= 94 | go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= 95 | go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 96 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 97 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 98 | golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= 99 | golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 100 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 104 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 106 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 107 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 108 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 109 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 110 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 111 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 113 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 114 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 115 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 116 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 117 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 118 | -------------------------------------------------------------------------------- /images/telegram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyhelper/chat2api/1fc562b702817a27f304a444419213ba3af6b762/images/telegram.jpg -------------------------------------------------------------------------------- /images/wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyhelper/chat2api/1fc562b702817a27f304a444419213ba3af6b762/images/wx.jpg -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "chat2api/config" 5 | "chat2api/v1/chat" 6 | 7 | "github.com/gogf/gf/v2/frame/g" 8 | "github.com/gogf/gf/v2/net/ghttp" 9 | ) 10 | 11 | func main() { 12 | // go ghttp.StartPProfServer(8199) 13 | s := g.Server() 14 | s.SetPort(config.PORT) 15 | s.BindHandler("/", Index) 16 | chatGroup := s.Group("/v1/chat") 17 | chatGroup.Middleware(MiddlewareCORS) 18 | 19 | chatGroup.ALL("/completions", chat.Completions) 20 | chatGroup.ALL("/gpt4v", chat.Gpt4v) 21 | chatGroup.ALL("/dalle3", chat.Dalle3) 22 | s.Run() 23 | } 24 | func MiddlewareCORS(r *ghttp.Request) { 25 | r.Response.CORSDefault() 26 | r.Middleware.Next() 27 | } 28 | 29 | func Index(r *ghttp.Request) { 30 | r.Response.Write("Hello Xyhelper! This is chat2api.") 31 | } 32 | -------------------------------------------------------------------------------- /manifest/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM loads/alpine:3.8 2 | 3 | ############################################################################### 4 | # INSTALLATION 5 | ############################################################################### 6 | 7 | ENV WORKDIR /app 8 | # ADD resource $WORKDIR/resource 9 | ADD ./temp/linux_amd64/main $WORKDIR/main 10 | RUN chmod +x $WORKDIR/main 11 | 12 | ############################################################################### 13 | # START 14 | ############################################################################### 15 | WORKDIR $WORKDIR 16 | CMD ./main 17 | -------------------------------------------------------------------------------- /manifest/docker/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This shell is executed before docker build. 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | gf build main.go -a amd64 -s linux -p ./temp 6 | gf docker main.go -p -t xyhelper/chat2api:latest -------------------------------------------------------------------------------- /stream.sh: -------------------------------------------------------------------------------- 1 | curl http://127.0.0.1:8080/v1/chat/completions \ 2 | -H "Content-Type: application/json" \ 3 | -H "Authorization: Bearer sk-api-xyhelper-cn-free-token-for-everyone-xyhelper" \ 4 | -d '{ 5 | "model": "gpt-3.5-turbo", 6 | "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}], 7 | "stream": true 8 | }' 9 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | curl http://127.0.0.1:8000/v1/chat/completions \ 2 | -H "Content-Type: application/json" \ 3 | -H "Authorization: Bearer sk-api-xyhelper-cn-free-token-for-everyone-xyhelper" \ 4 | -d '{ 5 | "model": "gpt-3.5-turbo", 6 | "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "计算一下圆周率"}], 7 | "stream": true 8 | }' 9 | -------------------------------------------------------------------------------- /v1/chat/completions.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "chat2api/apireq" 5 | "chat2api/apirespstream" 6 | "chat2api/config" 7 | "context" 8 | "fmt" 9 | "io" 10 | "strings" 11 | "time" 12 | 13 | "github.com/gogf/gf/v2/encoding/gjson" 14 | "github.com/gogf/gf/v2/frame/g" 15 | "github.com/gogf/gf/v2/net/ghttp" 16 | "github.com/gogf/gf/v2/text/gstr" 17 | "github.com/gogf/gf/v2/util/gconv" 18 | "github.com/google/uuid" 19 | jsoniter "github.com/json-iterator/go" 20 | "github.com/launchdarkly/eventsource" 21 | ) 22 | 23 | var ( 24 | // client = g.Client() 25 | ErrNoAuth = `{ 26 | "error": { 27 | "message": "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.", 28 | "type": "invalid_request_error", 29 | "param": null, 30 | "code": null 31 | } 32 | }` 33 | ErrKeyInvalid = `{ 34 | "error": { 35 | "message": "Incorrect API key provided: sk-4yNZz***************************************6mjw. You can find your API key at https://platform.openai.com/account/api-keys.", 36 | "type": "invalid_request_error", 37 | "param": null, 38 | "code": "invalid_api_key" 39 | } 40 | }` 41 | ChatReqStr = `{ 42 | "action": "next", 43 | "messages": [ 44 | { 45 | "id": "aaa2f210-64e1-4f0d-aa51-e73fe1ae74af", 46 | "author": { "role": "user" }, 47 | "content": { "content_type": "text", "parts": ["1\n"] }, 48 | "metadata": {} 49 | } 50 | ], 51 | "parent_message_id": "aaa1a8ab-61d6-4fc0-a5f5-181015c2ebaf", 52 | "model": "text-davinci-002-render-sha", 53 | "timezone_offset_min": -480, 54 | "suggestions": [ 55 | "Brainstorm 5 episode ideas for my new podcast on urban design.", 56 | "Come up with 5 concepts for a retro-style arcade game.", 57 | "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.", 58 | "Show me a code snippet of a website's sticky header in CSS and JavaScript." 59 | ], 60 | "history_and_training_disabled": true, 61 | "conversation_mode": { "kind": "primary_assistant" }, 62 | "force_paragen": false, 63 | "force_rate_limit": false 64 | } 65 | ` 66 | ChatTurboReqStr = ` 67 | { 68 | "action": "next", 69 | "messages": [ 70 | { 71 | "id": "aaa2b2cc-e7e9-47c5-8171-0ff8a6d9d6d3", 72 | "author": { 73 | "role": "user" 74 | }, 75 | "content": { 76 | "content_type": "text", 77 | "parts": [ 78 | "你好" 79 | ] 80 | }, 81 | "metadata": {} 82 | } 83 | ], 84 | "parent_message_id": "aaa1403d-c61e-4818-90e0-93a99465aec6", 85 | "model": "gpt-4", 86 | "timezone_offset_min": -480, 87 | "suggestions": [ 88 | "Design a database schema for an online merch store.", 89 | "Can you come up with some names for a mocktail (non-alcoholic cocktail) with Coke and pomegranate syrup?", 90 | "I'm going to cook for my date who claims to be a picky eater. Can you recommend me a dish that's easy to cook?", 91 | "Make a content strategy for a newsletter featuring free local weekend events." 92 | ], 93 | "history_and_training_disabled": true, 94 | "conversation_mode": { 95 | "kind": "primary_assistant" 96 | }, 97 | "force_paragen": false, 98 | "force_rate_limit": false 99 | }` 100 | Chat4ReqStr = ` 101 | { 102 | "action": "next", 103 | "messages": [ 104 | { 105 | "id": "aaa2b182-d834-4f30-91f3-f791fa953204", 106 | "author": { 107 | "role": "user" 108 | }, 109 | "content": { 110 | "content_type": "text", 111 | "parts": [ 112 | "画一只猫1231231231" 113 | ] 114 | }, 115 | "metadata": {} 116 | } 117 | ], 118 | "parent_message_id": "aaa11581-bceb-46c5-bc76-cb84be69725e", 119 | "model": "gpt-4-gizmo", 120 | "timezone_offset_min": -480, 121 | "suggestions": [], 122 | "history_and_training_disabled": true, 123 | "conversation_mode": { 124 | "gizmo": { 125 | "gizmo": { 126 | "id": "g-YyyyMT9XH", 127 | "organization_id": "org-OROoM5KiDq6bcfid37dQx4z4", 128 | "short_url": "g-YyyyMT9XH-chatgpt-classic", 129 | "author": { 130 | "user_id": "user-u7SVk5APwT622QC7DPe41GHJ", 131 | "display_name": "ChatGPT", 132 | "selected_display": "name", 133 | "is_verified": true 134 | }, 135 | "voice": { 136 | "id": "ember" 137 | }, 138 | "display": { 139 | "name": "ChatGPT Classic", 140 | "description": "The latest version of GPT-4 with no additional capabilities", 141 | "welcome_message": "Hello", 142 | "profile_picture_url": "https://files.oaiusercontent.com/file-i9IUxiJyRubSIOooY5XyfcmP?se=2123-10-13T01%3A11%3A31Z&sp=r&sv=2021-08-06&sr=b&rscc=max-age%3D31536000%2C%20immutable&rscd=attachment%3B%20filename%3Dgpt-4.jpg&sig=ZZP%2B7IWlgVpHrIdhD1C9wZqIvEPkTLfMIjx4PFezhfE%3D", 143 | "categories": [] 144 | }, 145 | "share_recipient": "link", 146 | "updated_at": "2023-11-06T01:11:32.191060+00:00", 147 | "last_interacted_at": "2023-11-18T07:50:19.340421+00:00", 148 | "tags": [ 149 | "public", 150 | "first_party" 151 | ] 152 | }, 153 | "tools": [], 154 | "files": [], 155 | "product_features": { 156 | "attachments": { 157 | "type": "retrieval", 158 | "accepted_mime_types": [ 159 | "text/x-c", 160 | "text/html", 161 | "application/x-latext", 162 | "text/plain", 163 | "text/x-ruby", 164 | "text/x-typescript", 165 | "text/x-c++", 166 | "text/x-java", 167 | "text/x-sh", 168 | "application/vnd.openxmlformats-officedocument.presentationml.presentation", 169 | "text/x-script.python", 170 | "text/javascript", 171 | "text/x-tex", 172 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 173 | "application/msword", 174 | "application/pdf", 175 | "text/x-php", 176 | "text/markdown", 177 | "application/json", 178 | "text/x-csharp" 179 | ], 180 | "image_mime_types": [ 181 | "image/jpeg", 182 | "image/png", 183 | "image/gif", 184 | "image/webp" 185 | ], 186 | "can_accept_all_mime_types": true 187 | } 188 | } 189 | }, 190 | "kind": "gizmo_interaction", 191 | "gizmo_id": "g-YyyyMT9XH" 192 | }, 193 | "force_paragen": false, 194 | "force_rate_limit": false 195 | }` 196 | ApiRespStr = `{ 197 | "id": "chatcmpl-LLKfuOEHqVW2AtHks7wAekyrnPAoj", 198 | "object": "chat.completion", 199 | "created": 1689864805, 200 | "model": "gpt-3.5-turbo", 201 | "usage": { 202 | "prompt_tokens": 0, 203 | "completion_tokens": 0, 204 | "total_tokens": 0 205 | }, 206 | "choices": [ 207 | { 208 | "message": { 209 | "role": "assistant", 210 | "content": "Hello! How can I assist you today?" 211 | }, 212 | "finish_reason": "stop", 213 | "index": 0 214 | } 215 | ] 216 | }` 217 | ApiRespStrStream = `{ 218 | "id": "chatcmpl-afUFyvbTa7259yNeDqaHRBQxH2PLH", 219 | "object": "chat.completion.chunk", 220 | "created": 1689867370, 221 | "model": "gpt-3.5-turbo", 222 | "choices": [ 223 | { 224 | "delta": { 225 | "content": "Hello" 226 | }, 227 | "index": 0, 228 | "finish_reason": null 229 | } 230 | ] 231 | }` 232 | ApiRespStrStreamEnd = `{"id":"apirespid","object":"chat.completion.chunk","created":apicreated,"model": "apirespmodel","choices":[{"delta": {},"index": 0,"finish_reason": "stop"}]}` 233 | ) 234 | 235 | func Completions(r *ghttp.Request) { 236 | ctx := r.Context() 237 | // g.Log().Debug(ctx, "Conversation start....................") 238 | if config.MAXTIME > 0 { 239 | // 创建带有超时的context 240 | var cancel context.CancelFunc 241 | ctx, cancel = context.WithTimeout(ctx, time.Duration(config.MAXTIME)*time.Second) 242 | defer cancel() 243 | } 244 | 245 | authkey := strings.TrimPrefix(r.Header.Get("authorization"), "Bearer ") 246 | if authkey == "" { 247 | r.Response.Status = 401 248 | r.Response.WriteJson(gjson.New(ErrNoAuth)) 249 | return 250 | } 251 | // g.Log().Info(ctx, "authkey: ", authkey) 252 | var token string 253 | if config.PASSMODE { 254 | token = authkey 255 | } else { 256 | token = config.SK2TOKEN(ctx, authkey) 257 | } 258 | if token == "" { 259 | r.Response.Status = 401 260 | r.Response.WriteJson(gjson.New(ErrKeyInvalid)) 261 | return 262 | } 263 | // g.Log().Debug(ctx, "token: ", token) 264 | ChatGPTAccountID := r.Header.Get("ChatGPT-Account-ID") 265 | g.Log().Debug(ctx, "ChatGPTAccountID: ", ChatGPTAccountID) 266 | // 从请求中获取参数 267 | req := &apireq.Req{} 268 | err := r.GetRequestStruct(req) 269 | if err != nil { 270 | g.Log().Error(ctx, "r.GetRequestStruct(req) error: ", err) 271 | r.Response.Status = 400 272 | r.Response.WriteJson(gjson.New(`{"error": "bad request"}`)) 273 | return 274 | } 275 | // g.Dump(req) 276 | // 遍历 req.Messages 拼接 newMessages 277 | newMessages := "" 278 | for _, message := range req.Messages { 279 | newMessages += message.Content + "\n" 280 | } 281 | // g.Dump(newMessages) 282 | var ChatReq *gjson.Json 283 | if gstr.HasPrefix(req.Model, "gpt-4") { 284 | ChatReq = gjson.New(Chat4ReqStr) 285 | } else { 286 | ChatReq = gjson.New(ChatReqStr) 287 | } 288 | 289 | if gstr.HasPrefix(req.Model, "gpt-4-turbo") { 290 | ChatReq = gjson.New(ChatTurboReqStr) 291 | } 292 | 293 | ChatReq.Set("messages.0.content.parts.0", newMessages) 294 | ChatReq.Set("messages.0.id", uuid.NewString()) 295 | ChatReq.Set("parent_message_id", uuid.NewString()) 296 | if len(req.PluginIds) > 0 { 297 | ChatReq.Set("plugin_ids", req.PluginIds) 298 | } 299 | if config.KEEPHISTORY { 300 | ChatReq.Set("history_and_training_disabled", false) 301 | } 302 | // ChatReq.Dump() 303 | reqHeader := g.MapStrStr{ 304 | "Authorization": "Bearer " + token, 305 | "Content-Type": "application/json", 306 | } 307 | if ChatGPTAccountID != "" { 308 | reqHeader["ChatGPT-Account-ID"] = ChatGPTAccountID 309 | } 310 | // 请求openai 311 | resp, err := g.Client().SetHeaderMap(reqHeader).Post(ctx, config.APISERVER, ChatReq.MustToJson()) 312 | if err != nil { 313 | g.Log().Error(ctx, "g.Client().Post error: ", err) 314 | r.Response.Status = 500 315 | r.Response.WriteJson(gjson.New(`{"detail": "internal server error"}`)) 316 | return 317 | } 318 | defer resp.Close() 319 | // defer resp.Body.Close() 320 | // 如果返回结果不是200 321 | if resp.StatusCode != 200 { 322 | g.Log().Error(ctx, "resp.StatusCode: ", resp.StatusCode) 323 | r.Response.Status = resp.StatusCode 324 | r.Response.WriteJson(gjson.New(resp.ReadAllString())) 325 | return 326 | } 327 | // if resp.Header.Get("Content-Type") != "text/event-stream; charset=utf-8" && resp.Header.Get("Content-Type") != "text/event-stream" { 328 | // g.Log().Error(ctx, "resp.Header.Get(Content-Type): ", resp.Header.Get("Content-Type")) 329 | // r.Response.Status = 500 330 | // r.Response.WriteJson(gjson.New(`{"detail": "internal server error"}`)) 331 | // return 332 | // } 333 | 334 | // 流式返回 335 | if req.Stream { 336 | r.Response.Header().Set("Content-Type", "text/event-stream; charset=utf-8") 337 | r.Response.Header().Set("Cache-Control", "no-cache") 338 | r.Response.Header().Set("Connection", "keep-alive") 339 | // r.Response.Flush() 340 | message := "" 341 | decoder := eventsource.NewDecoder(resp.Body) 342 | defer decoder.Decode() 343 | 344 | id := config.GenerateID(29) 345 | for { 346 | event, err := decoder.Decode() 347 | if err != nil { 348 | // if err == io.EOF { 349 | // break 350 | // } 351 | // g.Log().Info(ctx, "释放资源") 352 | break 353 | } 354 | text := event.Data() 355 | // g.Log().Debug(ctx, "text: ", text) 356 | if text == "" { 357 | continue 358 | } 359 | if text == "[DONE]" { 360 | apiRespStrEnd := gstr.Replace(ApiRespStrStreamEnd, "apirespid", id) 361 | apiRespStrEnd = gstr.Replace(apiRespStrEnd, "apicreated", gconv.String(time.Now().Unix())) 362 | apiRespStrEnd = gstr.Replace(apiRespStrEnd, "apirespmodel", req.Model) 363 | r.Response.Writefln("data: " + apiRespStrEnd + "\n\n") 364 | r.Response.Flush() 365 | r.Response.Writefln("data: " + text + "\n\n") 366 | r.Response.Flush() 367 | continue 368 | // resp.Close() 369 | 370 | // break 371 | } 372 | // gjson.New(text).Dump() 373 | role := gjson.New(text).Get("message.author.role").String() 374 | if role == "assistant" { 375 | messageTemp := gjson.New(text).Get("message.content.parts.0").String() 376 | // g.Log().Debug(ctx, "messageTemp: ", messageTemp) 377 | // 如果 messageTemp 不包含 message 且plugin_ids为空 378 | if !gstr.Contains(messageTemp, message) && len(req.PluginIds) == 0 { 379 | continue 380 | } 381 | 382 | content := strings.Replace(messageTemp, message, "", 1) 383 | if content == "" { 384 | continue 385 | } 386 | message = messageTemp 387 | apiResp := gjson.New(ApiRespStrStream) 388 | apiResp.Set("id", id) 389 | apiResp.Set("created", time.Now().Unix()) 390 | apiResp.Set("choices.0.delta.content", content) 391 | // if req.Model == "gpt-4" { 392 | // apiResp.Set("model", "gpt-4") 393 | // } 394 | apiResp.Set("model", req.Model) 395 | apiRespStruct := &apirespstream.ApiRespStreamStruct{} 396 | gconv.Struct(apiResp, apiRespStruct) 397 | // g.Dump(apiRespStruct) 398 | // 创建一个jsoniter的Encoder 399 | json := jsoniter.ConfigCompatibleWithStandardLibrary 400 | 401 | // 将结构体转换为JSON文本并保持顺序 402 | sortJson, err := json.Marshal(apiRespStruct) 403 | if err != nil { 404 | fmt.Println("转换JSON出错:", err) 405 | continue 406 | } 407 | r.Response.Writeln("data: " + string(sortJson) + "\n\n") 408 | r.Response.Flush() 409 | } 410 | 411 | } 412 | 413 | } else { 414 | // 非流式回应 415 | content := "" 416 | decoder := eventsource.NewDecoder(resp.Body) 417 | defer decoder.Decode() 418 | 419 | for { 420 | event, err := decoder.Decode() 421 | if err != nil { 422 | if err == io.EOF { 423 | break 424 | } 425 | continue 426 | } 427 | text := event.Data() 428 | if text == "" { 429 | continue 430 | } 431 | if text == "[DONE]" { 432 | resp.Close() 433 | break 434 | } 435 | // gjson.New(text).Dump() 436 | role := gjson.New(text).Get("message.author.role").String() 437 | if role == "assistant" { 438 | message := gjson.New(text).Get("message.content.parts.0").String() 439 | if message != "" { 440 | content = message 441 | } 442 | } 443 | } 444 | completionTokens := CountTokens(content) 445 | promptTokens := CountTokens(newMessages) 446 | totalTokens := completionTokens + promptTokens 447 | 448 | apiResp := gjson.New(ApiRespStr) 449 | apiResp.Set("choices.0.message.content", content) 450 | id := config.GenerateID(29) 451 | apiResp.Set("id", id) 452 | apiResp.Set("created", time.Now().Unix()) 453 | // if req.Model == "gpt-4" { 454 | // apiResp.Set("model", "gpt-4") 455 | // } 456 | apiResp.Set("model", req.Model) 457 | 458 | apiResp.Set("usage.prompt_tokens", promptTokens) 459 | apiResp.Set("usage.completion_tokens", completionTokens) 460 | apiResp.Set("usage.total_tokens", totalTokens) 461 | r.Response.WriteJson(apiResp) 462 | } 463 | 464 | } 465 | -------------------------------------------------------------------------------- /v1/chat/dalle3.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "chat2api/config" 5 | "strings" 6 | 7 | "github.com/gogf/gf/v2/encoding/gjson" 8 | "github.com/gogf/gf/v2/errors/gerror" 9 | "github.com/gogf/gf/v2/frame/g" 10 | "github.com/gogf/gf/v2/net/ghttp" 11 | "github.com/gogf/gf/v2/os/gtime" 12 | "github.com/gogf/gf/v2/text/gstr" 13 | "github.com/google/uuid" 14 | "github.com/launchdarkly/eventsource" 15 | ) 16 | 17 | var ( 18 | Dalle3req = ` 19 | { 20 | "action": "next", 21 | "messages": [ 22 | { 23 | "id": "aaa2577b-9d88-406a-b1fb-2ea23d8a08ad", 24 | "author": { "role": "user" }, 25 | "content": { "content_type": "text", "parts": ["画一只猫"] }, 26 | "metadata": {} 27 | } 28 | ], 29 | "parent_message_id": "aaa14d98-383c-498c-8ba4-2753bb5afbdd", 30 | "model": "gpt-4-gizmo", 31 | "timezone_offset_min": -480, 32 | "suggestions": [], 33 | "history_and_training_disabled": true, 34 | "conversation_mode": { 35 | "gizmo": { 36 | "gizmo": { 37 | "id": "g-2fkFE8rbu", 38 | "organization_id": "org-OROoM5KiDq6bcfid37dQx4z4", 39 | "short_url": "g-2fkFE8rbu-dall-e", 40 | "author": { 41 | "user_id": "user-u7SVk5APwT622QC7DPe41GHJ", 42 | "display_name": "ChatGPT", 43 | "link_to": null, 44 | "selected_display": "name", 45 | "is_verified": true 46 | }, 47 | "voice": { "id": "ember" }, 48 | "workspace_id": null, 49 | "model": null, 50 | "instructions": null, 51 | "settings": null, 52 | "display": { 53 | "name": "DALL·E", 54 | "description": "Let me turn your imagination into imagery", 55 | "welcome_message": "Hello", 56 | "prompt_starters": null, 57 | "profile_picture_url": "https://files.oaiusercontent.com/file-SxYQO0Fq1ZkPagkFtg67DRVb?se=2123-10-12T23%3A57%3A32Z&sp=r&sv=2021-08-06&sr=b&rscc=max-age%3D31536000%2C%20immutable&rscd=attachment%3B%20filename%3Dagent_3.webp&sig=pLlQh8oUktqQzhM09SDDxn5aakqFuM2FAPptuA0mbqc%3D", 58 | "categories": [] 59 | }, 60 | "share_recipient": "marketplace", 61 | "updated_at": "2023-11-12T19:29:32.777742+00:00", 62 | "last_interacted_at": "2023-11-24T09:27:13.855518+00:00", 63 | "tags": ["public", "first_party"], 64 | "version": null, 65 | "live_version": null, 66 | "training_disabled": null, 67 | "allowed_sharing_recipients": null, 68 | "review_info": null, 69 | "appeal_info": null, 70 | "vanity_metrics": null 71 | }, 72 | "tools": [ 73 | { 74 | "id": "gzm_cnf_KuQKBEnzFPMwdKIWYnOoetjx~gzm_tool_P9ZWt7cmybLejZWkNxDTEpIj", 75 | "type": "dalle", 76 | "settings": null, 77 | "metadata": null 78 | } 79 | ], 80 | "files": [], 81 | "product_features": { 82 | "attachments": { 83 | "type": "retrieval", 84 | "accepted_mime_types": [ 85 | "text/x-ruby", 86 | "text/x-tex", 87 | "application/msword", 88 | "text/x-script.python", 89 | "text/plain", 90 | "application/json", 91 | "application/pdf", 92 | "text/x-csharp", 93 | "text/x-typescript", 94 | "text/x-java", 95 | "text/x-c", 96 | "text/html", 97 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 98 | "application/x-latext", 99 | "text/javascript", 100 | "text/markdown", 101 | "application/vnd.openxmlformats-officedocument.presentationml.presentation", 102 | "text/x-php", 103 | "text/x-c++", 104 | "text/x-sh" 105 | ], 106 | "image_mime_types": [ 107 | "image/gif", 108 | "image/webp", 109 | "image/jpeg", 110 | "image/png" 111 | ], 112 | "can_accept_all_mime_types": true 113 | } 114 | } 115 | }, 116 | "kind": "gizmo_interaction", 117 | "gizmo_id": "g-2fkFE8rbu" 118 | }, 119 | "force_paragen": false, 120 | "force_rate_limit": false 121 | } 122 | ` 123 | ) 124 | 125 | type Dalle3RespData struct { 126 | RevisedPrompt string `json:"revised_prompt"` 127 | Url string `json:"url"` 128 | } 129 | type Dalle3Resp struct { 130 | Created int64 `json:"created"` 131 | Data []Dalle3RespData `json:"data"` 132 | } 133 | 134 | func Dalle3(r *ghttp.Request) { 135 | ctx := r.Context() 136 | if r.Method != "POST" { 137 | r.Response.Status = 405 138 | r.Response.WriteJson(g.Map{ 139 | "detail": "method not allowed", 140 | }) 141 | return 142 | } 143 | authkey := strings.TrimPrefix(r.Header.Get("authorization"), "Bearer ") 144 | if authkey == "" { 145 | r.Response.Status = 401 146 | r.Response.WriteJson(gjson.New(ErrNoAuth)) 147 | return 148 | } 149 | g.Log().Info(ctx, "authkey: ", authkey) 150 | var token string 151 | if config.PASSMODE { 152 | token = authkey 153 | } else { 154 | token = config.SK2TOKEN(ctx, authkey) 155 | } 156 | if token == "" { 157 | r.Response.Status = 401 158 | r.Response.WriteJson(gjson.New(ErrKeyInvalid)) 159 | return 160 | } 161 | g.Log().Debug(ctx, "token: ", token) 162 | prompt := r.Get("prompt").String() 163 | if prompt == "" { 164 | r.Response.Status = 400 165 | r.Response.WriteJson(g.Map{ 166 | "detail": "prompt is empty", 167 | }) 168 | return 169 | } 170 | reqJson := gjson.New(Dalle3req) 171 | reqJson.Set("messages.0.content.parts.0", prompt) 172 | reqJson.Set("messages.0.id", uuid.NewString()) 173 | reqJson.Set("parent_message_id", uuid.NewString()) 174 | 175 | resp, err := g.Client().SetHeaderMap(g.MapStrStr{ 176 | "Authorization": "Bearer " + token, 177 | "Content-Type": "application/json", 178 | "authkey": config.AUTHKEY, 179 | }).Post(ctx, config.APISERVER, reqJson) 180 | if err != nil { 181 | r.Response.Status = 400 182 | r.Response.WriteJson(g.Map{ 183 | "detail": err.Error(), 184 | }) 185 | return 186 | } 187 | defer resp.Close() 188 | if resp.StatusCode != 200 { 189 | r.Response.Status = resp.StatusCode 190 | r.Response.WriteJson(g.Map{ 191 | "detail": resp.ReadAllString(), 192 | }) 193 | return 194 | } 195 | decoder := eventsource.NewDecoder(resp.Body) 196 | defer decoder.Decode() 197 | dalle3resp := Dalle3Resp{} 198 | dalle3resp.Created = gtime.Now().Unix() 199 | for { 200 | event, err := decoder.Decode() 201 | if err != nil { 202 | break 203 | } 204 | text := event.Data() 205 | // g.Log().Debug(ctx, text) 206 | if text == "" { 207 | continue 208 | } 209 | if text == "[DONE]" { 210 | break 211 | } 212 | resJson := gjson.New(text) 213 | role := resJson.Get("message.author.role").String() 214 | // g.Log().Debug(ctx, "role: ", role) 215 | if role != "tool" { 216 | continue 217 | } 218 | content_type := resJson.Get("message.content.content_type").String() 219 | if content_type != "multimodal_text" { 220 | continue 221 | } 222 | // g.Log().Debug(ctx, "content_type: ", content_type) 223 | parts := resJson.GetJsons("message.content.parts") 224 | // g.Dump(parts) 225 | if len(parts) == 0 { 226 | continue 227 | } 228 | for _, part := range parts { 229 | // partJson := gjson.New(part) 230 | revised_prompt := part.Get("metadata.dalle.prompt").String() 231 | url := part.Get("asset_pointer").String() 232 | url, err := GetDownloadUrl(ctx, url, token) 233 | if err != nil { 234 | g.Log().Error(ctx, err) 235 | continue 236 | } 237 | dalle3resp.Data = append(dalle3resp.Data, Dalle3RespData{ 238 | RevisedPrompt: revised_prompt, 239 | Url: url, 240 | }) 241 | } 242 | } 243 | if len(dalle3resp.Data) == 0 { 244 | r.Response.Status = 400 245 | r.Response.WriteJson(g.Map{ 246 | "detail": "no data", 247 | }) 248 | return 249 | } 250 | r.Response.WriteJson(dalle3resp) 251 | 252 | } 253 | 254 | // https://demo.xyhelper.cn/backend-api/files/file-FLeoX7FluBQ1Ri5JHDOq7ZiN/download 255 | 256 | // file-service://file-1YBBS7IUuJaD3Qg7ro03mQ56 257 | func GetDownloadUrl(ctx g.Ctx, url string, token string) (download_url string, err error) { 258 | // 将url c //分割 获取file_id 259 | fileId := gstr.Split(url, "//")[1] 260 | 261 | resp, err := g.Client().SetHeaderMap(g.MapStrStr{ 262 | "Authorization": "Bearer " + token, 263 | "Content-Type": "application/json", 264 | "authkey": config.AUTHKEY, 265 | }).Get(ctx, config.APIHOST+"/backend-api/files/"+fileId+"/download") 266 | if err != nil { 267 | return 268 | } 269 | defer resp.Close() 270 | if resp.StatusCode != 200 { 271 | err = gerror.New(resp.ReadAllString()) 272 | return 273 | } 274 | respJson := gjson.New(resp.ReadAllString()) 275 | download_url = respJson.Get("download_url").String() 276 | return 277 | 278 | } 279 | -------------------------------------------------------------------------------- /v1/chat/gpt4v.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "chat2api/apirespstream" 5 | "chat2api/config" 6 | "fmt" 7 | "image" 8 | "io" 9 | "net/http" 10 | "strings" 11 | "time" 12 | 13 | _ "image/jpeg" 14 | _ "image/png" 15 | 16 | "github.com/gogf/gf/v2/encoding/gjson" 17 | "github.com/gogf/gf/v2/errors/gerror" 18 | "github.com/gogf/gf/v2/frame/g" 19 | "github.com/gogf/gf/v2/net/ghttp" 20 | "github.com/gogf/gf/v2/os/gfile" 21 | "github.com/gogf/gf/v2/text/gstr" 22 | "github.com/gogf/gf/v2/util/gconv" 23 | "github.com/google/uuid" 24 | jsoniter "github.com/json-iterator/go" 25 | "github.com/launchdarkly/eventsource" 26 | ) 27 | 28 | func Gpt4v(r *ghttp.Request) { 29 | ctx := r.Context() 30 | authkey := strings.TrimPrefix(r.Header.Get("authorization"), "Bearer ") 31 | if authkey == "" { 32 | r.Response.Status = 401 33 | r.Response.WriteJson(gjson.New(ErrNoAuth)) 34 | return 35 | } 36 | g.Log().Info(ctx, "authkey: ", authkey) 37 | var token string 38 | if config.PASSMODE { 39 | token = authkey 40 | } else { 41 | token = config.SK2TOKEN(ctx, authkey) 42 | } 43 | if token == "" { 44 | r.Response.Status = 401 45 | r.Response.WriteJson(gjson.New(ErrKeyInvalid)) 46 | return 47 | } 48 | g.Log().Debug(ctx, "token: ", token) 49 | ChatGPTAccountID := r.Header.Get("ChatGPT-Account-ID") 50 | message := r.Get("message").String() 51 | if message == "" { 52 | r.Response.Status = 400 53 | r.Response.WriteJson(g.Map{ 54 | "detail": "message is empty", 55 | }) 56 | return 57 | } 58 | stream := r.Get("stream").Bool() 59 | 60 | // 获取上传的文件 61 | files := r.GetUploadFiles("file") 62 | if len(files) == 0 { 63 | r.Response.Status = 400 64 | r.Response.WriteJsonExit(g.Map{ 65 | "code": 0, 66 | "detail": "upload file is empty", 67 | }) 68 | } 69 | // 检查 ./temp 目录是否存在 不在则创建 70 | if !gfile.Exists("./temp") { 71 | err := gfile.Mkdir("./temp") 72 | if err != nil { 73 | r.Response.Status = 400 74 | r.Response.WriteJsonExit(g.Map{ 75 | "code": 0, 76 | "detail": "create temp dir failed", 77 | }) 78 | } 79 | } 80 | filenames, err := files.Save("./temp", true) 81 | if err != nil { 82 | r.Response.Status = 400 83 | r.Response.WriteJsonExit(g.Map{ 84 | "code": 0, 85 | "detail": "upload file failed", 86 | }) 87 | } 88 | // 删除临时文件 89 | defer func() { 90 | for _, filename := range filenames { 91 | gfile.Remove("./temp/" + filename) 92 | } 93 | }() 94 | 95 | var file_ids []string 96 | var download_urls []string 97 | var widths []int 98 | var heights []int 99 | var size_bytess []int64 100 | // 上传文件到azure 101 | for _, filename := range filenames { 102 | file_id, download_url, width, height, size_bytes, err := UploadAzure(ctx, "./temp/"+filename, token, ChatGPTAccountID) 103 | if err != nil { 104 | g.Log().Error(ctx, err) 105 | r.Response.Status = 400 106 | r.Response.WriteJsonExit(g.Map{ 107 | "code": 0, 108 | "detail": err.Error(), 109 | }) 110 | } 111 | file_ids = append(file_ids, file_id) 112 | download_urls = append(download_urls, download_url) 113 | widths = append(widths, width) 114 | heights = append(heights, height) 115 | size_bytess = append(size_bytess, size_bytes) 116 | } 117 | // g.Dump(file_ids) 118 | // g.Dump(download_urls) 119 | ChatReq := gjson.New(ChatReqStr) 120 | for i, file_id := range file_ids { 121 | ChatReq.Set("messages.0.content.parts."+gconv.String(i)+".asset_pointer", "file-service://"+file_id) 122 | ChatReq.Set("messages.0.content.parts."+gconv.String(i)+".height", heights[i]) 123 | ChatReq.Set("messages.0.content.parts."+gconv.String(i)+".width", widths[i]) 124 | ChatReq.Set("messages.0.content.parts."+gconv.String(i)+".size_bytes", size_bytess[i]) 125 | } 126 | // messages.0.content.content_type multimodal_text 127 | ChatReq.Set("messages.0.content.content_type", "multimodal_text") 128 | ChatReq.Set("messages.0.content.parts."+gconv.String(len(file_ids)), message) 129 | ChatReq.Set("messages.0.id", uuid.NewString()) 130 | ChatReq.Set("parent_message_id", uuid.NewString()) 131 | ChatReq.Set("model", "gpt-4") 132 | // ChatReq.Remove("plugin_ids") 133 | 134 | // ChatReq.Dump() 135 | // 请求openai 136 | reqHeader := g.MapStrStr{ 137 | "Authorization": "Bearer " + token, 138 | "Content-Type": "application/json", 139 | } 140 | if ChatGPTAccountID != "" { 141 | reqHeader["ChatGPT-Account-ID"] = ChatGPTAccountID 142 | } 143 | resp, err := g.Client().SetHeaderMap(reqHeader).Post(ctx, config.APISERVER, ChatReq.MustToJson()) 144 | if err != nil { 145 | r.Response.Status = 500 146 | r.Response.WriteJson(gjson.New(`{"detail": "internal server error"}`)) 147 | return 148 | } 149 | defer resp.Close() 150 | // resp.RawDump() 151 | // 如果返回结果不是200 152 | if resp.StatusCode != 200 { 153 | r.Response.Status = resp.StatusCode 154 | r.Response.WriteJson(gjson.New(resp.ReadAllString())) 155 | return 156 | } 157 | if stream { 158 | // 流式返回 159 | rw := r.Response.RawWriter() 160 | flusher, ok := rw.(http.Flusher) 161 | if !ok { 162 | g.Log().Error(ctx, "rw.(http.Flusher) error") 163 | r.Response.WriteStatusExit(500) 164 | return 165 | } 166 | r.Response.Header().Set("Content-Type", "text/event-stream; charset=utf-8") 167 | r.Response.Header().Set("Cache-Control", "no-cache") 168 | r.Response.Header().Set("Connection", "keep-alive") 169 | // r.Response.Flush() 170 | message := "" 171 | decoder := eventsource.NewDecoder(resp.Body) 172 | defer decoder.Decode() 173 | 174 | id := config.GenerateID(29) 175 | for { 176 | event, err := decoder.Decode() 177 | if err != nil { 178 | if err == io.EOF { 179 | break 180 | } 181 | break 182 | } 183 | text := event.Data() 184 | if text == "" { 185 | continue 186 | } 187 | if text == "[DONE]" { 188 | apiRespStrEnd := gstr.Replace(ApiRespStrStreamEnd, "apirespid", id) 189 | apiRespStrEnd = gstr.Replace(apiRespStrEnd, "apicreated", gconv.String(time.Now().Unix())) 190 | apiRespStrEnd = gstr.Replace(apiRespStrEnd, "apirespmodel", "gpt-4") 191 | rw.Write([]byte("data: " + apiRespStrEnd + "\n\n")) 192 | 193 | rw.Write([]byte("data: " + text + "\n\n")) 194 | flusher.Flush() 195 | break 196 | } 197 | role := gjson.New(text).Get("message.author.role").String() 198 | if role == "assistant" { 199 | messageTemp := gjson.New(text).Get("message.content.parts.0").String() 200 | // 201 | content := strings.Replace(messageTemp, message, "", 1) 202 | if content == "" { 203 | continue 204 | } 205 | message = messageTemp 206 | apiResp := gjson.New(ApiRespStrStream) 207 | apiResp.Set("id", id) 208 | apiResp.Set("created", time.Now().Unix()) 209 | apiResp.Set("choices.0.delta.content", content) 210 | 211 | apiRespStruct := &apirespstream.ApiRespStreamStruct{} 212 | gconv.Struct(apiResp, apiRespStruct) 213 | apiRespStruct.Model = "gpt-4" 214 | // g.Dump(apiRespStruct) 215 | // 创建一个jsoniter的Encoder 216 | json := jsoniter.ConfigCompatibleWithStandardLibrary 217 | 218 | // 将结构体转换为JSON文本并保持顺序 219 | sortJson, err := json.Marshal(apiRespStruct) 220 | if err != nil { 221 | fmt.Println("转换JSON出错:", err) 222 | return 223 | } 224 | rw.Write([]byte("data: " + string(sortJson) + "\n\n")) 225 | flusher.Flush() 226 | } 227 | 228 | } 229 | } else { 230 | // 非流式回应 231 | content := "" 232 | decoder := eventsource.NewDecoder(resp.Body) 233 | for { 234 | event, err := decoder.Decode() 235 | if err != nil { 236 | if err == io.EOF { 237 | break 238 | } 239 | continue 240 | } 241 | text := event.Data() 242 | if text == "" { 243 | continue 244 | } 245 | if text == "[DONE]" { 246 | break 247 | } 248 | // gjson.New(text).Dump() 249 | role := gjson.New(text).Get("message.author.role").String() 250 | if role == "assistant" { 251 | message := gjson.New(text).Get("message.content.parts.0").String() 252 | if message != "" { 253 | content = message 254 | } 255 | } 256 | } 257 | decoder.Decode() 258 | 259 | completionTokens := CountTokens(content) 260 | promptTokens := CountTokens(message) 261 | totalTokens := completionTokens + promptTokens 262 | 263 | apiResp := gjson.New(ApiRespStr) 264 | apiResp.Set("choices.0.message.content", content) 265 | id := config.GenerateID(29) 266 | apiResp.Set("id", id) 267 | apiResp.Set("created", time.Now().Unix()) 268 | apiResp.Set("model", "gpt-4") 269 | apiResp.Set("usage.prompt_tokens", promptTokens) 270 | apiResp.Set("usage.completion_tokens", completionTokens) 271 | apiResp.Set("usage.total_tokens", totalTokens) 272 | r.Response.WriteJson(apiResp) 273 | } 274 | 275 | } 276 | 277 | func UploadAzure(ctx g.Ctx, filepath string, token string, ChatGPTAccountID string) (file_id string, download_url string, width int, height int, size_bytes int64, err error) { 278 | // 检测文件是否存在 279 | if !gfile.Exists(filepath) { 280 | err = gerror.New("read file fail") 281 | return 282 | } 283 | 284 | fileName := gfile.Basename(filepath) 285 | fileSize := gfile.Size(filepath) 286 | apihost := config.APIHOST 287 | 288 | // 获取上传地址 backend-api/files POST 289 | filesReqHeader := g.MapStrStr{ 290 | "Authorization": "Bearer " + token, 291 | "Content-Type": "application/json", 292 | } 293 | if ChatGPTAccountID != "" { 294 | filesReqHeader["ChatGPT-Account-ID"] = ChatGPTAccountID 295 | } 296 | res, err := g.Client().SetHeaderMap(filesReqHeader).Post(ctx, apihost+"/backend-api/files", g.Map{ 297 | "file_name": fileName, 298 | "file_size": fileSize, 299 | "use_case": "multimodal", 300 | }) 301 | if err != nil { 302 | return 303 | } 304 | defer res.Close() 305 | if res.StatusCode != 200 { 306 | res.RawDump() 307 | err = gerror.New("get upload_url fail:" + res.Status) 308 | return 309 | } 310 | // 311 | resJson := gjson.New(res.ReadAllString()) 312 | // resJson.Dump() 313 | upload_url := resJson.Get("upload_url").String() 314 | file_id = resJson.Get("file_id").String() 315 | if upload_url == "" { 316 | err = gerror.New("get upload_url fail") 317 | return 318 | } 319 | // 获取图片宽高 320 | file, err := gfile.Open(filepath) 321 | if err != nil { 322 | return 323 | } 324 | defer file.Close() 325 | // 获取图片宽高 326 | img, _, err := image.DecodeConfig(file) 327 | if err != nil { 328 | return 329 | } 330 | width = img.Width 331 | height = img.Height 332 | size_bytes = fileSize 333 | 334 | // 以二进制流的方式上传文件 PUT 335 | filedata := gfile.GetBytes(filepath) 336 | 337 | resput, err := g.Client().SetHeaderMap(g.MapStrStr{ 338 | "x-ms-blob-type": "BlockBlob", 339 | "x-ms-version": "2020-04-08", 340 | }).Put(ctx, upload_url, filedata) 341 | if err != nil { 342 | return 343 | } 344 | defer resput.Close() 345 | // resput.RawDump() 346 | if resput.StatusCode != 201 { 347 | err = gerror.New("upload file fail") 348 | return 349 | } 350 | // 获取文件下载地址 backend-api/files/{file_id}/uploaded POST 351 | uploadedReqHeader := g.MapStrStr{ 352 | "Authorization": "Bearer " + token, 353 | "Content-Type": "application/json", 354 | } 355 | if ChatGPTAccountID != "" { 356 | uploadedReqHeader["ChatGPT-Account-ID"] = ChatGPTAccountID 357 | } 358 | resdown, err := g.Client().SetHeaderMap(uploadedReqHeader).Post(ctx, apihost+"/backend-api/files/"+file_id+"/uploaded") 359 | if err != nil { 360 | return 361 | } 362 | defer resdown.Close() 363 | resdown.RawDump() 364 | download_url = gjson.New(resdown.ReadAllString()).Get("download_url").String() 365 | if download_url == "" { 366 | err = gerror.New("get download_url fail") 367 | return 368 | } 369 | 370 | return 371 | } 372 | -------------------------------------------------------------------------------- /v1/chat/token.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "github.com/pkoukk/tiktoken-go" 5 | ) 6 | 7 | var ( 8 | Tke *tiktoken.Tiktoken 9 | ) 10 | 11 | func init() { 12 | // gpt-3.5-turbo encoding 13 | tke, err := tiktoken.GetEncoding("cl100k_base") 14 | if err != nil { 15 | panic(err) 16 | } 17 | Tke = tke 18 | 19 | } 20 | 21 | func CountTokens(text string) int { 22 | 23 | return len(Tke.Encode(text, nil, nil)) 24 | } 25 | --------------------------------------------------------------------------------