├── .env.example ├── .github ├── dependabot.yml └── workflows │ ├── closing_inactive_issues.yml │ └── docker-push.yaml ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── chatgpt │ ├── api.go │ ├── constant.go │ ├── health_check.go │ ├── login.go │ ├── turnstile.go │ └── typings.go ├── common.go ├── imitate │ ├── api.go │ ├── convert.go │ ├── request.go │ └── response.go └── platform │ ├── access_token.go │ ├── api.go │ ├── constant.go │ ├── login.go │ └── typings.go ├── compose.yaml ├── env └── env.go ├── example ├── chatgpt.http ├── imitate.http ├── ios.http └── platform.http ├── go.mod ├── go.sum ├── main.go ├── middleware ├── authorization.go └── cors.go └── render.yaml /.env.example: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | TZ=Asia/Shanghai 3 | PROXY= 4 | OPENAI_EMAIL= 5 | OPENAI_PASSWORD= 6 | CONTINUE_SIGNAL= 7 | ENABLE_HISTORY= 8 | IMITATE_ACCESS_TOKEN= -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/closing_inactive_issues.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "45 2 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 14 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 19 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/docker-push.yaml: -------------------------------------------------------------------------------- 1 | name: Docker build and push 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch: 7 | description: "Branch to build from" 8 | required: false 9 | default: 'main' 10 | image_tag: 11 | description: "Image tag" 12 | required: false 13 | default: 'latest' 14 | 15 | env: 16 | PLATFORMS: ${{ vars.PLATFORMS || 'linux/amd64,linux/arm64' }} 17 | 18 | jobs: 19 | docker-build-push: 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | packages: write 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | with: 28 | ref: ${{ github.event.inputs.branch }} 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v2 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v2 35 | 36 | - name: Cache Docker layers 37 | uses: actions/cache@v2 38 | with: 39 | path: /tmp/.buildx-cache 40 | key: ${{ runner.os }}-buildx-${{ github.sha }} 41 | restore-keys: | 42 | ${{ runner.os }}-buildx- 43 | 44 | - name: Login to DockerHub 45 | uses: docker/login-action@v2 46 | with: 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 49 | 50 | - name: Build and push 51 | uses: docker/build-push-action@v4 52 | with: 53 | context: . 54 | platforms: ${{ env.PLATFORMS }} 55 | push: true 56 | tags: ${{ github.actor }}/go-chatgpt-api:${{ github.event.inputs.image_tag }} 57 | cache-from: type=local,src=/tmp/.buildx-cache 58 | cache-to: type=local,dest=/tmp/.buildx-cache,mode=max 59 | 60 | - name: Log into ghcr 61 | uses: docker/login-action@v2 62 | if: ${{ vars.USE_GHCR == '1' }} 63 | with: 64 | registry: ghcr.io 65 | username: ${{ github.actor }} 66 | password: ${{ secrets.GITHUB_TOKEN }} 67 | 68 | - name: Build and push to ghcr 69 | uses: docker/build-push-action@v4 70 | if: ${{ vars.USE_GHCR == '1' }} 71 | with: 72 | context: . 73 | platforms: ${{ env.PLATFORMS }} 74 | push: true 75 | tags: ghcr.io/${{ github.actor }}/go-chatgpt-api:${{ github.event.inputs.image_tag }} 76 | cache-from: type=local,src=/tmp/.buildx-cache 77 | cache-to: type=local,dest=/tmp/.buildx-cache,mode=max 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | go-chatgpt-api 3 | __debug* 4 | chat.openai.com.har -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN go build -ldflags="-w -s" -o go-chatgpt-api main.go 5 | 6 | FROM alpine 7 | WORKDIR /app 8 | COPY --from=builder /app/go-chatgpt-api . 9 | RUN apk add --no-cache tzdata 10 | ENV TZ=Asia/Shanghai 11 | EXPOSE 8080 12 | CMD ["/app/go-chatgpt-api"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 linweiyuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-chatgpt-api 2 | 3 | # 由于OAI无节制的降智,此项目归档。 4 | 5 | # 此为个人维护的魔改版,相比原版,有以下改动/区别 6 | Docker image: maxduke/go-chatgpt-api:latest 7 | 8 | ## 20250210 9 | - [imitate]Add support for o3-mini 10 | - **OAI降智太严重了,本人已经退订Plus,后期随时停止更新** 11 | 12 | ## 20241206 13 | - [imitate]Add support for o1 && drop support for o1-preview 14 | 15 | ## 20241021 16 | - Remove unused WSS code 17 | - Update deps. 18 | 19 | ## 20240913 20 | - [imitate]Add support for o1-preview & o1-mini 21 | 22 | ## 20240719 23 | - [imitate]Add support for gpt-4o-mini 24 | - [imitate]Drop support for gpt-3.5(text-davinci-002-render-sha) 25 | - [imitate]*Plan to remove this endpoint in the future, recommend to use https://github.com/xqdoo00o/ChatGPT-to-API* 26 | 27 | ## 20240626 28 | - Adjust default UA 29 | - Test TurnstileToken generation (https://github.com/xqdoo00o/ChatGPT-to-API/commit/2cc5968fc70073c095d157fe8a9b5a2d66917eb6) 30 | - Add new env variable 31 | - PROCESS_TURNSTILE (set to "true" to enable turnstile token generation) 32 | 33 | ## 20240612 34 | - remove unused cookie 35 | - add error log for checkRequire 36 | 37 | ## 20240527 38 | - 尝试修复autoContiune, 增加环境变量 39 | - AUTO_CONTINUE - 默认 false 40 | 41 | ## 20240524 42 | - 增加环境变量 43 | - POW_RETRY_TIMES - POW难度太高时,尝试重新获取POW种子的次数,默认0 44 | - POW_MAX_DIFFICULTY - POW最高难度设定,高于次难度将重试获取POW种子,默认"000032" 45 | 46 | ## 20240523 47 | - 调整POW算法 48 | - 增加环境变量 49 | - HARDWARE - 整数型,CPU核心数+屏幕分辨率高度+屏幕分辨率宽度。 不填则随机 50 | - POW_MAX_CALC_TIMES - 整数型,默认500000次。 CPU运算快的可以适当提高,减少403错误的概率 51 | 52 | ## 20240522 53 | - 调整生成刷新 deviceID 逻辑 54 | - 增加log: POW difficulty 55 | - 更新POW算法 https://github.com/xqdoo00o/ChatGPT-to-API/commit/e69e3d8ab3de295eec7a73551fffdb3b9ce83b1b 56 | - 在conversation payload中添加了一些新的字段,跟官网同步 57 | 58 | ## 20240520 59 | - TEST: 尝试强制使用SSE,从而避免WSS的各种问题 (效果未知) 60 | - 从request payload中移除arkoseToken (现在官网只在request header中有) 61 | 62 | ## 20240514 63 | add gpt-4o support for imitate 64 | 65 | ## 20240507 66 | 调整backend域名 chat.openai.com => chatgpt.com 67 | 68 | 可能需要重新抓取har 69 | 70 | ## 20240506 71 | 调整POW以及更新部分接口域名 72 | 73 | 参照 https://github.com/xqdoo00o/ChatGPT-to-API/compare/2b6ea6e2208f11fbdf760ba23c1f704993901242...85aafb4cd9af1ed09e2120abc9bdae42bbacd271 74 | 75 | ## 20240503 76 | 后续计划移除imitate相关接口,有chat2api的需求的建议使用熊猫大佬的项目: 77 | 78 | https://github.com/xqdoo00o/ChatGPT-to-API 79 | 80 | ## 20240501 81 | 调整chatrequirement以及pow算法 82 | 83 | 参照 https://github.com/xqdoo00o/ChatGPT-to-API/compare/d26911c672c9b0ce3c7928fec1109ff8539a34c1...89a038c3ca32bdbfdfcd2eff730aaaa9ff80ac7f 84 | ## 20240426 85 | 默认关闭 healthcheck, 如需要开启请配置新的环境变量 86 | - ENABLE_HEALTHCHECK 87 | ## 20240422 add POW 88 | ## 20240416 fix 403 error 89 | 添加新的环境变量 90 | - CLIENT_PROFILE 91 | - UA 92 | 93 | 没需求不要动 94 | ## 20240320 fix 403 error 95 | ## 20240314 imitate 重构 96 | - 更新依赖 https://github.com/xqdoo00o/funcaptcha 97 | - 移除环境变量 ENABLE_ARKOSE_3 , 程序将自动判断 98 | - 尝试重构 imitate, 参照 https://github.com/xqdoo00o/ChatGPT-to-API 99 | ## 20240206 imitate 支持 WSS 100 | ## 20240205 尝试支持官网改动之后的WSS协议 (imitate未支持) 101 | 参考 [32c0cf4b709ad3a7540f4a8ab0f7200c2ba92a9f](https://github.com/linweiyuan/go-chatgpt-api/commit/32c0cf4b709ad3a7540f4a8ab0f7200c2ba92a9f) 102 | 103 | ## 自动获取内部和更新的 Access token 和 PUID 104 | 配置环境变量 105 | - OPENAI_EMAIL 106 | - OPENAI_PASSWORD 107 | 108 | 或 109 | 110 | - OPENAI_REFRESH_TOKEN 111 | 112 | 自动生成和更新内部 Access token (用于imitate)和 PUID(仅PLUS账户) 113 | 114 | 推荐使用 OPENAI_REFRESH_TOKEN 方式, 使用邮箱和用户名的方式能否成功取决于抓取的har登陆时能否不出码 115 | 116 | ## 如何使用内部的 Access token ? 117 | 使用环境变量 118 | - IMITATE_API_KEY 虚拟一个api key, 当client发送的api key匹配时, 使用内部的 access token 转 API 119 | 120 | 集成最新 [funcaptcha](https://github.com/xqdoo00o/funcaptcha) 支持harPool, 映射目录 /app/harPool 121 | 122 | [har获取方式](https://github.com/xqdoo00o/ChatGPT-to-API/blob/master/README_ZH.md#har%E6%96%87%E4%BB%B6%E6%B1%A0) 123 | 124 | ## ~~如何为3.5开启 ARKOSE TOKEN ?~~ 125 | ~~使用环境变量~~ 126 | - ~~ENABLE_ARKOSE_3=true~~ 127 | --- 128 | 129 |
130 | 原版 README.md 131 | 132 | ## 一个尝试绕过 `Cloudflare` 来使用 `ChatGPT` 接口的程序 133 | 134 | 由于本人工作性质发生变化,每天通勤来回 4 小时以上 + 白天全程内网开发接触不到自己的代码,没时间继续维护,**已弃坑**,请换用其它还活着的项目 135 | 136 |
137 | 或者有没有老板赏识的可以介绍份好工作,那么就可以在完成自己的工作任务后摸鱼继续研究,联系方式: 138 | 139 | ![](https://linweiyuan.github.io/about/mmqrcode.png) 140 |
141 | 142 | --- 143 | 144 | # 以下文档内容已过期,不一定支持(2023-10-24) 145 | 146 | ### 支持接口 147 | 148 | - https://chat.openai.com/auth/login 登录返回 `accessToken`(谷歌和微软账号暂不支持登录,但可正常使用其他接口) 149 | - 模型和插件查询 150 | - `GPT-3.5` 和 `GPT-4` 对话增删改查及分享 151 | - https://platform.openai.com/playground 登录返回 `apiKey` 152 | - `apiKey` 余额查询 153 | - 等等 ... 154 | - 支持 `ChatGPT` 转 `API`,接口 `/imitate/v1/chat/completions`,利用 `accessToken` 模拟 `apiKey`,实现伪免费使用 `API`,从而支持集成仅支持 `apiKey` 调用的第三方客户端项目,分享一个好用的脚本测试 `web-to-api` (https://github.com/linweiyuan/go-chatgpt-api/issues/251) 155 | 156 | ```python 157 | import openai 158 | 159 | openai.api_key = "这里填 access token,不是 api key" 160 | openai.api_base = "http://127.0.0.1:8080/imitate/v1" 161 | 162 | while True: 163 | text = input("请输入问题:") 164 | response = openai.ChatCompletion.create( 165 | model='gpt-3.5-turbo', 166 | messages=[ 167 | {'role': 'user', 'content': text}, 168 | ], 169 | stream=True 170 | ) 171 | 172 | for chunk in response: 173 | print(chunk.choices[0].delta.get("content", ""), end="", flush=True) 174 | print("\n") 175 | ``` 176 | 177 | 范例(URL 和参数基本保持着和官网一致,部分接口有些许改动),部分例子,不是全部,**理论上**全部基于文本传输的接口都支持 178 | 179 | https://github.com/linweiyuan/go-chatgpt-api/tree/main/example 180 | 181 | --- 182 | 183 | ### 使用的过程中遇到问题应该如何解决 184 | 185 | 汇总贴:https://github.com/linweiyuan/go-chatgpt-api/issues/74 186 | 187 | 如果有疑问而不是什么程序出错其实可以在 [Discussions](https://github.com/linweiyuan/go-chatgpt-api/discussions) 里发而不是新增 Issue 188 | 189 | 群聊:https://github.com/linweiyuan/go-chatgpt-api/discussions/197 190 | 191 | 再说一遍,不要来 `Issues` 提你的疑问(再提不回复直接关闭),有讨论区,有群,不要提脑残问题,反面教材:https://github.com/linweiyuan/go-chatgpt-api/issues/255 192 | 193 | --- 194 | 195 | ### 配置 196 | 197 | 如需设置代理,可以设置环境变量 `PROXY`,比如 `PROXY=http://127.0.0.1:20171` 或者 `PROXY=socks5://127.0.0.1:20170`,注释掉或者留空则不启用 198 | 199 | 如果代理需账号密码验证,则 `http://username:password@ip:port` 或者 `socks5://username:password@ip:port` 200 | 201 | 如需配合 `warp` 使用:`PROXY=socks5://chatgpt-proxy-server-warp:65535`,因为需要设置 `warp` 的场景已经默认可以直接访问 `ChatGPT` 官网,因此共用一个变量不冲突(国内 `VPS` 不在讨论范围内,请自行配置网络环境,`warp` 服务在魔法环境下才能正常工作) 202 | 203 | 家庭网络无需跑 `warp` 服务,跑了也没用,会报错,仅在服务器需要 204 | 205 | `CONTINUE_SIGNAL=1`,开启 `/imitate` 接口自动继续会话功能,留空关闭,默认关闭 206 | 207 | --- 208 | 209 | `GPT-4` 相关模型目前需要验证 `arkose_token`,社区已经有很多解决方案,请自行查找,其中一个能用的:https://github.com/linweiyuan/go-chatgpt-api/issues/252 210 | 211 | 参考配置视频(拉到文章最下面点开视频,需要自己有一定的动手能力,根据你的环境不同自行微调配置):[如何生成 GPT-4 arkose_token](https://linweiyuan.github.io/2023/06/24/%E5%A6%82%E4%BD%95%E7%94%9F%E6%88%90-GPT-4-arkose-token.html) 212 | 213 | --- 214 | 215 | 根据你的网络环境不同,可以展开查看对应配置,下面例子是基本参数,更多参数查看 [compose.yaml](https://github.com/linweiyuan/go-chatgpt-api/blob/main/compose.yaml) 216 | 217 |
218 | 219 | 直接利用现成的服务 220 | 221 | 服务器不定时维护,不保证高可用,利用这些服务导致的账号安全问题,与本项目无关 222 | 223 | - https://go-chatgpt-api.linweiyuan.com 224 | 225 |
226 | 227 |
228 | 229 | 网络在直连或者通过代理的情况下可以正常访问 ChatGPT 230 | 231 | ```yaml 232 | services: 233 | go-chatgpt-api: 234 | container_name: go-chatgpt-api 235 | image: linweiyuan/go-chatgpt-api 236 | ports: 237 | - 8080:8080 238 | environment: 239 | - TZ=Asia/Shanghai 240 | restart: unless-stopped 241 | ``` 242 | 243 |
244 | 245 |
246 | 247 | 服务器无法正常访问 ChatGPT 248 | 249 | ```yaml 250 | services: 251 | go-chatgpt-api: 252 | container_name: go-chatgpt-api 253 | image: linweiyuan/go-chatgpt-api 254 | ports: 255 | - 8080:8080 256 | environment: 257 | - TZ=Asia/Shanghai 258 | - PROXY=socks5://chatgpt-proxy-server-warp:65535 259 | depends_on: 260 | - chatgpt-proxy-server-warp 261 | restart: unless-stopped 262 | 263 | chatgpt-proxy-server-warp: 264 | container_name: chatgpt-proxy-server-warp 265 | image: linweiyuan/chatgpt-proxy-server-warp 266 | restart: unless-stopped 267 | ``` 268 | 269 |
270 | 271 | --- 272 | 273 | 目前 `warp` 容器检测到流量超过 1G 会自动重启,如果你知道什么是 `teams-enroll-token` (不知道就跳过),可以通过环境变量 `TEAMS_ENROLL_TOKEN` 设置它的值,然后利用这条命令来检查是否生效 274 | 275 | `docker-compose exec chatgpt-proxy-server-warp warp-cli --accept-tos account | awk 'NR==1'` 276 | 277 | ``` 278 | Account type: Free (没有生效) 279 | 280 | Account type: Team (设置正常) 281 | ``` 282 | 283 | --- 284 | 285 | ### Render部署 286 | 287 | 点击下面的按钮一键部署,缺点是免费版本冷启动比较慢 288 | 289 | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/linweiyuan/go-chatgpt-api) 290 | 291 | --- 292 | 293 | ### 如何集成其他第三方客户端(下面的内容不一定是最新,有问题请去各自项目查看) 294 | 295 | - [moeakwak/chatgpt-web-share](https://github.com/moeakwak/chatgpt-web-share) 296 | 297 | 环境变量 298 | 299 | ``` 300 | CHATGPT_BASE_URL=http://go-chatgpt-api:8080/chatgpt/backend-api/ 301 | ``` 302 | 303 | - [lss233/chatgpt-mirai-qq-bot](https://github.com/lss233/chatgpt-mirai-qq-bot) 304 | 305 | `config.cfg` 306 | 307 | ``` 308 | [openai] 309 | browserless_endpoint = "http://go-chatgpt-api:8080/chatgpt/backend-api/" 310 | ``` 311 | 312 | - [Kerwin1202/chatgpt-web](https://github.com/Kerwin1202/chatgpt-web) | [Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web) 313 | 314 | 环境变量 315 | 316 | ``` 317 | API_REVERSE_PROXY=http://go-chatgpt-api:8080/chatgpt/backend-api/conversation 318 | ``` 319 | 320 | - [pengzhile/pandora](https://github.com/pengzhile/pandora)(不完全兼容) 321 | 322 | 环境变量 323 | 324 | ``` 325 | CHATGPT_API_PREFIX=http://go-chatgpt-api:8080 326 | ``` 327 | 328 | --- 329 | 330 | - [1130600015/feishu-chatgpt](https://github.com/1130600015/feishu-chatgpt) 331 | 332 | `application.yaml` 333 | 334 | ```yaml 335 | proxy: 336 | url: http://go-chatgpt-api:8080 337 | ``` 338 | 339 | --- 340 | 341 | - [Yidadaa/ChatGPT-Next-Web](https://github.com/Yidadaa/ChatGPT-Next-Web) 342 | 343 | 环境变量 344 | 345 | ``` 346 | BASE_URL=http://go-chatgpt-api:8080/imitate 347 | ``` 348 | 349 | --- 350 | 351 | ### 相关博客(程序更新很多次,文章的内容可能和现在的不一样,仅供参考):[ChatGPT](https://linweiyuan.github.io/categories/ChatGPT/) 352 | 353 | - [如何生成 GPT-4 arkose_token](https://linweiyuan.github.io/2023/06/24/%E5%A6%82%E4%BD%95%E7%94%9F%E6%88%90-GPT-4-arkose-token.html) 354 | - [利用 HTTP Client 来调试 go-chatgpt-api](https://linweiyuan.github.io/2023/06/18/%E5%88%A9%E7%94%A8-HTTP-Client-%E6%9D%A5%E8%B0%83%E8%AF%95-go-chatgpt-api.html) 355 | - [一种解决 ChatGPT Access denied 的方法](https://linweiyuan.github.io/2023/04/15/%E4%B8%80%E7%A7%8D%E8%A7%A3%E5%86%B3-ChatGPT-Access-denied-%E7%9A%84%E6%96%B9%E6%B3%95.html) 356 | - [ChatGPT 如何自建代理](https://linweiyuan.github.io/2023/04/08/ChatGPT-%E5%A6%82%E4%BD%95%E8%87%AA%E5%BB%BA%E4%BB%A3%E7%90%86.html) 357 | - [一种取巧的方式绕过 Cloudflare v2 验证](https://linweiyuan.github.io/2023/03/14/%E4%B8%80%E7%A7%8D%E5%8F%96%E5%B7%A7%E7%9A%84%E6%96%B9%E5%BC%8F%E7%BB%95%E8%BF%87-Cloudflare-v2-%E9%AA%8C%E8%AF%81.html) 358 | 359 | --- 360 | 361 | ### 最后感谢各位同学 362 | 363 | 364 | 365 | 366 | 367 | --- 368 | 369 | ![](https://linweiyuan.github.io/about/mm_reward_qrcode.png) 370 | -------------------------------------------------------------------------------- /api/chatgpt/api.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "math/rand" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | http "github.com/bogdanfinn/fhttp" 18 | "github.com/gin-gonic/gin" 19 | "github.com/google/uuid" 20 | "github.com/PuerkitoBio/goquery" 21 | "golang.org/x/crypto/sha3" 22 | 23 | "github.com/maxduke/go-chatgpt-api/api" 24 | "github.com/linweiyuan/go-logger/logger" 25 | ) 26 | 27 | var ( 28 | autoContinue bool 29 | answers = map[string]string{} 30 | timeLocation, _ = time.LoadLocation("Asia/Shanghai") 31 | timeLayout = "Mon Jan 2 2006 15:04:05" 32 | cachedHardware = 0 33 | cachedSid = uuid.NewString() 34 | cachedScripts = []string{} 35 | cachedDpl = "" 36 | cachedRequireProof = "" 37 | 38 | PowRetryTimes = 0 39 | PowMaxDifficulty = "000032" 40 | powMaxCalcTimes = 500000 41 | navigatorKeys = []string{ 42 | "registerProtocolHandler−function registerProtocolHandler() { [native code] }", 43 | "storage−[object StorageManager]", 44 | "locks−[object LockManager]", 45 | "appCodeName−Mozilla", 46 | "permissions−[object Permissions]", 47 | "appVersion−5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0", 48 | "share−function share() { [native code] }", 49 | "webdriver−false", 50 | "managed−[object NavigatorManagedData]", 51 | "canShare−function canShare() { [native code] }", 52 | "vendor−Google Inc.", 53 | "vendor−Google Inc.", 54 | "mediaDevices−[object MediaDevices]", 55 | "vibrate−function vibrate() { [native code] }", 56 | "storageBuckets−[object StorageBucketManager]", 57 | "mediaCapabilities−[object MediaCapabilities]", 58 | "getGamepads−function getGamepads() { [native code] }", 59 | "bluetooth−[object Bluetooth]", 60 | "share−function share() { [native code] }", 61 | "cookieEnabled−true", 62 | "virtualKeyboard−[object VirtualKeyboard]", 63 | "product−Gecko", 64 | "mediaDevices−[object MediaDevices]", 65 | "canShare−function canShare() { [native code] }", 66 | "getGamepads−function getGamepads() { [native code] }", 67 | "product−Gecko", 68 | "xr−[object XRSystem]", 69 | "clipboard−[object Clipboard]", 70 | "storageBuckets−[object StorageBucketManager]", 71 | "unregisterProtocolHandler−function unregisterProtocolHandler() { [native code] }", 72 | "productSub−20030107", 73 | "login−[object NavigatorLogin]", 74 | "vendorSub−", 75 | "login−[object NavigatorLogin]", 76 | "userAgent−Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0", 77 | "getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }", 78 | "userAgent−Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0", 79 | "mediaDevices−[object MediaDevices]", 80 | "locks−[object LockManager]", 81 | "webkitGetUserMedia−function webkitGetUserMedia() { [native code] }", 82 | "vendor−Google Inc.", 83 | "xr−[object XRSystem]", 84 | "mediaDevices−[object MediaDevices]", 85 | "virtualKeyboard−[object VirtualKeyboard]", 86 | "userAgent−Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0", 87 | "virtualKeyboard−[object VirtualKeyboard]", 88 | "appName−Netscape", 89 | "storageBuckets−[object StorageBucketManager]", 90 | "presentation−[object Presentation]", 91 | "onLine−true", 92 | "mimeTypes−[object MimeTypeArray]", 93 | "credentials−[object CredentialsContainer]", 94 | "presentation−[object Presentation]", 95 | "getGamepads−function getGamepads() { [native code] }", 96 | "vendorSub−", 97 | "virtualKeyboard−[object VirtualKeyboard]", 98 | "serviceWorker−[object ServiceWorkerContainer]", 99 | "xr−[object XRSystem]", 100 | "product−Gecko", 101 | "keyboard−[object Keyboard]", 102 | "gpu−[object GPU]", 103 | "getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }", 104 | "webkitPersistentStorage−[object DeprecatedStorageQuota]", 105 | "doNotTrack", 106 | "clearAppBadge−function clearAppBadge() { [native code] }", 107 | "presentation−[object Presentation]", 108 | "serial−[object Serial]", 109 | "locks−[object LockManager]", 110 | "requestMIDIAccess−function requestMIDIAccess() { [native code] }", 111 | "locks−[object LockManager]", 112 | "requestMediaKeySystemAccess−function requestMediaKeySystemAccess() { [native code] }", 113 | "vendor−Google Inc.", 114 | "pdfViewerEnabled−true", 115 | "language−zh-CN", 116 | "setAppBadge−function setAppBadge() { [native code] }", 117 | "geolocation−[object Geolocation]", 118 | "userAgentData−[object NavigatorUAData]", 119 | "mediaCapabilities−[object MediaCapabilities]", 120 | "requestMIDIAccess−function requestMIDIAccess() { [native code] }", 121 | "getUserMedia−function getUserMedia() { [native code] }", 122 | "mediaDevices−[object MediaDevices]", 123 | "webkitPersistentStorage−[object DeprecatedStorageQuota]", 124 | "userAgent−Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0", 125 | "sendBeacon−function sendBeacon() { [native code] }", 126 | "hardwareConcurrency−32", 127 | "appVersion−5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0", 128 | "credentials−[object CredentialsContainer]", 129 | "storage−[object StorageManager]", 130 | "cookieEnabled−true", 131 | "pdfViewerEnabled−true", 132 | "windowControlsOverlay−[object WindowControlsOverlay]", 133 | "scheduling−[object Scheduling]", 134 | "pdfViewerEnabled−true", 135 | "hardwareConcurrency−32", 136 | "xr−[object XRSystem]", 137 | "userAgent−Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0", 138 | "webdriver−false", 139 | "getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }", 140 | "getInstalledRelatedApps−function getInstalledRelatedApps() { [native code] }", 141 | "bluetooth−[object Bluetooth]"} 142 | documentKeys = []string{"_reactListeningo743lnnpvdg", "location"} 143 | windowKeys = []string{ 144 | "0", 145 | "window", 146 | "self", 147 | "document", 148 | "name", 149 | "location", 150 | "customElements", 151 | "history", 152 | "navigation", 153 | "locationbar", 154 | "menubar", 155 | "personalbar", 156 | "scrollbars", 157 | "statusbar", 158 | "toolbar", 159 | "status", 160 | "closed", 161 | "frames", 162 | "length", 163 | "top", 164 | "opener", 165 | "parent", 166 | "frameElement", 167 | "navigator", 168 | "origin", 169 | "external", 170 | "screen", 171 | "innerWidth", 172 | "innerHeight", 173 | "scrollX", 174 | "pageXOffset", 175 | "scrollY", 176 | "pageYOffset", 177 | "visualViewport", 178 | "screenX", 179 | "screenY", 180 | "outerWidth", 181 | "outerHeight", 182 | "devicePixelRatio", 183 | "clientInformation", 184 | "screenLeft", 185 | "screenTop", 186 | "styleMedia", 187 | "onsearch", 188 | "isSecureContext", 189 | "trustedTypes", 190 | "performance", 191 | "onappinstalled", 192 | "onbeforeinstallprompt", 193 | "crypto", 194 | "indexedDB", 195 | "sessionStorage", 196 | "localStorage", 197 | "onbeforexrselect", 198 | "onabort", 199 | "onbeforeinput", 200 | "onbeforematch", 201 | "onbeforetoggle", 202 | "onblur", 203 | "oncancel", 204 | "oncanplay", 205 | "oncanplaythrough", 206 | "onchange", 207 | "onclick", 208 | "onclose", 209 | "oncontentvisibilityautostatechange", 210 | "oncontextlost", 211 | "oncontextmenu", 212 | "oncontextrestored", 213 | "oncuechange", 214 | "ondblclick", 215 | "ondrag", 216 | "ondragend", 217 | "ondragenter", 218 | "ondragleave", 219 | "ondragover", 220 | "ondragstart", 221 | "ondrop", 222 | "ondurationchange", 223 | "onemptied", 224 | "onended", 225 | "onerror", 226 | "onfocus", 227 | "onformdata", 228 | "oninput", 229 | "oninvalid", 230 | "onkeydown", 231 | "onkeypress", 232 | "onkeyup", 233 | "onload", 234 | "onloadeddata", 235 | "onloadedmetadata", 236 | "onloadstart", 237 | "onmousedown", 238 | "onmouseenter", 239 | "onmouseleave", 240 | "onmousemove", 241 | "onmouseout", 242 | "onmouseover", 243 | "onmouseup", 244 | "onmousewheel", 245 | "onpause", 246 | "onplay", 247 | "onplaying", 248 | "onprogress", 249 | "onratechange", 250 | "onreset", 251 | "onresize", 252 | "onscroll", 253 | "onsecuritypolicyviolation", 254 | "onseeked", 255 | "onseeking", 256 | "onselect", 257 | "onslotchange", 258 | "onstalled", 259 | "onsubmit", 260 | "onsuspend", 261 | "ontimeupdate", 262 | "ontoggle", 263 | "onvolumechange", 264 | "onwaiting", 265 | "onwebkitanimationend", 266 | "onwebkitanimationiteration", 267 | "onwebkitanimationstart", 268 | "onwebkittransitionend", 269 | "onwheel", 270 | "onauxclick", 271 | "ongotpointercapture", 272 | "onlostpointercapture", 273 | "onpointerdown", 274 | "onpointermove", 275 | "onpointerrawupdate", 276 | "onpointerup", 277 | "onpointercancel", 278 | "onpointerover", 279 | "onpointerout", 280 | "onpointerenter", 281 | "onpointerleave", 282 | "onselectstart", 283 | "onselectionchange", 284 | "onanimationend", 285 | "onanimationiteration", 286 | "onanimationstart", 287 | "ontransitionrun", 288 | "ontransitionstart", 289 | "ontransitionend", 290 | "ontransitioncancel", 291 | "onafterprint", 292 | "onbeforeprint", 293 | "onbeforeunload", 294 | "onhashchange", 295 | "onlanguagechange", 296 | "onmessage", 297 | "onmessageerror", 298 | "onoffline", 299 | "ononline", 300 | "onpagehide", 301 | "onpageshow", 302 | "onpopstate", 303 | "onrejectionhandled", 304 | "onstorage", 305 | "onunhandledrejection", 306 | "onunload", 307 | "crossOriginIsolated", 308 | "scheduler", 309 | "alert", 310 | "atob", 311 | "blur", 312 | "btoa", 313 | "cancelAnimationFrame", 314 | "cancelIdleCallback", 315 | "captureEvents", 316 | "clearInterval", 317 | "clearTimeout", 318 | "close", 319 | "confirm", 320 | "createImageBitmap", 321 | "fetch", 322 | "find", 323 | "focus", 324 | "getComputedStyle", 325 | "getSelection", 326 | "matchMedia", 327 | "moveBy", 328 | "moveTo", 329 | "open", 330 | "postMessage", 331 | "print", 332 | "prompt", 333 | "queueMicrotask", 334 | "releaseEvents", 335 | "reportError", 336 | "requestAnimationFrame", 337 | "requestIdleCallback", 338 | "resizeBy", 339 | "resizeTo", 340 | "scroll", 341 | "scrollBy", 342 | "scrollTo", 343 | "setInterval", 344 | "setTimeout", 345 | "stop", 346 | "structuredClone", 347 | "webkitCancelAnimationFrame", 348 | "webkitRequestAnimationFrame", 349 | "chrome", 350 | "caches", 351 | "cookieStore", 352 | "ondevicemotion", 353 | "ondeviceorientation", 354 | "ondeviceorientationabsolute", 355 | "launchQueue", 356 | "documentPictureInPicture", 357 | "getScreenDetails", 358 | "queryLocalFonts", 359 | "showDirectoryPicker", 360 | "showOpenFilePicker", 361 | "showSaveFilePicker", 362 | "originAgentCluster", 363 | "onpageswap", 364 | "onpagereveal", 365 | "credentialless", 366 | "speechSynthesis", 367 | "onscrollend", 368 | "webkitRequestFileSystem", 369 | "webkitResolveLocalFileSystemURL", 370 | "sendMsgToSolverCS", 371 | "webpackChunk_N_E", 372 | "__next_set_public_path__", 373 | "next", 374 | "__NEXT_DATA__", 375 | "__SSG_MANIFEST_CB", 376 | "__NEXT_P", 377 | "_N_E", 378 | "regeneratorRuntime", 379 | "__REACT_INTL_CONTEXT__", 380 | "DD_RUM", 381 | "_", 382 | "filterCSS", 383 | "filterXSS", 384 | "__SEGMENT_INSPECTOR__", 385 | "__NEXT_PRELOADREADY", 386 | "Intercom", 387 | "__MIDDLEWARE_MATCHERS", 388 | "__STATSIG_SDK__", 389 | "__STATSIG_JS_SDK__", 390 | "__STATSIG_RERENDER_OVERRIDE__", 391 | "_oaiHandleSessionExpired", 392 | "__BUILD_MANIFEST", 393 | "__SSG_MANIFEST", 394 | "__intercomAssignLocation", 395 | "__intercomReloadLocation"} 396 | ) 397 | 398 | func init() { 399 | autoContinue = os.Getenv("AUTO_CONTINUE") == "true" 400 | cores := []int{8, 12, 16, 24} 401 | screens := []int{3000, 4000, 6000} 402 | rand.New(rand.NewSource(time.Now().UnixNano())) 403 | core := cores[rand.Intn(4)] 404 | rand.New(rand.NewSource(time.Now().UnixNano())) 405 | screen := screens[rand.Intn(3)] 406 | cachedHardware = core + screen 407 | envHardware := os.Getenv("HARDWARE") 408 | if envHardware != "" { 409 | intValue, err := strconv.Atoi(envHardware) 410 | if err != nil { 411 | logger.Error(fmt.Sprintf("Error converting %s to integer: %v", envHardware, err)) 412 | } else { 413 | cachedHardware = intValue 414 | logger.Info(fmt.Sprintf("cachedHardware is set to : %d", cachedHardware)) 415 | } 416 | } 417 | envPowRetryTimes := os.Getenv("POW_RETRY_TIMES") 418 | if envPowRetryTimes != "" { 419 | intValue, err := strconv.Atoi(envPowRetryTimes) 420 | if err != nil { 421 | logger.Error(fmt.Sprintf("Error converting %s to integer: %v", envPowRetryTimes, err)) 422 | } else { 423 | PowRetryTimes = intValue 424 | logger.Info(fmt.Sprintf("PowRetryTimes is set to : %d", PowRetryTimes)) 425 | } 426 | } 427 | envpowMaxDifficulty := os.Getenv("POW_MAX_DIFFICULTY") 428 | if envpowMaxDifficulty != "" { 429 | PowMaxDifficulty = envpowMaxDifficulty 430 | logger.Info(fmt.Sprintf("PowMaxDifficulty is set to : %s", PowMaxDifficulty)) 431 | } 432 | envPowMaxCalcTimes := os.Getenv("POW_MAX_CALC_TIMES") 433 | if envPowMaxCalcTimes != "" { 434 | intValue, err := strconv.Atoi(envPowMaxCalcTimes) 435 | if err != nil { 436 | logger.Error(fmt.Sprintf("Error converting %s to integer: %v", envPowMaxCalcTimes, err)) 437 | } else { 438 | powMaxCalcTimes = intValue 439 | logger.Info(fmt.Sprintf("PowMaxCalcTimes is set to : %d", powMaxCalcTimes)) 440 | } 441 | } 442 | } 443 | 444 | func CreateConversation(c *gin.Context) { 445 | var request CreateConversationRequest 446 | 447 | if err := c.BindJSON(&request); err != nil { 448 | c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(parseJsonErrorMessage)) 449 | return 450 | } 451 | 452 | if len(request.Messages) != 0 { 453 | message := request.Messages[0] 454 | if message.Author.Role == "" { 455 | message.Author.Role = defaultRole 456 | } 457 | 458 | if message.Metadata == nil { 459 | message.Metadata = map[string]string{} 460 | } 461 | 462 | request.Messages[0] = message 463 | } 464 | 465 | // get accessToken 466 | authHeader := c.GetHeader(api.AuthorizationHeader) 467 | if strings.HasPrefix(authHeader, "Bearer") { 468 | authHeader = strings.Replace(authHeader, "Bearer ", "", 1) 469 | } 470 | chat_require, p := CheckRequire(authHeader, api.OAIDID) 471 | if chat_require == nil { 472 | logger.Error("unable to check chat requirement") 473 | return 474 | } 475 | for i := 0; i < PowRetryTimes; i++ { 476 | if chat_require.Proof.Required && chat_require.Proof.Difficulty <= PowMaxDifficulty { 477 | logger.Warn(fmt.Sprintf("Proof of work difficulty too high: %s. Retrying... %d/%d ", chat_require.Proof.Difficulty, i + 1, PowRetryTimes)) 478 | chat_require, _ = CheckRequire(authHeader, api.OAIDID) 479 | if chat_require == nil { 480 | logger.Error("unable to check chat requirement") 481 | return 482 | } 483 | } else { 484 | break 485 | } 486 | } 487 | 488 | var arkoseToken string 489 | arkoseToken = c.GetHeader(api.ArkoseTokenHeader) 490 | if chat_require.Arkose.Required == true && arkoseToken == "" { 491 | token, err := GetArkoseTokenForModel(request.Model, chat_require.Arkose.DX) 492 | arkoseToken = token 493 | if err != nil || arkoseToken == "" { 494 | c.AbortWithStatusJSON(http.StatusForbidden, api.ReturnMessage(err.Error())) 495 | return 496 | } 497 | } 498 | 499 | var proofToken string 500 | if chat_require.Proof.Required { 501 | proofToken = CalcProofToken(chat_require) 502 | } 503 | 504 | var turnstileToken string 505 | if chat_require.Turnstile.Required { 506 | turnstileToken = ProcessTurnstile(chat_require.Turnstile.DX, p) 507 | } 508 | 509 | // TEST: force to use SSE 510 | request.ForceUseSse = true 511 | 512 | resp, done := sendConversationRequest(c, request, authHeader, api.OAIDID, arkoseToken, chat_require.Token, proofToken, turnstileToken) 513 | if done { 514 | return 515 | } 516 | 517 | handleConversationResponse(c, resp, request, authHeader, api.OAIDID) 518 | } 519 | 520 | func sendConversationRequest(c *gin.Context, request CreateConversationRequest, accessToken string, deviceId string, arkoseToken string, chat_token string, proofToken string, turnstileToken string) (*http.Response, bool) { 521 | apiUrl := api.ChatGPTApiUrlPrefix+"/backend-api/conversation" 522 | jsonBytes, _ := json.Marshal(request) 523 | req, err := NewRequest(http.MethodPost, apiUrl, bytes.NewReader(jsonBytes), accessToken, deviceId) 524 | req.Header.Set("Content-Type", "application/json") 525 | req.Header.Set("Accept", "text/event-stream") 526 | if arkoseToken != "" { 527 | req.Header.Set("Openai-Sentinel-Arkose-Token", arkoseToken) 528 | } 529 | if chat_token != "" { 530 | req.Header.Set("Openai-Sentinel-Chat-Requirements-Token", chat_token) 531 | } 532 | if proofToken != "" { 533 | req.Header.Set("Openai-Sentinel-Proof-Token", proofToken) 534 | } 535 | if turnstileToken != "" { 536 | req.Header.Set("Openai-Sentinel-Turnstile-Token", turnstileToken) 537 | } 538 | req.Header.Set("Origin", api.ChatGPTApiUrlPrefix) 539 | if request.ConversationID != "" { 540 | req.Header.Set("Referer", api.ChatGPTApiUrlPrefix+"/c/"+request.ConversationID) 541 | } else { 542 | req.Header.Set("Referer", api.ChatGPTApiUrlPrefix+"/") 543 | } 544 | resp, err := api.Client.Do(req) 545 | if err != nil { 546 | c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error())) 547 | return nil, true 548 | } 549 | 550 | // 检查状态码是否大于299 551 | if resp.StatusCode > 299 { 552 | 553 | defer resp.Body.Close() 554 | 555 | // 设置响应头 556 | for name, values := range resp.Header { 557 | c.Writer.Header()[name] = values 558 | } 559 | c.Writer.WriteHeader(resp.StatusCode) 560 | 561 | // 直接转发响应体 562 | body, err := io.ReadAll(resp.Body) 563 | if err != nil { 564 | logger.Error(err.Error()) 565 | } 566 | 567 | c.Writer.Write(body) 568 | return nil, true 569 | } 570 | 571 | return resp, false 572 | } 573 | 574 | func handleConversationResponse(c *gin.Context, resp *http.Response, request CreateConversationRequest, accessToken string, deviceId string) { 575 | c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8") 576 | 577 | isMaxTokens := false 578 | continueParentMessageID := "" 579 | continueConversationID := "" 580 | 581 | var arkoseToken string 582 | var proofToken string 583 | var turnstileToken string 584 | 585 | defer resp.Body.Close() 586 | reader := bufio.NewReader(resp.Body) 587 | 588 | for { 589 | if c.Request.Context().Err() != nil { 590 | break 591 | } 592 | 593 | line, err := reader.ReadString('\n') 594 | if err != nil { 595 | break 596 | } 597 | 598 | line = strings.TrimSpace(line) 599 | if strings.HasPrefix(line, "event") || 600 | strings.HasPrefix(line, "data: 20") || 601 | strings.HasPrefix(line, `data: {"conversation_id"`) || 602 | line == "" { 603 | continue 604 | } 605 | 606 | responseJson := line[6:] 607 | if strings.HasPrefix(responseJson, "[DONE]") && isMaxTokens && autoContinue { 608 | continue 609 | } 610 | 611 | // no need to unmarshal every time, but if response content has this "max_tokens", need to further check 612 | if strings.TrimSpace(responseJson) != "" && strings.Contains(responseJson, responseTypeMaxTokens) { 613 | var createConversationResponse CreateConversationResponse 614 | json.Unmarshal([]byte(responseJson), &createConversationResponse) 615 | message := createConversationResponse.Message 616 | if message.Metadata.FinishDetails.Type == responseTypeMaxTokens && createConversationResponse.Message.Status == responseStatusFinishedSuccessfully { 617 | isMaxTokens = true 618 | continueParentMessageID = message.ID 619 | continueConversationID = createConversationResponse.ConversationID 620 | } 621 | } 622 | 623 | c.Writer.Write([]byte(line + "\n\n")) 624 | c.Writer.Flush() 625 | } 626 | 627 | if isMaxTokens && autoContinue { 628 | logger.Info("Continuing conversation") 629 | continueConversationRequest := CreateConversationRequest{ 630 | ConversationMode: request.ConversationMode, 631 | ForceNulligen: request.ForceNulligen, 632 | ForceParagen: request.ForceParagen, 633 | ForceParagenModelSlug: request.ForceParagenModelSlug, 634 | ForceRateLimit: request.ForceRateLimit, 635 | ForceUseSse: request.ForceUseSse, 636 | HistoryAndTrainingDisabled: request.HistoryAndTrainingDisabled, 637 | Model: request.Model, 638 | ResetRateLimits: request.ResetRateLimits, 639 | TimezoneOffsetMin: request.TimezoneOffsetMin, 640 | 641 | Action: actionContinue, 642 | ParentMessageID: continueParentMessageID, 643 | ConversationID: continueConversationID, 644 | WebsocketRequestId: uuid.NewString(), 645 | } 646 | chat_require, p := CheckRequire(accessToken, deviceId) 647 | if chat_require == nil { 648 | logger.Error("unable to check chat requirement") 649 | return 650 | } 651 | for i := 0; i < PowRetryTimes; i++ { 652 | if chat_require.Proof.Required && chat_require.Proof.Difficulty <= PowMaxDifficulty { 653 | logger.Warn(fmt.Sprintf("Proof of work difficulty too high: %s. Retrying... %d/%d ", chat_require.Proof.Difficulty, i + 1, PowRetryTimes)) 654 | chat_require, _ = CheckRequire(accessToken, api.OAIDID) 655 | if chat_require == nil { 656 | logger.Error("unable to check chat requirement") 657 | return 658 | } 659 | } else { 660 | break 661 | } 662 | } 663 | if chat_require.Proof.Required { 664 | proofToken = CalcProofToken(chat_require) 665 | } 666 | if chat_require.Arkose.Required { 667 | token, err := GetArkoseTokenForModel(continueConversationRequest.Model, chat_require.Arkose.DX) 668 | arkoseToken = token 669 | if err != nil || arkoseToken == "" { 670 | c.AbortWithStatusJSON(http.StatusForbidden, api.ReturnMessage(err.Error())) 671 | return 672 | } 673 | } 674 | if chat_require.Turnstile.Required { 675 | turnstileToken = ProcessTurnstile(chat_require.Turnstile.DX, p) 676 | } 677 | resp, done := sendConversationRequest(c, continueConversationRequest, accessToken, deviceId, arkoseToken, chat_require.Token, proofToken, turnstileToken) 678 | if done { 679 | return 680 | } 681 | 682 | handleConversationResponse(c, resp, continueConversationRequest, accessToken, deviceId) 683 | } 684 | } 685 | 686 | func NewRequest(method string, url string, body io.Reader, token string, deviceId string) (*http.Request, error) { 687 | request, err := http.NewRequest(method, url, body) 688 | if err != nil { 689 | return &http.Request{}, err 690 | } 691 | request.Header.Set("User-Agent", api.UserAgent) 692 | request.Header.Set("Accept", "*/*") 693 | if deviceId != "" { 694 | request.Header.Set("Cookie", request.Header.Get("Cookie")+"oai-did="+deviceId+";") 695 | request.Header.Set("Oai-Device-Id", deviceId) 696 | } 697 | request.Header.Set("Oai-Language", api.Language) 698 | if token != "" { 699 | request.Header.Set("Authorization", "Bearer "+token) 700 | } 701 | if api.PUID != "" { 702 | request.Header.Set("Cookie", "_puid="+api.PUID+";") 703 | } 704 | // if secret.TeamUserID != "" { 705 | // request.Header.Set("Chatgpt-Account-Id", secret.TeamUserID) 706 | // } 707 | return request, nil 708 | } 709 | 710 | func CheckRequire(access_token string, deviceId string) (*ChatRequire, string) { 711 | if cachedRequireProof == "" { 712 | cachedRequireProof = "gAAAAAC" + generateAnswer(strconv.FormatFloat(rand.Float64(), 'f', -1, 64), "0") 713 | } 714 | body := bytes.NewBuffer([]byte(`{"p":"` + cachedRequireProof + `"}`)) 715 | var apiUrl string 716 | apiUrl = api.ChatGPTApiUrlPrefix+"/backend-api/sentinel/chat-requirements" 717 | request, err := NewRequest(http.MethodPost, apiUrl, body, access_token, deviceId) 718 | if err != nil { 719 | return nil, "" 720 | } 721 | request.Header.Set("Content-Type", "application/json") 722 | request.Header.Set("Origin", api.ChatGPTApiUrlPrefix) 723 | request.Header.Set("Referer", api.ChatGPTApiUrlPrefix+"/") 724 | response, err := api.Client.Do(request) 725 | if err != nil { 726 | return nil, "" 727 | } 728 | defer response.Body.Close() 729 | var require ChatRequire 730 | err = json.NewDecoder(response.Body).Decode(&require) 731 | if err != nil { 732 | logger.Error(err.Error()) 733 | return nil, "" 734 | } 735 | return &require, cachedRequireProof 736 | } 737 | 738 | type ProofWork struct { 739 | Difficulty string `json:"difficulty,omitempty"` 740 | Required bool `json:"required"` 741 | Seed string `json:"seed,omitempty"` 742 | } 743 | 744 | func getParseTime() string { 745 | now := time.Now() 746 | now = now.In(timeLocation) 747 | return now.Format(timeLayout) + " GMT+0800 (中国标准时间)" 748 | } 749 | func GetDpl() { 750 | if len(cachedScripts) > 0 { 751 | return 752 | } 753 | cachedScripts = append(cachedScripts, "https://cdn.oaistatic.com/_next/static/cXh69klOLzS0Gy2joLDRS/_ssgManifest.js?dpl=453ebaec0d44c2decab71692e1bfe39be35a24b3") 754 | cachedDpl = "dpl=453ebaec0d44c2decab71692e1bfe39be35a24b3" 755 | request, err := http.NewRequest(http.MethodGet, "https://chatgpt.com", nil) 756 | request.Header.Set("User-Agent", api.UserAgent) 757 | request.Header.Set("Accept", "*/*") 758 | if err != nil { 759 | return 760 | } 761 | response, err := api.Client.Do(request) 762 | if err != nil { 763 | return 764 | } 765 | defer response.Body.Close() 766 | doc, _ := goquery.NewDocumentFromReader(response.Body) 767 | scripts := []string{} 768 | doc.Find("script[src]").Each(func(i int, s *goquery.Selection) { 769 | src, exists := s.Attr("src") 770 | if exists { 771 | scripts = append(scripts, src) 772 | if cachedDpl == "" { 773 | idx := strings.Index(src, "dpl") 774 | if idx >= 0 { 775 | cachedDpl = src[idx:] 776 | } 777 | } 778 | } 779 | }) 780 | if len(scripts) != 0 { 781 | cachedScripts = scripts 782 | } 783 | } 784 | func getConfig() []interface{} { 785 | rand.New(rand.NewSource(time.Now().UnixNano())) 786 | script := cachedScripts[rand.Intn(len(cachedScripts))] 787 | timeNum := (float64(time.Since(api.StartTime).Nanoseconds()) + rand.Float64()) / 1e6 788 | rand.New(rand.NewSource(time.Now().UnixNano())) 789 | navigatorKey := navigatorKeys[rand.Intn(len(navigatorKeys))] 790 | rand.New(rand.NewSource(time.Now().UnixNano())) 791 | documentKey := documentKeys[rand.Intn(len(documentKeys))] 792 | rand.New(rand.NewSource(time.Now().UnixNano())) 793 | windowKey := windowKeys[rand.Intn(len(windowKeys))] 794 | return []interface{}{cachedHardware, getParseTime(), int64(4294705152), 0, api.UserAgent, script, cachedDpl, api.Language, api.Language+","+api.Language[:2], 0, navigatorKey, documentKey, windowKey, timeNum, cachedSid} 795 | } 796 | 797 | func CalcProofToken(require *ChatRequire) string { 798 | start := time.Now() 799 | proof := generateAnswer(require.Proof.Seed, require.Proof.Difficulty) 800 | elapsed := time.Since(start) 801 | // POW logging 802 | logger.Info(fmt.Sprintf("POW Difficulty: %s , took %v ms", require.Proof.Difficulty, elapsed.Milliseconds())) 803 | return "gAAAAAB" + proof 804 | } 805 | 806 | func generateAnswer(seed string, diff string) string { 807 | GetDpl() 808 | config := getConfig() 809 | diffLen := len(diff) 810 | hasher := sha3.New512() 811 | for i := 0; i < powMaxCalcTimes; i++ { 812 | config[3] = i 813 | config[9] = (i + 2) / 2 814 | json, _ := json.Marshal(config) 815 | base := base64.StdEncoding.EncodeToString(json) 816 | hasher.Write([]byte(seed + base)) 817 | hash := hasher.Sum(nil) 818 | hasher.Reset() 819 | if hex.EncodeToString(hash[:diffLen])[:diffLen] <= diff { 820 | return base 821 | } 822 | } 823 | return "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + base64.StdEncoding.EncodeToString([]byte(`"`+seed+`"`)) 824 | } 825 | 826 | func GetArkoseTokenForModel(model string, dx string) (string, error) { 827 | var api_version int 828 | if strings.HasPrefix(model, "gpt-4") { 829 | api_version = 4 830 | } else { 831 | api_version = 3 832 | } 833 | return api.GetArkoseToken(api_version, dx) 834 | } -------------------------------------------------------------------------------- /api/chatgpt/constant.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | const ( 4 | defaultRole = "user" 5 | parseJsonErrorMessage = "failed to parse json request body" 6 | 7 | gpt4Model = "gpt-4" 8 | actionContinue = "continue" 9 | responseTypeMaxTokens = "max_tokens" 10 | responseStatusFinishedSuccessfully = "finished_successfully" 11 | noModelPermissionErrorMessage = "you have no permission to use this model" 12 | ) 13 | -------------------------------------------------------------------------------- /api/chatgpt/health_check.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/PuerkitoBio/goquery" 9 | http "github.com/bogdanfinn/fhttp" 10 | 11 | "github.com/maxduke/go-chatgpt-api/api" 12 | "github.com/linweiyuan/go-logger/logger" 13 | ) 14 | 15 | const ( 16 | healthCheckUrl = api.ChatGPTApiUrlPrefix+"/backend-api/accounts/check" 17 | infoHCnotEnable = "Health Check is disabled" 18 | errorHintBlock = "looks like you have bean blocked by OpenAI, please change to a new IP or have a try with WARP" 19 | errorHintFailedToStart = "failed to start, please try again later: %s" 20 | sleepHours = 8760 // 365 days 21 | ) 22 | 23 | func init() { 24 | proxyUrl := os.Getenv("PROXY") 25 | if proxyUrl != "" { 26 | logger.Info("PROXY: " + proxyUrl) 27 | api.Client.SetProxy(proxyUrl) 28 | } 29 | 30 | enableHC := os.Getenv("ENABLE_HEALTHCHECK") 31 | if enableHC == "" { 32 | logger.Info(infoHCnotEnable) 33 | return 34 | } 35 | 36 | if proxyUrl != "" { 37 | for { 38 | resp, err := healthCheck() 39 | if err != nil { 40 | // wait for proxy to be ready 41 | time.Sleep(time.Second) 42 | continue 43 | } 44 | 45 | checkHealthCheckStatus(resp) 46 | break 47 | } 48 | } else { 49 | resp, err := healthCheck() 50 | if err != nil { 51 | logger.Error("failed to health check: " + err.Error()) 52 | os.Exit(1) 53 | } 54 | 55 | checkHealthCheckStatus(resp) 56 | } 57 | } 58 | 59 | func healthCheck() (resp *http.Response, err error) { 60 | req, _ := http.NewRequest(http.MethodGet, healthCheckUrl, nil) 61 | req.Header.Set("User-Agent", api.DefaultUserAgent) 62 | resp, err = api.Client.Do(req) 63 | return 64 | } 65 | 66 | func checkHealthCheckStatus(resp *http.Response) { 67 | if resp != nil { 68 | defer resp.Body.Close() 69 | 70 | if resp.StatusCode == http.StatusUnauthorized { 71 | logger.Info(api.ReadyHint) 72 | } else { 73 | doc, _ := goquery.NewDocumentFromReader(resp.Body) 74 | alert := doc.Find(".message").Text() 75 | if alert != "" { 76 | logger.Error(errorHintBlock) 77 | } else { 78 | logger.Error(fmt.Sprintf(errorHintFailedToStart, resp.Status)) 79 | } 80 | time.Sleep(time.Hour * sleepHours) 81 | os.Exit(1) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /api/chatgpt/login.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | http "github.com/bogdanfinn/fhttp" 5 | "github.com/gin-gonic/gin" 6 | "github.com/xqdoo00o/OpenAIAuth/auth" 7 | 8 | "github.com/maxduke/go-chatgpt-api/api" 9 | ) 10 | 11 | func Login(c *gin.Context) { 12 | var loginInfo api.LoginInfo 13 | if err := c.ShouldBindJSON(&loginInfo); err != nil { 14 | c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(api.ParseUserInfoErrorMessage)) 15 | return 16 | } 17 | 18 | authenticator := auth.NewAuthenticator(loginInfo.Username, loginInfo.Password, api.ProxyUrl) 19 | if err := authenticator.Begin(); err != nil { 20 | c.AbortWithStatusJSON(err.StatusCode, api.ReturnMessage(err.Details)) 21 | return 22 | } 23 | 24 | c.JSON(http.StatusOK, gin.H{ 25 | "accessToken": authenticator.GetAccessToken(), 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /api/chatgpt/turnstile.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "math/rand" 9 | "os" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/maxduke/go-chatgpt-api/api" 16 | ) 17 | 18 | type OrderedMap struct { 19 | Keys []string 20 | Values map[string]interface{} 21 | } 22 | 23 | func NewOrderedMap() *OrderedMap { 24 | return &OrderedMap{ 25 | Values: make(map[string]interface{}), 26 | } 27 | } 28 | 29 | func (o *OrderedMap) Add(key string, value interface{}) { 30 | if _, exists := o.Values[key]; !exists { 31 | o.Keys = append(o.Keys, key) 32 | } 33 | o.Values[key] = value 34 | } 35 | 36 | func (o *OrderedMap) MarshalJSON() ([]byte, error) { 37 | buffer := bytes.NewBufferString("{") 38 | length := len(o.Keys) 39 | for i, key := range o.Keys { 40 | jsonValue, err := json.Marshal(o.Values[key]) 41 | if err != nil { 42 | return nil, err 43 | } 44 | buffer.WriteString(fmt.Sprintf("\"%s\":%s", key, jsonValue)) 45 | if i < length-1 { 46 | buffer.WriteString(",") 47 | } 48 | } 49 | buffer.WriteString("}") 50 | return buffer.Bytes(), nil 51 | } 52 | 53 | type turnTokenList [][]any 54 | 55 | func getTurnstileToken(dx, p string) (string, error) { 56 | decodedBytes, err := base64.StdEncoding.DecodeString(dx) 57 | if err != nil { 58 | return "", err 59 | } 60 | return porcessTurnstileToken(string(decodedBytes), p) 61 | } 62 | func porcessTurnstileToken(dx, p string) (string, error) { 63 | var result []rune 64 | dxRunes := []rune(dx) 65 | pRunes := []rune(p) 66 | pLength := len(pRunes) 67 | if pLength != 0 { 68 | for i, r := range dxRunes { 69 | result = append(result, r^pRunes[i%pLength]) 70 | } 71 | } else { 72 | result = dxRunes 73 | } 74 | return string(result), nil 75 | } 76 | 77 | type FuncType func(args ...any) any 78 | 79 | type FloatMap map[float64]any 80 | 81 | type StringMap map[string]any 82 | 83 | func isSlice(input any) bool { 84 | return reflect.TypeOf(input).Kind() == reflect.Slice 85 | } 86 | func isFloat64(input any) bool { 87 | _, ok := input.(float64) 88 | return ok 89 | } 90 | func isString(input any) bool { 91 | _, ok := input.(string) 92 | return ok 93 | } 94 | func toStr(input any) string { 95 | var output string 96 | if input == nil { 97 | output = "undefined" 98 | } else if isFloat64(input) { 99 | output = strconv.FormatFloat(input.(float64), 'f', -1, 64) 100 | } else if isString(input) { 101 | inputStr := input.(string) 102 | if inputStr == "window.Math" { 103 | output = "[object Math]" 104 | } else if inputStr == "window.Reflect" { 105 | output = "[object Reflect]" 106 | } else if inputStr == "window.performance" { 107 | output = "[object Performance]" 108 | } else if inputStr == "window.localStorage" { 109 | output = "[object Storage]" 110 | } else if inputStr == "window.Object" { 111 | output = "function Object() { [native code] }" 112 | } else if inputStr == "window.Reflect.set" { 113 | output = "function set() { [native code] }" 114 | } else if inputStr == "window.performance.now" { 115 | output = "function () { [native code] }" 116 | } else if inputStr == "window.Object.create" { 117 | output = "function create() { [native code] }" 118 | } else if inputStr == "window.Object.keys" { 119 | output = "function keys() { [native code] }" 120 | } else if inputStr == "window.Math.random" { 121 | output = "function random() { [native code] }" 122 | } else { 123 | output = inputStr 124 | } 125 | } else if inputArray, ok := input.([]string); ok { 126 | output = strings.Join(inputArray, ",") 127 | } else { 128 | fmt.Printf("Type of input is: %s\n", reflect.TypeOf(input)) 129 | } 130 | return output 131 | } 132 | 133 | func getFuncMap() FloatMap { 134 | var processMap FloatMap = FloatMap{} 135 | processMap[1] = FuncType(func(args ...any) any { 136 | e := args[0].(float64) 137 | t := args[1].(float64) 138 | estr := toStr(processMap[e]) 139 | tstr := toStr(processMap[t]) 140 | res, err := porcessTurnstileToken(estr, tstr) 141 | if err != nil { 142 | fmt.Println(err) 143 | } 144 | processMap[e] = res 145 | return nil 146 | }) 147 | processMap[2] = FuncType(func(args ...any) any { 148 | e := args[0].(float64) 149 | t := args[1] 150 | processMap[e] = t 151 | return nil 152 | }) 153 | processMap[5] = FuncType(func(args ...any) any { 154 | e := args[0].(float64) 155 | t := args[1].(float64) 156 | n := processMap[e] 157 | tres := processMap[t] 158 | if isSlice(n) { 159 | nt := n.([]any) 160 | nt = append(nt, tres) 161 | processMap[e] = nt 162 | } else { 163 | var res any 164 | if isString(n) || isString(tres) { 165 | nstr := toStr(n) 166 | tstr := toStr(tres) 167 | res = nstr + tstr 168 | } else if isFloat64(n) && isFloat64(tres) { 169 | nnum := n.(float64) 170 | tnum := tres.(float64) 171 | res = nnum + tnum 172 | } else { 173 | res = "NaN" 174 | } 175 | processMap[e] = res 176 | } 177 | return nil 178 | }) 179 | processMap[6] = FuncType(func(args ...any) any { 180 | e := args[0].(float64) 181 | t := args[1].(float64) 182 | n := args[2].(float64) 183 | tv := processMap[t] 184 | nv := processMap[n] 185 | if isString(tv) && isString(nv) { 186 | tstr := tv.(string) 187 | nstr := nv.(string) 188 | res := tstr + "." + nstr 189 | if res == "window.document.location" { 190 | processMap[e] = "https://chatgpt.com/" 191 | } else { 192 | processMap[e] = res 193 | } 194 | } else { 195 | fmt.Println("func type 6 error") 196 | } 197 | return nil 198 | }) 199 | processMap[24] = FuncType(func(args ...any) any { 200 | e := args[0].(float64) 201 | t := args[1].(float64) 202 | n := args[2].(float64) 203 | tv := processMap[t] 204 | nv := processMap[n] 205 | if isString(tv) && isString(nv) { 206 | tstr := tv.(string) 207 | nstr := nv.(string) 208 | processMap[e] = tstr + "." + nstr 209 | } else { 210 | fmt.Println("func type 24 error") 211 | } 212 | return nil 213 | }) 214 | processMap[7] = FuncType(func(args ...any) any { 215 | e := args[0].(float64) 216 | t := len(args[1:]) 217 | n := []any{} 218 | for i := range t { 219 | v := args[i+1].(float64) 220 | vv := processMap[v] 221 | n = append(n, vv) 222 | } 223 | ev := processMap[e] 224 | switch ev := ev.(type) { 225 | case string: 226 | if ev == "window.Reflect.set" { 227 | object := n[0].(*OrderedMap) 228 | keyStr := strconv.FormatFloat(n[1].(float64), 'f', -1, 64) 229 | val := n[2] 230 | object.Add(keyStr, val) 231 | } 232 | case FuncType: 233 | ev(n...) 234 | } 235 | return nil 236 | }) 237 | processMap[17] = FuncType(func(args ...any) any { 238 | e := args[0].(float64) 239 | t := args[1].(float64) 240 | n := len(args[2:]) 241 | i := []any{} 242 | for idx := range n { 243 | v := args[idx+2].(float64) 244 | vv := processMap[v] 245 | i = append(i, vv) 246 | } 247 | tv := processMap[t] 248 | var res any 249 | switch tv := tv.(type) { 250 | case string: 251 | if tv == "window.performance.now" { 252 | res = (float64(time.Since(api.StartTime).Nanoseconds()) + rand.Float64()) / 1e6 253 | } else if tv == "window.Object.create" { 254 | res = NewOrderedMap() 255 | } else if tv == "window.Object.keys" { 256 | if input, ok := i[0].(string); ok { 257 | if input == "window.localStorage" { 258 | res = []string{"STATSIG_LOCAL_STORAGE_INTERNAL_STORE_V4", "STATSIG_LOCAL_STORAGE_STABLE_ID", "client-correlated-secret", "oai/apps/capExpiresAt", "oai-did", "STATSIG_LOCAL_STORAGE_LOGGING_REQUEST", "UiState.isNavigationCollapsed.1"} 259 | } 260 | } 261 | } else if tv == "window.Math.random" { 262 | rand.NewSource(time.Now().UnixNano()) 263 | res = rand.Float64() 264 | } 265 | case FuncType: 266 | res = tv(i...) 267 | } 268 | processMap[e] = res 269 | return nil 270 | }) 271 | processMap[8] = FuncType(func(args ...any) any { 272 | e := args[0].(float64) 273 | t := args[1].(float64) 274 | tv := processMap[t] 275 | processMap[e] = tv 276 | return nil 277 | }) 278 | processMap[10] = "window" 279 | processMap[14] = FuncType(func(args ...any) any { 280 | e := args[0].(float64) 281 | t := args[1].(float64) 282 | tv := processMap[t] 283 | if isString(tv) { 284 | var tokenList turnTokenList 285 | err := json.Unmarshal([]byte(tv.(string)), &tokenList) 286 | if err != nil { 287 | fmt.Println(err) 288 | } 289 | processMap[e] = tokenList 290 | } else { 291 | fmt.Println("func type 14 error") 292 | } 293 | return nil 294 | }) 295 | processMap[15] = FuncType(func(args ...any) any { 296 | e := args[0].(float64) 297 | t := args[1].(float64) 298 | tv := processMap[t] 299 | tres, err := json.Marshal(tv) 300 | if err != nil { 301 | fmt.Println(err) 302 | } 303 | processMap[e] = string(tres) 304 | return nil 305 | }) 306 | processMap[18] = FuncType(func(args ...any) any { 307 | e := args[0].(float64) 308 | ev := processMap[e] 309 | estr := toStr(ev) 310 | decoded, err := base64.StdEncoding.DecodeString(estr) 311 | if err != nil { 312 | fmt.Println(err) 313 | } 314 | processMap[e] = string(decoded) 315 | return nil 316 | }) 317 | processMap[19] = FuncType(func(args ...any) any { 318 | e := args[0].(float64) 319 | ev := processMap[e] 320 | estr := toStr(ev) 321 | encoded := base64.StdEncoding.EncodeToString([]byte(estr)) 322 | processMap[e] = encoded 323 | return nil 324 | }) 325 | processMap[20] = FuncType(func(args ...any) any { 326 | e := args[0].(float64) 327 | t := args[1].(float64) 328 | n := args[2].(float64) 329 | i := len(args[3:]) 330 | o := []any{} 331 | for idx := range i { 332 | v := args[idx+3].(float64) 333 | vv := processMap[v] 334 | o = append(o, vv) 335 | } 336 | ev := processMap[e] 337 | tv := processMap[t] 338 | if ev == tv { 339 | nv := processMap[n] 340 | switch nv := nv.(type) { 341 | case FuncType: 342 | nv(o...) 343 | default: 344 | fmt.Println("func type 20 error") 345 | } 346 | } 347 | return nil 348 | }) 349 | processMap[21] = FuncType(func(args ...any) any { 350 | return nil 351 | }) 352 | processMap[23] = FuncType(func(args ...any) any { 353 | e := args[0].(float64) 354 | t := args[1].(float64) 355 | n := len(args[2:]) 356 | i := []any{} 357 | for idx := range n { 358 | v := args[idx+2].(float64) 359 | i = append(i, v) 360 | } 361 | ev := processMap[e] 362 | tv := processMap[t] 363 | if ev != nil { 364 | switch tv := tv.(type) { 365 | case FuncType: 366 | tv(i...) 367 | } 368 | } 369 | return nil 370 | }) 371 | return processMap 372 | } 373 | 374 | func ProcessTurnstile(dx, p string) string { 375 | // current don't process turnstile 376 | if os.Getenv("PROCESS_TURNSTILE") != "true" { 377 | return "" 378 | } 379 | tokens, _ := getTurnstileToken(dx, p) 380 | var tokenList turnTokenList 381 | err := json.Unmarshal([]byte(tokens), &tokenList) 382 | if err != nil { 383 | fmt.Println(err) 384 | } 385 | var res string 386 | processMap := getFuncMap() 387 | processMap[3] = FuncType(func(args ...any) any { 388 | e := args[0].(string) 389 | res = base64.StdEncoding.EncodeToString([]byte(e)) 390 | return nil 391 | }) 392 | processMap[9] = tokenList 393 | processMap[16] = p 394 | for len(processMap[9].(turnTokenList)) > 0 { 395 | list := processMap[9].(turnTokenList) 396 | token := list[0] 397 | processMap[9] = list[1:] 398 | e := token[0].(float64) 399 | t := token[1:] 400 | f := processMap[e].(FuncType) 401 | f(t...) 402 | } 403 | return res 404 | } -------------------------------------------------------------------------------- /api/chatgpt/typings.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type CreateConversationRequest struct { 8 | Action string `json:"action"` 9 | ConversationID string `json:"conversation_id,omitempty"` 10 | ConversationMode ConvMode `json:"conversation_mode"` 11 | ForceNulligen bool `json:"force_nulligen"` 12 | ForceParagen bool `json:"force_paragen"` 13 | ForceParagenModelSlug string `json:"force_paragen_model_slug"` 14 | ForceRateLimit bool `json:"force_rate_limit"` 15 | ForceUseSse bool `json:"force_use_sse"` 16 | HistoryAndTrainingDisabled bool `json:"history_and_training_disabled"` 17 | Messages []Message `json:"messages,omitempty"` 18 | Model string `json:"model"` 19 | ParentMessageID string `json:"parent_message_id,omitempty"` 20 | ResetRateLimits bool `json:"reset_rate_limits"` 21 | Suggestions []string `json:"suggestions"` 22 | TimezoneOffsetMin int `json:"timezone_offset_min"` 23 | WebsocketRequestId string `json:"websocket_request_id"` 24 | } 25 | 26 | type ConvMode struct { 27 | Kind string `json:"kind"` 28 | GizmoId string `json:"gizmo_id,omitempty"` 29 | } 30 | 31 | func (c *CreateConversationRequest) AddMessage(role string, content string) { 32 | c.Messages = append(c.Messages, Message{ 33 | ID: uuid.New().String(), 34 | Author: Author{Role: role}, 35 | Content: Content{ContentType: "text", Parts: []interface{}{content}}, 36 | Metadata: map[string]string{}, 37 | }) 38 | } 39 | 40 | type Message struct { 41 | Author Author `json:"author"` 42 | Content Content `json:"content"` 43 | ID string `json:"id"` 44 | Metadata interface{} `json:"metadata"` 45 | } 46 | 47 | type Author struct { 48 | Role string `json:"role"` 49 | } 50 | 51 | type Content struct { 52 | ContentType string `json:"content_type"` 53 | Parts []interface{} `json:"parts"` 54 | } 55 | 56 | type CreateConversationResponse struct { 57 | Message struct { 58 | ID string `json:"id"` 59 | Author struct { 60 | Role string `json:"role"` 61 | Name interface{} `json:"name"` 62 | Metadata struct { 63 | } `json:"metadata"` 64 | } `json:"author"` 65 | CreateTime float64 `json:"create_time"` 66 | UpdateTime interface{} `json:"update_time"` 67 | Content struct { 68 | ContentType string `json:"content_type"` 69 | Parts []string `json:"parts"` 70 | } `json:"content"` 71 | Status string `json:"status"` 72 | EndTurn bool `json:"end_turn"` 73 | Weight float64 `json:"weight"` 74 | Metadata struct { 75 | MessageType string `json:"message_type"` 76 | ModelSlug string `json:"model_slug"` 77 | FinishDetails struct { 78 | Type string `json:"type"` 79 | } `json:"finish_details"` 80 | } `json:"metadata"` 81 | Recipient string `json:"recipient"` 82 | } `json:"message"` 83 | ConversationID string `json:"conversation_id"` 84 | Error interface{} `json:"error"` 85 | } 86 | 87 | type GetModelsResponse struct { 88 | Models []struct { 89 | Slug string `json:"slug"` 90 | MaxTokens int `json:"max_tokens"` 91 | Title string `json:"title"` 92 | Description string `json:"description"` 93 | Tags []string `json:"tags"` 94 | Capabilities struct { 95 | } `json:"capabilities"` 96 | EnabledTools []string `json:"enabled_tools,omitempty"` 97 | } `json:"models"` 98 | Categories []struct { 99 | Category string `json:"category"` 100 | HumanCategoryName string `json:"human_category_name"` 101 | SubscriptionLevel string `json:"subscription_level"` 102 | DefaultModel string `json:"default_model"` 103 | CodeInterpreterModel string `json:"code_interpreter_model"` 104 | PluginsModel string `json:"plugins_model"` 105 | } `json:"categories"` 106 | } 107 | 108 | type ChatGPTWSSResponse struct { 109 | WssUrl string `json:"wss_url"` 110 | ConversationId string `json:"conversation_id,omitempty"` 111 | ResponseId string `json:"response_id,omitempty"` 112 | } 113 | 114 | type WSSMsgResponse struct { 115 | SequenceId int `json:"sequenceId"` 116 | Type string `json:"type"` 117 | From string `json:"from"` 118 | DataType string `json:"dataType"` 119 | Data WSSMsgResponseData `json:"data"` 120 | } 121 | 122 | type WSSSequenceAckMessage struct { 123 | Type string `json:"type"` 124 | SequenceId int `json:"sequenceId"` 125 | } 126 | 127 | type WSSMsgResponseData struct { 128 | Type string `json:"type"` 129 | Body string `json:"body"` 130 | MoreBody bool `json:"more_body"` 131 | ResponseId string `json:"response_id"` 132 | ConversationId string `json:"conversation_id"` 133 | } 134 | 135 | type ChatRequire struct { 136 | Token string `json:"token"` 137 | Proof ProofWork `json:"proofofwork,omitempty"` 138 | Arkose struct { 139 | Required bool `json:"required"` 140 | DX string `json:"dx,omitempty"` 141 | } `json:"arkose"` 142 | Turnstile struct { 143 | Required bool `json:"required"` 144 | DX string `json:"dx,omitempty"` 145 | } `json:"turnstile"` 146 | } 147 | 148 | type FileInfo struct { 149 | DownloadURL string `json:"download_url"` 150 | Status string `json:"status"` 151 | } 152 | 153 | type DalleContent struct { 154 | AssetPointer string `json:"asset_pointer"` 155 | Metadata struct { 156 | Dalle struct { 157 | Prompt string `json:"prompt"` 158 | } `json:"dalle"` 159 | } `json:"metadata"` 160 | } -------------------------------------------------------------------------------- /api/common.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | http "github.com/bogdanfinn/fhttp" 13 | tls_client "github.com/bogdanfinn/tls-client" 14 | "github.com/bogdanfinn/tls-client/profiles" 15 | "github.com/gin-gonic/gin" 16 | "github.com/google/uuid" 17 | "github.com/xqdoo00o/OpenAIAuth/auth" 18 | "github.com/xqdoo00o/funcaptcha" 19 | 20 | "github.com/linweiyuan/go-logger/logger" 21 | ) 22 | 23 | const ( 24 | ChatGPTApiPrefix = "/chatgpt" 25 | ImitateApiPrefix = "/imitate/v1" 26 | ChatGPTApiUrlPrefix = "https://chatgpt.com" 27 | 28 | PlatformApiPrefix = "/platform" 29 | PlatformApiUrlPrefix = "https://api.openai.com" 30 | 31 | defaultErrorMessageKey = "errorMessage" 32 | AuthorizationHeader = "Authorization" 33 | XAuthorizationHeader = "X-Authorization" 34 | ArkoseTokenHeader = "Openai-Sentinel-Arkose-Token" 35 | ContentType = "application/x-www-form-urlencoded" 36 | DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0" 37 | Auth0Url = "https://auth0.openai.com" 38 | LoginUsernameUrl = Auth0Url + "/u/login/identifier?state=" 39 | LoginPasswordUrl = Auth0Url + "/u/login/password?state=" 40 | ParseUserInfoErrorMessage = "failed to parse user login info" 41 | GetAuthorizedUrlErrorMessage = "failed to get authorized url" 42 | EmailInvalidErrorMessage = "email is not valid" 43 | EmailOrPasswordInvalidErrorMessage = "email or password is not correct" 44 | GetAccessTokenErrorMessage = "failed to get access token" 45 | defaultTimeoutSeconds = 600 46 | 47 | EmailKey = "email" 48 | AccountDeactivatedErrorMessage = "account %s is deactivated" 49 | 50 | ReadyHint = "service go-chatgpt-api is ready" 51 | 52 | refreshPuidErrorMessage = "failed to refresh PUID" 53 | 54 | Language = "en-US" 55 | 56 | ClientProfileMessage = "ClientProfile: %s is used" 57 | ) 58 | 59 | var ( 60 | Client tls_client.HttpClient 61 | ArkoseClient tls_client.HttpClient 62 | PUID string 63 | OAIDID string 64 | ProxyUrl string 65 | IMITATE_accessToken string 66 | ClientProfile profiles.ClientProfile 67 | UserAgent string 68 | StartTime = time.Now() 69 | ) 70 | 71 | type LoginInfo struct { 72 | Username string `json:"username"` 73 | Password string `json:"password"` 74 | } 75 | 76 | type AuthLogin interface { 77 | GetAuthorizedUrl(csrfToken string) (string, int, error) 78 | GetState(authorizedUrl string) (string, int, error) 79 | CheckUsername(state string, username string) (int, error) 80 | CheckPassword(state string, username string, password string) (string, int, error) 81 | GetAccessToken(code string) (string, int, error) 82 | GetAccessTokenFromHeader(c *gin.Context) (string, int, error) 83 | } 84 | 85 | func init() { 86 | ClientProfileStr := os.Getenv("CLIENT_PROFILE") 87 | if ClientProfileStr == "" { 88 | ClientProfile = profiles.Okhttp4Android13 89 | } else { 90 | // 从map中查找配置 91 | if profile, ok := profiles.MappedTLSClients [ClientProfileStr]; ok { 92 | ClientProfile = profile 93 | logger.Info(fmt.Sprintf(ClientProfileMessage, ClientProfileStr)) 94 | } else { 95 | ClientProfile = profiles.DefaultClientProfile // 找不到配置时使用默认配置 96 | logger.Info("Using default ClientProfile") 97 | } 98 | } 99 | UserAgent = os.Getenv("UA") 100 | if UserAgent == "" { 101 | UserAgent = DefaultUserAgent 102 | } 103 | Client, _ = tls_client.NewHttpClient(tls_client.NewNoopLogger(), []tls_client.HttpClientOption{ 104 | tls_client.WithCookieJar(tls_client.NewCookieJar()), 105 | tls_client.WithTimeoutSeconds(defaultTimeoutSeconds), 106 | tls_client.WithClientProfile(ClientProfile), 107 | }...) 108 | ArkoseClient = getHttpClient() 109 | 110 | setupIDs() 111 | } 112 | 113 | func NewHttpClient() tls_client.HttpClient { 114 | client := getHttpClient() 115 | 116 | ProxyUrl = os.Getenv("PROXY") 117 | if ProxyUrl != "" { 118 | client.SetProxy(ProxyUrl) 119 | } 120 | 121 | return client 122 | } 123 | 124 | func getHttpClient() tls_client.HttpClient { 125 | client, _ := tls_client.NewHttpClient(tls_client.NewNoopLogger(), []tls_client.HttpClientOption{ 126 | tls_client.WithCookieJar(tls_client.NewCookieJar()), 127 | tls_client.WithClientProfile(ClientProfile), 128 | }...) 129 | return client 130 | } 131 | 132 | func Proxy(c *gin.Context) { 133 | url := c.Request.URL.Path 134 | if strings.Contains(url, ChatGPTApiPrefix) { 135 | url = strings.ReplaceAll(url, ChatGPTApiPrefix, ChatGPTApiUrlPrefix) 136 | } else if strings.Contains(url, ImitateApiPrefix) { 137 | url = strings.ReplaceAll(url, ImitateApiPrefix, ChatGPTApiUrlPrefix+"/backend-api") 138 | } else { 139 | url = strings.ReplaceAll(url, PlatformApiPrefix, PlatformApiUrlPrefix) 140 | } 141 | 142 | method := c.Request.Method 143 | queryParams := c.Request.URL.Query().Encode() 144 | if queryParams != "" { 145 | url += "?" + queryParams 146 | } 147 | 148 | // if not set, will return 404 149 | c.Status(http.StatusOK) 150 | 151 | var req *http.Request 152 | if method == http.MethodGet { 153 | req, _ = http.NewRequest(http.MethodGet, url, nil) 154 | } else { 155 | body, _ := io.ReadAll(c.Request.Body) 156 | req, _ = http.NewRequest(method, url, bytes.NewReader(body)) 157 | } 158 | req.Header.Set("User-Agent", UserAgent) 159 | req.Header.Set(AuthorizationHeader, GetAccessToken(c)) 160 | req.Header.Set("Oai-Language", Language) 161 | req.Header.Set("Oai-Device-Id", OAIDID) 162 | req.Header.Set("Cookie", req.Header.Get("Cookie")+"oai-did="+OAIDID+";") 163 | resp, err := Client.Do(req) 164 | if err != nil { 165 | c.AbortWithStatusJSON(http.StatusInternalServerError, ReturnMessage(err.Error())) 166 | return 167 | } 168 | 169 | defer resp.Body.Close() 170 | if resp.StatusCode != http.StatusOK { 171 | if resp.StatusCode == http.StatusUnauthorized { 172 | logger.Error(fmt.Sprintf(AccountDeactivatedErrorMessage, c.GetString(EmailKey))) 173 | } 174 | 175 | responseMap := make(map[string]interface{}) 176 | json.NewDecoder(resp.Body).Decode(&responseMap) 177 | c.AbortWithStatusJSON(resp.StatusCode, responseMap) 178 | return 179 | } 180 | 181 | io.Copy(c.Writer, resp.Body) 182 | } 183 | 184 | func ReturnMessage(msg string) gin.H { 185 | logger.Warn(msg) 186 | 187 | return gin.H{ 188 | defaultErrorMessageKey: msg, 189 | } 190 | } 191 | 192 | func GetAccessToken(c *gin.Context) string { 193 | accessToken := c.GetString(AuthorizationHeader) 194 | if !strings.HasPrefix(accessToken, "Bearer") { 195 | return "Bearer " + accessToken 196 | } 197 | 198 | return accessToken 199 | } 200 | 201 | func GetArkoseToken(api_version int, dx string) (string, error) { 202 | return funcaptcha.GetOpenAIToken(api_version, PUID, dx, ProxyUrl) 203 | } 204 | 205 | func setupIDs() { 206 | // get device id from env. 207 | OAIDID = os.Getenv("OPENAI_DEVICE_ID") 208 | 209 | username := os.Getenv("OPENAI_EMAIL") 210 | password := os.Getenv("OPENAI_PASSWORD") 211 | refreshtoken := os.Getenv("OPENAI_REFRESH_TOKEN") 212 | if username != "" && password != "" { 213 | go func() { 214 | for { 215 | authenticator := auth.NewAuthenticator(username, password, ProxyUrl) 216 | if err := authenticator.Begin(); err != nil { 217 | logger.Warn(fmt.Sprintf("%s: %s", refreshPuidErrorMessage, err.Details)) 218 | return 219 | } 220 | 221 | accessToken := authenticator.GetAccessToken() 222 | if accessToken == "" { 223 | logger.Error(refreshPuidErrorMessage) 224 | return 225 | } 226 | 227 | puid := GetPUID(accessToken) 228 | if puid == "" { 229 | logger.Warn(refreshPuidErrorMessage) 230 | } else { 231 | PUID = puid 232 | logger.Info(fmt.Sprintf("PUID is updated")) 233 | } 234 | 235 | // store IMITATE_accessToken 236 | IMITATE_accessToken = accessToken 237 | 238 | time.Sleep(time.Hour * 24 * 7) 239 | } 240 | }() 241 | } else if refreshtoken != "" { 242 | go func() { 243 | for { 244 | accessToken := RefreshAccessToken(refreshtoken) 245 | if accessToken == "" { 246 | logger.Error(refreshPuidErrorMessage) 247 | return 248 | } else { 249 | logger.Info(fmt.Sprintf("accessToken is updated")) 250 | } 251 | 252 | puid := GetPUID(accessToken) 253 | if puid == "" { 254 | logger.Warn(refreshPuidErrorMessage) 255 | } else { 256 | PUID = puid 257 | logger.Info(fmt.Sprintf("PUID is updated")) 258 | } 259 | 260 | // store IMITATE_accessToken 261 | IMITATE_accessToken = accessToken 262 | 263 | time.Sleep(time.Hour * 24 * 7) 264 | } 265 | }() 266 | } else { 267 | PUID = os.Getenv("PUID") 268 | IMITATE_accessToken = os.Getenv("IMITATE_ACCESS_TOKEN") 269 | } 270 | 271 | if OAIDID == "" { 272 | seed := uuid.NewString() 273 | // get device id seed 274 | if username != "" { 275 | seed = username 276 | } else if refreshtoken != "" { 277 | seed = refreshtoken 278 | } else if IMITATE_accessToken != "" { 279 | seed = IMITATE_accessToken 280 | } 281 | // generate device id 282 | OAIDID = uuid.NewSHA1(uuid.MustParse("12345678-1234-5678-1234-567812345678"), []byte(seed)).String() 283 | } 284 | } 285 | 286 | func RefreshAccessToken(refreshToken string) string { 287 | data := map[string]interface{}{ 288 | "redirect_uri": "com.openai.chat://auth0.openai.com/ios/com.openai.chat/callback" , 289 | "grant_type": "refresh_token", 290 | "client_id": "pdlLIX2Y72MIl2rhLhTE9VV9bN905kBh", 291 | "refresh_token": refreshToken, 292 | } 293 | jsonData, err := json.Marshal(data) 294 | 295 | if err != nil { 296 | logger.Error(fmt.Sprintf("Failed to marshal data: %v", err)) 297 | } 298 | 299 | req, err := http.NewRequest(http.MethodPost, "https://auth0.openai.com/oauth/token", bytes.NewBuffer(jsonData)) 300 | req.Header.Set("User-Agent", UserAgent) 301 | req.Header.Set("Content-Type", "application/json") 302 | resp, err := NewHttpClient().Do(req) 303 | if err != nil { 304 | logger.Error(fmt.Sprintf("Failed to refresh token: %v", err)) 305 | return "" 306 | } 307 | 308 | defer resp.Body.Close() 309 | if resp.StatusCode != http.StatusOK { 310 | logger.Error(fmt.Sprintf("Server responded with status code: %d", resp.StatusCode)) 311 | } 312 | var result map[string]interface{} 313 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 314 | logger.Error(fmt.Sprintf("Failed to decode json: %v", err)) 315 | return "" 316 | } 317 | // Check if access token in data 318 | if _, ok := result["access_token"]; !ok { 319 | logger.Error(fmt.Sprintf("missing access token: %v", result)) 320 | return "" 321 | } 322 | return result["access_token"].(string) 323 | } 324 | 325 | func GetPUID(accessToken string) string { 326 | var puid string 327 | // Check if user has access token 328 | if accessToken == "" { 329 | logger.Error("GetPUID: Missing access token") 330 | return "" 331 | } 332 | 333 | // Make request to https://chatgpt.com/backend-api/models 334 | req, _ := http.NewRequest("GET", ChatGPTApiUrlPrefix+"/backend-api/models?history_and_training_disabled=false", nil) 335 | // Add headers 336 | req.Header.Add("Authorization", "Bearer "+accessToken) 337 | req.Header.Add("User-Agent", UserAgent) 338 | req.Header.Set("Cookie", req.Header.Get("Cookie")+"oai-did="+OAIDID+";") 339 | 340 | resp, err := NewHttpClient().Do(req) 341 | if err != nil { 342 | logger.Error("GetPUID: Missing access token") 343 | return "" 344 | } 345 | defer resp.Body.Close() 346 | if resp.StatusCode != 200 { 347 | logger.Error(fmt.Sprintf("GetPUID: Server responded with status code: %d", resp.StatusCode)) 348 | return "" 349 | } 350 | // Find `_puid` cookie in response 351 | for _, cookie := range resp.Cookies() { 352 | if cookie.Name == "_puid" { 353 | puid = cookie.Value 354 | break 355 | } 356 | } 357 | 358 | if puid == "" { 359 | logger.Error("GetPUID: PUID cookie not found") 360 | } 361 | return puid 362 | } -------------------------------------------------------------------------------- /api/imitate/api.go: -------------------------------------------------------------------------------- 1 | package imitate 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "net/url" 11 | "os" 12 | "regexp" 13 | "strings" 14 | "sync" 15 | 16 | http "github.com/bogdanfinn/fhttp" 17 | "github.com/gin-gonic/gin" 18 | "github.com/google/uuid" 19 | 20 | "github.com/maxduke/go-chatgpt-api/api" 21 | "github.com/maxduke/go-chatgpt-api/api/chatgpt" 22 | "github.com/linweiyuan/go-logger/logger" 23 | ) 24 | 25 | var ( 26 | reg *regexp.Regexp 27 | token string 28 | gptsRegexp = regexp.MustCompile(`-gizmo-g-(\w+)`) 29 | ) 30 | 31 | func init() { 32 | reg, _ = regexp.Compile("[^a-zA-Z0-9]+") 33 | } 34 | 35 | func CreateChatCompletions(c *gin.Context) { 36 | var original_request APIRequest 37 | err := c.BindJSON(&original_request) 38 | if err != nil { 39 | c.JSON(400, gin.H{"error": gin.H{ 40 | "message": "Request must be proper JSON", 41 | "type": "invalid_request_error", 42 | "param": nil, 43 | "code": err.Error(), 44 | }}) 45 | return 46 | } 47 | 48 | authHeader := c.GetHeader(api.AuthorizationHeader) 49 | imitate_api_key := os.Getenv("IMITATE_API_KEY") 50 | if authHeader != "" { 51 | customAccessToken := strings.Replace(authHeader, "Bearer ", "", 1) 52 | // Check if customAccessToken starts with eyJhbGciOiJSUzI1NiI 53 | if strings.HasPrefix(customAccessToken, "eyJhbGciOiJSUzI1NiI") { 54 | token = customAccessToken 55 | // use defiend access token if the provided api key is equal to "IMITATE_API_KEY" 56 | } else if imitate_api_key != "" && customAccessToken == imitate_api_key { 57 | token = os.Getenv("IMITATE_ACCESS_TOKEN") 58 | if token == "" { 59 | token = api.IMITATE_accessToken 60 | } 61 | } 62 | } 63 | 64 | if token == "" { 65 | c.JSON(400, gin.H{"error": gin.H{ 66 | "message": "API KEY is missing or invalid", 67 | "type": "invalid_request_error", 68 | "param": nil, 69 | "code": "400", 70 | }}) 71 | return 72 | } 73 | 74 | uid := uuid.NewString() 75 | var chat_require *chatgpt.ChatRequire 76 | var p string 77 | 78 | chat_require, p = chatgpt.CheckRequire(token, api.OAIDID) 79 | if chat_require == nil { 80 | c.JSON(500, gin.H{"error": "unable to check chat requirement"}) 81 | return 82 | } 83 | for i := 0; i < chatgpt.PowRetryTimes; i++ { 84 | if chat_require.Proof.Required && chat_require.Proof.Difficulty <= chatgpt.PowMaxDifficulty { 85 | logger.Warn(fmt.Sprintf("Proof of work difficulty too high: %s. Retrying... %d/%d ", chat_require.Proof.Difficulty, i + 1, chatgpt.PowRetryTimes)) 86 | chat_require, _ = chatgpt.CheckRequire(token, api.OAIDID) 87 | if chat_require == nil { 88 | c.JSON(500, gin.H{"error": "unable to check chat requirement"}) 89 | return 90 | } 91 | } else { 92 | break 93 | } 94 | } 95 | if chat_require == nil { 96 | c.JSON(500, gin.H{"error": "unable to check chat requirement"}) 97 | return 98 | } 99 | 100 | var proofToken string 101 | if chat_require.Proof.Required { 102 | proofToken = chatgpt.CalcProofToken(chat_require) 103 | } 104 | 105 | var arkoseToken string 106 | if chat_require.Arkose.Required { 107 | token, err := chatgpt.GetArkoseTokenForModel(original_request.Model, chat_require.Arkose.DX) 108 | arkoseToken = token 109 | if err != nil || arkoseToken == "" { 110 | c.AbortWithStatusJSON(http.StatusForbidden, api.ReturnMessage(err.Error())) 111 | return 112 | } 113 | } 114 | 115 | var turnstileToken string 116 | if chat_require.Turnstile.Required { 117 | turnstileToken = chatgpt.ProcessTurnstile(chat_require.Turnstile.DX, p) 118 | } 119 | 120 | // Convert the chat request to a ChatGPT request 121 | translated_request := convertAPIRequest(original_request) 122 | 123 | response, done := sendConversationRequest(c, translated_request, token, api.OAIDID, arkoseToken, chat_require.Token, proofToken, turnstileToken) 124 | if done { 125 | return 126 | } 127 | 128 | defer func(Body io.ReadCloser) { 129 | err := Body.Close() 130 | if err != nil { 131 | return 132 | } 133 | }(response.Body) 134 | 135 | if HandleRequestError(c, response) { 136 | return 137 | } 138 | 139 | var full_response string 140 | 141 | for i := 3; i > 0; i-- { 142 | var continue_info *ContinueInfo 143 | var response_part string 144 | response_part, continue_info = Handler(c, response, token, uid, original_request.Stream) 145 | full_response += response_part 146 | if continue_info == nil { 147 | break 148 | } 149 | logger.Info("Continuing conversation") 150 | translated_request.Messages = nil 151 | translated_request.Action = "continue" 152 | translated_request.ConversationID = continue_info.ConversationID 153 | translated_request.ParentMessageID = continue_info.ParentID 154 | chat_require, _ = chatgpt.CheckRequire(token, api.OAIDID) 155 | if chat_require == nil { 156 | c.JSON(500, gin.H{"error": "unable to check chat requirement"}) 157 | return 158 | } 159 | for i := 0; i < chatgpt.PowRetryTimes; i++ { 160 | if chat_require.Proof.Required && chat_require.Proof.Difficulty <= chatgpt.PowMaxDifficulty { 161 | logger.Warn(fmt.Sprintf("Proof of work difficulty too high: %s. Retrying... %d/%d ", chat_require.Proof.Difficulty, i + 1, chatgpt.PowRetryTimes)) 162 | chat_require, _ = chatgpt.CheckRequire(token, api.OAIDID) 163 | if chat_require == nil { 164 | c.JSON(500, gin.H{"error": "unable to check chat requirement"}) 165 | return 166 | } 167 | } else { 168 | break 169 | } 170 | } 171 | if chat_require.Proof.Required { 172 | proofToken = chatgpt.CalcProofToken(chat_require) 173 | } 174 | if chat_require.Arkose.Required { 175 | arkoseToken, err := chatgpt.GetArkoseTokenForModel(translated_request.Model, chat_require.Arkose.DX) 176 | arkoseToken = token 177 | if err != nil || arkoseToken == "" { 178 | c.AbortWithStatusJSON(http.StatusForbidden, api.ReturnMessage(err.Error())) 179 | return 180 | } 181 | } 182 | if chat_require.Turnstile.Required { 183 | turnstileToken = chatgpt.ProcessTurnstile(chat_require.Turnstile.DX, p) 184 | } 185 | response, done = sendConversationRequest(c, translated_request, token, api.OAIDID, arkoseToken, chat_require.Token, proofToken, turnstileToken) 186 | 187 | if done { 188 | return 189 | } 190 | 191 | // 以下修复代码来自ChatGPT 192 | // 在循环内部创建一个局部作用域,并将资源的引用传递给匿名函数,保证资源将在每次迭代结束时被正确释放 193 | func() { 194 | defer func(Body io.ReadCloser) { 195 | err := Body.Close() 196 | if err != nil { 197 | return 198 | } 199 | }(response.Body) 200 | }() 201 | 202 | if HandleRequestError(c, response) { 203 | return 204 | } 205 | } 206 | 207 | if c.Writer.Status() != 200 { 208 | return 209 | } 210 | if !original_request.Stream { 211 | c.JSON(200, newChatCompletion(full_response, translated_request.Model, uid)) 212 | } else { 213 | c.String(200, "data: [DONE]\n\n") 214 | } 215 | 216 | } 217 | 218 | func generateId() string { 219 | id := uuid.NewString() 220 | id = strings.ReplaceAll(id, "-", "") 221 | id = base64.StdEncoding.EncodeToString([]byte(id)) 222 | id = reg.ReplaceAllString(id, "") 223 | return "chatcmpl-" + id 224 | } 225 | 226 | func convertAPIRequest(api_request APIRequest) (chatgpt.CreateConversationRequest) { 227 | chatgpt_request := NewChatGPTRequest() 228 | 229 | if strings.HasPrefix(api_request.Model, "gpt-4o-mini") { 230 | chatgpt_request.Model = "gpt-4o-mini" 231 | } else if strings.HasPrefix(api_request.Model, "gpt-4o") { 232 | chatgpt_request.Model = "gpt-4o" 233 | } else if strings.HasPrefix(api_request.Model, "gpt-4") { 234 | chatgpt_request.Model = "gpt-4" 235 | } else if strings.HasPrefix(api_request.Model, "o1-mini") { 236 | chatgpt_request.Model = "o1-mini" 237 | } else if strings.HasPrefix(api_request.Model, "o1") { 238 | chatgpt_request.Model = "o1" 239 | } else if strings.HasPrefix(api_request.Model, "o3-mini") { 240 | chatgpt_request.Model = "o3-mini" 241 | } 242 | matches := gptsRegexp.FindStringSubmatch(api_request.Model) 243 | if len(matches) == 2 { 244 | chatgpt_request.ConversationMode.Kind = "gizmo_interaction" 245 | chatgpt_request.ConversationMode.GizmoId = "g-" + matches[1] 246 | } 247 | 248 | for _, api_message := range api_request.Messages { 249 | if api_message.Role == "system" { 250 | api_message.Role = "critic" 251 | } 252 | chatgpt_request.AddMessage(api_message.Role, api_message.Content) 253 | } 254 | return chatgpt_request 255 | } 256 | 257 | func NewChatGPTRequest() chatgpt.CreateConversationRequest { 258 | disable_history := os.Getenv("ENABLE_HISTORY") != "true" 259 | return chatgpt.CreateConversationRequest{ 260 | Action: "next", 261 | ParentMessageID: uuid.NewString(), 262 | Model: "text-davinci-002-render-sha", 263 | ForceUseSse: true, 264 | HistoryAndTrainingDisabled: disable_history, 265 | ConversationMode: chatgpt.ConvMode{Kind: "primary_assistant"}, 266 | WebsocketRequestId: uuid.NewString(), 267 | } 268 | } 269 | 270 | func sendConversationRequest(c *gin.Context, request chatgpt.CreateConversationRequest, accessToken string, deviceId string, arkoseToken string, chat_token string, proofToken string, turnstileToken string) (*http.Response, bool) { 271 | apiUrl := api.ChatGPTApiUrlPrefix+"/backend-api/conversation" 272 | jsonBytes, _ := json.Marshal(request) 273 | req, err := chatgpt.NewRequest(http.MethodPost, apiUrl, bytes.NewReader(jsonBytes), accessToken, deviceId) 274 | req.Header.Set("Content-Type", "application/json") 275 | req.Header.Set("Accept", "text/event-stream") 276 | if arkoseToken != "" { 277 | req.Header.Set("Openai-Sentinel-Arkose-Token", arkoseToken) 278 | } 279 | if chat_token != "" { 280 | req.Header.Set("Openai-Sentinel-Chat-Requirements-Token", chat_token) 281 | } 282 | if proofToken != "" { 283 | req.Header.Set("Openai-Sentinel-Proof-Token", proofToken) 284 | } 285 | if turnstileToken != "" { 286 | req.Header.Set("Openai-Sentinel-Turnstile-Token", turnstileToken) 287 | } 288 | req.Header.Set("Origin", api.ChatGPTApiUrlPrefix) 289 | req.Header.Set("Referer", api.ChatGPTApiUrlPrefix+"/") 290 | resp, err := api.Client.Do(req) 291 | if err != nil { 292 | c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error())) 293 | return nil, true 294 | } 295 | 296 | if resp.StatusCode != http.StatusOK { 297 | if resp.StatusCode == http.StatusUnauthorized { 298 | logger.Error(fmt.Sprintf(api.AccountDeactivatedErrorMessage, c.GetString(api.EmailKey))) 299 | } 300 | 301 | responseMap := make(map[string]interface{}) 302 | json.NewDecoder(resp.Body).Decode(&responseMap) 303 | c.AbortWithStatusJSON(resp.StatusCode, responseMap) 304 | return nil, true 305 | } 306 | 307 | return resp, false 308 | } 309 | 310 | func GetImageSource(wg *sync.WaitGroup, url string, prompt string, token string, idx int, imgSource []string) { 311 | defer wg.Done() 312 | request, err := http.NewRequest(http.MethodGet, url, nil) 313 | if err != nil { 314 | return 315 | } 316 | // Clear cookies 317 | if api.PUID != "" { 318 | request.Header.Set("Cookie", "_puid="+api.PUID+";") 319 | } 320 | request.Header.Set("Oai-Language", api.Language) 321 | if api.OAIDID != "" { 322 | request.Header.Set("Cookie", request.Header.Get("Cookie")+"oai-did="+api.OAIDID+";") 323 | request.Header.Set("Oai-Device-Id", api.OAIDID) 324 | } 325 | request.Header.Set("User-Agent", api.UserAgent) 326 | request.Header.Set("Accept", "*/*") 327 | if token != "" { 328 | request.Header.Set("Authorization", "Bearer "+token) 329 | } 330 | response, err := api.Client.Do(request) 331 | if err != nil { 332 | return 333 | } 334 | defer response.Body.Close() 335 | var file_info chatgpt.FileInfo 336 | err = json.NewDecoder(response.Body).Decode(&file_info) 337 | if err != nil || file_info.Status != "success" { 338 | return 339 | } 340 | imgSource[idx] = "[![image](" + file_info.DownloadURL + " \"" + prompt + "\")](" + file_info.DownloadURL + ")" 341 | } 342 | 343 | func Handler(c *gin.Context, response *http.Response, token string, uuid string, stream bool) (string, *ContinueInfo) { 344 | max_tokens := false 345 | 346 | // Create a bufio.Reader from the response body 347 | reader := bufio.NewReader(response.Body) 348 | 349 | // Read the response byte by byte until a newline character is encountered 350 | if stream { 351 | // Response content type is text/event-stream 352 | c.Header("Content-Type", "text/event-stream") 353 | } else { 354 | // Response content type is application/json 355 | c.Header("Content-Type", "application/json") 356 | } 357 | var finish_reason string 358 | var previous_text StringStruct 359 | var original_response ChatGPTResponse 360 | var isRole = true 361 | var waitSource = false 362 | var imgSource []string 363 | var convId string 364 | var msgId string 365 | 366 | for { 367 | var line string 368 | var err error 369 | line, err = reader.ReadString('\n') 370 | if err != nil { 371 | if err == io.EOF { 372 | break 373 | } 374 | return "", nil 375 | } 376 | if len(line) < 6 { 377 | continue 378 | } 379 | // Remove "data: " from the beginning of the line 380 | line = line[6:] 381 | // Check if line starts with [DONE] 382 | if !strings.HasPrefix(line, "[DONE]") { 383 | // Parse the line as JSON 384 | err = json.Unmarshal([]byte(line), &original_response) 385 | if err != nil { 386 | continue 387 | } 388 | if original_response.Error != nil { 389 | c.JSON(500, gin.H{"error": original_response.Error}) 390 | return "", nil 391 | } 392 | if original_response.ConversationID != convId { 393 | if convId == "" { 394 | convId = original_response.ConversationID 395 | } else { 396 | continue 397 | } 398 | } 399 | if !(original_response.Message.Author.Role == "assistant" || (original_response.Message.Author.Role == "tool" && original_response.Message.Content.ContentType != "text")) || original_response.Message.Content.Parts == nil { 400 | continue 401 | } 402 | if original_response.Message.Metadata.MessageType == "" { 403 | continue 404 | } 405 | if original_response.Message.Metadata.MessageType != "next" && original_response.Message.Metadata.MessageType != "continue" || !strings.HasSuffix(original_response.Message.Content.ContentType, "text") { 406 | continue 407 | } 408 | if original_response.Message.Content.ContentType == "text" && original_response.Message.ID != msgId { 409 | if msgId == "" && original_response.Message.Content.Parts[0].(string) == "" { 410 | msgId = original_response.Message.ID 411 | } else { 412 | continue 413 | } 414 | } 415 | if original_response.Message.EndTurn != nil && !original_response.Message.EndTurn.(bool) { 416 | if waitSource { 417 | waitSource = false 418 | } 419 | msgId = "" 420 | } 421 | if len(original_response.Message.Metadata.Citations) != 0 { 422 | r := []rune(original_response.Message.Content.Parts[0].(string)) 423 | if waitSource { 424 | if string(r[len(r)-1:]) == "】" { 425 | waitSource = false 426 | } else { 427 | continue 428 | } 429 | } 430 | offset := 0 431 | for _, citation := range original_response.Message.Metadata.Citations { 432 | rl := len(r) 433 | u, _ := url.Parse(citation.Metadata.URL) 434 | baseURL := u.Scheme + "://" + u.Host + "/" 435 | attr := urlAttrMap[baseURL] 436 | if attr == "" { 437 | attr = getURLAttribution(token, api.PUID, api.OAIDID, baseURL) 438 | if attr != "" { 439 | urlAttrMap[baseURL] = attr 440 | } 441 | } 442 | original_response.Message.Content.Parts[0] = string(r[:citation.StartIx+offset]) + " ([" + attr + "](" + citation.Metadata.URL + " \"" + citation.Metadata.Title + "\"))" + string(r[citation.EndIx+offset:]) 443 | r = []rune(original_response.Message.Content.Parts[0].(string)) 444 | offset += len(r) - rl 445 | } 446 | } else if waitSource { 447 | continue 448 | } 449 | response_string := "" 450 | if original_response.Message.Recipient != "all" { 451 | continue 452 | } 453 | if original_response.Message.Content.ContentType == "multimodal_text" { 454 | apiUrl := api.ChatGPTApiUrlPrefix+"/backend-api/files/" 455 | FILES_REVERSE_PROXY := os.Getenv("FILES_REVERSE_PROXY") 456 | if FILES_REVERSE_PROXY != "" { 457 | apiUrl = FILES_REVERSE_PROXY 458 | } 459 | imgSource = make([]string, len(original_response.Message.Content.Parts)) 460 | var wg sync.WaitGroup 461 | for index, part := range original_response.Message.Content.Parts { 462 | jsonItem, _ := json.Marshal(part) 463 | var dalle_content chatgpt.DalleContent 464 | err = json.Unmarshal(jsonItem, &dalle_content) 465 | if err != nil { 466 | continue 467 | } 468 | url := apiUrl + strings.Split(dalle_content.AssetPointer, "//")[1] + "/download" 469 | wg.Add(1) 470 | go GetImageSource(&wg, url, dalle_content.Metadata.Dalle.Prompt, token, index, imgSource) 471 | } 472 | wg.Wait() 473 | translated_response := NewChatCompletionChunk(strings.Join(imgSource, "")) 474 | if isRole { 475 | translated_response.Choices[0].Delta.Role = original_response.Message.Author.Role 476 | } 477 | response_string = "data: " + translated_response.String() + "\n\n" 478 | } 479 | if response_string == "" { 480 | response_string = ConvertToString(&original_response, &previous_text, isRole) 481 | } 482 | if isRole && response_string != "" { 483 | isRole = false 484 | } 485 | if response_string == "【" { 486 | waitSource = true 487 | continue 488 | } 489 | if stream && response_string != "" { 490 | _, err = c.Writer.WriteString(response_string) 491 | if err != nil { 492 | return "", nil 493 | } 494 | } 495 | // Flush the response writer buffer to ensure that the client receives each line as it's written 496 | c.Writer.Flush() 497 | 498 | if original_response.Message.Metadata.FinishDetails != nil { 499 | if original_response.Message.Metadata.FinishDetails.Type == "max_tokens" { 500 | max_tokens = true 501 | } 502 | finish_reason = original_response.Message.Metadata.FinishDetails.Type 503 | } 504 | } else { 505 | if stream { 506 | final_line := StopChunk(finish_reason) 507 | c.Writer.WriteString("data: " + final_line.String() + "\n\n") 508 | } 509 | } 510 | } 511 | respText := strings.Join(imgSource, "") 512 | if respText != "" { 513 | respText += "\n" 514 | } 515 | respText += previous_text.Text 516 | if !max_tokens { 517 | return respText, nil 518 | } 519 | return respText, &ContinueInfo{ 520 | ConversationID: original_response.ConversationID, 521 | ParentID: original_response.Message.ID, 522 | } 523 | 524 | } 525 | 526 | var urlAttrMap = make(map[string]string) 527 | 528 | type urlAttr struct { 529 | Url string `json:"url"` 530 | Attribution string `json:"attribution"` 531 | } 532 | 533 | func getURLAttribution(access_token string, puid string, deviceId string, url string) string { 534 | request, err := chatgpt.NewRequest(http.MethodPost, api.ChatGPTApiUrlPrefix+"/backend-api/attributions", bytes.NewBuffer([]byte(`{"urls":["`+url+`"]}`)), access_token, deviceId) 535 | if err != nil { 536 | return "" 537 | } 538 | request.Header.Set("Content-Type", "application/json") 539 | response, err := api.Client.Do(request) 540 | if err != nil { 541 | return "" 542 | } 543 | defer response.Body.Close() 544 | var attr urlAttr 545 | err = json.NewDecoder(response.Body).Decode(&attr) 546 | if err != nil { 547 | return "" 548 | } 549 | return attr.Attribution 550 | } -------------------------------------------------------------------------------- /api/imitate/convert.go: -------------------------------------------------------------------------------- 1 | package imitate 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func ConvertToString(chatgpt_response *ChatGPTResponse, previous_text *StringStruct, role bool) string { 8 | translated_response := NewChatCompletionChunk(strings.Replace(chatgpt_response.Message.Content.Parts[0].(string), previous_text.Text, "", 1)) 9 | if role { 10 | translated_response.Choices[0].Delta.Role = chatgpt_response.Message.Author.Role 11 | } else if translated_response.Choices[0].Delta.Content == "" || (strings.HasPrefix(chatgpt_response.Message.Metadata.ModelSlug, "gpt-4") && translated_response.Choices[0].Delta.Content == "【") { 12 | return translated_response.Choices[0].Delta.Content 13 | } 14 | previous_text.Text = chatgpt_response.Message.Content.Parts[0].(string) 15 | return "data: " + translated_response.String() + "\n\n" 16 | } -------------------------------------------------------------------------------- /api/imitate/request.go: -------------------------------------------------------------------------------- 1 | package imitate 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | http "github.com/bogdanfinn/fhttp" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type ContinueInfo struct { 12 | ConversationID string `json:"conversation_id"` 13 | ParentID string `json:"parent_id"` 14 | } 15 | 16 | type APIRequest struct { 17 | Messages []ApiMessage `json:"messages"` 18 | Stream bool `json:"stream"` 19 | Model string `json:"model"` 20 | } 21 | 22 | type ApiMessage struct { 23 | Role string `json:"role"` 24 | Content string `json:"content"` 25 | } 26 | 27 | func HandleRequestError(c *gin.Context, response *http.Response) bool { 28 | if response.StatusCode != 200 { 29 | // Try read response body as JSON 30 | var errorResponse map[string]interface{} 31 | err := json.NewDecoder(response.Body).Decode(&errorResponse) 32 | if err != nil { 33 | // Read response body 34 | body, _ := io.ReadAll(response.Body) 35 | c.JSON(500, gin.H{"error": gin.H{ 36 | "message": "Unknown error", 37 | "type": "internal_server_error", 38 | "param": nil, 39 | "code": "500", 40 | "details": string(body), 41 | }}) 42 | return true 43 | } 44 | c.JSON(response.StatusCode, gin.H{"error": gin.H{ 45 | "message": errorResponse["detail"], 46 | "type": response.Status, 47 | "param": nil, 48 | "code": "error", 49 | }}) 50 | return true 51 | } 52 | return false 53 | } 54 | -------------------------------------------------------------------------------- /api/imitate/response.go: -------------------------------------------------------------------------------- 1 | package imitate 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/maxduke/go-chatgpt-api/api/chatgpt" 8 | ) 9 | 10 | type ChatCompletionChunk struct { 11 | ID string `json:"id"` 12 | Object string `json:"object"` 13 | Created int64 `json:"created"` 14 | Model string `json:"model"` 15 | Choices []Choices `json:"choices"` 16 | } 17 | 18 | func (chunk *ChatCompletionChunk) String() string { 19 | resp, _ := json.Marshal(chunk) 20 | return string(resp) 21 | } 22 | 23 | type Choices struct { 24 | Delta Delta `json:"delta"` 25 | Index int `json:"index"` 26 | FinishReason interface{} `json:"finish_reason"` 27 | } 28 | 29 | type Delta struct { 30 | Content string `json:"content,omitempty"` 31 | Role string `json:"role,omitempty"` 32 | } 33 | 34 | func NewChatCompletionChunk(text string) ChatCompletionChunk { 35 | return ChatCompletionChunk{ 36 | ID: "chatcmpl-QXlha2FBbmROaXhpZUFyZUF3ZXNvbWUK", 37 | Object: "chat.completion.chunk", 38 | Created: 0, 39 | Model: "gpt-3.5-turbo-0301", 40 | Choices: []Choices{ 41 | { 42 | Index: 0, 43 | Delta: Delta{ 44 | Content: text, 45 | }, 46 | FinishReason: nil, 47 | }, 48 | }, 49 | } 50 | } 51 | 52 | func StopChunk(reason string) ChatCompletionChunk { 53 | return ChatCompletionChunk{ 54 | ID: "chatcmpl-QXlha2FBbmROaXhpZUFyZUF3ZXNvbWUK", 55 | Object: "chat.completion.chunk", 56 | Created: 0, 57 | Model: "gpt-3.5-turbo-0301", 58 | Choices: []Choices{ 59 | { 60 | Index: 0, 61 | FinishReason: reason, 62 | }, 63 | }, 64 | } 65 | } 66 | 67 | type ChatCompletion struct { 68 | ID string `json:"id"` 69 | Object string `json:"object"` 70 | Created int64 `json:"created"` 71 | Model string `json:"model"` 72 | Usage usage `json:"usage"` 73 | Choices []Choice `json:"choices"` 74 | } 75 | type Msg struct { 76 | Role string `json:"role"` 77 | Content string `json:"content"` 78 | } 79 | type Choice struct { 80 | Index int `json:"index"` 81 | Message Msg `json:"message"` 82 | FinishReason interface{} `json:"finish_reason"` 83 | } 84 | type usage struct { 85 | PromptTokens int `json:"prompt_tokens"` 86 | CompletionTokens int `json:"completion_tokens"` 87 | TotalTokens int `json:"total_tokens"` 88 | } 89 | 90 | type ChatGPTResponse struct { 91 | Message Message `json:"message"` 92 | ConversationID string `json:"conversation_id"` 93 | Error interface{} `json:"error"` 94 | } 95 | 96 | type Message struct { 97 | ID string `json:"id"` 98 | Author chatgpt.Author `json:"author"` 99 | CreateTime float64 `json:"create_time"` 100 | UpdateTime interface{} `json:"update_time"` 101 | Content chatgpt.Content `json:"content"` 102 | EndTurn interface{} `json:"end_turn"` 103 | Weight float64 `json:"weight"` 104 | Metadata Metadata `json:"metadata"` 105 | Recipient string `json:"recipient"` 106 | } 107 | 108 | type Metadata struct { 109 | Citations []Citation `json:"citations,omitempty"` 110 | MessageType string `json:"message_type"` 111 | FinishDetails *FinishDetails `json:"finish_details"` 112 | ModelSlug string `json:"model_slug"` 113 | } 114 | 115 | type Citation struct { 116 | Metadata CitaMeta `json:"metadata"` 117 | StartIx int `json:"start_ix"` 118 | EndIx int `json:"end_ix"` 119 | } 120 | 121 | type CitaMeta struct { 122 | URL string `json:"url"` 123 | Title string `json:"title"` 124 | } 125 | 126 | type FinishDetails struct { 127 | Type string `json:"type"` 128 | Stop string `json:"stop"` 129 | } 130 | 131 | type StringStruct struct { 132 | Text string `json:"text"` 133 | } 134 | 135 | func newChatCompletion(fullTest, model string, id string) ChatCompletion { 136 | return ChatCompletion{ 137 | ID: id, 138 | Object: "chat.completion", 139 | Created: time.Now().Unix(), 140 | Model: model, 141 | Usage: usage{ 142 | PromptTokens: 0, 143 | CompletionTokens: 0, 144 | TotalTokens: 0, 145 | }, 146 | Choices: []Choice{ 147 | { 148 | Message: Msg{ 149 | Content: fullTest, 150 | Role: "assistant", 151 | }, 152 | Index: 0, 153 | }, 154 | }, 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /api/platform/access_token.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/url" 8 | "strings" 9 | 10 | http "github.com/bogdanfinn/fhttp" 11 | 12 | "github.com/maxduke/go-chatgpt-api/api" 13 | ) 14 | 15 | func (userLogin *UserLogin) GetAuthorizedUrl(csrfToken string) (string, int, error) { 16 | urlParams := url.Values{ 17 | "client_id": {platformAuthClientID}, 18 | "audience": {platformAuthAudience}, 19 | "redirect_uri": {platformAuthRedirectURL}, 20 | "scope": {platformAuthScope}, 21 | "response_type": {platformAuthResponseType}, 22 | } 23 | req, _ := http.NewRequest(http.MethodGet, platformAuth0Url+urlParams.Encode(), nil) 24 | req.Header.Set("Content-Type", api.ContentType) 25 | req.Header.Set("User-Agent", api.UserAgent) 26 | resp, err := userLogin.client.Do(req) 27 | if err != nil { 28 | return "", http.StatusInternalServerError, err 29 | } 30 | 31 | defer resp.Body.Close() 32 | if resp.StatusCode != http.StatusOK { 33 | return "", resp.StatusCode, errors.New(api.GetAuthorizedUrlErrorMessage) 34 | } 35 | 36 | return resp.Request.URL.String(), http.StatusOK, nil 37 | } 38 | 39 | func (userLogin *UserLogin) GetState(authorizedUrl string) (string, int, error) { 40 | split := strings.Split(authorizedUrl, "=") 41 | return split[1], http.StatusOK, nil 42 | } 43 | 44 | func (userLogin *UserLogin) CheckUsername(state string, username string) (int, error) { 45 | formParams := url.Values{ 46 | "state": {state}, 47 | "username": {username}, 48 | "js-available": {"true"}, 49 | "webauthn-available": {"true"}, 50 | "is-brave": {"false"}, 51 | "webauthn-platform-available": {"false"}, 52 | "action": {"default"}, 53 | } 54 | req, _ := http.NewRequest(http.MethodPost, api.LoginUsernameUrl+state, strings.NewReader(formParams.Encode())) 55 | req.Header.Set("Content-Type", api.ContentType) 56 | req.Header.Set("User-Agent", api.UserAgent) 57 | resp, err := userLogin.client.Do(req) 58 | if err != nil { 59 | return http.StatusInternalServerError, err 60 | } 61 | 62 | defer resp.Body.Close() 63 | if resp.StatusCode != http.StatusOK { 64 | return resp.StatusCode, errors.New(api.EmailInvalidErrorMessage) 65 | } 66 | 67 | return http.StatusOK, nil 68 | } 69 | 70 | func (userLogin *UserLogin) CheckPassword(state string, username string, password string) (string, int, error) { 71 | formParams := url.Values{ 72 | "state": {state}, 73 | "username": {username}, 74 | "password": {password}, 75 | "action": {"default"}, 76 | } 77 | req, _ := http.NewRequest(http.MethodPost, api.LoginPasswordUrl+state, strings.NewReader(formParams.Encode())) 78 | req.Header.Set("Content-Type", api.ContentType) 79 | req.Header.Set("User-Agent", api.UserAgent) 80 | resp, err := userLogin.client.Do(req) 81 | if err != nil { 82 | return "", http.StatusInternalServerError, err 83 | } 84 | 85 | defer resp.Body.Close() 86 | if resp.StatusCode != http.StatusOK { 87 | return "", resp.StatusCode, errors.New(api.EmailOrPasswordInvalidErrorMessage) 88 | } 89 | 90 | return resp.Request.URL.Query().Get("code"), http.StatusOK, nil 91 | } 92 | 93 | func (userLogin *UserLogin) GetAccessToken(code string) (string, int, error) { 94 | jsonBytes, _ := json.Marshal(GetAccessTokenRequest{ 95 | ClientID: platformAuthClientID, 96 | Code: code, 97 | GrantType: platformAuthGrantType, 98 | RedirectURI: platformAuthRedirectURL, 99 | }) 100 | req, _ := http.NewRequest(http.MethodPost, getTokenUrl, strings.NewReader(string(jsonBytes))) 101 | req.Header.Set("Content-Type", "application/json") 102 | req.Header.Set("User-Agent", api.UserAgent) 103 | resp, err := userLogin.client.Do(req) 104 | if err != nil { 105 | return "", http.StatusInternalServerError, err 106 | } 107 | 108 | defer resp.Body.Close() 109 | if resp.StatusCode != http.StatusOK { 110 | return "", resp.StatusCode, errors.New(api.GetAccessTokenErrorMessage) 111 | } 112 | 113 | data, _ := io.ReadAll(resp.Body) 114 | return string(data), http.StatusOK, nil 115 | } 116 | -------------------------------------------------------------------------------- /api/platform/api.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "strings" 10 | 11 | http "github.com/bogdanfinn/fhttp" 12 | "github.com/gin-gonic/gin" 13 | 14 | "github.com/maxduke/go-chatgpt-api/api" 15 | "github.com/linweiyuan/go-logger/logger" 16 | ) 17 | 18 | func CreateChatCompletions(c *gin.Context) { 19 | body, _ := io.ReadAll(c.Request.Body) 20 | var request struct { 21 | Stream bool `json:"stream"` 22 | } 23 | json.Unmarshal(body, &request) 24 | 25 | url := c.Request.URL.Path 26 | if strings.Contains(url, "/chat") { 27 | url = apiCreateChatCompletions 28 | } else { 29 | url = apiCreateCompletions 30 | } 31 | 32 | resp, err := handlePost(c, url, body, request.Stream) 33 | if err != nil { 34 | return 35 | } 36 | 37 | defer resp.Body.Close() 38 | if resp.StatusCode == http.StatusUnauthorized { 39 | logger.Error(fmt.Sprintf(api.AccountDeactivatedErrorMessage, c.GetString(api.EmailKey))) 40 | responseMap := make(map[string]interface{}) 41 | json.NewDecoder(resp.Body).Decode(&responseMap) 42 | c.AbortWithStatusJSON(resp.StatusCode, responseMap) 43 | return 44 | } 45 | 46 | if request.Stream { 47 | handleCompletionsResponse(c, resp) 48 | } else { 49 | io.Copy(c.Writer, resp.Body) 50 | } 51 | } 52 | 53 | func CreateCompletions(c *gin.Context) { 54 | CreateChatCompletions(c) 55 | } 56 | 57 | func handleCompletionsResponse(c *gin.Context, resp *http.Response) { 58 | c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8") 59 | 60 | reader := bufio.NewReader(resp.Body) 61 | for { 62 | if c.Request.Context().Err() != nil { 63 | break 64 | } 65 | 66 | line, err := reader.ReadString('\n') 67 | if err != nil { 68 | break 69 | } 70 | 71 | line = strings.TrimSpace(line) 72 | if strings.HasPrefix(line, "event") || 73 | strings.HasPrefix(line, "data: 20") || 74 | line == "" { 75 | continue 76 | } 77 | 78 | c.Writer.Write([]byte(line + "\n\n")) 79 | c.Writer.Flush() 80 | } 81 | } 82 | 83 | func handlePost(c *gin.Context, url string, data []byte, stream bool) (*http.Response, error) { 84 | req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(data)) 85 | req.Header.Set(api.AuthorizationHeader, api.GetAccessToken(c)) 86 | if stream { 87 | req.Header.Set("Accept", "text/event-stream") 88 | } 89 | req.Header.Set("Content-Type", "application/json") 90 | resp, err := api.Client.Do(req) 91 | if err != nil { 92 | c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error())) 93 | return nil, err 94 | } 95 | 96 | return resp, nil 97 | } 98 | -------------------------------------------------------------------------------- /api/platform/constant.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import "github.com/maxduke/go-chatgpt-api/api" 4 | 5 | const ( 6 | apiCreateChatCompletions = api.PlatformApiUrlPrefix + "/v1/chat/completions" 7 | apiCreateCompletions = api.PlatformApiUrlPrefix + "/v1/completions" 8 | 9 | platformAuthClientID = "DRivsnm2Mu42T3KOpqdtwB3NYviHYzwD" 10 | platformAuthAudience = "https://api.openai.com/v1" 11 | platformAuthRedirectURL = "https://platform.openai.com/auth/callback" 12 | platformAuthScope = "openid profile email offline_access" 13 | platformAuthResponseType = "code" 14 | platformAuthGrantType = "authorization_code" 15 | platformAuth0Url = api.Auth0Url + "/authorize?" 16 | getTokenUrl = api.Auth0Url + "/oauth/token" 17 | auth0Client = "eyJuYW1lIjoiYXV0aDAtc3BhLWpzIiwidmVyc2lvbiI6IjEuMjEuMCJ9" // '{"name":"auth0-spa-js","version":"1.21.0"}' 18 | auth0LogoutUrl = api.Auth0Url + "/v2/logout?returnTo=https%3A%2F%2Fplatform.openai.com%2Floggedout&client_id=" + platformAuthClientID + "&auth0Client=" + auth0Client 19 | dashboardLoginUrl = "https://api.openai.com/dashboard/onboarding/login" 20 | getSessionKeyErrorMessage = "failed to get session key" 21 | ) 22 | -------------------------------------------------------------------------------- /api/platform/login.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "strings" 7 | 8 | http "github.com/bogdanfinn/fhttp" 9 | "github.com/gin-gonic/gin" 10 | 11 | "github.com/maxduke/go-chatgpt-api/api" 12 | ) 13 | 14 | func Login(c *gin.Context) { 15 | var loginInfo api.LoginInfo 16 | if err := c.ShouldBindJSON(&loginInfo); err != nil { 17 | c.AbortWithStatusJSON(http.StatusBadRequest, api.ReturnMessage(api.ParseUserInfoErrorMessage)) 18 | return 19 | } 20 | 21 | userLogin := UserLogin{ 22 | client: api.NewHttpClient(), 23 | } 24 | 25 | // hard refresh cookies 26 | resp, _ := userLogin.client.Get(auth0LogoutUrl) 27 | defer resp.Body.Close() 28 | 29 | // get authorized url 30 | authorizedUrl, statusCode, err := userLogin.GetAuthorizedUrl("") 31 | if err != nil { 32 | c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error())) 33 | return 34 | } 35 | 36 | // get state 37 | state, _, _ := userLogin.GetState(authorizedUrl) 38 | 39 | // check username 40 | statusCode, err = userLogin.CheckUsername(state, loginInfo.Username) 41 | if err != nil { 42 | c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error())) 43 | return 44 | } 45 | 46 | // check password 47 | code, statusCode, err := userLogin.CheckPassword(state, loginInfo.Username, loginInfo.Password) 48 | if err != nil { 49 | c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error())) 50 | return 51 | } 52 | 53 | // get access token 54 | accessToken, statusCode, err := userLogin.GetAccessToken(code) 55 | if err != nil { 56 | c.AbortWithStatusJSON(statusCode, api.ReturnMessage(err.Error())) 57 | return 58 | } 59 | 60 | // get session key 61 | var getAccessTokenResponse GetAccessTokenResponse 62 | json.Unmarshal([]byte(accessToken), &getAccessTokenResponse) 63 | req, _ := http.NewRequest(http.MethodPost, dashboardLoginUrl, strings.NewReader("{}")) 64 | req.Header.Set("Content-Type", "application/json") 65 | req.Header.Set("User-Agent", api.UserAgent) 66 | req.Header.Set(api.AuthorizationHeader, "Bearer "+getAccessTokenResponse.AccessToken) 67 | resp, err = userLogin.client.Do(req) 68 | if err != nil { 69 | c.AbortWithStatusJSON(http.StatusInternalServerError, api.ReturnMessage(err.Error())) 70 | return 71 | } 72 | 73 | defer resp.Body.Close() 74 | if resp.StatusCode != http.StatusOK { 75 | c.AbortWithStatusJSON(resp.StatusCode, api.ReturnMessage(getSessionKeyErrorMessage)) 76 | return 77 | } 78 | 79 | io.Copy(c.Writer, resp.Body) 80 | } 81 | -------------------------------------------------------------------------------- /api/platform/typings.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import tls_client "github.com/bogdanfinn/tls-client" 4 | 5 | type UserLogin struct { 6 | client tls_client.HttpClient 7 | } 8 | 9 | type GetAccessTokenRequest struct { 10 | ClientID string `json:"client_id"` 11 | GrantType string `json:"grant_type"` 12 | Code string `json:"code"` 13 | RedirectURI string `json:"redirect_uri"` 14 | } 15 | 16 | type GetAccessTokenResponse struct { 17 | AccessToken string `json:"access_token"` 18 | RefreshToken string `json:"refresh_token"` 19 | IDToken string `json:"id_token"` 20 | Scope string `json:"scope"` 21 | ExpiresIn int `json:"expires_in"` 22 | TokenType string `json:"token_type"` 23 | } 24 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | go-chatgpt-api: 3 | build: . 4 | container_name: go-chatgpt-api 5 | image: linweiyuan/go-chatgpt-api 6 | ports: 7 | - 8080:8080 8 | environment: 9 | - PORT= 10 | - TZ=Asia/Shanghai 11 | - PROXY= 12 | - OPENAI_EMAIL= 13 | - OPENAI_PASSWORD= 14 | - CONTINUE_SIGNAL= 15 | - ENABLE_HISTORY= 16 | - IMITATE_ACCESS_TOKEN= 17 | volumes: 18 | - ./chat.openai.com.har:/app/chat.openai.com.har 19 | restart: unless-stopped 20 | -------------------------------------------------------------------------------- /env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "github.com/joho/godotenv" 5 | ) 6 | 7 | func init() { 8 | godotenv.Load() 9 | } 10 | -------------------------------------------------------------------------------- /example/chatgpt.http: -------------------------------------------------------------------------------- 1 | ### login 2 | POST {{baseUrl}}/chatgpt/login 3 | Content-Type: application/json 4 | 5 | { 6 | "username": "{{username}}", 7 | "password": "{{password}}" 8 | } 9 | 10 | ### get conversations 11 | GET {{baseUrl}}/chatgpt/backend-api/conversations?offset=0&limit=3&order=updated 12 | Authorization: Bearer {{accessToken}} 13 | 14 | ### get conversation 15 | GET {{baseUrl}}/chatgpt/backend-api/conversation/id 16 | Authorization: Bearer {{accessToken}} 17 | 18 | ### create conversation 19 | POST {{baseUrl}}/chatgpt/backend-api/conversation 20 | Authorization: Bearer {{accessToken}} 21 | Content-Type: application/json 22 | Accept: text/event-stream 23 | 24 | { 25 | "action": "next", 26 | "messages": [ 27 | { 28 | "id": "{{$guid}}", 29 | "author": { 30 | "role": "user" 31 | }, 32 | "content": { 33 | "content_type": "text", 34 | "parts": [ 35 | "hello" 36 | ] 37 | }, 38 | "metadata": {} 39 | } 40 | ], 41 | "model": "gpt-4", 42 | "timezone_offset_min": -480, 43 | "history_and_training_disabled": false 44 | } 45 | 46 | ### get models 47 | GET {{baseUrl}}/chatgpt/backend-api/models?history_and_training_disabled=false 48 | Authorization: Bearer {{accessToken}} 49 | 50 | ### check account 51 | GET {{baseUrl}}/chatgpt/backend-api/accounts/check 52 | Authorization: Bearer {{accessToken}} 53 | 54 | ### check account v4 55 | GET {{baseUrl}}/chatgpt/backend-api/accounts/check/v4-2023-04-27 56 | Authorization: Bearer {{accessToken}} 57 | 58 | ### get settings beta features 59 | GET {{baseUrl}}/chatgpt/backend-api/settings/beta_features 60 | Authorization: Bearer {{accessToken}} 61 | 62 | ### get conversation limit (no need to pass access token) 63 | GET {{baseUrl}}/chatgpt/public-api/conversation_limit 64 | Authorization: Bearer {{accessToken}} 65 | 66 | ### get models with pandora enabled 67 | GET {{baseUrl}}/api/models?history_and_training_disabled=false 68 | Authorization: Bearer {{accessToken}} 69 | 70 | ### share link to chat 71 | POST {{baseUrl}}/chatgpt/backend-api/share/create 72 | Authorization: Bearer {{accessToken}} 73 | Content-Type: application/json 74 | 75 | { 76 | "current_node_id": "9020711b-3dcf-4705-82ac-46b5af30fc7b", 77 | "conversation_id": "74c406dd-a2e8-477a-b420-90ed57a55bf9", 78 | "is_anonymous": false 79 | } 80 | 81 | ### copy link 82 | PATCH {{baseUrl}}/chatgpt/backend-api/share/{share_id} 83 | Authorization: Bearer {{accessToken}} 84 | Content-Type: application/json 85 | 86 | { 87 | "share_id": "49cd2432-d084-4ab7-8549-4ee18046812b", 88 | "highlighted_message_id": null, 89 | "title": "Summarize Request and Response 11122", 90 | "is_public": false, 91 | "is_visible": false, 92 | "is_anonymous": true 93 | } 94 | 95 | ### continue shared conversation 96 | POST {{baseUrl}}/chatgpt/backend-api/conversation 97 | Authorization: Bearer {{accessToken}} 98 | Content-Type: application/json 99 | 100 | { 101 | "action": "next", 102 | "messages": [{ 103 | "id": "{{$guid}}", 104 | "author": { 105 | "role": "user" 106 | }, 107 | "content": { 108 | "content_type": "text", 109 | "parts": [ 110 | "hello again" 111 | ] 112 | }, 113 | "metadata": {} 114 | }], 115 | "continue_from_shared_conversation_id": "this is the share_id", 116 | "parent_message_id": "this is the current_node_id", 117 | "model": "text-davinci-002-render-sha", 118 | "timezone_offset_min": -480, 119 | "history_and_training_disabled": false, 120 | "arkose_token": null 121 | } 122 | 123 | ### get plugins 124 | GET {{baseUrl}}/chatgpt/backend-api/aip/p?offset=0&limit=250&statuses=approved 125 | Authorization: Bearer {{accessToken}} 126 | 127 | ### get payment url 128 | GET {{baseUrl}}/chatgpt/backend-api/payments/customer_portal 129 | Authorization: Bearer {{accessToken}} 130 | -------------------------------------------------------------------------------- /example/imitate.http: -------------------------------------------------------------------------------- 1 | ### Create chat completion 2 | POST {{baseUrl}}/imitate/v1/chat/completions 3 | Content-Type: application/json 4 | Authorization: Bearer {{accessToken}} 5 | 6 | { 7 | "model": "gpt-3.5-turbo", 8 | "messages": [ 9 | { 10 | "role": "system", 11 | "content": "You are a helpful assistant." 12 | }, 13 | { 14 | "role": "user", 15 | "content": "Hello!" 16 | } 17 | ], 18 | "stream": true 19 | } 20 | -------------------------------------------------------------------------------- /example/ios.http: -------------------------------------------------------------------------------- 1 | ### create conversation 2 | POST https://ios.chat.openai.com/backend-api/conversation 3 | Authorization: Bearer {{accessToken}} 4 | Cookie: _devicecheck=user-xxx 5 | Accept: text/event-stream 6 | Content-Type: application/json 7 | 8 | { 9 | "action": "next", 10 | "messages": [ 11 | { 12 | "id": "{{$guid}}", 13 | "author": { 14 | "role": "user" 15 | }, 16 | "content": { 17 | "content_type": "text", 18 | "parts": [ 19 | "hello" 20 | ] 21 | }, 22 | "metadata": {} 23 | } 24 | ], 25 | "model": "gpt-4", 26 | "timezone_offset_min": -480, 27 | "history_and_training_disabled": false, 28 | "supports_modapi": true 29 | } 30 | 31 | ### get conversations 32 | GET https://ios.chat.openai.com/backend-api/conversations?offset=0&limit=3&order=updated 33 | Authorization: Bearer {{accessToken}} 34 | Cookie: _devicecheck=user-xxx 35 | 36 | ### device check 37 | POST https://ios.chat.openai.com/backend-api/devicecheck 38 | Authorization: Bearer {{accessToken}} 39 | Cookie: _preauth_devicecheck=xxx 40 | Content-Type: application/json 41 | 42 | { 43 | "bundle_id": "com.openai.chat", 44 | "device_token": "ad" 45 | } 46 | 47 | ### me 48 | GET https://ios.chat.openai.com/backend-api/me?include_groups=true 49 | Authorization: Bearer {{accessToken}} 50 | Cookie: _devicecheck=user-xxx 51 | -------------------------------------------------------------------------------- /example/platform.http: -------------------------------------------------------------------------------- 1 | ### login 2 | POST {{baseUrl}}/platform/login 3 | Content-Type: application/json 4 | 5 | { 6 | "username": "{{username}}", 7 | "password": "{{password}}" 8 | } 9 | 10 | ### get models 11 | GET {{baseUrl}}/platform/v1/models 12 | Authorization: Bearer {{apiKey}} 13 | 14 | ### get model 15 | GET {{baseUrl}}/platform/v1/models/gpt-3.5-turbo-16k-0613 16 | Authorization: Bearer {{apiKey}} 17 | 18 | ### Create chat completion 19 | POST {{baseUrl}}/platform/v1/chat/completions 20 | Content-Type: application/json 21 | Authorization: Bearer {{apiKey}} 22 | 23 | { 24 | "model": "gpt-3.5-turbo", 25 | "messages": [ 26 | { 27 | "role": "system", 28 | "content": "You are a helpful assistant." 29 | }, 30 | { 31 | "role": "user", 32 | "content": "Hello!" 33 | } 34 | ], 35 | "stream": true 36 | } 37 | 38 | ### Create completion 39 | POST {{baseUrl}}/platform/v1/completions 40 | Content-Type: application/json 41 | Authorization: Bearer {{apiKey}} 42 | 43 | { 44 | "model": "text-davinci-003", 45 | "prompt": "Say this is a test", 46 | "max_tokens": 7, 47 | "temperature": 0, 48 | "stream": true 49 | } 50 | 51 | ### get user api_keys 52 | GET {{baseUrl}}/platform/dashboard/user/api_keys 53 | Authorization: Bearer {{apiKey}} 54 | 55 | ### get billing credit_grants 56 | GET {{baseUrl}}/platform/dashboard/billing/credit_grants 57 | Authorization: Bearer {{apiKey}} 58 | 59 | ### get billing subscription 60 | GET {{baseUrl}}/platform/dashboard/billing/subscription 61 | Authorization: Bearer {{apiKey}} 62 | 63 | ### get billing usage 64 | GET {{baseUrl}}/platform/dashboard/billing/usage?end_date=2023-07-01&start_date=2023-06-01 65 | Authorization: Bearer {{apiKey}} 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/maxduke/go-chatgpt-api 2 | 3 | go 1.23 4 | toolchain go1.24.1 5 | 6 | require ( 7 | github.com/PuerkitoBio/goquery v1.10.3 8 | github.com/bogdanfinn/fhttp v0.5.36 9 | github.com/bogdanfinn/tls-client v1.9.1 10 | github.com/gin-gonic/gin v1.10.0 11 | github.com/google/uuid v1.6.0 12 | github.com/joho/godotenv v1.5.1 13 | github.com/linweiyuan/go-logger v0.0.0-20230709142852-da1f090a7d4c 14 | github.com/xqdoo00o/OpenAIAuth v0.0.0-20240701110453-a742f7a5ea18 15 | github.com/xqdoo00o/funcaptcha v0.0.0-20240701110249-093b35d56d32 16 | golang.org/x/crypto v0.37.0 17 | ) 18 | 19 | require ( 20 | github.com/andybalholm/brotli v1.1.1 // indirect 21 | github.com/andybalholm/cascadia v1.3.3 // indirect 22 | github.com/bogdanfinn/utls v1.6.5 // indirect 23 | github.com/bytedance/sonic v1.12.5 // indirect 24 | github.com/bytedance/sonic/loader v0.2.1 // indirect 25 | github.com/cloudflare/circl v1.5.0 // indirect 26 | github.com/cloudwego/base64x v0.1.4 // indirect 27 | github.com/cloudwego/iasm v0.2.0 // indirect 28 | github.com/gabriel-vasile/mimetype v1.4.7 // indirect 29 | github.com/gin-contrib/sse v0.1.0 // indirect 30 | github.com/go-playground/locales v0.14.1 // indirect 31 | github.com/go-playground/universal-translator v0.18.1 // indirect 32 | github.com/go-playground/validator/v10 v10.23.0 // indirect 33 | github.com/goccy/go-json v0.10.3 // indirect 34 | github.com/json-iterator/go v1.1.12 // indirect 35 | github.com/klauspost/compress v1.17.11 // indirect 36 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 37 | github.com/leodido/go-urn v1.4.0 // indirect 38 | github.com/mattn/go-isatty v0.0.20 // indirect 39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 40 | github.com/modern-go/reflect2 v1.0.2 // indirect 41 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 42 | github.com/quic-go/quic-go v0.48.2 // indirect 43 | github.com/sirupsen/logrus v1.9.3 // indirect 44 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect 45 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 46 | github.com/ugorji/go/codec v1.2.12 // indirect 47 | golang.org/x/arch v0.12.0 // indirect 48 | golang.org/x/net v0.39.0 // indirect 49 | golang.org/x/sys v0.32.0 // indirect 50 | golang.org/x/text v0.24.0 // indirect 51 | google.golang.org/protobuf v1.35.2 // indirect 52 | gopkg.in/yaml.v3 v3.0.1 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= 2 | github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= 3 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 4 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 5 | github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= 6 | github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= 7 | github.com/bogdanfinn/fhttp v0.5.36 h1:t1sO/EkO4K40QD/Ti8f6t80leZIdh2AaeLfN7dMvjH8= 8 | github.com/bogdanfinn/fhttp v0.5.36/go.mod h1:BlcawVfXJ4uhk5yyNGOOY2bwo8UmMi6ccMszP1KGLkU= 9 | github.com/bogdanfinn/tls-client v1.9.1 h1:Br0WkKL+/7Q9FSNM1zBMdlYXW8bm+XXGMn9iyb9a/7Y= 10 | github.com/bogdanfinn/tls-client v1.9.1/go.mod h1:ehNITC7JBFeh6S7QNWtfD+PBKm0RsqvizAyyij2d/6g= 11 | github.com/bogdanfinn/utls v1.6.5 h1:rVMQvhyN3zodLxKFWMRLt19INGBCZ/OM2/vBWPNIt1w= 12 | github.com/bogdanfinn/utls v1.6.5/go.mod h1:czcHxHGsc1q9NjgWSeSinQZzn6MR76zUmGVIGanSXO0= 13 | github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w= 14 | github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 15 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 16 | github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= 17 | github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 18 | github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= 19 | github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 20 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 21 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 22 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 23 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= 28 | github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= 29 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 30 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 31 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 32 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 33 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 34 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 35 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 36 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 37 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 38 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 39 | github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= 40 | github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 41 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 42 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 43 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 44 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 45 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 46 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 47 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 48 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 49 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 50 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 51 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 52 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 53 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 54 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 55 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 56 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 57 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 58 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 59 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 60 | github.com/linweiyuan/go-logger v0.0.0-20230709142852-da1f090a7d4c h1:KJqkWkepk+PGSCC3i+ANrLnthM/x9qyfG4uCTGg2B8E= 61 | github.com/linweiyuan/go-logger v0.0.0-20230709142852-da1f090a7d4c/go.mod h1:U37nbW0kovzaMnrX5sBmxOwidA7P4ymM3/K64oaTQJc= 62 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 63 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 64 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 65 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 66 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 67 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 68 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 69 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 70 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 71 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= 74 | github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 75 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 76 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 79 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 80 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 81 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 84 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 85 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 86 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 87 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= 88 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= 89 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 90 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 91 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 92 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 93 | github.com/xqdoo00o/OpenAIAuth v0.0.0-20240701110453-a742f7a5ea18 h1:4Q+83OuwbY25gGtFK2fv5O+AIVmn7WI0NPH79u0HH0M= 94 | github.com/xqdoo00o/OpenAIAuth v0.0.0-20240701110453-a742f7a5ea18/go.mod h1:fwYwpN94kChGIoana10/wD+ytALJbG2I8JQvwMhOVKE= 95 | github.com/xqdoo00o/funcaptcha v0.0.0-20240701110249-093b35d56d32 h1:6QUL4meBfwSu1CRqrsYVPQxBTa0yt/9NKl8sUt14FEo= 96 | github.com/xqdoo00o/funcaptcha v0.0.0-20240701110249-093b35d56d32/go.mod h1:MmsYdzgYjO6NQYNS8vtecBWUFWwD4TApVzMp8iwKiR0= 97 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 98 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 99 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 100 | golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= 101 | golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 102 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 103 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 104 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 105 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 106 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 107 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 108 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 109 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 110 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 111 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 112 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 113 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 114 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 115 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 116 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 117 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 118 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 119 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 120 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 121 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 122 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 123 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 124 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 125 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 126 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 127 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 128 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 131 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 132 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 133 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 134 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 135 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 146 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 147 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 148 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 149 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 150 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 151 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 152 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 153 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 154 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 155 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 156 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 157 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 158 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 159 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 160 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 161 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 162 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 163 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 164 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 165 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 166 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 167 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 168 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 169 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 170 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 171 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 172 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 173 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 174 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 175 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 176 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 177 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 178 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 179 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 180 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 181 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 182 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 183 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 184 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 185 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | http "github.com/bogdanfinn/fhttp" 9 | "github.com/gin-gonic/gin" 10 | 11 | "github.com/maxduke/go-chatgpt-api/api" 12 | "github.com/maxduke/go-chatgpt-api/api/chatgpt" 13 | "github.com/maxduke/go-chatgpt-api/api/imitate" 14 | "github.com/maxduke/go-chatgpt-api/api/platform" 15 | _ "github.com/maxduke/go-chatgpt-api/env" 16 | "github.com/maxduke/go-chatgpt-api/middleware" 17 | ) 18 | 19 | func init() { 20 | gin.ForceConsoleColor() 21 | gin.SetMode(gin.ReleaseMode) 22 | } 23 | 24 | func main() { 25 | router := gin.Default() 26 | 27 | router.Use(middleware.CORS()) 28 | router.Use(middleware.Authorization()) 29 | 30 | setupChatGPTAPIs(router) 31 | setupPlatformAPIs(router) 32 | setupPandoraAPIs(router) 33 | setupImitateAPIs(router) 34 | router.NoRoute(api.Proxy) 35 | 36 | router.GET("/", func(c *gin.Context) { 37 | c.String(http.StatusOK, api.ReadyHint) 38 | }) 39 | 40 | port := os.Getenv("PORT") 41 | if port == "" { 42 | port = "8080" 43 | } 44 | err := router.Run(":" + port) 45 | if err != nil { 46 | log.Fatal("failed to start server: " + err.Error()) 47 | } 48 | } 49 | 50 | func setupChatGPTAPIs(router *gin.Engine) { 51 | chatgptGroup := router.Group("/chatgpt") 52 | { 53 | chatgptGroup.POST("/login", chatgpt.Login) 54 | chatgptGroup.POST("/backend-api/login", chatgpt.Login) // add support for other projects 55 | 56 | conversationGroup := chatgptGroup.Group("/backend-api/conversation") 57 | { 58 | conversationGroup.POST("", chatgpt.CreateConversation) 59 | } 60 | } 61 | } 62 | 63 | func setupPlatformAPIs(router *gin.Engine) { 64 | platformGroup := router.Group("/platform") 65 | { 66 | platformGroup.POST("/login", platform.Login) 67 | platformGroup.POST("/v1/login", platform.Login) 68 | 69 | apiGroup := platformGroup.Group("/v1") 70 | { 71 | apiGroup.POST("/chat/completions", platform.CreateChatCompletions) 72 | apiGroup.POST("/completions", platform.CreateCompletions) 73 | } 74 | } 75 | } 76 | 77 | func setupPandoraAPIs(router *gin.Engine) { 78 | router.Any("/api/*path", func(c *gin.Context) { 79 | c.Request.URL.Path = strings.ReplaceAll(c.Request.URL.Path, "/api", "/chatgpt/backend-api") 80 | router.HandleContext(c) 81 | }) 82 | } 83 | 84 | func setupImitateAPIs(router *gin.Engine) { 85 | imitateGroup := router.Group("/imitate") 86 | { 87 | imitateGroup.POST("/login", chatgpt.Login) 88 | 89 | apiGroup := imitateGroup.Group("/v1") 90 | { 91 | apiGroup.POST("/chat/completions", imitate.CreateChatCompletions) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /middleware/authorization.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | 14 | "github.com/maxduke/go-chatgpt-api/api" 15 | ) 16 | 17 | const ( 18 | emptyAccessTokenErrorMessage = "please provide a valid access token or api key in 'Authorization' header" 19 | accessTokenHasExpiredErrorMessage = "the accessToken for account %s has expired" 20 | ) 21 | 22 | type AccessToken struct { 23 | HTTPSAPIOpenaiComProfile struct { 24 | Email string `json:"email"` 25 | EmailVerified bool `json:"email_verified"` 26 | } `json:"https://api.openai.com/profile"` 27 | HTTPSAPIOpenaiComAuth struct { 28 | UserID string `json:"user_id"` 29 | } `json:"https://api.openai.com/auth"` 30 | Iss string `json:"iss"` 31 | Sub string `json:"sub"` 32 | Aud []string `json:"aud"` 33 | Iat int `json:"iat"` 34 | Exp int `json:"exp"` 35 | Azp string `json:"azp"` 36 | Scope string `json:"scope"` 37 | } 38 | 39 | func Authorization() gin.HandlerFunc { 40 | return func(c *gin.Context) { 41 | authorization := c.GetHeader(api.AuthorizationHeader) 42 | if authorization == "" { 43 | authorization = c.GetHeader(api.XAuthorizationHeader) 44 | } 45 | 46 | if authorization == "" { 47 | if c.Request.URL.Path == "/" { 48 | c.Header("Content-Type", "text/plain") 49 | } else if strings.HasSuffix(c.Request.URL.Path, "/login") || 50 | strings.HasPrefix(c.Request.URL.Path, "/chatgpt/public-api") || 51 | (strings.HasPrefix(c.Request.URL.Path, "/imitate") && os.Getenv("IMITATE_ACCESS_TOKEN") != "") { 52 | c.Header("Content-Type", "application/json") 53 | } else if c.Request.URL.Path == "/favicon.ico" { 54 | c.Abort() 55 | return 56 | } else { 57 | c.AbortWithStatusJSON(http.StatusUnauthorized, api.ReturnMessage(emptyAccessTokenErrorMessage)) 58 | return 59 | } 60 | 61 | c.Next() 62 | } else { 63 | if expired := isExpired(c); expired { 64 | c.AbortWithStatusJSON(http.StatusUnauthorized, api.ReturnMessage(fmt.Sprintf(accessTokenHasExpiredErrorMessage, c.GetString(api.EmailKey)))) 65 | return 66 | } 67 | 68 | c.Set(api.AuthorizationHeader, authorization) 69 | } 70 | } 71 | } 72 | 73 | func isExpired(c *gin.Context) bool { 74 | accessToken := c.GetHeader(api.AuthorizationHeader) 75 | split := strings.Split(accessToken, ".") 76 | if len(split) == 3 { 77 | rawDecodedText, _ := base64.RawStdEncoding.DecodeString(split[1]) 78 | var accessToken AccessToken 79 | json.Unmarshal(rawDecodedText, &accessToken) 80 | 81 | c.Set(api.EmailKey, accessToken.HTTPSAPIOpenaiComProfile.Email) 82 | 83 | exp := int64(accessToken.Exp) 84 | expTime := time.Unix(exp, 0) 85 | now := time.Now() 86 | 87 | return now.After(expTime) 88 | } 89 | 90 | // apiKey 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | http "github.com/bogdanfinn/fhttp" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func CORS() gin.HandlerFunc { 9 | return func(c *gin.Context) { 10 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 11 | c.Writer.Header().Set("Access-Control-Allow-Headers", "*") 12 | c.Writer.Header().Set("Access-Control-Allow-Methods", "*") 13 | 14 | if c.Request.Method == http.MethodOptions { 15 | c.AbortWithStatus(http.StatusNoContent) 16 | return 17 | } 18 | 19 | c.Next() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | name: go-chatgpt-api 4 | runtime: go 5 | plan: free 6 | buildCommand: go build -ldflags="-w -s" -o go-chatgpt-api main.go 7 | startCommand: ./go-chatgpt-api --------------------------------------------------------------------------------