├── .github
└── workflows
│ ├── ci.yaml
│ └── tag.yaml
├── .gitignore
├── License
├── README.md
├── assets
└── logo.svg
├── dist
├── assets
│ ├── index-9a4663b1.js
│ ├── index-ef8ba4ac.css
│ ├── logo-4312ea85.svg
│ ├── team-6ddf162b.jpg
│ └── usersdomain-73f886cd.jpg
└── index.html
├── doc
├── API.md
├── ama_pandora_chat_test.png
├── ama_pandora_config_client.png
├── ama_pandora_copy_config.png
├── ama_pandora_provider.png
├── azure.md
├── azure_ama.png
├── azure_key&endpoint.png
├── azure_openai_for_team.png
├── deploy.md
├── gemini.md
├── gemini_key.jpg
└── pandora.md
├── docker-compose.yml
├── docker
├── Dockerfile
└── docker-compose.yml
├── go.mod
├── go.sum
├── makefile
├── opencat.go
├── pkg
├── azureopenai
│ └── azureopenai.go
├── claude
│ ├── chat.go
│ └── claude.go
├── error
│ └── errdata.go
├── google
│ └── chat.go
├── openai
│ ├── chat.go
│ ├── dall-e.go
│ ├── realtime.go
│ ├── tts.go
│ └── whisper.go
├── search
│ └── bing.go
├── team
│ ├── key.go
│ ├── me.go
│ ├── middleware.go
│ ├── usage.go
│ └── user.go
├── tokenizer
│ └── tokenizer.go
└── vertexai
│ └── auth.go
├── router
├── chat.go
└── router.go
├── store
├── cache.go
├── db.go
├── keydb.go
├── usage.go
└── userdb.go
└── web
├── .gitignore
├── .vscode
└── extensions.json
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── App.vue
├── assets
│ ├── chatgpt_client.jpg
│ ├── logo.svg
│ ├── team.jpg
│ └── usersdomain.jpg
├── components
│ └── HelloWorld.vue
├── main.js
└── style.css
├── tailwind.config.js
└── vite.config.js
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Docker Image CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - dev
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | submodules: 'true'
16 | - name: Get current date
17 | id: date
18 | run: echo "today=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT
19 |
20 | - name: Set up QEMU
21 | uses: docker/setup-qemu-action@v2
22 |
23 | - name: Set up Docker Buildx
24 | id: buildx
25 | uses: docker/setup-buildx-action@v2
26 | - name: Available platforms
27 | run: echo ${{ steps.buildx.outputs.platforms }}
28 | - name: Login to DockerHub
29 | uses: docker/login-action@v2
30 | with:
31 | username: ${{ secrets.DOCKERHUB_USERNAME }}
32 | password: ${{ secrets.DOCKERHUB_TOKEN }}
33 |
34 | - name: Build and push
35 | uses: docker/build-push-action@v4
36 | with:
37 | context: .
38 | file: ./docker/Dockerfile
39 | push: true
40 | platforms: linux/amd64,linux/arm64
41 | tags: |
42 | ${{ github.repository }}:${{ steps.date.outputs.today }}
43 | ${{ github.repository }}:${{ contains(github.ref,'main') && 'latest' || github.ref_name }}
44 | - name: Docker Hub Description
45 | uses: peter-evans/dockerhub-description@v3
46 | with:
47 | username: ${{ secrets.DOCKERHUB_USERNAME }}
48 | password: ${{ secrets.DOCKERHUB_TOKEN }}
49 | repository: ${{ github.repository }}
50 | readme-filepath: ./README.md
--------------------------------------------------------------------------------
/.github/workflows/tag.yaml:
--------------------------------------------------------------------------------
1 | name: Docker Image CI Tag
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | with:
13 | submodules: 'true'
14 |
15 | - name: Get version
16 | id: vars
17 | run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
18 |
19 | - name: Set up QEMU
20 | uses: docker/setup-qemu-action@v2
21 |
22 | - name: Set up Docker Buildx
23 | id: buildx
24 | uses: docker/setup-buildx-action@v2
25 | - name: Available platforms
26 | run: echo ${{ steps.buildx.outputs.platforms }}
27 | - name: Login to DockerHub
28 | uses: docker/login-action@v2
29 | with:
30 | username: ${{ secrets.DOCKERHUB_USERNAME }}
31 | password: ${{ secrets.DOCKERHUB_TOKEN }}
32 |
33 | - name: Build and push
34 | uses: docker/build-push-action@v4
35 | with:
36 | context: .
37 | file: ./docker/Dockerfile
38 | push: true
39 | platforms: linux/amd64,linux/arm64
40 | tags: |
41 | ${{ github.repository }}:${{ steps.vars.outputs.tag }}
42 | ${{ github.repository }}:latest
43 | - name: Docker Hub Description
44 | uses: peter-evans/dockerhub-description@v3
45 | with:
46 | username: ${{ secrets.DOCKERHUB_USERNAME }}
47 | password: ${{ secrets.DOCKERHUB_TOKEN }}
48 | repository: ${{ github.repository }}
49 | readme-filepath: ./README.md
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | test/
3 | *.log
4 | *.db
5 | demo/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ~~opencatd-open~~ [OpenTeam](https://github.com/mirrors2/opencatd-open)
2 |
3 | 本项目即将更名,后续请关注 👉🏻 https://github.com/mirrors2/openteam
4 |
5 |
6 |
7 |
8 |
9 | [](https://t.me/OpenTeamChat) [](https://t.me/OpenTeamLLM)
10 |
11 | opencatd-open is an open-source, team-shared service for ChatGPT API that can be safely shared with others for API usage.
12 |
13 | ---
14 | OpenCat for Team的开源实现
15 |
16 | ~~基本~~实现了opencatd的全部功能
17 |
18 | (openai附属能力:whisper,tts,dall-e(text to image)...)
19 |
20 | ## Extra Support:
21 |
22 | | 🎯 | 🚧 |Extra Provider|
23 | | --- | --- | --- |
24 | |[OpenAI](./doc/azure.md) | ✅|Azure, Github Marketplace|
25 | |[Claude](./doc/azure.md) | ✅|VertexAI|
26 | |[Gemini](./doc/gemini.md) | ✅||
27 | | ... | ... |
28 |
29 |
30 |
31 | ## 快速上手
32 | ```
33 | docker run -d --name opencatd -p 80:80 -v /etc/opencatd:/app/db mirrors2/opencatd-open
34 | ```
35 | ## docker-compose
36 |
37 | ```
38 | version: '3.7'
39 | services:
40 | opencatd:
41 | image: mirrors2/opencatd-open
42 | container_name: opencatd-open
43 | restart: unless-stopped
44 | ports:
45 | - 80:80
46 | volumes:
47 | - /etc/opencatd:/app/db
48 |
49 | ```
50 | or
51 |
52 | ```
53 | wget https://github.com/mirrors2/opencatd-open/raw/main/docker/docker-compose.yml
54 | ```
55 | ## 支持的命令
56 | >获取 root 的 token
57 | - `docker exec opencatd-open opencatd root_token`
58 |
59 | >重置 root 的 token
60 | - `docker exec opencatd-open opencatd reset_root`
61 |
62 | >导出 user info -> user.json (docker file path: /app/db/user.json)
63 | - `docker exec opencatd-open opencatd save`
64 |
65 | >导入 user.json -> db
66 | - `docker exec opencatd-open opencatd load`
67 |
68 | ## Q&A
69 | 关于证书?
70 | - docker部署会白白占用掉VPS的80,443很不河里,建议用Nginx/Caddy/Traefik等反代并自动管理HTTPS证书.
71 |
72 | 没有服务器?
73 | - 可以白嫖一些免费的容器托管服务:如:
74 | - [](https://railway.app/template/ppAoCV?referralCode=TW5RNa)
75 | - [Zeabur](https://zeabur.com/zh-CN)
76 | - [koyeb](https://koyeb.io/)
77 | - [Fly.io](https://fly.io/)
78 | - 或者其他
79 |
80 | 使用Nginx + Docker部署
81 | - [使用Nginx + Docker部署](./doc/deploy.md)
82 |
83 | pandora for team
84 | - [pandora for team](./doc/pandora.md)
85 |
86 | 如何自定义HOST地址? (仅OpenAI)
87 | - 需修改环境变量,优先级递增(全局配置谨慎修改)
88 | - Cloudflare AI Gateway地址 `AIGateWay_Endpoint=https://gateway.ai.cloudflare.com/v1/123456789/xxxx/openai/chat/completions`
89 | - 自定义的endpoint `OpenAI_Endpoint=https://your.domain/v1/chat/completions`
90 |
91 | 设置主页跳转地址?
92 | - 修改环境变量 `CUSTOM_REDIRECT=https://your.domain`
93 |
94 | ## 获取更多信息
95 | [](https://t.me/OpenTeamLLM)
96 |
97 | ## 赞助
98 | [](https://www.buymeacoffee.com/littlecjun)
99 |
100 | # License
101 |
102 | [](https://github.com/mirrors2/opencatd-open/blob/main/License)
103 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dist/assets/index-ef8ba4ac.css:
--------------------------------------------------------------------------------
1 | *,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.top-0{top:0px}.z-10{z-index:10}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-60{margin-left:15rem;margin-right:15rem}.my-0{margin-top:0;margin-bottom:0}.my-12{margin-top:3rem;margin-bottom:3rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-5{margin-top:1.25rem}.mt-8{margin-top:2rem}.flex{display:flex}.h-10{height:2.5rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-auto{height:auto}.h-screen{height:100vh}.w-10\/12{width:83.333333%}.w-60{width:15rem}.w-full{width:100%}.w-screen{width:100vw}.flex-1{flex:1 1 0%}.flex-grow{flex-grow:1}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-zinc-700{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity))}.p-12{padding:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-medium{font-weight:500}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-rose-500{--tw-text-opacity: 1;color:rgb(244 63 94 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.hover\:shadow-2xl:hover{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}@media (min-width: 640px){.sm\:max-w-full{max-width:100%}}@media (min-width: 768px){.md\:max-w-sm{max-width:24rem}}.bg-diy[data-v-b28a9e78]{background-color:#f0f0f0}.logo[data-v-b28a9e78]{height:6em;padding:1.5em;will-change:filter;transition:filter .3s}.logo[data-v-b28a9e78]:hover{filter:drop-shadow(0 0 3em #45f5e3aa)}p[data-v-b28a9e78]{margin-bottom:4px}blockquote[data-v-b28a9e78]{padding:0 1em;border-left:.25em solid #838989aa}
2 |
--------------------------------------------------------------------------------
/dist/assets/logo-4312ea85.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dist/assets/team-6ddf162b.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/dist/assets/team-6ddf162b.jpg
--------------------------------------------------------------------------------
/dist/assets/usersdomain-73f886cd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/dist/assets/usersdomain-73f886cd.jpg
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | opencatd-open
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/doc/API.md:
--------------------------------------------------------------------------------
1 | # API Documentation
2 |
3 | ## 用户
4 |
5 | ### 初始化用户
6 |
7 | - URL: `/1/users/init`
8 | - Method: `POST`
9 | - Description: 初始化用户
10 |
11 | Resp:
12 | ```
13 | {
14 | "id" : 1,
15 | "updatedAt" : "2023-05-28T18:47:19+08:00",
16 | "name" : "root",
17 | "token" : "df5982d6-d8fb-4eef-a513-24d0831f61ad",
18 | "createdAt" : "2023-05-28T18:47:19+08:00"
19 | }
20 | ```
21 |
22 | ### 获取当前用户信息
23 |
24 | - URL: `/1/me`
25 | - Method: `GET`
26 | - Description: 获取当前用户信息
27 | - Requires authentication: Yes
28 | - Headers:
29 | - Authorization: Bearer {token}
30 |
31 | Resp:
32 | ```
33 | {
34 | "id" : 1,
35 | "updatedAt" : "2023-05-28T18:47:19+08:00",
36 | "name" : "root",
37 | "token" : "df5982d6-d8fb-4eef-a513-24d0831f61ad",
38 | "createdAt" : "2023-05-28T18:47:19+08:00"
39 | }
40 | ```
41 |
42 | ### 获取所有用户信息
43 |
44 | - URL: `/1/users`
45 | - Method: `GET`
46 | - Description: 获取所有用户信息
47 | - Requires authentication: Yes
48 | - Headers:
49 | - Authorization: Bearer {token}
50 |
51 | Resp:
52 | ```
53 | [
54 | {
55 | "createdAt" : "2023-05-28T18:47:19.711027498+08:00",
56 | "id" : 1,
57 | "updatedAt" : "2023-05-28T18:47:19.711027498+08:00",
58 | "IsDelete" : false,
59 | "name" : "root",
60 | "token" : "df5982d6-d8fb-4eef-a513-24d0831f61ad"
61 | },
62 | {
63 | "createdAt" : "2023-05-28T18:48:29.018428441+08:00",
64 | "id" : 2,
65 | "updatedAt" : "2023-05-28T18:48:29.018428441+08:00",
66 | "IsDelete" : false,
67 | "name" : "u1",
68 | "token" : "6ac4bd1a-18a6-4c25-922f-db689a299e38"
69 | }
70 | ]
71 | ```
72 |
73 | ### 添加用户
74 |
75 | - URL: `/1/users`
76 | - Method: `POST`
77 | - Description: 添加用户
78 | - Headers:
79 | - Authorization: Bearer {token}
80 |
81 | Req:
82 | ```
83 | {
84 | "name" : "u1"
85 | }
86 | ```
87 |
88 | Resp:
89 | ```
90 | {
91 | "createdAt" : "2023-05-28T18:48:29.018428441+08:00",
92 | "id" : 2,
93 | "updatedAt" : "2023-05-28T18:48:29.018428441+08:00",
94 | "IsDelete" : false,
95 | "name" : "u1",
96 | "token" : "6ac4bd1a-18a6-4c25-922f-db689a299e38"
97 | }
98 | ```
99 |
100 | ### 删除用户
101 |
102 | - URL: `/1/users/:id`
103 | - Method: `DELETE`
104 | - Description: 删除用户
105 | - Headers:
106 | - Authorization: Bearer {token}
107 |
108 | Resp:
109 | ```
110 | {
111 | "message" : "ok"
112 | }
113 | ```
114 |
115 | ### 重置用户 Token
116 |
117 | - URL: `/1/users/:id/reset`
118 | - URL: `/1/users/:id/reset?token={new user token}`
119 | - Method: `POST`
120 | - Description: 重置用户 Token 默认生成新 Token 也可以指定
121 | - Headers:
122 | - Authorization: Bearer {token}
123 |
124 | Resp:
125 | ```
126 | {
127 | "createdAt" : "2023-05-28T19:33:54.83935763+08:00",
128 | "id" : 2,
129 | "updatedAt" : "2023-05-28T19:34:33.387843151+08:00",
130 | "IsDelete" : false,
131 | "name" : "u2",
132 | "token" : "881a30d2-2fc8-4758-a07e-7d9ad5f34266"
133 | }
134 | ```
135 | ## Key
136 |
137 | ### 获取所有 Key
138 |
139 | - URL: `/1/keys`
140 | - Method: `GET`
141 | - Description: 获取所有 Key
142 | - Headers:
143 | - Authorization: Bearer {token}
144 |
145 | ```
146 | [
147 | {
148 | "id" : 1,
149 | "key" : "sk-zsbdzsbdzsbdzsbdzsbdzsbdzsbd",
150 | "createdAt" : "2023-05-28T18:47:49.936644953+08:00",
151 | "updatedAt" : "2023-05-28T18:47:49.936644953+08:00",
152 | "name" : "key",
153 | "ApiType" : "openai"
154 | },
155 | {
156 | "id" : 2,
157 | "key" : "1234567890qwertyuiopasdfghjklzxcvbnm",
158 | "createdAt" : "2023-05-28T18:48:18.548627422+08:00",
159 | "updatedAt" : "2023-05-28T18:48:18.548627422+08:00",
160 | "name" : "key2",
161 | "ApiType" : "openai"
162 | }
163 | ]
164 | ```
165 |
166 | ### 添加 Key
167 |
168 | - URL: `/1/keys`
169 | - Method: `POST`
170 | - Description: 添加 Key
171 | - Headers:
172 | - Authorization: Bearer {token}
173 |
174 | Req:
175 | ```
176 | {
177 | "key" : "sk-zsbdzsbdzsbdzsbdzsbdzsbdzsbd",
178 | "name" : "key",
179 | "api_type": "openai",
180 | "endpoint": ""
181 |
182 | }
183 | ```
184 | api_type:不传的话默认为“openai”;当前可选值[openai,azure,claude]
185 | endpoint: 当 api_type 为 azure_openai时传入(目前暂未使用)
186 |
187 | Resp:
188 | ```
189 | {
190 | "id" : 1,
191 | "key" : "sk-zsbdzsbdzsbdzsbdzsbdzsbdzsbd",
192 | "createdAt" : "2023-05-28T18:47:49.936644953+08:00",
193 | "updatedAt" : "2023-05-28T18:47:49.936644953+08:00",
194 | "name" : "key",
195 | "ApiType" : "openai"
196 | }
197 | ```
198 |
199 | ### 删除 Key
200 |
201 | - URL: `/1/keys/:id`
202 | - Method: `DELETE`
203 | - Description: 删除 Key
204 | - Headers:
205 | - Authorization: Bearer {token}
206 |
207 | Resp:
208 |
209 | ```
210 | {
211 | "message" : "ok"
212 | }
213 | ```
214 |
215 | ## Usages
216 |
217 | ### 获取用量信息
218 |
219 | - URL: `/1/usages?from=2023-03-18&to=2023-04-18`
220 | - Method: `GET`
221 | - Description: 获取用量信息
222 | - Headers:
223 | - Authorization: Bearer {token}
224 |
225 | Resp:
226 | ```
227 | [
228 | {
229 | "cost" : "0.000110",
230 | "userId" : 1,
231 | "totalUnit" : 55
232 | },
233 | {
234 | "cost" : "0.000110",
235 | "userId" : 2,
236 | "totalUnit" : 55
237 | }
238 | ]
239 | ```
240 |
241 | ## Whisper接口
242 | ### 与openai一致
243 |
--------------------------------------------------------------------------------
/doc/ama_pandora_chat_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/ama_pandora_chat_test.png
--------------------------------------------------------------------------------
/doc/ama_pandora_config_client.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/ama_pandora_config_client.png
--------------------------------------------------------------------------------
/doc/ama_pandora_copy_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/ama_pandora_copy_config.png
--------------------------------------------------------------------------------
/doc/ama_pandora_provider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/ama_pandora_provider.png
--------------------------------------------------------------------------------
/doc/azure.md:
--------------------------------------------------------------------------------
1 | # Azure OpenAI for team
2 |
3 | 1.需要获取 api-key和endpoint [https://[resource name].openai.azure.com/)
4 | 
5 |
6 | > 2.Pleause use model name as deployment name
7 |
8 | | model name | deployment name |
9 | | --- | --- |
10 | |gpt-35-turbo | gpt-35-turbo |
11 | |gpt-35-turbo-16k | gpt-35-turbo-16k |
12 | | gpt-4 | gpt-4 |
13 |
14 | ## How to use
15 | - opencat 使用方式
16 | - key name以 azure.[resource name]的方式添加
17 | - 密钥任取一个
18 | -
19 | - [AMA(问天)](http://bytemyth.com/ama) 使用方式
20 | - 
21 | - 每个 team server 用户旁边有一个复制按钮,点击后,把复制的链接粘贴到浏览器,可以一键设置
22 |
23 | ## Claude
24 |
25 | - opencat 添加Claude api, key name以 "claude.key名称",即("Api类型.Key名称")
26 |
--------------------------------------------------------------------------------
/doc/azure_ama.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/azure_ama.png
--------------------------------------------------------------------------------
/doc/azure_key&endpoint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/azure_key&endpoint.png
--------------------------------------------------------------------------------
/doc/azure_openai_for_team.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/azure_openai_for_team.png
--------------------------------------------------------------------------------
/doc/deploy.md:
--------------------------------------------------------------------------------
1 |
2 | # nginx + docker
3 | 自行安装相关环境,省事直接用了宝塔面板
4 |
5 | docker-compose.yml
6 | ```
7 | version: '3.7'
8 | services:
9 | opencatd:
10 | image: mirrors2/opencatd-open
11 | container_name: opencatd-open
12 | restart: unless-stopped
13 | ports:
14 | - 8088:80
15 | volumes:
16 | - $PWD/db:/app/db
17 | ```
18 | nginx配置
19 | ```
20 | location /
21 | {
22 | proxy_pass http://localhost:8088;
23 | proxy_set_header Host localhost;
24 | proxy_set_header X-Real-IP $remote_addr;
25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
26 | proxy_set_header REMOTE-HOST $remote_addr;
27 | }
28 | ```
--------------------------------------------------------------------------------
/doc/gemini.md:
--------------------------------------------------------------------------------
1 | ## 添加ApiKey
2 | gemini的"ApiType":"google"
3 |
4 | 或者使用 google.xxxx 的apikey名称 添加
5 | 
6 |
--------------------------------------------------------------------------------
/doc/gemini_key.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mirrors2/openteam/1e00905dcb4d06e17273f76f12861af768d9c459/doc/gemini_key.jpg
--------------------------------------------------------------------------------
/doc/pandora.md:
--------------------------------------------------------------------------------
1 | # pandora for team
2 |
3 | [pandora](https://github.com/pengzhile/pandora)是一个把ChatGPT(web/App)接口化的项目,可以看做是第三方 OpenAI API 提供方(接口和OpenAI一致)
4 |
5 | ## 准备
6 | - https://ai.fakeopen.com/auth1 获取accesstoken
7 |
8 | - https://ai.fakeopen.com/token 创建apikey
9 |
10 | ## 客户端设置
11 |
12 | 1.添加接口
13 | 
14 |
15 | 2.创建用户&Copy Config
16 | 
17 |
18 | Ex:`ama://set-api-key?server=http%3A%2F%2F123.456.7.89&key=8fc322fa-15d2-43d7-bc59-621554e82c2a`
19 |
20 | 3.Configure Client
21 | 
22 |
23 | 3.测试聊天
24 | 
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # Email: admin@example.com
2 | # Password: changeme
3 | version: '3'
4 |
5 | services:
6 | npm:
7 | image: jc21/nginx-proxy-manager
8 | network_mode: host
9 | ports:
10 | - '80:80'
11 | - '81:81'
12 | - '443:443'
13 | volumes:
14 | - $PWD/data:/data
15 | - $PWD/www:/var/www
16 | - $PWD/letsencrypt:/etc/letsencrypt
17 | environment:
18 | - "TZ=Asia/Shanghai" # set timezone, default UTC
19 | - "PUID=1000" # set group id, default 0 (root)
20 | - "PGID=1000"
21 |
22 | # certbot:
23 | # image: certbot/certbot
24 | # volumes:
25 | # - $PWD/data/certbot/conf:/etc/letsencrypt
26 | # - $PWD/data/certbot/www:/var/www/certbot
27 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine AS frontend
2 | WORKDIR /frontend-build
3 | COPY ./web/ .
4 | RUN npm install && npm run build && rm -rf node_modules
5 |
6 | FROM golang:1.23-alpine as builder
7 | LABEL anther="github.com/Sakurasan"
8 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk --no-cache add make cmake upx
9 | WORKDIR /build
10 | COPY --from=frontend /frontend-build/dist /build/dist
11 | COPY . /build
12 | ENV GO111MODULE=on
13 | # ENV GOPROXY=https://goproxy.cn,direct
14 | CMD [ "go mod tidy","go mod download" ]
15 | RUN make build
16 |
17 | FROM alpine:latest AS runner
18 | # 设置alpine 时间为上海时间
19 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk --no-cache add tzdata ffmpeg && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
20 | && echo "Asia/Shanghai" > /etc/timezone
21 | # RUN apk update && apk --no-cache add openssl libgcc libstdc++ binutils
22 | WORKDIR /app
23 | COPY --from=builder /build/bin/opencatd /app/opencatd
24 | ENV GIN_MODE=release
25 | ENV PATH=$PATH:/app
26 | EXPOSE 80
27 | ENTRYPOINT ["/app/opencatd"]
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 | services:
3 | opencatd:
4 | image: mirrors2/opencatd-open
5 | container_name: opencatd-open
6 | restart: unless-stopped
7 | #network_mode: host
8 | ports:
9 | - 80:80
10 | volumes:
11 | - $PWD/db:/app/db
12 | logging:
13 | # driver: "json-file"
14 | options:
15 | max-size: 10m
16 | max-file: 3
17 | # environment:
18 | # Vertex: |
19 | # {
20 | # "type": "service_account",
21 | # "universe_domain": "googleapis.com"
22 | # }
23 |
24 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module opencatd-open
2 |
3 | go 1.23.2
4 |
5 | require (
6 | cloud.google.com/go/vertexai v0.13.1
7 | github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d
8 | github.com/coder/websocket v1.8.12
9 | github.com/duke-git/lancet/v2 v2.3.3
10 | github.com/faiface/beep v1.1.0
11 | github.com/gin-gonic/gin v1.10.0
12 | github.com/glebarez/sqlite v1.11.0
13 | github.com/golang-jwt/jwt v3.2.2+incompatible
14 | github.com/google/generative-ai-go v0.18.0
15 | github.com/google/uuid v1.6.0
16 | github.com/gorilla/websocket v1.5.3
17 | github.com/joho/godotenv v1.5.1
18 | github.com/patrickmn/go-cache v2.1.0+incompatible
19 | github.com/pkoukk/tiktoken-go v0.1.7
20 | github.com/sashabaranov/go-openai v1.32.2
21 | github.com/tidwall/gjson v1.18.0
22 | golang.org/x/sync v0.8.0
23 | google.golang.org/api v0.201.0
24 | gopkg.in/vansante/go-ffprobe.v2 v2.2.0
25 | gorm.io/gorm v1.25.12
26 | )
27 |
28 | require (
29 | cloud.google.com/go v0.116.0 // indirect
30 | cloud.google.com/go/ai v0.8.2 // indirect
31 | cloud.google.com/go/aiplatform v1.68.0 // indirect
32 | cloud.google.com/go/auth v0.9.8 // indirect
33 | cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
34 | cloud.google.com/go/compute/metadata v0.5.2 // indirect
35 | cloud.google.com/go/iam v1.2.1 // indirect
36 | cloud.google.com/go/longrunning v0.6.1 // indirect
37 | github.com/bytedance/sonic v1.12.3 // indirect
38 | github.com/bytedance/sonic/loader v0.2.1 // indirect
39 | github.com/cloudwego/base64x v0.1.4 // indirect
40 | github.com/cloudwego/iasm v0.2.0 // indirect
41 | github.com/dlclark/regexp2 v1.11.4 // indirect
42 | github.com/dustin/go-humanize v1.0.1 // indirect
43 | github.com/felixge/httpsnoop v1.0.4 // indirect
44 | github.com/gabriel-vasile/mimetype v1.4.6 // indirect
45 | github.com/gin-contrib/cors v1.7.2 // indirect
46 | github.com/gin-contrib/sse v0.1.0 // indirect
47 | github.com/glebarez/go-sqlite v1.22.0 // indirect
48 | github.com/go-logr/logr v1.4.2 // indirect
49 | github.com/go-logr/stdr v1.2.2 // indirect
50 | github.com/go-playground/locales v0.14.1 // indirect
51 | github.com/go-playground/universal-translator v0.18.1 // indirect
52 | github.com/go-playground/validator/v10 v10.22.1 // indirect
53 | github.com/goccy/go-json v0.10.3 // indirect
54 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
55 | github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect
56 | github.com/google/s2a-go v0.1.8 // indirect
57 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
58 | github.com/googleapis/gax-go/v2 v2.13.0 // indirect
59 | github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
60 | github.com/jinzhu/inflection v1.0.0 // indirect
61 | github.com/jinzhu/now v1.1.5 // indirect
62 | github.com/json-iterator/go v1.1.12 // indirect
63 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect
64 | github.com/leodido/go-urn v1.4.0 // indirect
65 | github.com/mattn/go-isatty v0.0.20 // indirect
66 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
67 | github.com/modern-go/reflect2 v1.0.2 // indirect
68 | github.com/ncruces/go-strftime v0.1.9 // indirect
69 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect
70 | github.com/pkg/errors v0.9.1 // indirect
71 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
72 | github.com/tidwall/match v1.1.1 // indirect
73 | github.com/tidwall/pretty v1.2.0 // indirect
74 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
75 | github.com/ugorji/go/codec v1.2.12 // indirect
76 | go.opencensus.io v0.24.0 // indirect
77 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect
78 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
79 | go.opentelemetry.io/otel v1.31.0 // indirect
80 | go.opentelemetry.io/otel/metric v1.31.0 // indirect
81 | go.opentelemetry.io/otel/trace v1.31.0 // indirect
82 | golang.org/x/arch v0.11.0 // indirect
83 | golang.org/x/crypto v0.28.0 // indirect
84 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
85 | golang.org/x/net v0.30.0 // indirect
86 | golang.org/x/oauth2 v0.23.0 // indirect
87 | golang.org/x/sys v0.26.0 // indirect
88 | golang.org/x/text v0.19.0 // indirect
89 | golang.org/x/time v0.7.0 // indirect
90 | google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 // indirect
91 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect
92 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
93 | google.golang.org/grpc v1.67.1 // indirect
94 | google.golang.org/protobuf v1.35.1 // indirect
95 | gopkg.in/yaml.v3 v3.0.1 // indirect
96 | modernc.org/libc v1.61.0 // indirect
97 | modernc.org/mathutil v1.6.0 // indirect
98 | modernc.org/memory v1.8.0 // indirect
99 | modernc.org/sqlite v1.33.1 // indirect
100 | )
101 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
3 | cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
4 | cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
5 | cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
6 | cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
7 | cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
8 | cloud.google.com/go/ai v0.8.2 h1:LEaQwqBv+k2ybrcdTtCTc9OPZXoEdcQaGrfvDYS6Bnk=
9 | cloud.google.com/go/ai v0.8.2/go.mod h1:Wb3EUUGWwB6yHBaUf/+oxUq/6XbCaU1yh0GrwUS8lr4=
10 | cloud.google.com/go/aiplatform v1.68.0 h1:EPPqgHDJpBZKRvv+OsB3cr0jYz3EL2pZ+802rBPcG8U=
11 | cloud.google.com/go/aiplatform v1.68.0/go.mod h1:105MFA3svHjC3Oazl7yjXAmIR89LKhRAeNdnDKJczME=
12 | cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8=
13 | cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
14 | cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
15 | cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
16 | cloud.google.com/go/compute v1.28.1 h1:XwPcZjgMCnU2tkwY10VleUjSAfpTj9RDn+kGrbYsi8o=
17 | cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
18 | cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
19 | cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU=
20 | cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g=
21 | cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc=
22 | cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0=
23 | cloud.google.com/go/vertexai v0.13.1 h1:E6I+eA6vNQxz7/rb0wdILdKg4hFmMNWZLp+dSy9DnEo=
24 | cloud.google.com/go/vertexai v0.13.1/go.mod h1:25DzKFzP9JByYxcNjJefu/px2dRjcRpCDSdULYL2avI=
25 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
26 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
27 | github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d h1:3v1QFdgk450QH+7C+lw1k+olbjK4fKGsrEfnEG/HLkY=
28 | github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d/go.mod h1:2sp0vsMyh5sqmKl5N+ps/cSspqLkoXUlesSzsufIGRU=
29 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
30 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
31 | github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
32 | github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
33 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
34 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
35 | github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
36 | github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
37 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
38 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
39 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
40 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
41 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
42 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
43 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
44 | github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
45 | github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
46 | github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
47 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
48 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
49 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
50 | github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
51 | github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
52 | github.com/duke-git/lancet/v2 v2.3.2 h1:Cv+uNkx5yGqDSvGc5Vu9eiiZobsPIf0Ng7NGy5hEdow=
53 | github.com/duke-git/lancet/v2 v2.3.2/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
54 | github.com/duke-git/lancet/v2 v2.3.3 h1:OhqzNzkbJBS9ZlWLo/C7g+WSAOAAyNj7p9CAiEHurUc=
55 | github.com/duke-git/lancet/v2 v2.3.3/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
56 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
57 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
58 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
59 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
60 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
61 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
62 | github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
63 | github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
64 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
65 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
66 | github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
67 | github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
68 | github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
69 | github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
70 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
71 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
72 | github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
73 | github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
74 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
75 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
76 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
77 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
78 | github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
79 | github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
80 | github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
81 | github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
82 | github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
83 | github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
84 | github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
85 | github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
86 | github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
87 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
88 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
89 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
90 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
91 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
92 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
93 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
94 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
95 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
96 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
97 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
98 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
99 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
100 | github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
101 | github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
102 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
103 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
104 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
105 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
106 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
107 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
108 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
109 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
110 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
111 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
112 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
113 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
114 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
115 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
116 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
117 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
118 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
119 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
120 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
121 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
122 | github.com/google/generative-ai-go v0.18.0 h1:6ybg9vOCLcI/UpBBYXOTVgvKmcUKFRNj+2Cj3GnebSo=
123 | github.com/google/generative-ai-go v0.18.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
124 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
125 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
126 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
127 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
128 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
129 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
130 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
131 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
132 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
133 | github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA=
134 | github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
135 | github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
136 | github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
137 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
138 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
139 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
140 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
141 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
142 | github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
143 | github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
144 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
145 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
146 | github.com/hajimehoshi/go-mp3 v0.3.0 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bHy4g=
147 | github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
148 | github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
149 | github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
150 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
151 | github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
152 | github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
153 | github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
154 | github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
155 | github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
156 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
157 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
158 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
159 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
160 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
161 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
162 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
163 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
164 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
165 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
166 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
167 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
168 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
169 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
170 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
171 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
172 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
173 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
174 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
175 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
176 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
177 | github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
178 | github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
179 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
180 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
181 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
182 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
183 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
184 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
185 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
186 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
187 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
188 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
189 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
190 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
191 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
192 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
193 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
194 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
195 | github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
196 | github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
197 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
198 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
199 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
200 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
201 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
202 | github.com/sashabaranov/go-openai v1.31.0 h1:rGe77x7zUeCjtS2IS7NCY6Tp4bQviXNMhkQM6hz/UC4=
203 | github.com/sashabaranov/go-openai v1.31.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
204 | github.com/sashabaranov/go-openai v1.32.2 h1:8z9PfYaLPbRzmJIYpwcWu6z3XU8F+RwVMF1QRSeSF2M=
205 | github.com/sashabaranov/go-openai v1.32.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
206 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
207 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
208 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
209 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
210 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
211 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
212 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
213 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
214 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
215 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
216 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
217 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
218 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
219 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
220 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
221 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
222 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
223 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
224 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
225 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
226 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
227 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
228 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
229 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
230 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
231 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
232 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0=
233 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8=
234 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
235 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
236 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
237 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
238 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
239 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
240 | go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
241 | go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
242 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
243 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
244 | go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
245 | go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
246 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
247 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
248 | go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
249 | go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
250 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
251 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
252 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
253 | golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
254 | golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
255 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
256 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
257 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
258 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
259 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
260 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
261 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
262 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
263 | golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
264 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
265 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
266 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
267 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
268 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
269 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
270 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
271 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
272 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
273 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
274 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
275 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
276 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
277 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
278 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
279 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
280 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
281 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
282 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
283 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
284 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
285 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
286 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
287 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
288 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
289 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
290 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
291 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
292 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
293 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
294 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
295 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
296 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
297 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
298 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
299 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
300 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
301 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
302 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
303 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
304 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
305 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
306 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
307 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
308 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
309 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
310 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
311 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
312 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
313 | google.golang.org/api v0.200.0 h1:0ytfNWn101is6e9VBoct2wrGDjOi5vn7jw5KtaQgDrU=
314 | google.golang.org/api v0.200.0/go.mod h1:Tc5u9kcbjO7A8SwGlYj4IiVifJU01UqXtEgDMYmBmV8=
315 | google.golang.org/api v0.201.0 h1:+7AD9JNM3tREtawRMu8sOjSbb8VYcYXJG/2eEOmfDu0=
316 | google.golang.org/api v0.201.0/go.mod h1:HVY0FCHVs89xIW9fzf/pBvOEm+OolHa86G/txFezyq4=
317 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
318 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
319 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
320 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
321 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
322 | google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 h1:nFS3IivktIU5Mk6KQa+v6RKkHUpdQpphqGNLxqNnbEk=
323 | google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:tEzYTYZxbmVNOu0OAFH9HzdJtLn6h4Aj89zzlBCdHms=
324 | google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f h1:jTm13A2itBi3La6yTGqn8bVSrc3ZZ1r8ENHlIXBfnRA=
325 | google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f/go.mod h1:CLGoBuH1VHxAUXVPP8FfPwPEVJB6lz3URE5mY2SuayE=
326 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U=
327 | google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4=
328 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
329 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
330 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
331 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
332 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
333 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
334 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
335 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
336 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
337 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
338 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
339 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
340 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
341 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
342 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
343 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
344 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
345 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
346 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
347 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
348 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
349 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
350 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
351 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
352 | gopkg.in/vansante/go-ffprobe.v2 v2.2.0 h1:iuOqTsbfYuqIz4tAU9NWh22CmBGxlGHdgj4iqP+NUmY=
353 | gopkg.in/vansante/go-ffprobe.v2 v2.2.0/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=
354 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
355 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
356 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
357 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
358 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
359 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
360 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
361 | modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
362 | modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
363 | modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
364 | modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
365 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
366 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
367 | modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
368 | modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
369 | modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
370 | modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
371 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
372 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
373 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
374 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
375 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
376 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
377 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
378 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
379 | modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
380 | modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
381 | modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
382 | modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
383 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
384 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
385 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
386 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
387 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | GOPATH:=$(shell go env GOPATH)
2 | VERSION=$(shell git describe --tags --always)
3 | # 获取源码最近一次 git commit log,包含 commit sha 值,以及 commit message
4 | GitCommitLog=$(shell git log)
5 | # 检查源码在最近一次 git commit 基础上,是否有本地修改,且未提交的文件
6 | GitStatus=$(shell git status -s)
7 | # 获取当前时间
8 | BuildTime=$(shell date +'%Y.%m.%d %H:%M:%S')
9 | # 获取Go的版本
10 | BuildGoVersion=$(shell go version)
11 |
12 | LDFlags=" \
13 | -X 'main.Version=$(VERSION)' \
14 | -X 'main.GitCommitLog=$(GitCommitLog)' \
15 | -X 'main.BuildTime=$(BuildTime)' \
16 | -X 'main.BuildGoVersion=$(BuildGoVersion)'"
17 |
18 | .PHONY: web
19 | # web
20 | web:
21 | cd web && npm install && npm run build && mv dist ..
22 |
23 | .PHONY: build
24 | # build
25 | build:
26 | # mkdir -p bin/ && go build -ldflags $(LDFlags) -o ./bin/ ./...
27 | rm -rf bin
28 | mkdir -p bin/ && go build -ldflags "-s -w" -o ./bin/opencatd .
29 | upx -9 bin/opencatd
30 |
31 | .PHONY:docker
32 | # build docker images
33 | docker:
34 | docker run --privileged --rm tonistiigi/binfmt --install all
35 | docker buildx create --use --name xbuilder --driver docker-container
36 | docker buildx inspect xbuilder --bootstrap
37 | docker buildx build --platform linux/amd64,linux/arm64 -t mirrors2/opencatd:latest -f docker/Dockerfile . --push
38 |
39 | .PHONY: clean
40 | # clean
41 | clean:
42 | rm -rf bin/
43 |
44 | .PHONY: all
45 | # generate all
46 | all:
47 | make build;
48 |
49 |
50 | # show help
51 | help:
52 | @echo ''
53 | @echo 'Usage:'
54 | @echo ' make [target]'
55 | @echo ''
56 | @echo 'Targets:'
57 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \
58 | helpMessage = match(lastLine, /^# (.*)/); \
59 | if (helpMessage) { \
60 | helpCommand = substr($$1, 0, index($$1, ":")-1); \
61 | helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
62 | printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \
63 | } \
64 | } \
65 | { lastLine = $$0 }' $(MAKEFILE_LIST)
66 |
67 | .DEFAULT_GOAL := help
68 |
--------------------------------------------------------------------------------
/opencat.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "embed"
6 | "encoding/json"
7 | "fmt"
8 | "io/fs"
9 | "log"
10 | "net/http"
11 | "opencatd-open/pkg/team"
12 | "opencatd-open/router"
13 | "opencatd-open/store"
14 | "os"
15 |
16 | "github.com/duke-git/lancet/v2/fileutil"
17 | "github.com/gin-gonic/gin"
18 | "github.com/google/uuid"
19 | "gorm.io/gorm"
20 | )
21 |
22 | //go:embed dist/*
23 | var web embed.FS
24 |
25 | func getFileSystem(path string) http.FileSystem {
26 | fs, err := fs.Sub(web, path)
27 | if err != nil {
28 | panic(err)
29 | }
30 |
31 | return http.FS(fs)
32 | }
33 |
34 | func main() {
35 | args := os.Args[1:]
36 | if len(args) > 0 {
37 | type user struct {
38 | ID uint
39 | Name string
40 | Token string
41 | }
42 | var us []user
43 | switch args[0] {
44 | case "reset_root":
45 | log.Println("reset root token...")
46 | if _, err := store.GetUserByID(uint(1)); err != nil {
47 | if err == gorm.ErrRecordNotFound {
48 | log.Println("请在opencat(或其他APP)客户端完成team初始化")
49 | return
50 | } else {
51 | log.Fatalln(err)
52 | return
53 | }
54 | }
55 | ntoken := uuid.NewString()
56 | if err := store.UpdateUser(uint(1), ntoken); err != nil {
57 | log.Fatalln(err)
58 | return
59 | }
60 | log.Println("[success]new root token:", ntoken)
61 | return
62 | case "root_token":
63 | log.Println("query root token...")
64 | if user, err := store.GetUserByID(uint(1)); err != nil {
65 | log.Fatalln(err)
66 | return
67 | } else {
68 | log.Println("[success]root token:", user.Token)
69 | return
70 | }
71 | case "save":
72 | log.Println("backup user info -> user.json")
73 | if users, err := store.GetAllUsers(); err != nil {
74 | log.Fatalln(err)
75 | return
76 | } else {
77 | for _, u := range users {
78 | us = append(us, user{ID: u.ID, Name: u.Name, Token: u.Token})
79 | }
80 | }
81 | if !fileutil.IsExist("./db/user.json") {
82 | file, err := os.Create("./db/user.json")
83 | if err != nil {
84 | log.Fatalln(err)
85 | return
86 | }
87 | defer file.Close()
88 | } else {
89 | // 文件存在,打开文件
90 | file, _ := os.OpenFile("./db/user.json", os.O_RDWR|os.O_TRUNC, 0666)
91 | defer file.Close()
92 |
93 | buff := bytes.NewBuffer(nil)
94 | json.NewEncoder(buff).Encode(us)
95 |
96 | file.WriteString(buff.String())
97 | fmt.Println("------- END -------")
98 | return
99 | }
100 | case "load":
101 | fmt.Println("\nimport user.json -> db")
102 | if !fileutil.IsExist("./db/user.json") {
103 | log.Fatalln("404! user.json is not found.")
104 | return
105 | }
106 | users, err := store.GetAllUsers()
107 | if err != nil {
108 | log.Println(err)
109 | return
110 | }
111 | if len(users) != 0 {
112 | log.Println("user db 存在数据,取消导入")
113 | return
114 | }
115 | file, err := os.Open("./db/user.json")
116 | if err != nil {
117 | fmt.Println("Error opening file:", err)
118 | return
119 | }
120 | defer file.Close()
121 |
122 | decoder := json.NewDecoder(file)
123 | err = decoder.Decode(&us)
124 | if err != nil {
125 | fmt.Println("Error decoding JSON:", err)
126 | return
127 | }
128 | for _, u := range us {
129 | log.Println(u.ID, u.Name, u.Token)
130 | err := store.CreateUser(&store.User{ID: u.ID, Name: u.Name, Token: u.Token})
131 | if err != nil {
132 | log.Println(err)
133 | }
134 | }
135 | fmt.Println("------- END -------")
136 | return
137 |
138 | default:
139 | return
140 | }
141 |
142 | }
143 | port := os.Getenv("PORT")
144 | r := gin.Default()
145 | r.Use(team.CORS())
146 | group := r.Group("/1")
147 | {
148 | group.Use(team.AuthMiddleware())
149 |
150 | // 获取当前用户信息
151 | group.GET("/me", team.HandleMe)
152 | group.GET("/me/usages", team.HandleMeUsage)
153 |
154 | group.GET("/keys", team.HandleKeys) // 获取所有Key
155 | group.POST("/keys", team.HandleAddKey) // 添加Key
156 | group.DELETE("/keys/:id", team.HandleDelKey) // 删除Key
157 |
158 | group.GET("/users", team.HandleUsers) // 获取所有用户信息
159 | group.POST("/users", team.HandleAddUser) // 添加用户
160 | group.DELETE("/users/:id", team.HandleDelUser) // 删除用户
161 |
162 | group.GET("/usages", team.HandleUsage)
163 |
164 | // 重置用户Token
165 | group.POST("/users/:id/reset", team.HandleResetUserToken)
166 | }
167 | // 初始化用户
168 | r.POST("/1/users/init", team.Handleinit)
169 |
170 | r.Any("/v1/*proxypath", router.HandleProxy)
171 |
172 | // r.POST("/v1/chat/completions", router.HandleProy)
173 | // r.GET("/v1/models", router.HandleProy)
174 | // r.GET("/v1/dashboard/billing/subscription", router.HandleProy)
175 |
176 | // r.Use(static.Serve("/", static.LocalFile("dist", false)))
177 | idxFS, err := fs.Sub(web, "dist")
178 | if err != nil {
179 | panic(err)
180 | }
181 | redirect := os.Getenv("CUSTOM_REDIRECT")
182 | if redirect != "" {
183 | r.GET("/", func(c *gin.Context) {
184 | c.Redirect(http.StatusMovedPermanently, redirect)
185 | })
186 |
187 | } else {
188 | r.GET("/", gin.WrapH(http.FileServer(http.FS(idxFS))))
189 | }
190 | assetsFS, err := fs.Sub(web, "dist/assets")
191 | if err != nil {
192 | panic(err)
193 | }
194 | r.GET("/assets/*filepath", gin.WrapH(http.StripPrefix("/assets/", http.FileServer(http.FS(assetsFS)))))
195 | if port == "" {
196 | port = "80"
197 | }
198 | r.Run(":" + port)
199 | }
200 |
--------------------------------------------------------------------------------
/pkg/azureopenai/azureopenai.go:
--------------------------------------------------------------------------------
1 | /*
2 | https://learn.microsoft.com/zh-cn/azure/cognitive-services/openai/chatgpt-quickstart
3 | https://learn.microsoft.com/zh-cn/azure/ai-services/openai/reference#chat-completions
4 |
5 | curl $AZURE_OPENAI_ENDPOINT/openai/deployments/gpt-35-turbo/chat/completions?api-version=2023-03-15-preview \
6 | -H "Content-Type: application/json" \
7 | -H "api-key: $AZURE_OPENAI_KEY" \
8 | -d '{
9 | "model": "gpt-3.5-turbo",
10 | "messages": [{"role": "user", "content": "你好"}]
11 | }'
12 |
13 | https://learn.microsoft.com/zh-cn/rest/api/cognitiveservices/azureopenaistable/models/list?tabs=HTTP
14 |
15 | curl $AZURE_OPENAI_ENDPOINT/openai/deployments?api-version=2022-12-01 \
16 | -H "Content-Type: application/json" \
17 | -H "api-key: $AZURE_OPENAI_KEY" \
18 |
19 | > GPT-4 Turbo
20 | https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-service-launches-gpt-4-turbo-and-gpt-3-5-turbo-1106/ba-p/3985962
21 |
22 | */
23 |
24 | package azureopenai
25 |
26 | import (
27 | "encoding/json"
28 | "net/http"
29 | "regexp"
30 | "strings"
31 | )
32 |
33 | var (
34 | ENDPOINT string
35 | API_KEY string
36 | DEPLOYMENT_NAME string
37 | )
38 |
39 | type ModelsList struct {
40 | Data []struct {
41 | ScaleSettings struct {
42 | ScaleType string `json:"scale_type"`
43 | } `json:"scale_settings"`
44 | Model string `json:"model"`
45 | Owner string `json:"owner"`
46 | ID string `json:"id"`
47 | Status string `json:"status"`
48 | CreatedAt int `json:"created_at"`
49 | UpdatedAt int `json:"updated_at"`
50 | Object string `json:"object"`
51 | } `json:"data"`
52 | Object string `json:"object"`
53 | }
54 |
55 | func Models(endpoint, apikey string) (*ModelsList, error) {
56 | endpoint = RemoveTrailingSlash(endpoint)
57 | var modelsl ModelsList
58 | req, _ := http.NewRequest(http.MethodGet, endpoint+"/openai/deployments?api-version=2022-12-01", nil)
59 | req.Header.Set("api-key", apikey)
60 | resp, err := http.DefaultClient.Do(req)
61 | if err != nil {
62 | return nil, err
63 | }
64 | defer resp.Body.Close()
65 | err = json.NewDecoder(resp.Body).Decode(&modelsl)
66 | if err != nil {
67 | return nil, err
68 | }
69 | return &modelsl, nil
70 |
71 | }
72 |
73 | func RemoveTrailingSlash(s string) string {
74 | const prefix = "openai.azure.com/"
75 | if strings.HasSuffix(strings.TrimSpace(s), prefix) && strings.HasSuffix(s, "/") {
76 | return s[:len(s)-1]
77 | }
78 | return s
79 | }
80 |
81 | func GetResourceName(url string) string {
82 | re := regexp.MustCompile(`https?://(.+)\.openai\.azure\.com/?`)
83 | match := re.FindStringSubmatch(url)
84 | if len(match) > 1 {
85 | return match[1]
86 | }
87 | return ""
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/claude/chat.go:
--------------------------------------------------------------------------------
1 | // https://docs.anthropic.com/claude/reference/messages_post
2 |
3 | package claude
4 |
5 | import (
6 | "bufio"
7 | "bytes"
8 | "encoding/json"
9 | "fmt"
10 | "io"
11 | "log"
12 | "net/http"
13 | "opencatd-open/pkg/error"
14 | "opencatd-open/pkg/openai"
15 | "opencatd-open/pkg/tokenizer"
16 | "opencatd-open/pkg/vertexai"
17 | "opencatd-open/store"
18 | "strings"
19 |
20 | "github.com/gin-gonic/gin"
21 | )
22 |
23 | func ChatProxy(c *gin.Context, chatReq *openai.ChatCompletionRequest) {
24 | ChatMessages(c, chatReq)
25 | }
26 |
27 | func ChatTextCompletions(c *gin.Context, chatReq *openai.ChatCompletionRequest) {
28 |
29 | }
30 |
31 | type ChatRequest struct {
32 | Model string `json:"model,omitempty"`
33 | Messages any `json:"messages,omitempty"`
34 | MaxTokens int `json:"max_tokens,omitempty"`
35 | Stream bool `json:"stream,omitempty"`
36 | System string `json:"system,omitempty"`
37 | TopK int `json:"top_k,omitempty"`
38 | TopP float64 `json:"top_p,omitempty"`
39 | Temperature float64 `json:"temperature,omitempty"`
40 | AnthropicVersion string `json:"anthropic_version,omitempty"`
41 | }
42 |
43 | func (c *ChatRequest) ByteJson() []byte {
44 | bytejson, _ := json.Marshal(c)
45 | return bytejson
46 | }
47 |
48 | type ChatMessage struct {
49 | Role string `json:"role,omitempty"`
50 | Content string `json:"content,omitempty"`
51 | }
52 |
53 | type VisionMessages struct {
54 | Role string `json:"role,omitempty"`
55 | Content []VisionContent `json:"content,omitempty"`
56 | }
57 |
58 | type VisionContent struct {
59 | Type string `json:"type,omitempty"`
60 | Source *VisionSource `json:"source,omitempty"`
61 | Text string `json:"text,omitempty"`
62 | }
63 |
64 | type VisionSource struct {
65 | Type string `json:"type,omitempty"`
66 | MediaType string `json:"media_type,omitempty"`
67 | Data string `json:"data,omitempty"`
68 | }
69 |
70 | type ChatResponse struct {
71 | ID string `json:"id"`
72 | Type string `json:"type"`
73 | Role string `json:"role"`
74 | Model string `json:"model"`
75 | StopSequence any `json:"stop_sequence"`
76 | Usage struct {
77 | InputTokens int `json:"input_tokens"`
78 | OutputTokens int `json:"output_tokens"`
79 | } `json:"usage"`
80 | Content []struct {
81 | Type string `json:"type"`
82 | Text string `json:"text"`
83 | } `json:"content"`
84 | StopReason string `json:"stop_reason"`
85 | }
86 |
87 | type ClaudeStreamResponse struct {
88 | Type string `json:"type"`
89 | Index int `json:"index"`
90 | ContentBlock struct {
91 | Type string `json:"type"`
92 | Text string `json:"text"`
93 | } `json:"content_block"`
94 | Delta struct {
95 | Type string `json:"type"`
96 | Text string `json:"text"`
97 | StopReason string `json:"stop_reason"`
98 | StopSequence any `json:"stop_sequence"`
99 | } `json:"delta"`
100 | Message struct {
101 | ID string `json:"id"`
102 | Type string `json:"type"`
103 | Role string `json:"role"`
104 | Content []any `json:"content"`
105 | Model string `json:"model"`
106 | StopReason string `json:"stop_reason"`
107 | StopSequence any `json:"stop_sequence"`
108 | Usage struct {
109 | InputTokens int `json:"input_tokens"`
110 | OutputTokens int `json:"output_tokens"`
111 | } `json:"usage"`
112 | } `json:"message"`
113 | Error struct {
114 | Type string `json:"type"`
115 | Message string `json:"message"`
116 | } `json:"error"`
117 | Usage struct {
118 | OutputTokens int `json:"output_tokens"`
119 | } `json:"usage"`
120 | }
121 |
122 | func ChatMessages(c *gin.Context, chatReq *openai.ChatCompletionRequest) {
123 | var (
124 | req *http.Request
125 | targetURL = ClaudeMessageEndpoint
126 | )
127 |
128 | apiKey, err := store.SelectKeyCache("claude")
129 | if err != nil {
130 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
131 | return
132 | }
133 |
134 | usagelog := store.Tokens{Model: chatReq.Model}
135 | var claudReq ChatRequest
136 | claudReq.Model = chatReq.Model
137 | claudReq.Stream = chatReq.Stream
138 | // claudReq.Temperature = chatReq.Temperature
139 | claudReq.TopP = chatReq.TopP
140 | claudReq.MaxTokens = 4096
141 | if apiKey.ApiType == "vertex" {
142 | claudReq.AnthropicVersion = "vertex-2023-10-16"
143 | claudReq.Model = ""
144 | }
145 |
146 | var claudecontent []VisionContent
147 | var prompt string
148 | for _, msg := range chatReq.Messages {
149 | switch ct := msg.Content.(type) {
150 | case string:
151 | prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n"
152 | if msg.Role == "system" {
153 | claudReq.System = msg.Content.(string)
154 | continue
155 | }
156 | claudecontent = append(claudecontent, VisionContent{Type: "text", Text: msg.Role + ":" + msg.Content.(string)})
157 | case []any:
158 | for _, item := range ct {
159 | if m, ok := item.(map[string]interface{}); ok {
160 | if m["type"] == "text" {
161 | prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n"
162 | claudecontent = append(claudecontent, VisionContent{Type: "text", Text: msg.Role + ":" + m["text"].(string)})
163 | } else if m["type"] == "image_url" {
164 | if url, ok := m["image_url"].(map[string]interface{}); ok {
165 | fmt.Printf(" URL: %v\n", url["url"])
166 | if strings.HasPrefix(url["url"].(string), "http") {
167 | fmt.Println("网络图片:", url["url"].(string))
168 | } else if strings.HasPrefix(url["url"].(string), "data:image") {
169 | fmt.Println("base64:", url["url"].(string)[:20])
170 | var mediaType string
171 | if strings.HasPrefix(url["url"].(string), "data:image/jpeg") {
172 | mediaType = "image/jpeg"
173 | }
174 | if strings.HasPrefix(url["url"].(string), "data:image/png") {
175 | mediaType = "image/png"
176 | }
177 | claudecontent = append(claudecontent, VisionContent{Type: "image", Source: &VisionSource{Type: "base64", MediaType: mediaType, Data: strings.Split(url["url"].(string), ",")[1]}})
178 | }
179 | }
180 | }
181 | }
182 | }
183 | default:
184 | c.JSON(http.StatusInternalServerError, gin.H{
185 | "error": gin.H{
186 | "message": "Invalid content type",
187 | },
188 | })
189 | return
190 | }
191 | if len(chatReq.Tools) > 0 {
192 | tooljson, _ := json.Marshal(chatReq.Tools)
193 | prompt += ": " + string(tooljson) + "\n"
194 | }
195 | }
196 |
197 | claudReq.Messages = []VisionMessages{{Role: "user", Content: claudecontent}}
198 |
199 | usagelog.PromptCount = tokenizer.NumTokensFromStr(prompt, chatReq.Model)
200 |
201 | if apiKey.ApiType == "vertex" {
202 | var vertexSecret vertexai.VertexSecretKey
203 | if err := json.Unmarshal([]byte(apiKey.ApiSecret), &vertexSecret); err != nil {
204 | c.JSON(http.StatusInternalServerError, error.ErrorData(err.Error()))
205 | return
206 | }
207 |
208 | vcmodel, ok := vertexai.VertexClaudeModelMap[chatReq.Model]
209 | if !ok {
210 | c.JSON(http.StatusInternalServerError, error.ErrorData("Model not found"))
211 | return
212 | }
213 |
214 | // 获取gcloud token,临时放置在apiKey.Key中
215 | gcloudToken, err := vertexai.GcloudAuth(vertexSecret.ClientEmail, vertexSecret.PrivateKey)
216 | if err != nil {
217 | c.JSON(http.StatusInternalServerError, error.ErrorData(err.Error()))
218 | return
219 | }
220 |
221 | // 拼接vertex的请求地址
222 | targetURL = fmt.Sprintf("https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:streamRawPredict", vcmodel.Region, vertexSecret.ProjectID, vcmodel.Region, vcmodel.VertexName)
223 |
224 | req, _ = http.NewRequest("POST", targetURL, bytes.NewReader(claudReq.ByteJson()))
225 | req.Header.Set("Authorization", "Bearer "+gcloudToken)
226 | req.Header.Set("Content-Type", "application/json")
227 | req.Header.Set("Accept", "text/event-stream")
228 | req.Header.Set("Accept-Encoding", "identity")
229 | } else {
230 | req, _ = http.NewRequest("POST", targetURL, bytes.NewReader(claudReq.ByteJson()))
231 | req.Header.Set("x-api-key", apiKey.Key)
232 | req.Header.Set("anthropic-version", "2023-06-01")
233 | req.Header.Set("Content-Type", "application/json")
234 | }
235 |
236 | client := http.DefaultClient
237 | rsp, err := client.Do(req)
238 | if err != nil {
239 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
240 | return
241 | }
242 | defer rsp.Body.Close()
243 | if rsp.StatusCode != http.StatusOK {
244 | io.Copy(c.Writer, rsp.Body)
245 | return
246 | }
247 | var buffer bytes.Buffer
248 | teeReader := io.TeeReader(rsp.Body, &buffer)
249 |
250 | dataChan := make(chan string)
251 | // stopChan := make(chan bool)
252 |
253 | var result string
254 |
255 | scanner := bufio.NewScanner(teeReader)
256 |
257 | go func() {
258 | for scanner.Scan() {
259 | line := scanner.Bytes()
260 | if len(line) > 0 && bytes.HasPrefix(line, []byte("data: ")) {
261 | if bytes.HasPrefix(line, []byte("data: [DONE]")) {
262 | dataChan <- string(line) + "\n"
263 | break
264 | }
265 | var claudeResp ClaudeStreamResponse
266 | line = bytes.Replace(line, []byte("data: "), []byte(""), -1)
267 | line = bytes.TrimSpace(line)
268 | if err := json.Unmarshal(line, &claudeResp); err != nil {
269 | continue
270 | }
271 |
272 | if claudeResp.Type == "message_start" {
273 | if claudeResp.Message.Role != "" {
274 | result += "<" + claudeResp.Message.Role + ">"
275 | }
276 | } else if claudeResp.Type == "message_stop" {
277 | break
278 | }
279 |
280 | if claudeResp.Delta.Text != "" {
281 | result += claudeResp.Delta.Text
282 | }
283 | var choice openai.Choice
284 | choice.Delta.Role = claudeResp.Message.Role
285 | choice.Delta.Content = claudeResp.Delta.Text
286 | choice.FinishReason = claudeResp.Delta.StopReason
287 |
288 | chatResp := openai.ChatCompletionStreamResponse{
289 | Model: chatReq.Model,
290 | Choices: []openai.Choice{choice},
291 | }
292 | dataChan <- "data: " + string(chatResp.ByteJson()) + "\n"
293 | if claudeResp.Delta.StopReason != "" {
294 | dataChan <- "\ndata: [DONE]\n"
295 | }
296 | }
297 | }
298 | defer close(dataChan)
299 | }()
300 |
301 | c.Writer.Header().Set("Content-Type", "text/event-stream")
302 | c.Writer.Header().Set("Cache-Control", "no-cache")
303 | c.Writer.Header().Set("Connection", "keep-alive")
304 | c.Writer.Header().Set("Transfer-Encoding", "chunked")
305 | c.Writer.Header().Set("X-Accel-Buffering", "no")
306 |
307 | c.Stream(func(w io.Writer) bool {
308 | if data, ok := <-dataChan; ok {
309 | if strings.HasPrefix(data, "data: ") {
310 | c.Writer.WriteString(data)
311 | // c.Writer.WriteString("\n\n")
312 | } else {
313 | c.Writer.WriteHeader(http.StatusBadGateway)
314 | c.Writer.WriteString(data)
315 | }
316 | c.Writer.Flush()
317 | return true
318 | }
319 | go func() {
320 | usagelog.CompletionCount = tokenizer.NumTokensFromStr(result, chatReq.Model)
321 | usagelog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(usagelog.Model, usagelog.PromptCount, usagelog.CompletionCount))
322 | if err := store.Record(&usagelog); err != nil {
323 | log.Println(err)
324 | }
325 | if err := store.SumDaily(usagelog.UserID); err != nil {
326 | log.Println(err)
327 | }
328 | }()
329 | return false
330 | })
331 | }
332 |
--------------------------------------------------------------------------------
/pkg/claude/claude.go:
--------------------------------------------------------------------------------
1 | /*
2 | https://docs.anthropic.com/claude/reference/complete_post
3 |
4 | curl --request POST \
5 | --url https://api.anthropic.com/v1/complete \
6 | --header "anthropic-version: 2023-06-01" \
7 | --header "content-type: application/json" \
8 | --header "x-api-key: $ANTHROPIC_API_KEY" \
9 | --data '
10 | {
11 | "model": "claude-2",
12 | "prompt": "\n\nHuman: Hello, world!\n\nAssistant:",
13 | "max_tokens_to_sample": 256,
14 | "stream": true
15 | }
16 | '
17 |
18 | {"completion":" Hello! Nice to meet you.","stop_reason":"stop_sequence","model":"claude-2.0","stop":"\n\nHuman:","log_id":"727bded01002627057967d02b3d557a01aa73266849b62f5aa0b97dec1247ed3"}
19 |
20 | event: completion
21 | data: {"completion":"","stop_reason":"stop_sequence","model":"claude-2.0","stop":"\n\nHuman:","log_id":"dfd42341ad08856ff01811885fb8640a1bf977551d8331f81fe9a6c8182c6c63"}
22 |
23 | # Model Pricing
24 |
25 | Claude Instant |100,000 tokens |Prompt $1.63/million tokens |Completion $5.51/million tokens
26 |
27 | Claude 2 |100,000 tokens |Prompt $11.02/million tokens |Completion $32.68/million tokens
28 | *Claude 1 is still accessible and offered at the same price as Claude 2.
29 |
30 | # AWS
31 | https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-service.html
32 | https://aws.amazon.com/cn/bedrock/pricing/
33 | Anthropic models Price for 1000 input tokens Price for 1000 output tokens
34 | Claude Instant $0.00163 $0.00551
35 |
36 | Claude $0.01102 $0.03268
37 |
38 | https://docs.aws.amazon.com/bedrock/latest/userguide/endpointsTable.html
39 | 地区名称 地区 端点 协议
40 | 美国东部(弗吉尼亚北部) 美国东部1 bedrock-runtime.us-east-1.amazonaws.com HTTPS
41 | bedrock-runtime-fips.us-east-1.amazonaws.com HTTPS
42 | 美国西部(俄勒冈州) 美国西2号 bedrock-runtime.us-west-2.amazonaws.com HTTPS
43 | bedrock-runtime-fips.us-west-2.amazonaws.com HTTPS
44 | 亚太地区(新加坡) ap-东南-1 bedrock-runtime.ap-southeast-1.amazonaws.com HTTPS
45 | */
46 |
47 | // package anthropic
48 | package claude
49 |
50 | import (
51 | "bufio"
52 | "bytes"
53 | "encoding/json"
54 | "fmt"
55 | "io"
56 | "log"
57 | "net/http"
58 | "net/http/httputil"
59 | "net/url"
60 | "opencatd-open/pkg/tokenizer"
61 | "opencatd-open/store"
62 | "strings"
63 | "sync"
64 | "time"
65 |
66 | "github.com/gin-gonic/gin"
67 | "github.com/sashabaranov/go-openai"
68 | )
69 |
70 | var (
71 | ClaudeUrl = "https://api.anthropic.com/v1/complete"
72 | ClaudeMessageEndpoint = "https://api.anthropic.com/v1/messages"
73 | )
74 |
75 | type MessageModule struct {
76 | Assistant string // returned data (do not modify)
77 | Human string // input content
78 | }
79 |
80 | type CompleteRequest struct {
81 | Model string `json:"model,omitempty"` //*
82 | Prompt string `json:"prompt,omitempty"` //*
83 | MaxTokensToSample int `json:"max_tokens_to_sample,omitempty"` //*
84 | StopSequences string `json:"stop_sequences,omitempty"`
85 | Temperature int `json:"temperature,omitempty"`
86 | TopP int `json:"top_p,omitempty"`
87 | TopK int `json:"top_k,omitempty"`
88 | Stream bool `json:"stream,omitempty"`
89 | Metadata struct {
90 | UserId string `json:"user_Id,omitempty"`
91 | } `json:"metadata,omitempty"`
92 | }
93 |
94 | type CompleteResponse struct {
95 | Completion string `json:"completion"`
96 | StopReason string `json:"stop_reason"`
97 | Model string `json:"model"`
98 | Stop string `json:"stop"`
99 | LogID string `json:"log_id"`
100 | }
101 |
102 | func Create() {
103 | complet := CompleteRequest{
104 | Model: "claude-2",
105 | Prompt: "Human: Hello, world!\\n\\nAssistant:",
106 | Stream: true,
107 | }
108 | var payload *bytes.Buffer
109 | json.NewEncoder(payload).Encode(complet)
110 |
111 | // payload := strings.NewReader("{\"model\":\"claude-2\",\"prompt\":\"\\n\\nHuman: Hello, world!\\n\\nAssistant:\",\"max_tokens_to_sample\":256}")
112 |
113 | req, _ := http.NewRequest("POST", ClaudeUrl, payload)
114 |
115 | req.Header.Add("accept", "application/json")
116 | req.Header.Add("anthropic-version", "2023-06-01")
117 | req.Header.Add("x-api-key", "$ANTHROPIC_API_KEY")
118 | req.Header.Add("content-type", "application/json")
119 |
120 | res, _ := http.DefaultClient.Do(req)
121 |
122 | defer res.Body.Close()
123 | // body, _ := io.ReadAll(res.Body)
124 |
125 | // fmt.Println(string(body))
126 | reader := bufio.NewReader(res.Body)
127 | for {
128 | line, err := reader.ReadString('\n')
129 | if err == nil {
130 | if strings.HasPrefix(line, "data:") {
131 | fmt.Println(line)
132 | // var result CompleteResponse
133 | // json.Unmarshal()
134 | } else {
135 | continue
136 | }
137 | } else {
138 | break
139 | }
140 | }
141 | }
142 |
143 | func ClaudeProxy(c *gin.Context) {
144 | var chatlog store.Tokens
145 | var complete CompleteRequest
146 |
147 | byteBody, _ := io.ReadAll(c.Request.Body)
148 | c.Request.Body = io.NopCloser(bytes.NewBuffer(byteBody))
149 |
150 | if err := json.Unmarshal(byteBody, &complete); err != nil {
151 | c.AbortWithError(http.StatusBadRequest, err)
152 | return
153 | }
154 |
155 | key, err := store.SelectKeyCache("claude") //anthropic
156 | if err != nil {
157 | c.JSON(http.StatusInternalServerError, gin.H{
158 | "error": gin.H{
159 | "message": err.Error(),
160 | },
161 | })
162 | return
163 | }
164 |
165 | chatlog.Model = complete.Model
166 |
167 | token, _ := c.Get("localuser")
168 |
169 | lu, err := store.GetUserByToken(token.(string))
170 | if err != nil {
171 | c.JSON(http.StatusInternalServerError, gin.H{
172 | "error": gin.H{
173 | "message": err.Error(),
174 | },
175 | })
176 | return
177 | }
178 | chatlog.UserID = int(lu.ID)
179 |
180 | chatlog.PromptCount = tokenizer.NumTokensFromStr(complete.Prompt, complete.Model)
181 |
182 | if key.EndPoint == "" {
183 | key.EndPoint = "https://api.anthropic.com"
184 | }
185 | targetUrl, _ := url.ParseRequestURI(key.EndPoint + c.Request.URL.String())
186 |
187 | proxy := httputil.NewSingleHostReverseProxy(targetUrl)
188 | proxy.Director = func(req *http.Request) {
189 | req.Host = targetUrl.Host
190 | req.URL.Scheme = targetUrl.Scheme
191 | req.URL.Host = targetUrl.Host
192 |
193 | req.Header.Set("anthropic-version", "2023-06-01")
194 | req.Header.Set("content-type", "application/json")
195 | req.Header.Set("x-api-key", key.Key)
196 | }
197 |
198 | proxy.ModifyResponse = func(resp *http.Response) error {
199 | if resp.StatusCode != http.StatusOK {
200 | return nil
201 | }
202 | var byteResp []byte
203 | byteResp, _ = io.ReadAll(resp.Body)
204 | resp.Body = io.NopCloser(bytes.NewBuffer(byteResp))
205 | if complete.Stream != true {
206 | var complete_resp CompleteResponse
207 |
208 | if err := json.Unmarshal(byteResp, &complete_resp); err != nil {
209 | log.Println(err)
210 | return nil
211 | }
212 | chatlog.CompletionCount = tokenizer.NumTokensFromStr(complete_resp.Completion, chatlog.Model)
213 | } else {
214 | var completion string
215 | for {
216 | line, err := bufio.NewReader(bytes.NewBuffer(byteResp)).ReadString('\n')
217 | if err != nil {
218 | if strings.HasPrefix(line, "data:") {
219 | line = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
220 | if strings.HasSuffix(line, "[DONE]") {
221 | break
222 | }
223 | line = strings.TrimSpace(line)
224 | var complete_resp CompleteResponse
225 | if err := json.Unmarshal([]byte(line), &complete_resp); err != nil {
226 | log.Println(err)
227 | break
228 | }
229 | completion += line
230 | }
231 | }
232 | }
233 | log.Println("completion:", completion)
234 | chatlog.CompletionCount = tokenizer.NumTokensFromStr(completion, chatlog.Model)
235 | }
236 |
237 | // calc cost
238 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
239 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
240 |
241 | if err := store.Record(&chatlog); err != nil {
242 | log.Println(err)
243 | }
244 | if err := store.SumDaily(chatlog.UserID); err != nil {
245 | log.Println(err)
246 | }
247 | return nil
248 | }
249 | proxy.ServeHTTP(c.Writer, c.Request)
250 | }
251 |
252 | func TransReq(chatreq *openai.ChatCompletionRequest) (*bytes.Buffer, error) {
253 | transReq := CompleteRequest{
254 | Model: chatreq.Model,
255 | Temperature: int(chatreq.Temperature),
256 | TopP: int(chatreq.TopP),
257 | Stream: chatreq.Stream,
258 | MaxTokensToSample: chatreq.MaxTokens,
259 | }
260 | if transReq.MaxTokensToSample == 0 {
261 | transReq.MaxTokensToSample = 100000
262 | }
263 | var prompt string
264 | for _, msg := range chatreq.Messages {
265 | switch msg.Role {
266 | case "system":
267 | prompt += fmt.Sprintf("\n\nHuman:%s", msg.Content)
268 | case "user":
269 | prompt += fmt.Sprintf("\n\nHuman:%s", msg.Content)
270 | case "assistant":
271 | prompt += fmt.Sprintf("\n\nAssistant:%s", msg.Content)
272 | }
273 | }
274 | transReq.Prompt = prompt + "\n\nAssistant:"
275 | var payload = bytes.NewBuffer(nil)
276 | if err := json.NewEncoder(payload).Encode(transReq); err != nil {
277 | return nil, err
278 | }
279 | return payload, nil
280 | }
281 |
282 | func TransRsp(c *gin.Context, isStream bool, chatlog store.Tokens, reader *bufio.Reader) {
283 | if !isStream {
284 | var completersp CompleteResponse
285 | var chatrsp openai.ChatCompletionResponse
286 | json.NewDecoder(reader).Decode(&completersp)
287 | chatrsp.Model = completersp.Model
288 | chatrsp.ID = completersp.LogID
289 | chatrsp.Object = "chat.completion"
290 | chatrsp.Created = time.Now().Unix()
291 | choice := openai.ChatCompletionChoice{
292 | Index: 0,
293 | FinishReason: "stop",
294 | Message: openai.ChatCompletionMessage{
295 | Role: "assistant",
296 | Content: completersp.Completion,
297 | },
298 | }
299 | chatrsp.Choices = append(chatrsp.Choices, choice)
300 | var payload *bytes.Buffer
301 | if err := json.NewEncoder(payload).Encode(chatrsp); err != nil {
302 | c.JSON(http.StatusInternalServerError, gin.H{
303 | "error": gin.H{
304 | "message": err.Error(),
305 | },
306 | })
307 | return
308 | }
309 | chatlog.CompletionCount = tokenizer.NumTokensFromStr(completersp.Completion, chatlog.Model)
310 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
311 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
312 | if err := store.Record(&chatlog); err != nil {
313 | log.Println(err)
314 | }
315 | if err := store.SumDaily(chatlog.UserID); err != nil {
316 | log.Println(err)
317 | }
318 |
319 | c.JSON(http.StatusOK, payload)
320 | return
321 | } else {
322 | var (
323 | wg sync.WaitGroup
324 | dataChan = make(chan string)
325 | stopChan = make(chan bool)
326 | complete_resp string
327 | )
328 | wg.Add(2)
329 | go func() {
330 | defer wg.Done()
331 | for {
332 | line, err := reader.ReadString('\n')
333 | if err == nil {
334 | if strings.HasPrefix(line, "data: ") {
335 | var result CompleteResponse
336 | json.NewDecoder(strings.NewReader(line[6:])).Decode(&result)
337 | if result.StopReason == "" {
338 | if result.Completion != "" {
339 | complete_resp += result.Completion
340 | chatrsp := openai.ChatCompletionStreamResponse{
341 | ID: result.LogID,
342 | Model: result.Model,
343 | Object: "chat.completion",
344 | Created: time.Now().Unix(),
345 | }
346 | choice := openai.ChatCompletionStreamChoice{
347 | Delta: openai.ChatCompletionStreamChoiceDelta{
348 | Role: "assistant",
349 | Content: result.Completion,
350 | },
351 | FinishReason: "",
352 | }
353 | chatrsp.Choices = append(chatrsp.Choices, choice)
354 | bytedate, _ := json.Marshal(chatrsp)
355 | dataChan <- string(bytedate)
356 | }
357 | } else {
358 | chatrsp := openai.ChatCompletionStreamResponse{
359 | ID: result.LogID,
360 | Model: result.Model,
361 | Object: "chat.completion",
362 | Created: time.Now().Unix(),
363 | }
364 | choice := openai.ChatCompletionStreamChoice{
365 | Delta: openai.ChatCompletionStreamChoiceDelta{
366 | Role: "assistant",
367 | Content: result.Completion,
368 | },
369 | }
370 | choice.FinishReason = openai.FinishReason(TranslatestopReason(result.StopReason))
371 | chatrsp.Choices = append(chatrsp.Choices, choice)
372 | bytedate, _ := json.Marshal(chatrsp)
373 | dataChan <- string(bytedate)
374 | dataChan <- "[DONE]"
375 | break
376 | }
377 | } else {
378 | continue
379 | }
380 | } else {
381 | break
382 | }
383 | }
384 |
385 | close(dataChan)
386 | stopChan <- true
387 | close(stopChan)
388 | }()
389 |
390 | go func() {
391 | defer wg.Done()
392 | Loop:
393 | for {
394 | select {
395 | case data := <-dataChan:
396 | if data != "" {
397 | c.Writer.WriteString("data: " + data)
398 | c.Writer.WriteString("\n\n")
399 | c.Writer.Flush()
400 | }
401 | case <-stopChan:
402 | break Loop
403 | }
404 | }
405 | }()
406 | wg.Wait()
407 | chatlog.CompletionCount = tokenizer.NumTokensFromStr(complete_resp, chatlog.Model)
408 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
409 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
410 | if err := store.Record(&chatlog); err != nil {
411 | log.Println(err)
412 | }
413 | if err := store.SumDaily(chatlog.UserID); err != nil {
414 | log.Println(err)
415 | }
416 | }
417 | }
418 |
419 | // claude -> openai
420 | func TranslatestopReason(reason string) string {
421 | switch reason {
422 | case "stop_sequence":
423 | return "stop"
424 | case "max_tokens":
425 | return "length"
426 | default:
427 | return reason
428 | }
429 | }
430 |
--------------------------------------------------------------------------------
/pkg/error/errdata.go:
--------------------------------------------------------------------------------
1 | package error
2 |
3 | import "github.com/gin-gonic/gin"
4 |
5 | func ErrorData(message string) gin.H {
6 | return gin.H{
7 | "error": gin.H{
8 | "message": message,
9 | },
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/google/chat.go:
--------------------------------------------------------------------------------
1 | // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/grounding-search-entry-points?authuser=2&hl=zh-cn
2 | //
3 | // https://cloud.google.com/vertex-ai/docs/generative-ai/quotas-genai
4 | package google
5 |
6 | import (
7 | "context"
8 | "encoding/base64"
9 | "encoding/json"
10 | "fmt"
11 | "io"
12 | "log"
13 | "net/http"
14 | "opencatd-open/pkg/openai"
15 | "opencatd-open/pkg/tokenizer"
16 | "opencatd-open/store"
17 | "strings"
18 |
19 | "github.com/gin-gonic/gin"
20 | "github.com/google/generative-ai-go/genai"
21 | "google.golang.org/api/iterator"
22 | "google.golang.org/api/option"
23 | )
24 |
25 | type GeminiChatRequest struct {
26 | Contents []GeminiContent `json:"contents,omitempty"`
27 | }
28 |
29 | func (g GeminiChatRequest) ByteJson() []byte {
30 | bytejson, _ := json.Marshal(g)
31 | return bytejson
32 | }
33 |
34 | type GeminiContent struct {
35 | Role string `json:"role,omitempty"`
36 | Parts []GeminiPart `json:"parts,omitempty"`
37 | }
38 | type GeminiPart struct {
39 | Text string `json:"text,omitempty"`
40 | // InlineData GeminiPartInlineData `json:"inlineData,omitempty"`
41 | }
42 | type GeminiPartInlineData struct {
43 | MimeType string `json:"mimeType,omitempty"`
44 | Data string `json:"data,omitempty"` // base64
45 | }
46 |
47 | type GeminiResponse struct {
48 | Candidates []struct {
49 | Content struct {
50 | Parts []struct {
51 | Text string `json:"text"`
52 | } `json:"parts"`
53 | Role string `json:"role"`
54 | } `json:"content"`
55 | FinishReason string `json:"finishReason"`
56 | Index int `json:"index"`
57 | SafetyRatings []struct {
58 | Category string `json:"category"`
59 | Probability string `json:"probability"`
60 | } `json:"safetyRatings"`
61 | } `json:"candidates"`
62 | PromptFeedback struct {
63 | SafetyRatings []struct {
64 | Category string `json:"category"`
65 | Probability string `json:"probability"`
66 | } `json:"safetyRatings"`
67 | } `json:"promptFeedback"`
68 | Error struct {
69 | Code int `json:"code"`
70 | Message string `json:"message"`
71 | Status string `json:"status"`
72 | Details []struct {
73 | Type string `json:"@type"`
74 | FieldViolations []struct {
75 | Field string `json:"field"`
76 | Description string `json:"description"`
77 | } `json:"fieldViolations"`
78 | } `json:"details"`
79 | } `json:"error"`
80 | }
81 |
82 | func ChatProxy(c *gin.Context, chatReq *openai.ChatCompletionRequest) {
83 | usagelog := store.Tokens{Model: chatReq.Model}
84 |
85 | token, _ := c.Get("localuser")
86 |
87 | lu, err := store.GetUserByToken(token.(string))
88 | if err != nil {
89 | c.JSON(http.StatusInternalServerError, gin.H{
90 | "error": gin.H{
91 | "message": err.Error(),
92 | },
93 | })
94 | return
95 | }
96 | usagelog.UserID = int(lu.ID)
97 | var prompts []genai.Part
98 | var prompt string
99 | for _, msg := range chatReq.Messages {
100 | switch ct := msg.Content.(type) {
101 | case string:
102 | prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n"
103 | prompts = append(prompts, genai.Text("<"+msg.Role+">: "+msg.Content.(string)))
104 | case []any:
105 | for _, item := range ct {
106 | if m, ok := item.(map[string]interface{}); ok {
107 | if m["type"] == "text" {
108 | prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n"
109 | prompts = append(prompts, genai.Text("<"+msg.Role+">: "+m["text"].(string)))
110 | } else if m["type"] == "image_url" {
111 | if url, ok := m["image_url"].(map[string]interface{}); ok {
112 | if strings.HasPrefix(url["url"].(string), "http") {
113 | fmt.Println("网络图片:", url["url"].(string))
114 | } else if strings.HasPrefix(url["url"].(string), "data:image") {
115 | fmt.Println("base64:", url["url"].(string)[:20])
116 | var mime string
117 | // openai 会以 data:image 开头,则去掉 data:image/png;base64, 和 data:image/jpeg;base64,
118 | if strings.HasPrefix(url["url"].(string), "data:image/png") {
119 | mime = "image/png"
120 | } else if strings.HasPrefix(url["url"].(string), "data:image/jpeg") {
121 | mime = "image/jpeg"
122 | } else {
123 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Unsupported image format"})
124 | return
125 | }
126 | imageString := strings.Split(url["url"].(string), ",")[1]
127 | imageBytes, err := base64.StdEncoding.DecodeString(imageString)
128 | if err != nil {
129 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
130 | return
131 | }
132 | prompts = append(prompts, genai.Blob{MIMEType: mime, Data: imageBytes})
133 | }
134 | }
135 | }
136 | }
137 | }
138 | default:
139 | c.JSON(http.StatusInternalServerError, gin.H{
140 | "error": gin.H{
141 | "message": "Invalid content type",
142 | },
143 | })
144 | return
145 | }
146 | if len(chatReq.Tools) > 0 {
147 | tooljson, _ := json.Marshal(chatReq.Tools)
148 | prompt += ": " + string(tooljson) + "\n"
149 | }
150 | }
151 |
152 | usagelog.PromptCount = tokenizer.NumTokensFromStr(prompt, chatReq.Model)
153 |
154 | onekey, err := store.SelectKeyCache("google")
155 | if err != nil {
156 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
157 | return
158 | }
159 |
160 | ctx := context.Background()
161 |
162 | client, err := genai.NewClient(ctx, option.WithAPIKey(onekey.Key))
163 | if err != nil {
164 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
165 | return
166 | }
167 | defer client.Close()
168 |
169 | model := client.GenerativeModel(chatReq.Model)
170 | model.Tools = []*genai.Tool{}
171 |
172 | iter := model.GenerateContentStream(ctx, prompts...)
173 | datachan := make(chan string)
174 | // closechan := make(chan error)
175 | var result string
176 | go func() {
177 | for {
178 | resp, err := iter.Next()
179 | if err == iterator.Done {
180 |
181 | var chatResp openai.ChatCompletionStreamResponse
182 | chatResp.Model = chatReq.Model
183 | choice := openai.Choice{}
184 | choice.FinishReason = "stop"
185 | chatResp.Choices = append(chatResp.Choices, choice)
186 | datachan <- "data: " + string(chatResp.ByteJson())
187 | close(datachan)
188 | break
189 | }
190 | if err != nil {
191 | log.Println(err)
192 | var errResp openai.ErrResponse
193 | errResp.Error.Code = "500"
194 | errResp.Error.Message = err.Error()
195 | datachan <- string(errResp.ByteJson())
196 | close(datachan)
197 | break
198 | }
199 | var content string
200 | if resp.Candidates != nil && len(resp.Candidates) > 0 && len(resp.Candidates[0].Content.Parts) > 0 {
201 | if s, ok := resp.Candidates[0].Content.Parts[0].(genai.Text); ok {
202 | content = string(s)
203 | result += content
204 | }
205 | } else {
206 | continue
207 | }
208 |
209 | var chatResp openai.ChatCompletionStreamResponse
210 | chatResp.Model = chatReq.Model
211 | choice := openai.Choice{}
212 | choice.Delta.Role = resp.Candidates[0].Content.Role
213 | choice.Delta.Content = content
214 | chatResp.Choices = append(chatResp.Choices, choice)
215 |
216 | chunk := "data: " + string(chatResp.ByteJson()) + "\n\n"
217 | datachan <- chunk
218 | }
219 | }()
220 |
221 | c.Writer.Header().Set("Content-Type", "text/event-stream")
222 | c.Writer.Header().Set("Cache-Control", "no-cache")
223 | c.Writer.Header().Set("Connection", "keep-alive")
224 | c.Writer.Header().Set("Transfer-Encoding", "chunked")
225 | c.Writer.Header().Set("X-Accel-Buffering", "no")
226 |
227 | c.Stream(func(w io.Writer) bool {
228 | if data, ok := <-datachan; ok {
229 | if strings.HasPrefix(data, "data: ") {
230 | c.Writer.WriteString(data)
231 | // c.Writer.WriteString("\n\n")
232 | } else {
233 | c.Writer.WriteHeader(http.StatusBadGateway)
234 | c.Writer.WriteString(data)
235 | }
236 | c.Writer.Flush()
237 | return true
238 | }
239 | go func() {
240 |
241 | }()
242 | return false
243 | })
244 | }
245 |
--------------------------------------------------------------------------------
/pkg/openai/chat.go:
--------------------------------------------------------------------------------
1 | package openai
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "log"
10 | "net/http"
11 | "opencatd-open/pkg/tokenizer"
12 | "opencatd-open/store"
13 | "os"
14 | "strings"
15 |
16 | "github.com/gin-gonic/gin"
17 | )
18 |
19 | const (
20 | // https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation#latest-preview-api-releases
21 | AzureApiVersion = "2024-06-01"
22 | BaseHost = "api.openai.com"
23 | OpenAI_Endpoint = "https://api.openai.com/v1/chat/completions"
24 | Github_Marketplace = "https://models.inference.ai.azure.com/chat/completions"
25 | )
26 |
27 | var (
28 | Custom_Endpoint string
29 | AIGateWay_Endpoint string // "https://gateway.ai.cloudflare.com/v1/431ba10f11200d544922fbca177aaa7f/openai/openai/chat/completions"
30 | )
31 |
32 | func init() {
33 | if os.Getenv("OpenAI_Endpoint") != "" {
34 | Custom_Endpoint = os.Getenv("OpenAI_Endpoint")
35 | }
36 | if os.Getenv("AIGateWay_Endpoint") != "" {
37 | AIGateWay_Endpoint = os.Getenv("AIGateWay_Endpoint")
38 | }
39 | }
40 |
41 | // Vision Content
42 | type VisionContent struct {
43 | Type string `json:"type,omitempty"`
44 | Text string `json:"text,omitempty"`
45 | ImageURL *VisionImageURL `json:"image_url,omitempty"`
46 | }
47 | type VisionImageURL struct {
48 | URL string `json:"url,omitempty"`
49 | Detail string `json:"detail,omitempty"`
50 | }
51 |
52 | type ChatCompletionMessage struct {
53 | Role string `json:"role"`
54 | Content any `json:"content"`
55 | Name string `json:"name,omitempty"`
56 | // MultiContent []VisionContent
57 | }
58 |
59 | type FunctionDefinition struct {
60 | Name string `json:"name"`
61 | Description string `json:"description,omitempty"`
62 | Parameters any `json:"parameters"`
63 | }
64 |
65 | type Tool struct {
66 | Type string `json:"type"`
67 | Function *FunctionDefinition `json:"function,omitempty"`
68 | }
69 |
70 | type StreamOption struct {
71 | IncludeUsage bool `json:"include_usage,omitempty"`
72 | }
73 |
74 | type ChatCompletionRequest struct {
75 | Model string `json:"model"`
76 | Messages []ChatCompletionMessage `json:"messages"`
77 | MaxTokens int `json:"max_tokens,omitempty"`
78 | Temperature float64 `json:"temperature,omitempty"`
79 | TopP float64 `json:"top_p,omitempty"`
80 | N int `json:"n,omitempty"`
81 | Stream bool `json:"stream"`
82 | Stop []string `json:"stop,omitempty"`
83 | PresencePenalty float64 `json:"presence_penalty,omitempty"`
84 | FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
85 | LogitBias map[string]int `json:"logit_bias,omitempty"`
86 | User string `json:"user,omitempty"`
87 | // Functions []FunctionDefinition `json:"functions,omitempty"`
88 | // FunctionCall any `json:"function_call,omitempty"`
89 | Tools []Tool `json:"tools,omitempty"`
90 | ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
91 | // ToolChoice any `json:"tool_choice,omitempty"`
92 | StreamOptions *StreamOption `json:"stream_options,omitempty"`
93 | }
94 |
95 | func (c ChatCompletionRequest) ToByteJson() []byte {
96 | bytejson, _ := json.Marshal(c)
97 | return bytejson
98 | }
99 |
100 | type ToolCall struct {
101 | ID string `json:"id"`
102 | Type string `json:"type"`
103 | Function struct {
104 | Name string `json:"name"`
105 | Arguments string `json:"arguments"`
106 | } `json:"function"`
107 | }
108 |
109 | type ChatCompletionResponse struct {
110 | ID string `json:"id,omitempty"`
111 | Object string `json:"object,omitempty"`
112 | Created int `json:"created,omitempty"`
113 | Model string `json:"model,omitempty"`
114 | Choices []struct {
115 | Index int `json:"index,omitempty"`
116 | Message struct {
117 | Role string `json:"role,omitempty"`
118 | Content string `json:"content,omitempty"`
119 | ToolCalls []ToolCall `json:"tool_calls,omitempty"`
120 | } `json:"message,omitempty"`
121 | Logprobs string `json:"logprobs,omitempty"`
122 | FinishReason string `json:"finish_reason,omitempty"`
123 | } `json:"choices,omitempty"`
124 | Usage struct {
125 | PromptTokens int `json:"prompt_tokens,omitempty"`
126 | CompletionTokens int `json:"completion_tokens,omitempty"`
127 | TotalTokens int `json:"total_tokens,omitempty"`
128 | PromptTokensDetails struct {
129 | CachedTokens int `json:"cached_tokens,omitempty"`
130 | AudioTokens int `json:"audio_tokens,omitempty"`
131 | } `json:"prompt_tokens_details,omitempty"`
132 | CompletionTokensDetails struct {
133 | ReasoningTokens int `json:"reasoning_tokens,omitempty"`
134 | AudioTokens int `json:"audio_tokens,omitempty"`
135 | AcceptedPredictionTokens int `json:"accepted_prediction_tokens,omitempty"`
136 | RejectedPredictionTokens int `json:"rejected_prediction_tokens,omitempty"`
137 | } `json:"completion_tokens_details,omitempty"`
138 | } `json:"usage,omitempty"`
139 | SystemFingerprint string `json:"system_fingerprint,omitempty"`
140 | }
141 |
142 | type Choice struct {
143 | Index int `json:"index"`
144 | Delta struct {
145 | Role string `json:"role"`
146 | Content string `json:"content"`
147 | ToolCalls []ToolCall `json:"tool_calls"`
148 | } `json:"delta"`
149 | FinishReason string `json:"finish_reason"`
150 | Usage struct {
151 | PromptTokens int `json:"prompt_tokens"`
152 | CompletionTokens int `json:"completion_tokens"`
153 | TotalTokens int `json:"total_tokens"`
154 | } `json:"usage"`
155 | }
156 |
157 | type ChatCompletionStreamResponse struct {
158 | ID string `json:"id"`
159 | Object string `json:"object"`
160 | Created int `json:"created"`
161 | Model string `json:"model"`
162 | Choices []Choice `json:"choices"`
163 | }
164 |
165 | func (c *ChatCompletionStreamResponse) ByteJson() []byte {
166 | bytejson, _ := json.Marshal(c)
167 | return bytejson
168 | }
169 |
170 | func ChatProxy(c *gin.Context, chatReq *ChatCompletionRequest) {
171 | usagelog := store.Tokens{Model: chatReq.Model}
172 |
173 | token, _ := c.Get("localuser")
174 |
175 | lu, err := store.GetUserByToken(token.(string))
176 | if err != nil {
177 | c.JSON(http.StatusInternalServerError, gin.H{
178 | "error": gin.H{
179 | "message": err.Error(),
180 | },
181 | })
182 | return
183 | }
184 | usagelog.UserID = int(lu.ID)
185 |
186 | var prompt string
187 | for _, msg := range chatReq.Messages {
188 | switch ct := msg.Content.(type) {
189 | case string:
190 | prompt += "<" + msg.Role + ">: " + msg.Content.(string) + "\n"
191 | case []any:
192 | for _, item := range ct {
193 | if m, ok := item.(map[string]interface{}); ok {
194 | if m["type"] == "text" {
195 | prompt += "<" + msg.Role + ">: " + m["text"].(string) + "\n"
196 | } else if m["type"] == "image_url" {
197 | if url, ok := m["image_url"].(map[string]interface{}); ok {
198 | fmt.Printf(" URL: %v\n", url["url"])
199 | if strings.HasPrefix(url["url"].(string), "http") {
200 | fmt.Println("网络图片:", url["url"].(string))
201 | }
202 | }
203 | }
204 | }
205 | }
206 | default:
207 | c.JSON(http.StatusInternalServerError, gin.H{
208 | "error": gin.H{
209 | "message": "Invalid content type",
210 | },
211 | })
212 | return
213 | }
214 | if len(chatReq.Tools) > 0 {
215 | tooljson, _ := json.Marshal(chatReq.Tools)
216 | prompt += ": " + string(tooljson) + "\n"
217 | }
218 | }
219 | switch chatReq.Model {
220 | case "gpt-4o", "gpt-4o-mini", "chatgpt-4o-latest":
221 | chatReq.MaxTokens = 16384
222 | }
223 | if chatReq.Stream {
224 | chatReq.StreamOptions = &StreamOption{IncludeUsage: true}
225 | }
226 |
227 | usagelog.PromptCount = tokenizer.NumTokensFromStr(prompt, chatReq.Model)
228 |
229 | // onekey, err := store.SelectKeyCache("openai")
230 | onekey, err := store.SelectKeyCacheByModel(chatReq.Model)
231 | if err != nil {
232 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
233 | return
234 | }
235 |
236 | var req *http.Request
237 |
238 | switch onekey.ApiType {
239 | case "github":
240 | req, err = http.NewRequest(c.Request.Method, Github_Marketplace, bytes.NewReader(chatReq.ToByteJson()))
241 | req.Header = c.Request.Header
242 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", onekey.Key))
243 | case "azure":
244 | var buildurl string
245 | if onekey.EndPoint != "" {
246 | buildurl = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=%s", onekey.EndPoint, modelmap(chatReq.Model), AzureApiVersion)
247 | } else {
248 | buildurl = fmt.Sprintf("https://%s.openai.azure.com/openai/deployments/%s/chat/completions?api-version=%s", onekey.ResourceNmae, modelmap(chatReq.Model), AzureApiVersion)
249 | }
250 | req, err = http.NewRequest(c.Request.Method, buildurl, bytes.NewReader(chatReq.ToByteJson()))
251 | req.Header = c.Request.Header
252 | req.Header.Set("api-key", onekey.Key)
253 | default:
254 | req, err = http.NewRequest(c.Request.Method, OpenAI_Endpoint, bytes.NewReader(chatReq.ToByteJson())) // default endpoint
255 |
256 | if AIGateWay_Endpoint != "" { // cloudflare gateway的endpoint
257 | req, err = http.NewRequest(c.Request.Method, AIGateWay_Endpoint, bytes.NewReader(chatReq.ToByteJson()))
258 | }
259 | if Custom_Endpoint != "" { // 自定义endpoint
260 | req, err = http.NewRequest(c.Request.Method, Custom_Endpoint, bytes.NewReader(chatReq.ToByteJson()))
261 | }
262 | if onekey.EndPoint != "" { // 优先key的endpoint
263 | req, err = http.NewRequest(c.Request.Method, onekey.EndPoint+c.Request.RequestURI, bytes.NewReader(chatReq.ToByteJson()))
264 | }
265 |
266 | req.Header = c.Request.Header
267 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", onekey.Key))
268 | }
269 | if err != nil {
270 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
271 | return
272 | }
273 |
274 | resp, err := http.DefaultClient.Do(req)
275 | if err != nil {
276 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
277 | return
278 | }
279 | defer resp.Body.Close()
280 |
281 | var result string
282 | if chatReq.Stream {
283 | for key, value := range resp.Header {
284 | for _, v := range value {
285 | c.Writer.Header().Add(key, v)
286 | }
287 | }
288 | c.Writer.WriteHeader(resp.StatusCode)
289 | teeReader := io.TeeReader(resp.Body, c.Writer)
290 | // 流式响应
291 | scanner := bufio.NewScanner(teeReader)
292 |
293 | for scanner.Scan() {
294 | line := scanner.Bytes()
295 | if len(line) > 0 && bytes.HasPrefix(line, []byte("data: ")) {
296 | if bytes.HasPrefix(line, []byte("data: [DONE]")) {
297 | break
298 | }
299 | var opiResp ChatCompletionStreamResponse
300 | line = bytes.Replace(line, []byte("data: "), []byte(""), -1)
301 | line = bytes.TrimSpace(line)
302 | if err := json.Unmarshal(line, &opiResp); err != nil {
303 | continue
304 | }
305 |
306 | if opiResp.Choices != nil && len(opiResp.Choices) > 0 {
307 | if opiResp.Choices[0].Delta.Role != "" {
308 | result += "<" + opiResp.Choices[0].Delta.Role + "> "
309 | }
310 | result += opiResp.Choices[0].Delta.Content // 计算Content Token
311 |
312 | if len(opiResp.Choices[0].Delta.ToolCalls) > 0 { // 计算ToolCalls token
313 | if opiResp.Choices[0].Delta.ToolCalls[0].Function.Name != "" {
314 | result += "name:" + opiResp.Choices[0].Delta.ToolCalls[0].Function.Name + " arguments:"
315 | }
316 | result += opiResp.Choices[0].Delta.ToolCalls[0].Function.Arguments
317 | }
318 | } else {
319 | continue
320 | }
321 | }
322 |
323 | }
324 | } else {
325 |
326 | // 处理非流式响应
327 | body, err := io.ReadAll(resp.Body)
328 | if err != nil {
329 | fmt.Println("Error reading response body:", err)
330 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
331 | return
332 | }
333 | var opiResp ChatCompletionResponse
334 | if err := json.Unmarshal(body, &opiResp); err != nil {
335 | log.Println("Error parsing JSON:", err)
336 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
337 | return
338 | }
339 | if opiResp.Choices != nil && len(opiResp.Choices) > 0 {
340 | if opiResp.Choices[0].Message.Role != "" {
341 | result += "<" + opiResp.Choices[0].Message.Role + "> "
342 | }
343 | result += opiResp.Choices[0].Message.Content
344 |
345 | if len(opiResp.Choices[0].Message.ToolCalls) > 0 {
346 | if opiResp.Choices[0].Message.ToolCalls[0].Function.Name != "" {
347 | result += "name:" + opiResp.Choices[0].Message.ToolCalls[0].Function.Name + " arguments:"
348 | }
349 | result += opiResp.Choices[0].Message.ToolCalls[0].Function.Arguments
350 | }
351 |
352 | }
353 | for k, v := range resp.Header {
354 | c.Writer.Header().Set(k, v[0])
355 | }
356 |
357 | c.JSON(http.StatusOK, opiResp)
358 | }
359 | usagelog.CompletionCount = tokenizer.NumTokensFromStr(result, chatReq.Model)
360 | usagelog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(usagelog.Model, usagelog.PromptCount, usagelog.CompletionCount))
361 | if err := store.Record(&usagelog); err != nil {
362 | log.Println(err)
363 | }
364 | if err := store.SumDaily(usagelog.UserID); err != nil {
365 | log.Println(err)
366 | }
367 | }
368 |
369 | func modelmap(in string) string {
370 | // gpt-3.5-turbo -> gpt-35-turbo
371 | if strings.Contains(in, ".") {
372 | return strings.ReplaceAll(in, ".", "")
373 | }
374 | return in
375 | }
376 |
377 | type ErrResponse struct {
378 | Error struct {
379 | Message string `json:"message"`
380 | Code string `json:"code"`
381 | } `json:"error"`
382 | }
383 |
384 | func (e *ErrResponse) ByteJson() []byte {
385 | bytejson, _ := json.Marshal(e)
386 | return bytejson
387 | }
388 |
--------------------------------------------------------------------------------
/pkg/openai/dall-e.go:
--------------------------------------------------------------------------------
1 | package openai
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net/http"
10 | "net/http/httputil"
11 | "net/url"
12 | "opencatd-open/pkg/tokenizer"
13 | "opencatd-open/store"
14 | "strconv"
15 |
16 | "github.com/duke-git/lancet/v2/slice"
17 | "github.com/gin-gonic/gin"
18 | )
19 |
20 | const (
21 | DalleEndpoint = "https://api.openai.com/v1/images/generations"
22 | DalleEditEndpoint = "https://api.openai.com/v1/images/edits"
23 | DalleVariationEndpoint = "https://api.openai.com/v1/images/variations"
24 | )
25 |
26 | type DallERequest struct {
27 | Model string `json:"model"`
28 | Prompt string `json:"prompt"`
29 | N int `form:"n" json:"n,omitempty"`
30 | Size string `form:"size" json:"size,omitempty"`
31 | Quality string `json:"quality,omitempty"` // standard,hd
32 | Style string `json:"style,omitempty"` // vivid,natural
33 | ResponseFormat string `json:"response_format,omitempty"` // url or b64_json
34 | }
35 |
36 | func DallEProxy(c *gin.Context) {
37 |
38 | var dalleRequest DallERequest
39 | if err := c.ShouldBind(&dalleRequest); err != nil {
40 | c.JSON(400, gin.H{"error": err.Error()})
41 | return
42 | }
43 | if dalleRequest.N == 0 {
44 | dalleRequest.N = 1
45 | }
46 |
47 | if dalleRequest.Size == "" {
48 | dalleRequest.Size = "512x512"
49 | }
50 |
51 | model := dalleRequest.Model
52 |
53 | var chatlog store.Tokens
54 | chatlog.CompletionCount = dalleRequest.N
55 |
56 | if model == "dall-e" {
57 | model = "dall-e-2"
58 | }
59 | model = model + "." + dalleRequest.Size
60 |
61 | if dalleRequest.Model == "dall-e-2" || dalleRequest.Model == "dall-e" {
62 | if !slice.Contain([]string{"256x256", "512x512", "1024x1024"}, dalleRequest.Size) {
63 | c.JSON(http.StatusBadRequest, gin.H{
64 | "error": gin.H{
65 | "message": fmt.Sprintf("Invalid size: %s for %s", dalleRequest.Size, dalleRequest.Model),
66 | },
67 | })
68 | return
69 | }
70 | } else if dalleRequest.Model == "dall-e-3" {
71 | if !slice.Contain([]string{"256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"}, dalleRequest.Size) {
72 | c.JSON(http.StatusBadRequest, gin.H{
73 | "error": gin.H{
74 | "message": fmt.Sprintf("Invalid size: %s for %s", dalleRequest.Size, dalleRequest.Model),
75 | },
76 | })
77 | return
78 | }
79 | if dalleRequest.Quality == "hd" {
80 | model = model + ".hd"
81 | }
82 | } else {
83 | c.JSON(http.StatusBadRequest, gin.H{
84 | "error": gin.H{
85 | "message": fmt.Sprintf("Invalid model: %s", dalleRequest.Model),
86 | },
87 | })
88 | return
89 | }
90 | chatlog.Model = model
91 |
92 | token, _ := c.Get("localuser")
93 |
94 | lu, err := store.GetUserByToken(token.(string))
95 | if err != nil {
96 | c.JSON(http.StatusInternalServerError, gin.H{
97 | "error": gin.H{
98 | "message": err.Error(),
99 | },
100 | })
101 | return
102 | }
103 | chatlog.UserID = int(lu.ID)
104 |
105 | key, err := store.SelectKeyCache("openai")
106 | if err != nil {
107 | c.JSON(http.StatusInternalServerError, gin.H{
108 | "error": gin.H{
109 | "message": err.Error(),
110 | },
111 | })
112 | return
113 | }
114 |
115 | targetURL, _ := url.Parse(DalleEndpoint)
116 | proxy := httputil.NewSingleHostReverseProxy(targetURL)
117 | proxy.Director = func(req *http.Request) {
118 | req.Header.Set("Authorization", "Bearer "+key.Key)
119 | req.Header.Set("Content-Type", "application/json")
120 |
121 | req.Host = targetURL.Host
122 | req.URL.Scheme = targetURL.Scheme
123 | req.URL.Host = targetURL.Host
124 | req.URL.Path = targetURL.Path
125 | req.URL.RawPath = targetURL.RawPath
126 | req.URL.RawQuery = targetURL.RawQuery
127 |
128 | bytebody, _ := json.Marshal(dalleRequest)
129 | req.Body = io.NopCloser(bytes.NewBuffer(bytebody))
130 | req.ContentLength = int64(len(bytebody))
131 | req.Header.Set("Content-Length", strconv.Itoa(len(bytebody)))
132 | }
133 |
134 | proxy.ModifyResponse = func(resp *http.Response) error {
135 | if resp.StatusCode == http.StatusOK {
136 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
137 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
138 | if err := store.Record(&chatlog); err != nil {
139 | log.Println(err)
140 | }
141 | if err := store.SumDaily(chatlog.UserID); err != nil {
142 | log.Println(err)
143 | }
144 | }
145 | return nil
146 | }
147 |
148 | proxy.ServeHTTP(c.Writer, c.Request)
149 | }
150 |
--------------------------------------------------------------------------------
/pkg/openai/realtime.go:
--------------------------------------------------------------------------------
1 | /*
2 | https://platform.openai.com/docs/guides/realtime
3 | https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/audio-real-time
4 |
5 | wss://my-eastus2-openai-resource.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview-1001
6 | */
7 | package openai
8 |
9 | import (
10 | "context"
11 | "encoding/json"
12 | "fmt"
13 | "log"
14 | "net/http"
15 | "net/url"
16 | "opencatd-open/pkg/tokenizer"
17 | "opencatd-open/store"
18 | "os"
19 |
20 | "github.com/gin-gonic/gin"
21 | "github.com/gorilla/websocket"
22 | "golang.org/x/sync/errgroup"
23 | )
24 |
25 | // "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01"
26 | const realtimeURL = "wss://api.openai.com/v1/realtime"
27 | const azureRealtimeURL = "wss://%s.openai.azure.com/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview"
28 |
29 | var upgrader = websocket.Upgrader{
30 | CheckOrigin: func(r *http.Request) bool {
31 | return true
32 | },
33 | }
34 |
35 | type Message struct {
36 | Type string `json:"type"`
37 | Response Response `json:"response"`
38 | }
39 |
40 | type Response struct {
41 | Modalities []string `json:"modalities"`
42 | Instructions string `json:"instructions"`
43 | }
44 |
45 | type RealTimeResponse struct {
46 | Type string `json:"type"`
47 | EventID string `json:"event_id"`
48 | Response struct {
49 | Object string `json:"object"`
50 | ID string `json:"id"`
51 | Status string `json:"status"`
52 | StatusDetails any `json:"status_details"`
53 | Output []struct {
54 | ID string `json:"id"`
55 | Object string `json:"object"`
56 | Type string `json:"type"`
57 | Status string `json:"status"`
58 | Role string `json:"role"`
59 | Content []struct {
60 | Type string `json:"type"`
61 | Transcript string `json:"transcript"`
62 | } `json:"content"`
63 | } `json:"output"`
64 | Usage Usage `json:"usage"`
65 | } `json:"response"`
66 | }
67 |
68 | type Usage struct {
69 | TotalTokens int `json:"total_tokens"`
70 | InputTokens int `json:"input_tokens"`
71 | OutputTokens int `json:"output_tokens"`
72 | InputTokenDetails struct {
73 | CachedTokens int `json:"cached_tokens"`
74 | TextTokens int `json:"text_tokens"`
75 | AudioTokens int `json:"audio_tokens"`
76 | } `json:"input_token_details"`
77 | OutputTokenDetails struct {
78 | TextTokens int `json:"text_tokens"`
79 | AudioTokens int `json:"audio_tokens"`
80 | } `json:"output_token_details"`
81 | }
82 |
83 | func RealTimeProxy(c *gin.Context) {
84 | log.Println(c.Request.URL.String())
85 | var model string = c.Query("model")
86 | value := url.Values{}
87 | value.Add("model", model)
88 | realtimeURL := realtimeURL + "?" + value.Encode()
89 |
90 | // 升级 HTTP 连接为 WebSocket
91 | clientConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
92 | if err != nil {
93 | log.Println("Upgrade error:", err)
94 | return
95 | }
96 | defer clientConn.Close()
97 |
98 | apikey, err := store.SelectKeyCacheByModel(model)
99 | if err != nil {
100 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
101 | return
102 | }
103 | // 连接到 OpenAI WebSocket
104 | headers := http.Header{"OpenAI-Beta": []string{"realtime=v1"}}
105 |
106 | if apikey.ApiType == "azure" {
107 | headers.Set("api-key", apikey.Key)
108 | if apikey.EndPoint != "" {
109 | realtimeURL = fmt.Sprintf("%s/openai/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview", apikey.EndPoint)
110 | } else {
111 | realtimeURL = fmt.Sprintf(azureRealtimeURL, apikey.ResourceNmae)
112 | }
113 | } else {
114 | headers.Set("Authorization", "Bearer "+apikey.Key)
115 | }
116 |
117 | conn := websocket.DefaultDialer
118 | if os.Getenv("LOCAL_PROXY") != "" {
119 | proxyUrl, _ := url.Parse(os.Getenv("LOCAL_PROXY"))
120 | conn.Proxy = http.ProxyURL(proxyUrl)
121 | }
122 |
123 | openAIConn, _, err := conn.Dial(realtimeURL, headers)
124 | if err != nil {
125 | log.Println("OpenAI dial error:", err)
126 | return
127 | }
128 | defer openAIConn.Close()
129 |
130 | ctx, cancel := context.WithCancel(c.Request.Context())
131 | defer cancel()
132 |
133 | g, ctx := errgroup.WithContext(ctx)
134 |
135 | g.Go(func() error {
136 | return forwardMessages(ctx, c, clientConn, openAIConn)
137 | })
138 |
139 | g.Go(func() error {
140 | return forwardMessages(ctx, c, openAIConn, clientConn)
141 | })
142 |
143 | if err := g.Wait(); err != nil {
144 | log.Println("Error in message forwarding:", err)
145 | return
146 | }
147 |
148 | }
149 |
150 | func forwardMessages(ctx context.Context, c *gin.Context, src, dst *websocket.Conn) error {
151 | usagelog := store.Tokens{Model: "gpt-4o-realtime-preview"}
152 |
153 | token, _ := c.Get("localuser")
154 |
155 | lu, err := store.GetUserByToken(token.(string))
156 | if err != nil {
157 | return err
158 | }
159 | usagelog.UserID = int(lu.ID)
160 | for {
161 | select {
162 | case <-ctx.Done():
163 | return ctx.Err()
164 | default:
165 | messageType, message, err := src.ReadMessage()
166 | if err != nil {
167 | if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
168 | return nil // 正常关闭,不报错
169 | }
170 | return err
171 | }
172 | if messageType == websocket.TextMessage {
173 | var usage Usage
174 | err := json.Unmarshal(message, &usage)
175 | if err == nil {
176 | usagelog.PromptCount += usage.InputTokens
177 | usagelog.CompletionCount += usage.OutputTokens
178 | }
179 |
180 | }
181 | err = dst.WriteMessage(messageType, message)
182 | if err != nil {
183 | return err
184 | }
185 | }
186 | }
187 | defer func() {
188 | usagelog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(usagelog.Model, usagelog.PromptCount, usagelog.CompletionCount))
189 | if err := store.Record(&usagelog); err != nil {
190 | log.Println(err)
191 | }
192 | if err := store.SumDaily(usagelog.UserID); err != nil {
193 | log.Println(err)
194 | }
195 | }()
196 | return nil
197 | }
198 |
--------------------------------------------------------------------------------
/pkg/openai/tts.go:
--------------------------------------------------------------------------------
1 | package openai
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net/http"
10 | "net/http/httputil"
11 | "net/url"
12 | "opencatd-open/pkg/tokenizer"
13 | "opencatd-open/store"
14 |
15 | "github.com/gin-gonic/gin"
16 | )
17 |
18 | const (
19 | SpeechEndpoint = "https://api.openai.com/v1/audio/speech"
20 | )
21 |
22 | type SpeechRequest struct {
23 | Model string `json:"model"`
24 | Input string `json:"input"`
25 | Voice string `json:"voice"`
26 | }
27 |
28 | func SpeechProxy(c *gin.Context) {
29 | var chatreq SpeechRequest
30 | if err := c.ShouldBindJSON(&chatreq); err != nil {
31 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
32 | return
33 | }
34 |
35 | var chatlog store.Tokens
36 | chatlog.Model = chatreq.Model
37 | chatlog.CompletionCount = len(chatreq.Input)
38 |
39 | token, _ := c.Get("localuser")
40 |
41 | lu, err := store.GetUserByToken(token.(string))
42 | if err != nil {
43 | c.JSON(http.StatusInternalServerError, gin.H{
44 | "error": gin.H{
45 | "message": err.Error(),
46 | },
47 | })
48 | return
49 | }
50 | chatlog.UserID = int(lu.ID)
51 |
52 | key, err := store.SelectKeyCache("openai")
53 | if err != nil {
54 | c.JSON(http.StatusInternalServerError, gin.H{
55 | "error": gin.H{
56 | "message": err.Error(),
57 | },
58 | })
59 | return
60 | }
61 |
62 | targetURL, _ := url.Parse(SpeechEndpoint)
63 | proxy := httputil.NewSingleHostReverseProxy(targetURL)
64 |
65 | proxy.Director = func(req *http.Request) {
66 | req.Header = c.Request.Header
67 | req.Header["Authorization"] = []string{"Bearer " + key.Key}
68 | req.Host = targetURL.Host
69 | req.URL.Scheme = targetURL.Scheme
70 | req.URL.Host = targetURL.Host
71 | req.URL.Path = targetURL.Path
72 | req.URL.RawPath = targetURL.RawPath
73 |
74 | reqBytes, _ := json.Marshal(chatreq)
75 | req.Body = io.NopCloser(bytes.NewReader(reqBytes))
76 | req.ContentLength = int64(len(reqBytes))
77 |
78 | }
79 | proxy.ModifyResponse = func(resp *http.Response) error {
80 | if resp.StatusCode == http.StatusOK {
81 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
82 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
83 | if err := store.Record(&chatlog); err != nil {
84 | log.Println(err)
85 | }
86 | if err := store.SumDaily(chatlog.UserID); err != nil {
87 | log.Println(err)
88 | }
89 | }
90 | return nil
91 | }
92 | proxy.ServeHTTP(c.Writer, c.Request)
93 | }
94 |
--------------------------------------------------------------------------------
/pkg/openai/whisper.go:
--------------------------------------------------------------------------------
1 | package openai
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "log"
10 | "mime/multipart"
11 | "net/http"
12 | "net/http/httputil"
13 | "net/url"
14 | "opencatd-open/pkg/tokenizer"
15 | "opencatd-open/store"
16 | "path/filepath"
17 | "time"
18 |
19 | "github.com/faiface/beep"
20 | "github.com/faiface/beep/mp3"
21 | "github.com/faiface/beep/wav"
22 | "github.com/gin-gonic/gin"
23 | "gopkg.in/vansante/go-ffprobe.v2"
24 | )
25 |
26 | func WhisperProxy(c *gin.Context) {
27 | var chatlog store.Tokens
28 |
29 | byteBody, _ := io.ReadAll(c.Request.Body)
30 | c.Request.Body = io.NopCloser(bytes.NewBuffer(byteBody))
31 |
32 | model, _ := c.GetPostForm("model")
33 |
34 | key, err := store.SelectKeyCache("openai")
35 | if err != nil {
36 | c.JSON(http.StatusInternalServerError, gin.H{
37 | "error": gin.H{
38 | "message": err.Error(),
39 | },
40 | })
41 | return
42 | }
43 |
44 | chatlog.Model = model
45 |
46 | token, _ := c.Get("localuser")
47 |
48 | lu, err := store.GetUserByToken(token.(string))
49 | if err != nil {
50 | c.JSON(http.StatusInternalServerError, gin.H{
51 | "error": gin.H{
52 | "message": err.Error(),
53 | },
54 | })
55 | return
56 | }
57 | chatlog.UserID = int(lu.ID)
58 |
59 | if err := ParseWhisperRequestTokens(c, &chatlog, byteBody); err != nil {
60 | c.JSON(http.StatusInternalServerError, gin.H{
61 | "error": gin.H{
62 | "message": err.Error(),
63 | },
64 | })
65 | return
66 | }
67 | if key.EndPoint == "" {
68 | key.EndPoint = "https://api.openai.com"
69 | }
70 | targetUrl, _ := url.ParseRequestURI(key.EndPoint + c.Request.URL.String())
71 | log.Println(targetUrl)
72 | proxy := httputil.NewSingleHostReverseProxy(targetUrl)
73 | proxy.Director = func(req *http.Request) {
74 | req.Host = targetUrl.Host
75 | req.URL.Scheme = targetUrl.Scheme
76 | req.URL.Host = targetUrl.Host
77 |
78 | req.Header.Set("Authorization", "Bearer "+key.Key)
79 | }
80 |
81 | proxy.ModifyResponse = func(resp *http.Response) error {
82 | if resp.StatusCode != http.StatusOK {
83 | return nil
84 | }
85 | chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
86 | chatlog.Cost = fmt.Sprintf("%.6f", tokenizer.Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
87 | if err := store.Record(&chatlog); err != nil {
88 | log.Println(err)
89 | }
90 | if err := store.SumDaily(chatlog.UserID); err != nil {
91 | log.Println(err)
92 | }
93 | return nil
94 | }
95 | proxy.ServeHTTP(c.Writer, c.Request)
96 | }
97 |
98 | func probe(fileReader io.Reader) (time.Duration, error) {
99 | ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
100 | defer cancelFn()
101 |
102 | data, err := ffprobe.ProbeReader(ctx, fileReader)
103 | if err != nil {
104 | return 0, err
105 | }
106 |
107 | duration := data.Format.DurationSeconds
108 | pduration, err := time.ParseDuration(fmt.Sprintf("%fs", duration))
109 | if err != nil {
110 | return 0, fmt.Errorf("Error parsing duration: %s", err)
111 | }
112 | return pduration, nil
113 | }
114 |
115 | func getAudioDuration(file *multipart.FileHeader) (time.Duration, error) {
116 | var (
117 | streamer beep.StreamSeekCloser
118 | format beep.Format
119 | err error
120 | )
121 |
122 | f, err := file.Open()
123 | defer f.Close()
124 |
125 | // Get the file extension to determine the audio file type
126 | fileType := filepath.Ext(file.Filename)
127 |
128 | switch fileType {
129 | case ".mp3":
130 | streamer, format, err = mp3.Decode(f)
131 | case ".wav":
132 | streamer, format, err = wav.Decode(f)
133 | case ".m4a":
134 | duration, err := probe(f)
135 | if err != nil {
136 | return 0, err
137 | }
138 | return duration, nil
139 | default:
140 | return 0, errors.New("unsupported audio file format")
141 | }
142 |
143 | if err != nil {
144 | return 0, err
145 | }
146 | defer streamer.Close()
147 |
148 | // Calculate the audio file's duration.
149 | numSamples := streamer.Len()
150 | sampleRate := format.SampleRate
151 | duration := time.Duration(numSamples) * time.Second / time.Duration(sampleRate)
152 |
153 | return duration, nil
154 | }
155 |
156 | func ParseWhisperRequestTokens(c *gin.Context, usage *store.Tokens, byteBody []byte) error {
157 | file, _ := c.FormFile("file")
158 | model, _ := c.GetPostForm("model")
159 | usage.Model = model
160 |
161 | if file != nil {
162 | duration, err := getAudioDuration(file)
163 | if err != nil {
164 | return fmt.Errorf("Error getting audio duration:%s", err)
165 | }
166 |
167 | if duration > 5*time.Minute {
168 | return fmt.Errorf("Audio duration exceeds 5 minutes")
169 | }
170 | // 计算时长,四舍五入到最接近的秒数
171 | usage.PromptCount = int(duration.Round(time.Second).Seconds())
172 | }
173 |
174 | c.Request.Body = io.NopCloser(bytes.NewBuffer(byteBody))
175 |
176 | return nil
177 | }
178 |
--------------------------------------------------------------------------------
/pkg/search/bing.go:
--------------------------------------------------------------------------------
1 | /*
2 | 文档 https://www.microsoft.com/en-us/bing/apis/bing-web-search-api
3 | 价格 https://www.microsoft.com/en-us/bing/apis/pricing
4 |
5 | curl -H "Ocp-Apim-Subscription-Key: " https://api.bing.microsoft.com/v7.0/search?q=今天上海天气怎么样
6 | curl -H "Ocp-Apim-Subscription-Key: 6fc7c97ebed54f75a5e383ee2272c917" https://api.bing.microsoft.com/v7.0/search?q=今天上海天气怎么样
7 | */
8 |
9 | package search
10 |
11 | import (
12 | "fmt"
13 | "io"
14 | "log"
15 | "net/http"
16 | "net/url"
17 | "os"
18 |
19 | "github.com/tidwall/gjson"
20 | )
21 |
22 | const (
23 | bingEndpoint = "https://api.bing.microsoft.com/v7.0/search"
24 | )
25 |
26 | var subscriptionKey string
27 |
28 | func init() {
29 | if os.Getenv("bing") != "" {
30 | subscriptionKey = os.Getenv("bing")
31 | } else {
32 | log.Println("bing key not found")
33 | }
34 | }
35 |
36 | func BingSearch(searchParams SearchParams) (any, error) {
37 | params := url.Values{}
38 | params.Set("q", searchParams.Query)
39 | params.Set("count", "5")
40 | if searchParams.Num > 0 {
41 | params.Set("count", fmt.Sprintf("%d", searchParams.Num))
42 | }
43 |
44 | reqURL, _ := url.Parse(bingEndpoint)
45 | reqURL.RawQuery = params.Encode()
46 |
47 | req, _ := http.NewRequest("GET", reqURL.String(), nil)
48 | req.Header.Set("Ocp-Apim-Subscription-Key", subscriptionKey)
49 |
50 | client := &http.Client{}
51 | resp, err := client.Do(req)
52 | defer resp.Body.Close()
53 | if err != nil {
54 | fmt.Println("Error sending request:", err)
55 | return nil, err
56 | }
57 |
58 | body, err := io.ReadAll(resp.Body)
59 | if err != nil {
60 | fmt.Println("Error reading response:", err)
61 | return nil, err
62 | }
63 | result := gjson.ParseBytes(body).Get("webPages.value")
64 |
65 | return result.Raw, nil
66 |
67 | }
68 |
69 | type SearchParams struct {
70 | Query string `form:"q"`
71 | Num int `form:"num,default=5"`
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/team/key.go:
--------------------------------------------------------------------------------
1 | package team
2 |
3 | import (
4 | "net/http"
5 | "opencatd-open/pkg/azureopenai"
6 | "opencatd-open/store"
7 | "strings"
8 |
9 | "github.com/Sakurasan/to"
10 | "github.com/gin-gonic/gin"
11 | )
12 |
13 | type Key struct {
14 | ID int `json:"id,omitempty"`
15 | Key string `json:"key,omitempty"`
16 | Name string `json:"name,omitempty"`
17 | ApiType string `json:"api_type,omitempty"`
18 | Endpoint string `json:"endpoint,omitempty"`
19 | UpdatedAt string `json:"updatedAt,omitempty"`
20 | CreatedAt string `json:"createdAt,omitempty"`
21 | }
22 |
23 | func HandleKeys(c *gin.Context) {
24 | keys, err := store.GetAllKeys()
25 | if err != nil {
26 | c.JSON(http.StatusOK, gin.H{
27 | "error": err.Error(),
28 | })
29 | }
30 |
31 | c.JSON(http.StatusOK, keys)
32 | }
33 |
34 | func HandleAddKey(c *gin.Context) {
35 | var body Key
36 | if err := c.BindJSON(&body); err != nil {
37 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
38 | "message": err.Error(),
39 | }})
40 | return
41 | }
42 | body.Name = strings.ToLower(strings.TrimSpace(body.Name))
43 | body.Key = strings.TrimSpace(body.Key)
44 | if strings.HasPrefix(body.Name, "azure.") {
45 | keynames := strings.Split(body.Name, ".")
46 | if len(keynames) < 2 {
47 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
48 | "message": "Invalid Key Name",
49 | }})
50 | return
51 | }
52 | k := &store.Key{
53 | ApiType: "azure",
54 | Name: body.Name,
55 | Key: body.Key,
56 | ResourceNmae: keynames[1],
57 | EndPoint: body.Endpoint,
58 | }
59 | if err := store.CreateKey(k); err != nil {
60 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
61 | "message": err.Error(),
62 | }})
63 | return
64 | }
65 | } else if strings.HasPrefix(body.Name, "claude.") {
66 | keynames := strings.Split(body.Name, ".")
67 | if len(keynames) < 2 {
68 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
69 | "message": "Invalid Key Name",
70 | }})
71 | return
72 | }
73 | if body.Endpoint == "" {
74 | body.Endpoint = "https://api.anthropic.com"
75 | }
76 | k := &store.Key{
77 | // ApiType: "anthropic",
78 | ApiType: "claude",
79 | Name: body.Name,
80 | Key: body.Key,
81 | ResourceNmae: keynames[1],
82 | EndPoint: body.Endpoint,
83 | }
84 | if err := store.CreateKey(k); err != nil {
85 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
86 | "message": err.Error(),
87 | }})
88 | return
89 | }
90 | } else if strings.HasPrefix(body.Name, "google.") {
91 | keynames := strings.Split(body.Name, ".")
92 | if len(keynames) < 2 {
93 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
94 | "message": "Invalid Key Name",
95 | }})
96 | return
97 | }
98 |
99 | k := &store.Key{
100 | // ApiType: "anthropic",
101 | ApiType: "google",
102 | Name: body.Name,
103 | Key: body.Key,
104 | ResourceNmae: keynames[1],
105 | EndPoint: body.Endpoint,
106 | }
107 | if err := store.CreateKey(k); err != nil {
108 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
109 | "message": err.Error(),
110 | }})
111 | return
112 | }
113 | } else if strings.HasPrefix(body.Name, "github.") {
114 | keynames := strings.Split(body.Name, ".")
115 | if len(keynames) < 2 {
116 | c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
117 | "message": "Invalid Key Name",
118 | }})
119 | return
120 | }
121 |
122 | k := &store.Key{
123 | ApiType: "github",
124 | Name: body.Name,
125 | Key: body.Key,
126 | ResourceNmae: keynames[1],
127 | EndPoint: body.Endpoint,
128 | }
129 | if err := store.CreateKey(k); err != nil {
130 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
131 | "message": err.Error(),
132 | }})
133 | return
134 | }
135 | } else {
136 | if body.ApiType == "" {
137 | if err := store.AddKey("openai", body.Key, body.Name); err != nil {
138 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
139 | "message": err.Error(),
140 | }})
141 | return
142 | }
143 | } else {
144 | k := &store.Key{
145 | ApiType: body.ApiType,
146 | Name: body.Name,
147 | Key: body.Key,
148 | ResourceNmae: azureopenai.GetResourceName(body.Endpoint),
149 | EndPoint: body.Endpoint,
150 | }
151 | if err := store.CreateKey(k); err != nil {
152 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
153 | "message": err.Error(),
154 | }})
155 | return
156 | }
157 | }
158 |
159 | }
160 |
161 | k, err := store.GetKeyrByName(body.Name)
162 | if err != nil {
163 | c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
164 | "message": err.Error(),
165 | }})
166 | return
167 | }
168 | c.JSON(http.StatusOK, k)
169 | }
170 |
171 | func HandleDelKey(c *gin.Context) {
172 | id := to.Int(c.Param("id"))
173 | if id < 1 {
174 | c.JSON(http.StatusOK, gin.H{"error": "invalid key id"})
175 | return
176 | }
177 | if err := store.DeleteKey(uint(id)); err != nil {
178 | c.JSON(http.StatusOK, gin.H{"error": "invalid key id"})
179 | return
180 | }
181 | c.JSON(http.StatusOK, gin.H{"message": "ok"})
182 | }
183 |
--------------------------------------------------------------------------------
/pkg/team/me.go:
--------------------------------------------------------------------------------
1 | package team
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "opencatd-open/store"
7 | "time"
8 |
9 | "github.com/Sakurasan/to"
10 | "github.com/gin-gonic/gin"
11 | "github.com/google/uuid"
12 | "gorm.io/gorm"
13 | )
14 |
15 | func Handleinit(c *gin.Context) {
16 | user, err := store.GetUserByID(1)
17 | if err != nil {
18 | if errors.Is(err, gorm.ErrRecordNotFound) {
19 | u := store.User{Name: "root", Token: uuid.NewString()}
20 | u.ID = 1
21 | if err := store.CreateUser(&u); err != nil {
22 | c.JSON(http.StatusForbidden, gin.H{
23 | "error": err.Error(),
24 | })
25 | return
26 | } else {
27 | rootToken = u.Token
28 | resJSON := User{
29 | false,
30 | int(u.ID),
31 | u.UpdatedAt.Format(time.RFC3339),
32 | u.Name,
33 | u.Token,
34 | u.CreatedAt.Format(time.RFC3339),
35 | }
36 | c.JSON(http.StatusOK, resJSON)
37 | return
38 | }
39 | }
40 | c.JSON(http.StatusOK, gin.H{
41 | "error": err.Error(),
42 | })
43 | return
44 | }
45 | if user.ID == uint(1) {
46 | c.JSON(http.StatusForbidden, gin.H{
47 | "error": "super user already exists, use cli to reset password",
48 | })
49 | }
50 | }
51 |
52 | func HandleMe(c *gin.Context) {
53 | token := c.GetHeader("Authorization")
54 | u, err := store.GetUserByToken(token[7:])
55 | if err != nil {
56 | c.JSON(http.StatusOK, gin.H{
57 | "error": err.Error(),
58 | })
59 | }
60 |
61 | resJSON := User{
62 | false,
63 | int(u.ID),
64 | u.UpdatedAt.Format(time.RFC3339),
65 | u.Name,
66 | u.Token,
67 | u.CreatedAt.Format(time.RFC3339),
68 | }
69 | c.JSON(http.StatusOK, resJSON)
70 | }
71 |
72 | func HandleMeUsage(c *gin.Context) {
73 | token := c.GetHeader("Authorization")
74 | fromStr := c.Query("from")
75 | toStr := c.Query("to")
76 | getMonthStartAndEnd := func() (start, end string) {
77 | loc, _ := time.LoadLocation("Local")
78 | now := time.Now().In(loc)
79 |
80 | year, month, _ := now.Date()
81 |
82 | startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc)
83 | endOfMonth := startOfMonth.AddDate(0, 1, 0)
84 |
85 | start = startOfMonth.Format("2006-01-02")
86 | end = endOfMonth.Format("2006-01-02")
87 | return
88 | }
89 | if fromStr == "" || toStr == "" {
90 | fromStr, toStr = getMonthStartAndEnd()
91 | }
92 | user, err := store.GetUserByToken(token)
93 | if err != nil {
94 | c.AbortWithError(http.StatusForbidden, err)
95 | return
96 | }
97 | usage, err := store.QueryUserUsage(to.String(user.ID), fromStr, toStr)
98 | if err != nil {
99 | c.AbortWithError(http.StatusForbidden, err)
100 | return
101 | }
102 |
103 | c.JSON(200, usage)
104 | }
105 |
--------------------------------------------------------------------------------
/pkg/team/middleware.go:
--------------------------------------------------------------------------------
1 | package team
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "opencatd-open/store"
7 | "strings"
8 |
9 | "github.com/gin-contrib/cors"
10 | "github.com/gin-gonic/gin"
11 | )
12 |
13 | var (
14 | rootToken string
15 | )
16 |
17 | func AuthMiddleware() gin.HandlerFunc {
18 | return func(c *gin.Context) {
19 | if rootToken == "" {
20 | u, err := store.GetUserByID(uint(1))
21 | if err != nil {
22 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
23 | c.Abort()
24 | return
25 | }
26 | rootToken = u.Token
27 | }
28 | token := c.GetHeader("Authorization")
29 | if token == "" || token[:7] != "Bearer " {
30 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
31 | c.Abort()
32 | return
33 | }
34 | if store.IsExistAuthCache(token[7:]) {
35 | if strings.HasPrefix(c.Request.URL.Path, "/1/me") {
36 | c.Next()
37 | return
38 | }
39 | }
40 | if token[7:] != rootToken {
41 | u, err := store.GetUserByID(uint(1))
42 | if err != nil {
43 | log.Println(err)
44 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
45 | c.Abort()
46 | return
47 | }
48 | if token[:7] != u.Token {
49 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
50 | c.Abort()
51 | return
52 | }
53 | rootToken = u.Token
54 | store.LoadAuthCache()
55 | }
56 | // 可以在这里对 token 进行验证并检查权限
57 |
58 | c.Next()
59 | }
60 | }
61 |
62 | func CORS() gin.HandlerFunc {
63 | config := cors.DefaultConfig()
64 | config.AllowAllOrigins = true
65 | config.AllowCredentials = true
66 | config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
67 | config.AllowHeaders = []string{"*"}
68 | return cors.New(config)
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/team/usage.go:
--------------------------------------------------------------------------------
1 | package team
2 |
3 | import (
4 | "net/http"
5 | "opencatd-open/store"
6 | "time"
7 |
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | func HandleUsage(c *gin.Context) {
12 | fromStr := c.Query("from")
13 | toStr := c.Query("to")
14 | getMonthStartAndEnd := func() (start, end string) {
15 | loc, _ := time.LoadLocation("Local")
16 | now := time.Now().In(loc)
17 |
18 | year, month, _ := now.Date()
19 |
20 | startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc)
21 | endOfMonth := startOfMonth.AddDate(0, 1, 0)
22 |
23 | start = startOfMonth.Format("2006-01-02")
24 | end = endOfMonth.Format("2006-01-02")
25 | return
26 | }
27 | if fromStr == "" || toStr == "" {
28 | fromStr, toStr = getMonthStartAndEnd()
29 | }
30 |
31 | usage, err := store.QueryUsage(fromStr, toStr)
32 | if err != nil {
33 | c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
34 | return
35 | }
36 |
37 | c.JSON(200, usage)
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/team/user.go:
--------------------------------------------------------------------------------
1 | package team
2 |
3 | import (
4 | "net/http"
5 | "opencatd-open/store"
6 |
7 | "github.com/Sakurasan/to"
8 | "github.com/gin-gonic/gin"
9 | "github.com/google/uuid"
10 | )
11 |
12 | type User struct {
13 | IsDelete bool `json:"IsDelete,omitempty"`
14 | ID int `json:"id,omitempty"`
15 | UpdatedAt string `json:"updatedAt,omitempty"`
16 | Name string `json:"name,omitempty"`
17 | Token string `json:"token,omitempty"`
18 | CreatedAt string `json:"createdAt,omitempty"`
19 | }
20 |
21 | func HandleUsers(c *gin.Context) {
22 | users, err := store.GetAllUsers()
23 | if err != nil {
24 | c.JSON(http.StatusOK, gin.H{
25 | "error": err.Error(),
26 | })
27 | }
28 |
29 | c.JSON(http.StatusOK, users)
30 | }
31 |
32 | func HandleAddUser(c *gin.Context) {
33 | var body User
34 | if err := c.BindJSON(&body); err != nil {
35 | c.JSON(http.StatusOK, gin.H{"error": err.Error()})
36 | return
37 | }
38 | if len(body.Name) == 0 {
39 | c.JSON(http.StatusOK, gin.H{"error": "invalid user name"})
40 | return
41 | }
42 |
43 | if err := store.AddUser(body.Name, uuid.NewString()); err != nil {
44 | c.JSON(http.StatusOK, gin.H{"error": err.Error()})
45 | return
46 | }
47 | u, err := store.GetUserByName(body.Name)
48 | if err != nil {
49 | c.JSON(http.StatusOK, gin.H{"error": err.Error()})
50 | return
51 | }
52 | c.JSON(http.StatusOK, u)
53 | }
54 |
55 | func HandleDelUser(c *gin.Context) {
56 | id := to.Int(c.Param("id"))
57 | if id <= 1 {
58 | c.JSON(http.StatusOK, gin.H{"error": "invalid user id"})
59 | return
60 | }
61 | if err := store.DeleteUser(uint(id)); err != nil {
62 | c.JSON(http.StatusOK, gin.H{"error": err.Error()})
63 | return
64 | }
65 |
66 | c.JSON(http.StatusOK, gin.H{"message": "ok"})
67 | }
68 |
69 | func HandleResetUserToken(c *gin.Context) {
70 | id := to.Int(c.Param("id"))
71 | newtoken := c.Query("token")
72 | if newtoken == "" {
73 | newtoken = uuid.NewString()
74 | }
75 |
76 | if err := store.UpdateUser(uint(id), newtoken); err != nil {
77 | c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
78 | return
79 | }
80 | u, err := store.GetUserByID(uint(id))
81 | if err != nil {
82 | c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
83 | return
84 | }
85 | if u.ID == uint(1) {
86 | rootToken = u.Token
87 | }
88 | c.JSON(http.StatusOK, u)
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/tokenizer/tokenizer.go:
--------------------------------------------------------------------------------
1 | package tokenizer
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 |
8 | "github.com/pkoukk/tiktoken-go"
9 | "github.com/sashabaranov/go-openai"
10 | )
11 |
12 | func NumTokensFromMessages(messages []openai.ChatCompletionMessage, model string) (numTokens int) {
13 | tkm, err := tiktoken.EncodingForModel(model)
14 | if err != nil {
15 | err = fmt.Errorf("EncodingForModel: %v", err)
16 | log.Println(err)
17 | return
18 | }
19 |
20 | var tokensPerMessage, tokensPerName int
21 |
22 | switch model {
23 | case "gpt-3.5-turbo",
24 | "gpt-3.5-turbo-0613",
25 | "gpt-3.5-turbo-16k",
26 | "gpt-3.5-turbo-16k-0613",
27 | "gpt-4",
28 | "gpt-4-0314",
29 | "gpt-4-0613",
30 | "gpt-4-32k",
31 | "gpt-4-32k-0314",
32 | "gpt-4-32k-0613":
33 | tokensPerMessage = 3
34 | tokensPerName = 1
35 | case "gpt-3.5-turbo-0301":
36 | tokensPerMessage = 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n
37 | tokensPerName = -1 // if there's a name, the role is omitted
38 | default:
39 | if strings.Contains(model, "gpt-3.5-turbo") {
40 | log.Println("warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.")
41 | return NumTokensFromMessages(messages, "gpt-3.5-turbo-0613")
42 | } else if strings.Contains(model, "gpt-4") {
43 | log.Println("warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")
44 | return NumTokensFromMessages(messages, "gpt-4-0613")
45 | } else {
46 | err = fmt.Errorf("warning: unknown model [%s]. Use default calculation method converted tokens.", model)
47 | log.Println(err)
48 | return NumTokensFromMessages(messages, "gpt-3.5-turbo-0613")
49 | }
50 | }
51 |
52 | for _, message := range messages {
53 | numTokens += tokensPerMessage
54 | numTokens += len(tkm.Encode(message.Content, nil, nil))
55 | numTokens += len(tkm.Encode(message.Role, nil, nil))
56 | numTokens += len(tkm.Encode(message.Name, nil, nil))
57 | if message.Name != "" {
58 | numTokens += tokensPerName
59 | }
60 | }
61 | numTokens += 3
62 | return numTokens
63 | }
64 |
65 | func NumTokensFromStr(messages string, model string) (num_tokens int) {
66 | tkm, err := tiktoken.EncodingForModel(model)
67 | if err != nil {
68 | fmt.Println(err)
69 | fmt.Println("Unsupport Model,use cl100k_base Encode")
70 | tkm, _ = tiktoken.GetEncoding("cl100k_base")
71 | }
72 |
73 | num_tokens += len(tkm.Encode(messages, nil, nil))
74 | return num_tokens
75 | }
76 |
77 | // https://openai.com/pricing
78 | func Cost(model string, promptCount, completionCount int) float64 {
79 | var cost, prompt, completion float64
80 | prompt = float64(promptCount)
81 | completion = float64(completionCount)
82 |
83 | switch model {
84 | case "gpt-3.5-turbo-0301":
85 | cost = 0.002 * float64((prompt+completion)/1000)
86 | case "gpt-3.5-turbo", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125":
87 | cost = 0.0015*float64((prompt)/1000) + 0.002*float64(completion/1000)
88 | case "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613":
89 | cost = 0.003*float64((prompt)/1000) + 0.004*float64(completion/1000)
90 | case "gpt-4", "gpt-4-0613", "gpt-4-0314":
91 | cost = 0.03*float64(prompt/1000) + 0.06*float64(completion/1000)
92 | case "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613":
93 | cost = 0.06*float64(prompt/1000) + 0.12*float64(completion/1000)
94 | case "gpt-4-1106-preview", "gpt-4-vision-preview", "gpt-4-0125-preview", "gpt-4-turbo-preview":
95 | cost = 0.01*float64(prompt/1000) + 0.03*float64(completion/1000)
96 | case "gpt-4-turbo", "gpt-4-turbo-2024-04-09":
97 | cost = 0.01*float64(prompt/1000) + 0.03*float64(completion/1000)
98 | // omni
99 | case "gpt-4o", "gpt-4o-2024-08-06":
100 | cost = 0.0025*float64(prompt/1000) + 0.01*float64(completion/1000)
101 | case "gpt-4o-2024-05-13":
102 | cost = 0.005*float64(prompt/1000) + 0.015*float64(completion/1000)
103 | case "gpt-4o-mini", "gpt-4o-mini-2024-07-18":
104 | cost = 0.00015*float64(prompt/1000) + 0.0006*float64(completion/1000)
105 | case "chatgpt-4o-latest":
106 | cost = 0.005*float64(prompt/1000) + 0.015*float64(completion/1000)
107 | // o1
108 | case "o1-preview", "o1-preview-2024-09-12":
109 | cost = 0.015*float64(prompt/1000) + 0.06*float64(completion/1000)
110 | case "o1-mini", "o1-mini-2024-09-12":
111 | cost = 0.003*float64(prompt/1000) + 0.012*float64(completion/1000)
112 | case "o3-mini", "o3-mini-2025-01-31":
113 | cost = 0.003*float64(prompt/1000) + 0.012*float64(completion/1000)
114 | // Realtime API
115 | // Audio*
116 | // $0.1 / 1K input tokens
117 | // $0.2 / 1K output tokens
118 | case "gpt-4o-audio-preview", "gpt-4o-audio-preview-2024-12-17":
119 | cost = 0.0025*float64(prompt/1000) + 0.01*float64(completion/1000)
120 | case "gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01":
121 | cost = 0.005*float64(prompt/1000) + 0.020*float64(completion/1000)
122 | case "gpt-4o-realtime-preview.audio", "gpt-4o-realtime-preview-2024-10-01.audio":
123 | cost = 0.1*float64(prompt/1000) + 0.2*float64(completion/1000)
124 |
125 | case "gpt-4o-mini-audio-preview", "gpt-4o-mini-audio-preview-2024-12-17":
126 | cost = 0.00015*float64(prompt/1000) + 0.0006*float64(completion/1000)
127 | case "gpt-4o-mini-realtime-preview", "gpt-4o-mini-realtime-preview-2024-12-17":
128 | cost = 0.0006*float64(prompt/1000) + 0.0024*float64(completion/1000)
129 |
130 | case "whisper-1":
131 | // 0.006$/min
132 | cost = 0.006 * float64(prompt+completion) / 60
133 | case "tts-1":
134 | cost = 0.015 * float64(prompt+completion)
135 | case "tts-1-hd":
136 | cost = 0.03 * float64(prompt+completion)
137 | case "dall-e-2.256x256":
138 | cost = float64(0.016 * completion)
139 | case "dall-e-2.512x512":
140 | cost = float64(0.018 * completion)
141 | case "dall-e-2.1024x1024":
142 | cost = float64(0.02 * completion)
143 | case "dall-e-3.256x256":
144 | cost = float64(0.04 * completion)
145 | case "dall-e-3.512x512":
146 | cost = float64(0.04 * completion)
147 | case "dall-e-3.1024x1024":
148 | cost = float64(0.04 * completion)
149 | case "dall-e-3.1024x1792", "dall-e-3.1792x1024":
150 | cost = float64(0.08 * completion)
151 | case "dall-e-3.256x256.hd":
152 | cost = float64(0.08 * completion)
153 | case "dall-e-3.512x512.hd":
154 | cost = float64(0.08 * completion)
155 | case "dall-e-3.1024x1024.hd":
156 | cost = float64(0.08 * completion)
157 | case "dall-e-3.1024x1792.hd", "dall-e-3.1792x1024.hd":
158 | cost = float64(0.12 * completion)
159 |
160 | // claude /million tokens
161 | // https://aws.amazon.com/cn/bedrock/pricing/
162 | case "claude-v1", "claude-v1-100k":
163 | cost = 11.02/1000000*float64(prompt) + (32.68/1000000)*float64(completion)
164 | case "claude-instant-v1", "claude-instant-v1-100k":
165 | cost = (1.63/1000000)*float64(prompt) + (5.51/1000000)*float64(completion)
166 | case "claude-2", "claude-2.1":
167 | cost = (8.0/1000000)*float64(prompt) + (24.0/1000000)*float64(completion)
168 | case "claude-3-haiku":
169 | cost = (0.00025/1000)*float64(prompt) + (0.00125/1000)*float64(completion)
170 | case "claude-3-sonnet":
171 | cost = (0.003/1000)*float64(prompt) + (0.015/1000)*float64(completion)
172 | case "claude-3-opus":
173 | cost = (0.015/1000)*float64(prompt) + (0.075/1000)*float64(completion)
174 | case "claude-3-haiku-20240307":
175 | cost = (0.00025/1000)*float64(prompt) + (0.00125/1000)*float64(completion)
176 | case "claude-3-5-haiku-latest", "claude-3-5-haiku-20241022":
177 | cost = (0.001/1000)*float64(prompt) + (0.005/1000)*float64(completion)
178 | case "claude-3-sonnet-20240229":
179 | cost = (0.003/1000)*float64(prompt) + (0.015/1000)*float64(completion)
180 | case "claude-3-opus-20240229":
181 | cost = (0.015/1000)*float64(prompt) + (0.075/1000)*float64(completion)
182 | case "claude-3-5-sonnet", "claude-3-5-sonnet-latest", "claude-3-5-sonnet-20240620", "claude-3-5-sonnet-20241022":
183 | cost = (0.003/1000)*float64(prompt) + (0.015/1000)*float64(completion)
184 | // google
185 | // https://ai.google.dev/pricing?hl=zh-cn
186 | case "gemini-pro":
187 | cost = (0.0005/1000)*float64(prompt) + (0.0015/1000)*float64(completion)
188 | case "gemini-pro-vision":
189 | cost = (0.0005/1000)*float64(prompt) + (0.0015/1000)*float64(completion)
190 | case "gemini-1.5-pro-latest":
191 | cost = (0.0035/1000)*float64(prompt) + (0.0105/1000)*float64(completion)
192 | case "gemini-1.5-flash-latest":
193 | cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion)
194 | case "gemini-2.0-flash-exp":
195 | cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion)
196 | case "gemini-2.0-flash-thinking-exp-1219", "gemini-2.0-flash-thinking-exp-01-21":
197 | cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion)
198 | case "learnlm-1.5-pro-experimental", " gemini-exp-1114", "gemini-exp-1121", "gemini-exp-1206":
199 | cost = (0.00035/1000)*float64(prompt) + (0.00053/1000)*float64(completion)
200 |
201 | // Mistral AI
202 | // https://docs.mistral.ai/platform/pricing/
203 | case "mistral-small-latest":
204 | cost = (0.002/1000)*float64(prompt) + (0.006/1000)*float64(completion)
205 | case "mistral-medium-latest":
206 | cost = (0.0027/1000)*float64(prompt) + (0.0081/1000)*float64(completion)
207 | case "mistral-large-latest":
208 | cost = (0.008/1000)*float64(prompt) + (0.024/1000)*float64(completion)
209 |
210 | default:
211 | if strings.Contains(model, "gpt-3.5-turbo") {
212 | cost = 0.003 * float64((prompt+completion)/1000)
213 | } else if strings.Contains(model, "gpt-4") {
214 | cost = 0.06 * float64((prompt+completion)/1000)
215 | } else {
216 | cost = 0.002 * float64((prompt+completion)/1000)
217 | }
218 | }
219 | return cost
220 | }
221 |
--------------------------------------------------------------------------------
/pkg/vertexai/auth.go:
--------------------------------------------------------------------------------
1 | /*
2 | https://docs.anthropic.com/zh-CN/api/claude-on-vertex-ai
3 |
4 | MODEL_ID=claude-3-5-sonnet@20240620
5 | REGION=us-east5
6 | PROJECT_ID=MY_PROJECT_ID
7 |
8 | curl \
9 | -X POST \
10 | -H "Authorization: Bearer $(gcloud auth print-access-token)" \
11 | -H "Content-Type: application/json" \
12 | https://$LOCATION-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/publishers/anthropic/models/${MODEL_ID}:streamRawPredict \
13 | -d '{
14 | "anthropic_version": "vertex-2023-10-16",
15 | "messages": [{
16 | "role": "user",
17 | "content": "介绍一下你自己"
18 | }],
19 | "stream": true,
20 | "max_tokens": 4096
21 | }'
22 |
23 | quota:
24 | https://console.cloud.google.com/iam-admin/quotas?hl=zh-cn
25 | */
26 |
27 | package vertexai
28 |
29 | import (
30 | "crypto/rsa"
31 | "crypto/x509"
32 | "encoding/json"
33 | "encoding/pem"
34 | "fmt"
35 | "net/http"
36 | "net/url"
37 | "os"
38 | "time"
39 |
40 | "github.com/golang-jwt/jwt"
41 | )
42 |
43 | // json文件存储在ApiKey.ApiSecret中
44 | type VertexSecretKey struct {
45 | Type string `json:"type"`
46 | ProjectID string `json:"project_id"`
47 | PrivateKeyID string `json:"private_key_id"`
48 | PrivateKey string `json:"private_key"`
49 | ClientEmail string `json:"client_email"`
50 | ClientID string `json:"client_id"`
51 | AuthURI string `json:"auth_uri"`
52 | TokenURI string `json:"token_uri"`
53 | AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
54 | ClientX509CertURL string `json:"client_x509_cert_url"`
55 | UniverseDomain string `json:"universe_domain"`
56 | }
57 |
58 | type VertexClaudeModel struct {
59 | VertexName string
60 | Region string
61 | }
62 |
63 | var VertexClaudeModelMap = map[string]VertexClaudeModel{
64 | "claude-3-opus": {
65 | VertexName: "claude-3-opus@20240229",
66 | Region: "us-east5",
67 | },
68 | "claude-3-sonnet": {
69 | VertexName: "claude-3-sonnet@20240229",
70 | Region: "us-central1",
71 | // Region: "asia-southeast1",
72 | },
73 | "claude-3-haiku": {
74 | VertexName: "claude-3-haiku@20240307",
75 | Region: "us-central1",
76 | // Region: "europe-west4",
77 | },
78 | "claude-3-opus-20240229": {
79 | VertexName: "claude-3-opus@20240229",
80 | Region: "us-east5",
81 | },
82 | "claude-3-sonnet-20240229": {
83 | VertexName: "claude-3-sonnet@20240229",
84 | Region: "us-central1",
85 | // Region: "asia-southeast1",
86 | },
87 | "claude-3-haiku-20240307": {
88 | VertexName: "claude-3-haiku@20240307",
89 | Region: "us-central1",
90 | // Region: "europe-west4",
91 | },
92 | "claude-3-5-sonnet": {
93 | VertexName: "claude-3-5-sonnet@20240620",
94 | Region: "us-east5",
95 | // Region: "europe-west1",
96 | },
97 | "claude-3-5-sonnet-20240620": {
98 | VertexName: "claude-3-5-sonnet@20240620",
99 | Region: "us-east5",
100 | // Region: "europe-west1",
101 | },
102 | "claude-3-5-sonnet-20241022": {
103 | VertexName: "claude-3-5-sonnet-v2@20241022",
104 | Region: "us-east5",
105 | },
106 | "claude-3-5-sonnet-latest": { //可能没有容量,指向老模型
107 | VertexName: "claude-3-5-sonnet@20240620",
108 | Region: "us-east5",
109 | },
110 | }
111 |
112 | func createSignedJWT(email, privateKeyPEM string) (string, error) {
113 | block, _ := pem.Decode([]byte(privateKeyPEM))
114 | if block == nil {
115 | return "", fmt.Errorf("failed to parse PEM block containing the private key")
116 | }
117 |
118 | privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
119 | if err != nil {
120 | return "", err
121 | }
122 |
123 | rsaKey, ok := privateKey.(*rsa.PrivateKey)
124 | if !ok {
125 | return "", fmt.Errorf("not an RSA private key")
126 | }
127 |
128 | now := time.Now()
129 | claims := jwt.MapClaims{
130 | "iss": email,
131 | "aud": "https://www.googleapis.com/oauth2/v4/token",
132 | "iat": now.Unix(),
133 | "exp": now.Add(10 * time.Minute).Unix(),
134 | "scope": "https://www.googleapis.com/auth/cloud-platform",
135 | }
136 |
137 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
138 | return token.SignedString(rsaKey)
139 | }
140 |
141 | func exchangeJwtForAccessToken(signedJWT string) (string, error) {
142 | authURL := "https://www.googleapis.com/oauth2/v4/token"
143 | data := url.Values{}
144 | data.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
145 | data.Set("assertion", signedJWT)
146 |
147 | client := http.DefaultClient
148 | if os.Getenv("LOCAL_PROXY") != "" {
149 | if proxyUrl, err := url.Parse(os.Getenv("LOCAL_PROXY")); err == nil {
150 | client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)}
151 | }
152 | }
153 |
154 | resp, err := client.PostForm(authURL, data)
155 | if err != nil {
156 | return "", err
157 | }
158 | defer resp.Body.Close()
159 |
160 | var result map[string]interface{}
161 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
162 | return "", err
163 | }
164 |
165 | accessToken, ok := result["access_token"].(string)
166 | if !ok {
167 | return "", fmt.Errorf("access token not found in response")
168 | }
169 |
170 | return accessToken, nil
171 | }
172 |
173 | // 获取gcloud auth token
174 | func GcloudAuth(ClientEmail, PrivateKey string) (string, error) {
175 | signedJWT, err := createSignedJWT(ClientEmail, PrivateKey)
176 | if err != nil {
177 | return "", err
178 | }
179 |
180 | token, err := exchangeJwtForAccessToken(signedJWT)
181 | if err != nil {
182 | return "", fmt.Errorf("Invalid jwt token: %v\n", err)
183 | }
184 |
185 | return token, nil
186 | }
187 |
--------------------------------------------------------------------------------
/router/chat.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 |
7 | "opencatd-open/pkg/claude"
8 | "opencatd-open/pkg/google"
9 | "opencatd-open/pkg/openai"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | func ChatHandler(c *gin.Context) {
15 | var chatreq openai.ChatCompletionRequest
16 | if err := c.ShouldBindJSON(&chatreq); err != nil {
17 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
18 | return
19 | }
20 |
21 | if strings.Contains(chatreq.Model, "gpt") || strings.HasPrefix(chatreq.Model, "o1") || strings.HasPrefix(chatreq.Model, "o3") {
22 | openai.ChatProxy(c, &chatreq)
23 | return
24 | }
25 |
26 | if strings.HasPrefix(chatreq.Model, "claude") {
27 | claude.ChatProxy(c, &chatreq)
28 | return
29 | }
30 |
31 | if strings.HasPrefix(chatreq.Model, "gemini") || strings.HasPrefix(chatreq.Model, "learnlm") {
32 | google.ChatProxy(c, &chatreq)
33 | return
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "crypto/tls"
5 | "net"
6 | "net/http"
7 | "net/http/httputil"
8 | "opencatd-open/pkg/claude"
9 | oai "opencatd-open/pkg/openai"
10 | "opencatd-open/store"
11 | "time"
12 |
13 | "github.com/gin-gonic/gin"
14 | )
15 |
16 | var (
17 | GPT3Dot5Turbo = "gpt-3.5-turbo"
18 | GPT4 = "gpt-4"
19 | )
20 |
21 | // type ChatCompletionMessage struct {
22 | // Role string `json:"role"`
23 | // Content string `json:"content"`
24 | // Name string `json:"name,omitempty"`
25 | // }
26 |
27 | // type ChatCompletionRequest struct {
28 | // Model string `json:"model"`
29 | // Messages []ChatCompletionMessage `json:"messages"`
30 | // MaxTokens int `json:"max_tokens,omitempty"`
31 | // Temperature float32 `json:"temperature,omitempty"`
32 | // TopP float32 `json:"top_p,omitempty"`
33 | // N int `json:"n,omitempty"`
34 | // Stream bool `json:"stream,omitempty"`
35 | // Stop []string `json:"stop,omitempty"`
36 | // PresencePenalty float32 `json:"presence_penalty,omitempty"`
37 | // FrequencyPenalty float32 `json:"frequency_penalty,omitempty"`
38 | // LogitBias map[string]int `json:"logit_bias,omitempty"`
39 | // User string `json:"user,omitempty"`
40 | // }
41 |
42 | // type ChatCompletionChoice struct {
43 | // Index int `json:"index"`
44 | // Message ChatCompletionMessage `json:"message"`
45 | // FinishReason string `json:"finish_reason"`
46 | // }
47 |
48 | // type ChatCompletionResponse struct {
49 | // ID string `json:"id"`
50 | // Object string `json:"object"`
51 | // Created int64 `json:"created"`
52 | // Model string `json:"model"`
53 | // Choices []ChatCompletionChoice `json:"choices"`
54 | // Usage struct {
55 | // PromptTokens int `json:"prompt_tokens"`
56 | // CompletionTokens int `json:"completion_tokens"`
57 | // TotalTokens int `json:"total_tokens"`
58 | // } `json:"usage"`
59 | // }
60 |
61 | func HandleProxy(c *gin.Context) {
62 | var (
63 | localuser bool
64 | )
65 | auth := c.Request.Header.Get("Authorization")
66 | if len(auth) > 7 && auth[:7] == "Bearer " {
67 | localuser = store.IsExistAuthCache(auth[7:])
68 | c.Set("localuser", auth[7:])
69 | }
70 | if c.Request.URL.Path == "/v1/complete" {
71 | if localuser {
72 | claude.ClaudeProxy(c)
73 | return
74 | } else {
75 | HandleReverseProxy(c, "api.anthropic.com")
76 | return
77 | }
78 |
79 | }
80 | if c.Request.URL.Path == "/v1/audio/transcriptions" {
81 | oai.WhisperProxy(c)
82 | return
83 | }
84 | if c.Request.URL.Path == "/v1/audio/speech" {
85 | oai.SpeechProxy(c)
86 | return
87 | }
88 |
89 | if c.Request.URL.Path == "/v1/images/generations" {
90 | oai.DallEProxy(c)
91 | return
92 | }
93 |
94 | if c.Request.URL.Path == "/v1/realtime" {
95 | oai.RealTimeProxy(c)
96 | return
97 | }
98 |
99 | if c.Request.URL.Path == "/v1/chat/completions" {
100 | if localuser {
101 | if store.KeysCache.ItemCount() == 0 {
102 | c.JSON(http.StatusBadGateway, gin.H{"error": gin.H{
103 | "message": "No Api-Key Available",
104 | }})
105 | return
106 | }
107 |
108 | ChatHandler(c)
109 | return
110 | }
111 | } else {
112 | HandleReverseProxy(c, "api.openai.com")
113 | return
114 | }
115 |
116 | }
117 |
118 | func HandleReverseProxy(c *gin.Context, targetHost string) {
119 | proxy := &httputil.ReverseProxy{
120 | Director: func(req *http.Request) {
121 | req.URL.Scheme = "https"
122 | req.URL.Host = targetHost
123 | },
124 | Transport: &http.Transport{
125 | Proxy: http.ProxyFromEnvironment,
126 | DialContext: (&net.Dialer{
127 | Timeout: 30 * time.Second,
128 | KeepAlive: 30 * time.Second,
129 | }).DialContext,
130 | ForceAttemptHTTP2: true,
131 | MaxIdleConns: 100,
132 | IdleConnTimeout: 90 * time.Second,
133 | TLSHandshakeTimeout: 10 * time.Second,
134 | ExpectContinueTimeout: 1 * time.Second,
135 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
136 | },
137 | }
138 |
139 | req, err := http.NewRequest(c.Request.Method, c.Request.URL.Path, c.Request.Body)
140 | if err != nil {
141 | c.JSON(http.StatusOK, gin.H{"error": err.Error()})
142 | return
143 | }
144 | req.Header = c.Request.Header
145 |
146 | proxy.ServeHTTP(c.Writer, req)
147 | return
148 | }
149 |
--------------------------------------------------------------------------------
/store/cache.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "errors"
5 | "log"
6 | "math/rand"
7 | "os"
8 | "strings"
9 | "time"
10 |
11 | "github.com/Sakurasan/to"
12 | "github.com/patrickmn/go-cache"
13 | )
14 |
15 | var (
16 | KeysCache *cache.Cache
17 | AuthCache *cache.Cache
18 | )
19 |
20 | func init() {
21 | KeysCache = cache.New(cache.NoExpiration, cache.NoExpiration)
22 | AuthCache = cache.New(cache.NoExpiration, cache.NoExpiration)
23 | }
24 |
25 | func LoadKeysCache() {
26 | KeysCache = cache.New(cache.NoExpiration, cache.NoExpiration)
27 | keys, err := GetAllKeys()
28 | if err != nil {
29 | log.Println(err)
30 | return
31 | }
32 | for idx, key := range keys {
33 | KeysCache.Set(to.String(idx), key, cache.NoExpiration)
34 | }
35 | }
36 |
37 | func FromKeyCacheRandomItemKey() Key {
38 | items := KeysCache.Items()
39 | if len(items) == 1 {
40 | return items[to.String(0)].Object.(Key)
41 | }
42 | idx := rand.Intn(len(items))
43 | item := items[to.String(idx)]
44 | return item.Object.(Key)
45 | }
46 |
47 | func SelectKeyCache(apitype string) (Key, error) {
48 | var keys []Key
49 | items := KeysCache.Items()
50 | for _, item := range items {
51 | if item.Object.(Key).ApiType == apitype {
52 | keys = append(keys, item.Object.(Key))
53 | }
54 | if apitype == "openai" {
55 | if item.Object.(Key).ApiType == "azure" {
56 | keys = append(keys, item.Object.(Key))
57 | }
58 | if item.Object.(Key).ApiType == "github" {
59 | keys = append(keys, item.Object.(Key))
60 | }
61 | }
62 | if apitype == "claude" {
63 | if item.Object.(Key).ApiType == "vertex" {
64 | keys = append(keys, item.Object.(Key))
65 | }
66 | }
67 | }
68 | if len(keys) == 0 {
69 | return Key{}, errors.New("No key found")
70 | } else if len(keys) == 1 {
71 | return keys[0], nil
72 | }
73 | rand.Seed(time.Now().UnixNano())
74 | idx := rand.Intn(len(keys))
75 | return keys[idx], nil
76 | }
77 |
78 | func SelectKeyCacheByModel(model string) (Key, error) {
79 | var keys []Key
80 | if os.Getenv("OPENAI_API_KEY") != "" {
81 | keys = append(keys, Key{ApiType: "openai", Key: os.Getenv("OPENAI_API_KEY")})
82 | }
83 | items := KeysCache.Items()
84 | for _, item := range items {
85 | if strings.Contains(model, "realtime") || strings.HasPrefix(model, "o1-") {
86 | if item.Object.(Key).ApiType == "openai" {
87 | keys = append(keys, item.Object.(Key))
88 | }
89 | if item.Object.(Key).ApiType == "azure" {
90 | keys = append(keys, item.Object.(Key))
91 | }
92 | }
93 | if strings.HasPrefix(model, "gpt-") {
94 | if item.Object.(Key).ApiType == "openai" {
95 | keys = append(keys, item.Object.(Key))
96 | }
97 | if item.Object.(Key).ApiType == "azure" {
98 | keys = append(keys, item.Object.(Key))
99 | }
100 | if item.Object.(Key).ApiType == "github" {
101 | keys = append(keys, item.Object.(Key))
102 | }
103 | }
104 | if strings.HasPrefix(model, "chatgpt-") {
105 | if item.Object.(Key).ApiType == "openai" {
106 | keys = append(keys, item.Object.(Key))
107 | }
108 | }
109 | }
110 | if len(keys) == 0 {
111 | return Key{}, errors.New("No key found")
112 | } else if len(keys) == 1 {
113 | return keys[0], nil
114 | }
115 | rand.Seed(time.Now().UnixNano())
116 | idx := rand.Intn(len(keys))
117 | return keys[idx], nil
118 | }
119 |
120 | func LoadAuthCache() {
121 | AuthCache = cache.New(cache.NoExpiration, cache.NoExpiration)
122 | users, err := GetAllUsers()
123 | if err != nil {
124 | log.Println(err)
125 | return
126 | }
127 | for _, user := range users {
128 | AuthCache.Set(user.Token, true, cache.NoExpiration)
129 | }
130 | }
131 |
132 | func IsExistAuthCache(auth string) bool {
133 | items := AuthCache.Items()
134 | _, ok := items[auth]
135 | return ok
136 | }
137 |
--------------------------------------------------------------------------------
/store/db.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | // "gorm.io/driver/sqlite"
8 | "github.com/glebarez/sqlite"
9 | "gorm.io/gorm"
10 | )
11 |
12 | var db *gorm.DB
13 |
14 | var usage *gorm.DB
15 |
16 | func init() {
17 | if _, err := os.Stat("db"); os.IsNotExist(err) {
18 | errDir := os.MkdirAll("db", 0755)
19 | if errDir != nil {
20 | log.Fatalln("Error creating directory:", err)
21 | }
22 | }
23 | var err error
24 | db, err = gorm.Open(sqlite.Open("./db/cat.db"), &gorm.Config{})
25 | if err != nil {
26 | panic("failed to connect database")
27 | }
28 |
29 | // 自动迁移 User 结构体
30 | err = db.AutoMigrate(&User{}, &Key{})
31 | if err != nil {
32 | panic(err)
33 | }
34 | LoadKeysCache()
35 | LoadAuthCache()
36 |
37 | usage, err = gorm.Open(sqlite.Open("./db/usage.db"), &gorm.Config{})
38 | if err != nil {
39 | panic(err)
40 | }
41 | err = usage.AutoMigrate(&DailyUsage{}, &Usage{})
42 | if err != nil {
43 | panic(err)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/store/keydb.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "opencatd-open/pkg/vertexai"
8 | "os"
9 | "time"
10 | )
11 |
12 | func init() {
13 | // check vertex
14 | if os.Getenv("Vertex") != "" {
15 | vertex_auth := os.Getenv("Vertex")
16 | var Vertex vertexai.VertexSecretKey
17 | if err := json.Unmarshal([]byte(vertex_auth), &Vertex); err != nil {
18 | log.Fatalln(fmt.Errorf("import vertex_auth json error: %w", err))
19 | return
20 | }
21 | key := Key{
22 | ApiType: "vertex",
23 | Name: Vertex.ProjectID,
24 | Key: vertex_auth,
25 | ApiSecret: vertex_auth,
26 | }
27 | if err := db.Where("name = ?", Vertex.ProjectID).FirstOrCreate(&key).Error; err != nil {
28 | log.Fatalln(fmt.Errorf("import vertex_auth json error: %w", err))
29 | return
30 | }
31 | }
32 | LoadKeysCache()
33 | }
34 |
35 | type Key struct {
36 | ID uint `gorm:"primarykey" json:"id,omitempty"`
37 | Key string `gorm:"unique;not null" json:"key,omitempty"`
38 | Name string `gorm:"unique;not null" json:"name,omitempty"`
39 | UserId string `json:"-,omitempty"`
40 | ApiType string `gorm:"column:api_type"`
41 | EndPoint string `gorm:"column:endpoint"`
42 | ResourceNmae string `gorm:"column:resource_name"`
43 | DeploymentName string `gorm:"column:deployment_name"`
44 | ApiSecret string `gorm:"column:api_secret"`
45 | CreatedAt time.Time `json:"createdAt,omitempty"`
46 | UpdatedAt time.Time `json:"updatedAt,omitempty"`
47 | }
48 |
49 | func (k Key) ToString() string {
50 | bdate, _ := json.Marshal(k)
51 | return string(bdate)
52 | }
53 |
54 | func GetKeyrByName(name string) (*Key, error) {
55 | var key Key
56 | result := db.First(&key, "name = ?", name)
57 | if result.Error != nil {
58 | return nil, result.Error
59 | }
60 | return &key, nil
61 | }
62 |
63 | func GetAllKeys() ([]Key, error) {
64 | var keys []Key
65 | if err := db.Find(&keys).Error; err != nil {
66 | return nil, err
67 | }
68 | return keys, nil
69 | }
70 |
71 | // 添加记录
72 | func AddKey(apitype, apikey, name string) error {
73 | key := Key{
74 | ApiType: apitype,
75 | Key: apikey,
76 | Name: name,
77 | }
78 | if err := db.Create(&key).Error; err != nil {
79 | return err
80 | }
81 | LoadKeysCache()
82 | return nil
83 | }
84 |
85 | func CreateKey(k *Key) error {
86 | if err := db.Create(&k).Error; err != nil {
87 | return err
88 | }
89 | LoadKeysCache()
90 | return nil
91 | }
92 |
93 | // 删除记录
94 | func DeleteKey(id uint) error {
95 | if err := db.Delete(&Key{}, id).Error; err != nil {
96 | return err
97 | }
98 | LoadKeysCache()
99 | return nil
100 | }
101 |
102 | // 更新记录
103 | func UpdateKey(id uint, apikey string, userId string) error {
104 | key := Key{
105 | Key: apikey,
106 | UserId: userId,
107 | }
108 | if err := db.Model(&Key{}).Where("id = ?", id).Updates(key).Error; err != nil {
109 | return err
110 | }
111 | LoadKeysCache()
112 | return nil
113 | }
114 |
--------------------------------------------------------------------------------
/store/usage.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "github.com/Sakurasan/to"
8 | "gorm.io/gorm"
9 | )
10 |
11 | type DailyUsage struct {
12 | ID int `gorm:"column:id"`
13 | UserID int `gorm:"column:user_id";primaryKey`
14 | Date time.Time `gorm:"column:date"`
15 | SKU string `gorm:"column:sku"`
16 | PromptUnits int `gorm:"column:prompt_units"`
17 | CompletionUnits int `gorm:"column:completion_units"`
18 | TotalUnit int `gorm:"column:total_unit"`
19 | Cost string `gorm:"column:cost"`
20 | }
21 |
22 | func (DailyUsage) TableName() string {
23 | return "daily_usages"
24 | }
25 |
26 | type Usage struct {
27 | ID int `gorm:"column:id"`
28 | PromptHash string `gorm:"column:prompt_hash"`
29 | UserID int `gorm:"column:user_id"`
30 | SKU string `gorm:"column:sku"`
31 | PromptUnits int `gorm:"column:prompt_units"`
32 | CompletionUnits int `gorm:"column:completion_units"`
33 | TotalUnit int `gorm:"column:total_unit"`
34 | Cost string `gorm:"column:cost"`
35 | Date time.Time `gorm:"column:date"`
36 | }
37 |
38 | func (Usage) TableName() string {
39 | return "usages"
40 | }
41 |
42 | type Summary struct {
43 | UserId int `gorm:"column:user_id"`
44 | SumPromptUnits int `gorm:"column:sum_prompt_units"`
45 | SumCompletionUnits int `gorm:"column:sum_completion_units"`
46 | SumTotalUnit int `gorm:"column:sum_total_unit"`
47 | SumCost float64 `gorm:"column:sum_cost"`
48 | }
49 | type CalcUsage struct {
50 | UserID int `json:"userId,omitempty"`
51 | TotalUnit int `json:"totalUnit,omitempty"`
52 | Cost string `json:"cost,omitempty"`
53 | }
54 |
55 | func QueryUsage(from, to string) ([]CalcUsage, error) {
56 | var results = []CalcUsage{}
57 | err := usage.Model(&DailyUsage{}).Select(`user_id,
58 | --SUM(prompt_units) AS prompt_units,
59 | -- SUM(completion_units) AS completion_units,
60 | SUM(total_unit) AS total_unit,
61 | printf('%.6f', SUM(cost)) AS cost`).
62 | Group("user_id").
63 | Where("date >= ? AND date < ?", from, to).
64 | Find(&results).Error
65 | if err != nil {
66 | return nil, err
67 | }
68 | return results, nil
69 | }
70 |
71 | func QueryUserUsage(userid, from, to string) (*CalcUsage, error) {
72 | var results = new(CalcUsage)
73 | err := usage.Model(&DailyUsage{}).Select(`user_id,
74 | --SUM(prompt_units) AS prompt_units,
75 | -- SUM(completion_units) AS completion_units,
76 | SUM(total_unit) AS total_unit,
77 | printf('%.6f', SUM(cost)) AS cost`).
78 | Where("user_id = ? AND date >= ? AND date < ?", userid, from, to).
79 | Find(&results).Error
80 | if err != nil {
81 | return nil, err
82 | }
83 | return results, nil
84 | }
85 |
86 | type Tokens struct {
87 | UserID int
88 | PromptCount int
89 | CompletionCount int
90 | TotalTokens int
91 | Cost string
92 | Model string
93 | PromptHash string
94 | }
95 |
96 | func Record(chatlog *Tokens) (err error) {
97 | u := &Usage{
98 | UserID: chatlog.UserID,
99 | SKU: chatlog.Model,
100 | PromptHash: chatlog.PromptHash,
101 | PromptUnits: chatlog.PromptCount,
102 | CompletionUnits: chatlog.CompletionCount,
103 | TotalUnit: chatlog.TotalTokens,
104 | Cost: to.String(chatlog.Cost),
105 | Date: time.Now(),
106 | }
107 | err = usage.Create(u).Error
108 | return
109 |
110 | }
111 |
112 | func SumDaily(userid int) error {
113 | var count int64
114 | err := usage.Model(&DailyUsage{}).Where("user_id = ? and date = ?", userid, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)).Count(&count).Error
115 | if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
116 | return err
117 | }
118 | if count == 0 {
119 | if err := insertSumDaily(userid); err != nil {
120 | return err
121 | }
122 | } else {
123 | if err := updateSumDaily(userid, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)); err != nil {
124 | return err
125 | }
126 | }
127 | return nil
128 | }
129 |
130 | func insertSumDaily(uid int) error {
131 | nowstr := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
132 | err := usage.Exec(`INSERT INTO daily_usages
133 | (user_id, date, sku, prompt_units, completion_units, total_unit, cost)
134 | SELECT
135 | user_id,
136 | ?,
137 | sku,
138 | SUM(prompt_units) AS sum_prompt_units,
139 | SUM(completion_units) AS sum_completion_units,
140 | SUM(total_unit) AS sum_total_unit,
141 | SUM(cost) AS sum_cost
142 | FROM usages
143 | WHERE date >= ?
144 | AND user_id = ?`, nowstr, nowstr, uid).Error
145 | if err != nil {
146 | return err
147 | }
148 | return nil
149 | }
150 |
151 | func updateSumDaily(uid int, date time.Time) error {
152 | // var u = Summary{}
153 | err := usage.Model(&Usage{}).Exec(`UPDATE daily_usages
154 | SET
155 | prompt_units = (SELECT SUM(prompt_units) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date),
156 | completion_units = (SELECT SUM(completion_units) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date),
157 | total_unit = (SELECT SUM(total_unit) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date),
158 | cost = (SELECT SUM(cost) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date)
159 | WHERE user_id = ? AND date >= ?`, uid, date).Error
160 | if err != nil {
161 | return err
162 | }
163 | return nil
164 | }
165 |
--------------------------------------------------------------------------------
/store/userdb.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type User struct {
10 | gorm.Model
11 | IsDelete bool `gorm:"default:false" json:"IsDelete"`
12 | ID uint `gorm:"primarykey autoIncrement;" json:"id,omitempty"`
13 | Name string `gorm:"unique;not null" json:"name,omitempty"`
14 | Token string `gorm:"unique;not null" json:"token,omitempty"`
15 | CreatedAt time.Time `json:"createdAt,omitempty"`
16 | UpdatedAt time.Time `json:"updatedAt,omitempty"`
17 | // DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
18 | }
19 |
20 | func CreateUser(u *User) error {
21 | result := db.Create(u)
22 | if result.Error != nil {
23 | return result.Error
24 | }
25 | LoadAuthCache()
26 | return nil
27 | }
28 |
29 | // 添加用户
30 | func AddUser(name, token string) error {
31 | user := &User{Name: name, Token: token}
32 | result := db.Create(&user)
33 | if result.Error != nil {
34 | return result.Error
35 | }
36 | LoadAuthCache()
37 | return nil
38 | }
39 |
40 | // 删除用户
41 | func DeleteUser(id uint) error {
42 | result := db.Delete(&User{}, id)
43 | if result.Error != nil {
44 | return result.Error
45 | }
46 | LoadAuthCache()
47 | return nil
48 | }
49 |
50 | // 修改用户
51 | func UpdateUser(id uint, token string) error {
52 | user := &User{Token: token}
53 | result := db.Model(&User{}).Where("id = ?", id).Updates(user)
54 | if result.Error != nil {
55 | return result.Error
56 | }
57 | LoadAuthCache()
58 | return nil
59 | }
60 |
61 | func GetUserByID(id uint) (*User, error) {
62 | var user User
63 | result := db.Where("id = ?", id).First(&user)
64 | if result.Error != nil {
65 | return nil, result.Error
66 | }
67 | return &user, nil
68 | }
69 |
70 | func GetUserByName(name string) (*User, error) {
71 | var user User
72 | result := db.Where(&User{Name: name}).First(&user)
73 | if result.Error != nil {
74 | return nil, result.Error
75 | }
76 | return &user, nil
77 | }
78 |
79 | func GetUserByToken(token string) (*User, error) {
80 | var user User
81 | result := db.Where("token = ?", token).First(&user)
82 | if result.Error != nil {
83 | return nil, result.Error
84 | }
85 | return &user, nil
86 | }
87 |
88 | func GetUserID(authkey string) (int, error) {
89 | var user User
90 | result := db.Where(&User{Token: authkey}).First(&user)
91 | if result.Error != nil {
92 | return 0, result.Error
93 | }
94 | return int(user.ID), nil
95 | }
96 |
97 | func GetAllUsers() ([]*User, error) {
98 | var users []*User
99 | result := db.Find(&users)
100 | if result.Error != nil {
101 | return nil, result.Error
102 | }
103 | return users, nil
104 | }
105 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/web/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 + Vite
2 |
3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `
12 |