├── .github ├── dependabot.yml └── workflows │ ├── check-semgrep.yml │ ├── ci.yml │ ├── release-openapi-mcp.yml │ └── release.yml ├── Dockerfile ├── LICENSE ├── README.md ├── README.zh.md ├── core ├── .gitignore ├── .golangci.yml ├── common │ ├── audio │ │ └── audio.go │ ├── balance │ │ ├── balance.go │ │ ├── mock.go │ │ └── sealos.go │ ├── color.go │ ├── config │ │ └── config.go │ ├── constants.go │ ├── consume │ │ ├── consume.go │ │ ├── consume_test.go │ │ └── record.go │ ├── conv │ │ └── any.go │ ├── database.go │ ├── env │ │ └── helper.go │ ├── fastJSONSerializer │ │ └── fastJSONSerializer.go │ ├── gin.go │ ├── image │ │ ├── image.go │ │ └── svg.go │ ├── ipblack │ │ ├── main.go │ │ ├── mem.go │ │ └── redis.go │ ├── mcpproxy │ │ ├── server.go │ │ ├── session.go │ │ ├── sse.go │ │ ├── stateless-streamable.go │ │ └── streamable.go │ ├── network │ │ ├── ip.go │ │ └── ip_test.go │ ├── notify │ │ ├── feishu.go │ │ ├── feishu_test.go │ │ ├── notify.go │ │ └── std.go │ ├── redis.go │ ├── render │ │ ├── event.go │ │ └── render.go │ ├── reqlimit │ │ ├── main.go │ │ ├── mem.go │ │ ├── mem_test.go │ │ └── redis.go │ ├── splitter │ │ ├── splitter.go │ │ └── think.go │ ├── tiktoken │ │ ├── assest.go │ │ ├── assets │ │ │ └── .gitkeep │ │ └── tiktoken.go │ ├── trunc.go │ ├── trylock │ │ ├── lock.go │ │ └── lock_test.go │ └── utils.go ├── controller │ ├── channel-billing.go │ ├── channel-test.go │ ├── channel.go │ ├── dashboard.go │ ├── embedmcp.go │ ├── group.go │ ├── groupmcp-server.go │ ├── groupmcp.go │ ├── import.go │ ├── log.go │ ├── misc.go │ ├── model.go │ ├── modelconfig.go │ ├── monitor.go │ ├── option.go │ ├── publicmcp-server.go │ ├── publicmcp.go │ ├── relay-controller.go │ ├── relay-dashboard.go │ ├── relay-model.go │ ├── relay.go │ ├── token.go │ └── utils.go ├── deploy │ ├── Kubefile │ ├── manifests │ │ ├── aiproxy-config.yaml.tmpl │ │ ├── deploy.yaml.tmpl │ │ ├── ingress.yaml.tmpl │ │ ├── pgsql-log.yaml │ │ ├── pgsql.yaml │ │ └── redis.yaml │ └── scripts │ │ └── init.sh ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml ├── go.mod ├── go.sum ├── main.go ├── middleware │ ├── auth.go │ ├── cors.go │ ├── ctxkey.go │ ├── distributor.go │ ├── distributor_test.go │ ├── ipblack.go │ ├── log.go │ ├── mcp.go │ ├── recover.go │ ├── reqid.go │ └── utils.go ├── model │ ├── batch.go │ ├── cache.go │ ├── channel.go │ ├── channeltest.go │ ├── chtype.go │ ├── configkey.go │ ├── consumeerr.go │ ├── group.go │ ├── groupmcp.go │ ├── groupmodel.go │ ├── groupsummary.go │ ├── log.go │ ├── main.go │ ├── modelconfig.go │ ├── option.go │ ├── owner.go │ ├── publicmcp.go │ ├── retrylog.go │ ├── summary.go │ ├── token.go │ └── utils.go ├── monitor │ ├── memmodel.go │ └── model.go ├── public │ ├── dist │ │ └── .gitkeep │ ├── public.go │ └── templates │ │ └── index.tmpl ├── relay │ ├── adaptor │ │ ├── ai360 │ │ │ ├── adaptor.go │ │ │ └── constants.go │ │ ├── ali │ │ │ ├── adaptor.go │ │ │ ├── config.go │ │ │ ├── constants.go │ │ │ ├── embeddings.go │ │ │ ├── fetures.go │ │ │ ├── image.go │ │ │ ├── model.go │ │ │ ├── rerank.go │ │ │ ├── stt-realtime.go │ │ │ └── tts.go │ │ ├── anthropic │ │ │ ├── adaptor.go │ │ │ ├── config.go │ │ │ ├── constants.go │ │ │ ├── error.go │ │ │ ├── event.go │ │ │ ├── fetures.go │ │ │ ├── main.go │ │ │ ├── model.go │ │ │ ├── openai.go │ │ │ └── render.go │ │ ├── aws │ │ │ ├── adaptor.go │ │ │ ├── claude │ │ │ │ ├── adapter.go │ │ │ │ ├── main.go │ │ │ │ └── model.go │ │ │ ├── key.go │ │ │ ├── llama3 │ │ │ │ ├── adapter.go │ │ │ │ ├── main.go │ │ │ │ ├── main_test.go │ │ │ │ └── model.go │ │ │ ├── registry.go │ │ │ └── utils │ │ │ │ └── adaptor.go │ │ ├── azure │ │ │ ├── key.go │ │ │ └── main.go │ │ ├── baichuan │ │ │ ├── adaptor.go │ │ │ └── constants.go │ │ ├── baidu │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ ├── embeddings.go │ │ │ ├── error.go │ │ │ ├── image.go │ │ │ ├── key.go │ │ │ ├── main.go │ │ │ ├── model.go │ │ │ ├── rerank.go │ │ │ └── token.go │ │ ├── baiduv2 │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ ├── key.go │ │ │ └── token.go │ │ ├── cloudflare │ │ │ ├── adaptor.go │ │ │ └── constant.go │ │ ├── cohere │ │ │ ├── adaptor.go │ │ │ ├── constant.go │ │ │ ├── main.go │ │ │ └── model.go │ │ ├── coze │ │ │ ├── adaptor.go │ │ │ ├── constant │ │ │ │ ├── contenttype │ │ │ │ │ └── define.go │ │ │ │ ├── event │ │ │ │ │ └── define.go │ │ │ │ └── messagetype │ │ │ │ │ └── define.go │ │ │ ├── constants.go │ │ │ ├── key.go │ │ │ ├── main.go │ │ │ └── model.go │ │ ├── deepseek │ │ │ ├── adaptor.go │ │ │ ├── balance.go │ │ │ └── constants.go │ │ ├── doc2x │ │ │ ├── adaptor.go │ │ │ ├── config.go │ │ │ ├── constants.go │ │ │ ├── html2md_test.go │ │ │ └── pdf.go │ │ ├── doubao │ │ │ ├── constants.go │ │ │ ├── fetures.go │ │ │ └── main.go │ │ ├── doubaoaudio │ │ │ ├── constants.go │ │ │ ├── fetures.go │ │ │ ├── key.go │ │ │ ├── main.go │ │ │ └── tts.go │ │ ├── gemini │ │ │ ├── adaptor.go │ │ │ ├── config.go │ │ │ ├── constants.go │ │ │ ├── embeddings.go │ │ │ ├── fetures.go │ │ │ ├── main.go │ │ │ └── model.go │ │ ├── geminiopenai │ │ │ ├── adaptor.go │ │ │ └── fetures.go │ │ ├── groq │ │ │ ├── adaptor.go │ │ │ └── constants.go │ │ ├── interface.go │ │ ├── jina │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ ├── embeddings.go │ │ │ ├── error.go │ │ │ ├── fetures.go │ │ │ └── rerank.go │ │ ├── lingyiwanwu │ │ │ ├── adaptor.go │ │ │ └── constants.go │ │ ├── minimax │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ ├── fetures.go │ │ │ ├── key.go │ │ │ └── tts.go │ │ ├── mistral │ │ │ ├── adaptor.go │ │ │ └── constants.go │ │ ├── moonshot │ │ │ ├── adaptor.go │ │ │ ├── balance.go │ │ │ └── constants.go │ │ ├── novita │ │ │ ├── adaptor.go │ │ │ └── constants.go │ │ ├── ollama │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ ├── error.go │ │ │ ├── fetures.go │ │ │ ├── main.go │ │ │ └── model.go │ │ ├── openai │ │ │ ├── adaptor.go │ │ │ ├── balance.go │ │ │ ├── config.go │ │ │ ├── constants.go │ │ │ ├── embeddings.go │ │ │ ├── error.go │ │ │ ├── fetures.go │ │ │ ├── helper.go │ │ │ ├── id.go │ │ │ ├── image.go │ │ │ ├── main.go │ │ │ ├── moderations.go │ │ │ ├── rerank.go │ │ │ ├── stt.go │ │ │ ├── token.go │ │ │ └── tts.go │ │ ├── openrouter │ │ │ ├── adaptor.go │ │ │ └── fetures.go │ │ ├── siliconflow │ │ │ ├── adaptor.go │ │ │ ├── balance.go │ │ │ ├── constants.go │ │ │ └── image.go │ │ ├── stepfun │ │ │ ├── adaptor.go │ │ │ └── constants.go │ │ ├── tencent │ │ │ ├── adaptor.go │ │ │ └── constants.go │ │ ├── text-embeddings-inference │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ ├── embeddings.go │ │ │ ├── error.go │ │ │ ├── fetures.go │ │ │ ├── rerank.go │ │ │ └── rerank_test.go │ │ ├── utils.go │ │ ├── vertexai │ │ │ ├── adaptor.go │ │ │ ├── claude │ │ │ │ ├── adapter.go │ │ │ │ ├── constants.go │ │ │ │ └── model.go │ │ │ ├── config.go │ │ │ ├── fetures.go │ │ │ ├── gemini │ │ │ │ └── adapter.go │ │ │ ├── key.go │ │ │ ├── registry.go │ │ │ └── token.go │ │ ├── xai │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ └── error.go │ │ ├── xunfei │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ ├── key.go │ │ │ └── main.go │ │ └── zhipu │ │ │ ├── adaptor.go │ │ │ ├── constants.go │ │ │ ├── main.go │ │ │ └── model.go │ ├── adaptors │ │ └── register.go │ ├── controller │ │ ├── anthropic.go │ │ ├── chat.go │ │ ├── completions.go │ │ ├── dohelper.go │ │ ├── edits.go │ │ ├── embed.go │ │ ├── handle.go │ │ ├── image.go │ │ ├── pdf.go │ │ ├── rerank.go │ │ ├── stt.go │ │ └── tts.go │ ├── meta │ │ └── meta.go │ ├── mode │ │ └── define.go │ ├── model │ │ ├── anthropic.go │ │ ├── chat.go │ │ ├── completions.go │ │ ├── constant.go │ │ ├── embed.go │ │ ├── errors.go │ │ ├── image.go │ │ ├── pdf.go │ │ ├── rerank.go │ │ ├── stt.go │ │ ├── tool.go │ │ └── tts.go │ ├── plugin │ │ ├── cache │ │ │ ├── README.md │ │ │ ├── README.zh.md │ │ │ ├── cache.go │ │ │ └── config.go │ │ ├── noop │ │ │ └── noop.go │ │ ├── thinksplit │ │ │ ├── README.md │ │ │ ├── README.zh.md │ │ │ ├── config.go │ │ │ └── split.go │ │ ├── types.go │ │ └── web-search │ │ │ ├── README.md │ │ │ ├── README.zh.md │ │ │ ├── config.go │ │ │ ├── prompts │ │ │ ├── arxiv.md │ │ │ ├── chinese-internet.md │ │ │ ├── full.md │ │ │ ├── internet.md │ │ │ └── private.md │ │ │ └── search.go │ └── utils │ │ ├── testreq.go │ │ └── utils.go ├── router │ ├── api.go │ ├── main.go │ ├── mcp.go │ ├── relay.go │ ├── static.go │ └── swagger.go └── scripts │ ├── swag.sh │ └── tiktoken.sh ├── docker-compose.yaml ├── go.work ├── go.work.sum ├── mcp-servers ├── .golangci.yml ├── README.md ├── aiproxy-openapi │ └── openapi.go ├── embedmcp.go ├── go.mod ├── go.sum ├── mcpregister │ └── init.go ├── register.go └── web-search │ ├── README.md │ ├── engine │ ├── arxiv.go │ ├── bing.go │ ├── google.go │ ├── searchxng.go │ └── types.go │ ├── features.md │ └── server.go ├── openapi-mcp ├── .gitignore ├── .golangci.yml ├── README.md ├── convert │ ├── convert.go │ └── parser.go ├── go.mod ├── go.sum └── main.go ├── scripts ├── golangci-lint-fix.sh └── update-go-mod.sh └── web ├── .env.template ├── .gitignore ├── README.md ├── components.json ├── context.md ├── eslint.config.js ├── index.html ├── openapi.txt ├── package.json ├── pnpm-lock.yaml ├── prompt.txt ├── public ├── locales │ ├── en │ │ └── translation.json │ └── zh │ │ └── translation.json └── logo.svg ├── src ├── App.tsx ├── api │ ├── auth.ts │ ├── channel.ts │ ├── index.ts │ ├── model.ts │ ├── services.ts │ └── token.ts ├── assets │ └── react.svg ├── components │ ├── common │ │ ├── LanguageSelector.tsx │ │ ├── LoadingFallBack.tsx │ │ ├── ThemeToggle.tsx │ │ └── error │ │ │ ├── errorConfig.tsx │ │ │ ├── errorDisplay.tsx │ │ │ └── errorTypes.ts │ ├── layout │ │ ├── AnimatedRoute.tsx │ │ ├── RootLayOut.tsx │ │ └── SideBar.tsx │ ├── select │ │ ├── ConstructMappingComponent.tsx │ │ ├── MultiSelectCombobox.tsx │ │ ├── Select.tsx │ │ └── SingleSelectCombobox.tsx │ ├── table │ │ ├── column-header.tsx │ │ ├── column-toggle.tsx │ │ ├── data-table.tsx │ │ ├── motion-data-table.tsx │ │ └── pagination.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── animation │ │ ├── button-animation.ts │ │ ├── collapse-animation.ts │ │ ├── components │ │ │ ├── animated-button.tsx │ │ │ ├── animated-container.tsx │ │ │ ├── animated-icon.tsx │ │ │ ├── collapse.tsx │ │ │ ├── display.tsx │ │ │ ├── particles-background.tsx │ │ │ ├── tab-animation.tsx │ │ │ └── table-scroll.tsx │ │ ├── container-animation.ts │ │ ├── dialog-animation.ts │ │ ├── display-animation.ts │ │ ├── grid-animation.ts │ │ ├── icon-animation.ts │ │ ├── route-animation.ts │ │ └── tab-animation.ts │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── collapsible.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ └── tooltip.tsx ├── constant │ └── index.ts ├── feature │ ├── auth │ │ ├── components │ │ │ └── ProtectedRoute.tsx │ │ └── hooks.ts │ ├── channel │ │ ├── components │ │ │ ├── ChannelDialog.tsx │ │ │ ├── ChannelForm.tsx │ │ │ ├── ChannelTable.tsx │ │ │ └── DeleteChannelDialog.tsx │ │ └── hooks.ts │ ├── model │ │ ├── components │ │ │ ├── DeleteModelDialog.tsx │ │ │ ├── ModelDialog.tsx │ │ │ ├── ModelForm.tsx │ │ │ ├── ModelTable.tsx │ │ │ └── api-doc │ │ │ │ ├── ApiDoc.tsx │ │ │ │ └── CodeHight.tsx │ │ └── hooks.ts │ └── token │ │ ├── components │ │ ├── DeleteTokenDialog.tsx │ │ ├── TokenDialog.tsx │ │ ├── TokenForm.tsx │ │ └── TokenTable.tsx │ │ └── hooks.ts ├── handler │ ├── ErrorBoundary.tsx │ ├── ThemeContext.tsx │ └── ThemeProvider.tsx ├── i18n.ts ├── index.css ├── lib │ └── utils.ts ├── main.tsx ├── pages │ ├── auth │ │ └── login.tsx │ ├── channel │ │ └── page.tsx │ ├── model │ │ └── page.tsx │ └── token │ │ └── page.tsx ├── routes │ ├── config.tsx │ ├── constants.ts │ └── index.tsx ├── store │ └── auth.ts ├── types │ ├── channel.ts │ ├── global.d.ts │ ├── i18next.d.ts │ ├── model.ts │ └── token.ts ├── utils │ └── env.ts ├── validation │ ├── auth.ts │ ├── channel.ts │ ├── model.ts │ └── token.ts └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/check-semgrep.yml: -------------------------------------------------------------------------------- 1 | # Name of this GitHub Actions workflow. 2 | name: Check-Semgrep 3 | 4 | on: 5 | workflow_call: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: 0 0 * * * 9 | push: 10 | branches: 11 | - "**" 12 | tags: 13 | - "v*.*.*" 14 | paths-ignore: 15 | - "**/*.md" 16 | - "**/*.yaml" 17 | pull_request: 18 | branches: 19 | - "**" 20 | paths-ignore: 21 | - "**/*.md" 22 | - "**/*.yaml" 23 | 24 | jobs: 25 | semgrep: 26 | name: Scan 27 | runs-on: ubuntu-24.04 28 | container: 29 | image: semgrep/semgrep:latest 30 | continue-on-error: true 31 | if: (github.actor != 'dependabot[bot]') 32 | steps: 33 | - uses: actions/checkout@v4 34 | - run: semgrep ci 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine AS frontend-builder 2 | 3 | WORKDIR /aiproxy/web 4 | 5 | COPY ./web/ ./ 6 | 7 | RUN npm install -g pnpm 8 | 9 | RUN pnpm install && pnpm run build 10 | 11 | FROM golang:1.24-alpine AS builder 12 | 13 | RUN apk add --no-cache curl 14 | 15 | WORKDIR /aiproxy/core 16 | 17 | COPY ./ /aiproxy 18 | 19 | COPY --from=frontend-builder /aiproxy/web/dist/ /aiproxy/core/public/dist/ 20 | 21 | RUN sh scripts/tiktoken.sh 22 | 23 | RUN go install github.com/swaggo/swag/cmd/swag@latest 24 | 25 | RUN sh scripts/swag.sh 26 | 27 | RUN go build -trimpath -tags "jsoniter" -ldflags "-s -w" -o aiproxy 28 | 29 | FROM alpine:latest 30 | 31 | RUN mkdir -p /aiproxy 32 | 33 | WORKDIR /aiproxy 34 | 35 | VOLUME /aiproxy 36 | 37 | RUN apk add --no-cache ca-certificates tzdata ffmpeg curl && \ 38 | rm -rf /var/cache/apk/* 39 | 40 | COPY --from=builder /aiproxy/core/aiproxy /usr/local/bin/aiproxy 41 | 42 | ENV PUID=0 PGID=0 UMASK=022 43 | 44 | ENV FFMPEG_ENABLED=true 45 | 46 | EXPOSE 3000 47 | 48 | ENTRYPOINT ["aiproxy"] 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 labring 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | aiproxy.db* 2 | aiproxy 3 | common/tiktoken/assets/* 4 | /public/dist/* 5 | !*.gitkeep 6 | -------------------------------------------------------------------------------- /core/common/balance/balance.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/labring/aiproxy/core/model" 7 | ) 8 | 9 | type GroupBalance interface { 10 | GetGroupRemainBalance( 11 | ctx context.Context, 12 | group model.GroupCache, 13 | ) (float64, PostGroupConsumer, error) 14 | } 15 | 16 | type PostGroupConsumer interface { 17 | PostGroupConsume(ctx context.Context, tokenName string, usage float64) (float64, error) 18 | } 19 | 20 | var ( 21 | mock GroupBalance = NewMockGroupBalance() 22 | Default = mock 23 | ) 24 | 25 | func MockGetGroupRemainBalance( 26 | ctx context.Context, 27 | group model.GroupCache, 28 | ) (float64, PostGroupConsumer, error) { 29 | return mock.GetGroupRemainBalance(ctx, group) 30 | } 31 | 32 | func GetGroupRemainBalance( 33 | ctx context.Context, 34 | group model.GroupCache, 35 | ) (float64, PostGroupConsumer, error) { 36 | return Default.GetGroupRemainBalance(ctx, group) 37 | } 38 | -------------------------------------------------------------------------------- /core/common/balance/mock.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/labring/aiproxy/core/model" 7 | ) 8 | 9 | var _ GroupBalance = (*MockGroupBalance)(nil) 10 | 11 | const ( 12 | mockBalance = 10000000 13 | ) 14 | 15 | type MockGroupBalance struct{} 16 | 17 | func NewMockGroupBalance() *MockGroupBalance { 18 | return &MockGroupBalance{} 19 | } 20 | 21 | func (q *MockGroupBalance) GetGroupRemainBalance( 22 | _ context.Context, 23 | _ model.GroupCache, 24 | ) (float64, PostGroupConsumer, error) { 25 | return mockBalance, q, nil 26 | } 27 | 28 | func (q *MockGroupBalance) PostGroupConsume( 29 | _ context.Context, 30 | _ string, 31 | usage float64, 32 | ) (float64, error) { 33 | return usage, nil 34 | } 35 | -------------------------------------------------------------------------------- /core/common/color.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/mattn/go-isatty" 8 | ) 9 | 10 | var ( 11 | needColor bool 12 | needColorOnce sync.Once 13 | ) 14 | 15 | func NeedColor() bool { 16 | needColorOnce.Do(func() { 17 | needColor = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) 18 | }) 19 | return needColor 20 | } 21 | -------------------------------------------------------------------------------- /core/common/constants.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "time" 4 | 5 | var StartTime = time.Now().UnixMilli() // unit: millisecond 6 | -------------------------------------------------------------------------------- /core/common/consume/record.go: -------------------------------------------------------------------------------- 1 | package consume 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/labring/aiproxy/core/model" 7 | "github.com/labring/aiproxy/core/relay/meta" 8 | ) 9 | 10 | func recordConsume( 11 | meta *meta.Meta, 12 | code int, 13 | firstByteAt time.Time, 14 | usage model.Usage, 15 | modelPrice model.Price, 16 | content string, 17 | ip string, 18 | requestDetail *model.RequestDetail, 19 | amount float64, 20 | retryTimes int, 21 | downstreamResult bool, 22 | user string, 23 | metadata map[string]string, 24 | channelModelRate model.RequestRate, 25 | groupModelTokenRate model.RequestRate, 26 | ) error { 27 | return model.BatchRecordLogs( 28 | meta.RequestID, 29 | meta.RequestAt, 30 | meta.RetryAt, 31 | firstByteAt, 32 | meta.Group.ID, 33 | code, 34 | meta.Channel.ID, 35 | meta.OriginModel, 36 | meta.Token.ID, 37 | meta.Token.Name, 38 | meta.Endpoint, 39 | content, 40 | int(meta.Mode), 41 | ip, 42 | retryTimes, 43 | requestDetail, 44 | downstreamResult, 45 | usage, 46 | modelPrice, 47 | amount, 48 | user, 49 | metadata, 50 | channelModelRate, 51 | groupModelTokenRate, 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /core/common/conv/any.go: -------------------------------------------------------------------------------- 1 | package conv 2 | 3 | import "unsafe" 4 | 5 | // The change of bytes will cause the change of string synchronously 6 | func BytesToString(b []byte) string { 7 | return unsafe.String(unsafe.SliceData(b), len(b)) 8 | } 9 | 10 | // If string is readonly, modifying bytes will cause panic 11 | func StringToBytes(s string) []byte { 12 | return unsafe.Slice(unsafe.StringData(s), len(s)) 13 | } 14 | -------------------------------------------------------------------------------- /core/common/database.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/common/env" 5 | ) 6 | 7 | var ( 8 | UsingSQLite = false 9 | UsingPostgreSQL = false 10 | UsingMySQL = false 11 | ) 12 | 13 | var ( 14 | SQLitePath = env.String("SQLITE_PATH", "aiproxy.db") 15 | SQLiteBusyTimeout = env.Int64("SQLITE_BUSY_TIMEOUT", 3000) 16 | ) 17 | -------------------------------------------------------------------------------- /core/common/env/helper.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "github.com/bytedance/sonic" 8 | "github.com/labring/aiproxy/core/common/conv" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func Bool(env string, defaultValue bool) bool { 13 | if env == "" { 14 | return defaultValue 15 | } 16 | e := os.Getenv(env) 17 | if e == "" { 18 | return defaultValue 19 | } 20 | p, err := strconv.ParseBool(e) 21 | if err != nil { 22 | log.Errorf("invalid %s: %s", env, e) 23 | return defaultValue 24 | } 25 | return p 26 | } 27 | 28 | func Int64(env string, defaultValue int64) int64 { 29 | if env == "" { 30 | return defaultValue 31 | } 32 | e := os.Getenv(env) 33 | if e == "" { 34 | return defaultValue 35 | } 36 | num, err := strconv.ParseInt(e, 10, 64) 37 | if err != nil { 38 | log.Errorf("invalid %s: %s", env, e) 39 | return defaultValue 40 | } 41 | return num 42 | } 43 | 44 | func Float64(env string, defaultValue float64) float64 { 45 | if env == "" { 46 | return defaultValue 47 | } 48 | e := os.Getenv(env) 49 | if e == "" { 50 | return defaultValue 51 | } 52 | num, err := strconv.ParseFloat(e, 64) 53 | if err != nil { 54 | log.Errorf("invalid %s: %s", env, e) 55 | return defaultValue 56 | } 57 | return num 58 | } 59 | 60 | func String(env, defaultValue string) string { 61 | if env == "" { 62 | return defaultValue 63 | } 64 | e := os.Getenv(env) 65 | if e == "" { 66 | return defaultValue 67 | } 68 | return e 69 | } 70 | 71 | func JSON[T any](env string, defaultValue T) T { 72 | if env == "" { 73 | return defaultValue 74 | } 75 | e := os.Getenv(env) 76 | if e == "" { 77 | return defaultValue 78 | } 79 | var t T 80 | if err := sonic.Unmarshal(conv.StringToBytes(e), &t); err != nil { 81 | log.Errorf("invalid %s: %s", env, e) 82 | return defaultValue 83 | } 84 | return t 85 | } 86 | -------------------------------------------------------------------------------- /core/common/fastJSONSerializer/fastJSONSerializer.go: -------------------------------------------------------------------------------- 1 | package fastjsonserializer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/bytedance/sonic" 9 | "github.com/labring/aiproxy/core/common/conv" 10 | "gorm.io/gorm/schema" 11 | ) 12 | 13 | type JSONSerializer struct{} 14 | 15 | func (*JSONSerializer) Scan( 16 | ctx context.Context, 17 | field *schema.Field, 18 | dst reflect.Value, 19 | dbValue any, 20 | ) (err error) { 21 | fieldValue := reflect.New(field.FieldType) 22 | 23 | if dbValue != nil { 24 | var bytes []byte 25 | switch v := dbValue.(type) { 26 | case []byte: 27 | bytes = v 28 | case string: 29 | bytes = conv.StringToBytes(v) 30 | default: 31 | return fmt.Errorf("failed to unmarshal JSONB value: %#v", dbValue) 32 | } 33 | 34 | if len(bytes) == 0 { 35 | field.ReflectValueOf(ctx, dst).Set(reflect.Zero(field.FieldType)) 36 | return nil 37 | } 38 | 39 | err = sonic.Unmarshal(bytes, fieldValue.Interface()) 40 | } 41 | 42 | field.ReflectValueOf(ctx, dst).Set(fieldValue.Elem()) 43 | return 44 | } 45 | 46 | func (*JSONSerializer) Value( 47 | _ context.Context, 48 | _ *schema.Field, 49 | _ reflect.Value, 50 | fieldValue any, 51 | ) (any, error) { 52 | return sonic.Marshal(fieldValue) 53 | } 54 | 55 | func init() { 56 | schema.RegisterSerializer("fastjson", new(JSONSerializer)) 57 | } 58 | -------------------------------------------------------------------------------- /core/common/image/svg.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "io" 7 | 8 | "github.com/srwiley/oksvg" 9 | "github.com/srwiley/rasterx" 10 | ) 11 | 12 | func Decode(r io.Reader) (image.Image, error) { 13 | icon, err := oksvg.ReadIconStream(r) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | w, h := int(icon.ViewBox.W), int(icon.ViewBox.H) 19 | icon.SetTarget(0, 0, float64(w), float64(h)) 20 | 21 | rgba := image.NewRGBA(image.Rect(0, 0, w, h)) 22 | icon.Draw(rasterx.NewDasher(w, h, rasterx.NewScannerGV(w, h, rgba, rgba.Bounds())), 1) 23 | 24 | return rgba, err 25 | } 26 | 27 | func DecodeConfig(r io.Reader) (image.Config, error) { 28 | var config image.Config 29 | 30 | icon, err := oksvg.ReadIconStream(r) 31 | if err != nil { 32 | return config, err 33 | } 34 | 35 | config.ColorModel = color.RGBAModel 36 | config.Width = int(icon.ViewBox.W) 37 | config.Height = int(icon.ViewBox.H) 38 | 39 | return config, nil 40 | } 41 | 42 | func init() { 43 | image.RegisterFormat("svg", " 0, nil 31 | } 32 | -------------------------------------------------------------------------------- /core/common/mcpproxy/session.go: -------------------------------------------------------------------------------- 1 | package mcpproxy 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/labring/aiproxy/core/common" 7 | ) 8 | 9 | // SessionManager defines the interface for managing session information 10 | type SessionManager interface { 11 | New() (sessionID string) 12 | // Set stores a sessionID and its corresponding backend endpoint 13 | Set(sessionID, endpoint string) 14 | // Get retrieves the backend endpoint for a sessionID 15 | Get(sessionID string) (string, bool) 16 | // Delete removes a sessionID from the store 17 | Delete(sessionID string) 18 | } 19 | 20 | // MemStore implements the SessionManager interface 21 | type MemStore struct { 22 | mu sync.RWMutex 23 | sessions map[string]string // sessionID -> host+endpoint 24 | } 25 | 26 | // NewMemStore creates a new session store 27 | func NewMemStore() *MemStore { 28 | return &MemStore{ 29 | sessions: make(map[string]string), 30 | } 31 | } 32 | 33 | func (s *MemStore) New() string { 34 | return common.ShortUUID() 35 | } 36 | 37 | // Set stores a sessionID and its corresponding backend endpoint 38 | func (s *MemStore) Set(sessionID, endpoint string) { 39 | s.mu.Lock() 40 | defer s.mu.Unlock() 41 | s.sessions[sessionID] = endpoint 42 | } 43 | 44 | // Get retrieves the backend endpoint for a sessionID 45 | func (s *MemStore) Get(sessionID string) (string, bool) { 46 | s.mu.RLock() 47 | defer s.mu.RUnlock() 48 | endpoint, ok := s.sessions[sessionID] 49 | return endpoint, ok 50 | } 51 | 52 | // Delete removes a sessionID from the store 53 | func (s *MemStore) Delete(sessionID string) { 54 | s.mu.Lock() 55 | defer s.mu.Unlock() 56 | delete(s.sessions, sessionID) 57 | } 58 | -------------------------------------------------------------------------------- /core/common/network/ip.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | func IsValidSubnet(subnet string) error { 9 | _, _, err := net.ParseCIDR(subnet) 10 | if err != nil { 11 | return fmt.Errorf("failed to parse subnet: %w", err) 12 | } 13 | return nil 14 | } 15 | 16 | func IsIPInSubnet(ip, subnet string) (bool, error) { 17 | _, ipNet, err := net.ParseCIDR(subnet) 18 | if err != nil { 19 | return false, fmt.Errorf("failed to parse subnet: %w", err) 20 | } 21 | return ipNet.Contains(net.ParseIP(ip)), nil 22 | } 23 | 24 | func IsValidSubnets(subnets []string) error { 25 | for _, subnet := range subnets { 26 | if err := IsValidSubnet(subnet); err != nil { 27 | return err 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | func IsIPInSubnets(ip string, subnets []string) (bool, error) { 34 | for _, subnet := range subnets { 35 | if ok, err := IsIPInSubnet(ip, subnet); err != nil { 36 | return false, err 37 | } else if ok { 38 | return true, nil 39 | } 40 | } 41 | return false, nil 42 | } 43 | -------------------------------------------------------------------------------- /core/common/network/ip_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/labring/aiproxy/core/common/network" 7 | "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestIsIpInSubnet(t *testing.T) { 11 | ip1 := "192.168.0.5" 12 | ip2 := "125.216.250.89" 13 | subnet := "192.168.0.0/24" 14 | convey.Convey("TestIsIpInSubnet", t, func() { 15 | if ok, err := network.IsIPInSubnet(ip1, subnet); err != nil { 16 | t.Errorf("failed to check ip in subnet: %s", err) 17 | } else { 18 | convey.So(ok, convey.ShouldBeTrue) 19 | } 20 | if ok, err := network.IsIPInSubnet(ip2, subnet); err != nil { 21 | t.Errorf("failed to check ip in subnet: %s", err) 22 | } else { 23 | convey.So(ok, convey.ShouldBeFalse) 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /core/common/notify/feishu_test.go: -------------------------------------------------------------------------------- 1 | package notify_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/labring/aiproxy/core/common/notify" 8 | ) 9 | 10 | func TestPostToFeiShuv2(t *testing.T) { 11 | fshook := os.Getenv("FEISHU_WEBHOOK") 12 | if fshook == "" { 13 | return 14 | } 15 | err := notify.PostToFeiShuv2( 16 | t.Context(), 17 | notify.FeishuColorRed, 18 | "Error", 19 | "Error Message", 20 | os.Getenv("FEISHU_WEBHOOK")) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/common/notify/std.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/labring/aiproxy/core/common/config" 7 | "github.com/labring/aiproxy/core/common/trylock" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type StdNotifier struct{} 12 | 13 | var ( 14 | infoLogrus = log.WithField("notify", "std") 15 | warnLogrus = log.WithField("notify", "std") 16 | errorLogrus = log.WithField("notify", "std") 17 | ) 18 | 19 | func (n *StdNotifier) Notify(level Level, title, message string) { 20 | note := config.GetNotifyNote() 21 | switch level { 22 | case LevelInfo: 23 | logrus := infoLogrus.WithField("title", title) 24 | if note != "" { 25 | logrus = logrus.WithField("note", note) 26 | } 27 | logrus.Info(message) 28 | case LevelWarn: 29 | logrus := warnLogrus.WithField("title", title) 30 | if note != "" { 31 | logrus = logrus.WithField("note", note) 32 | } 33 | logrus.Warn(message) 34 | case LevelError: 35 | logrus := errorLogrus.WithField("title", title) 36 | if note != "" { 37 | logrus = logrus.WithField("note", note) 38 | } 39 | logrus.Error(message) 40 | } 41 | } 42 | 43 | func (n *StdNotifier) NotifyThrottle( 44 | level Level, 45 | key string, 46 | expiration time.Duration, 47 | title, message string, 48 | ) { 49 | if !trylock.MemLock(key, expiration) { 50 | return 51 | } 52 | n.Notify(level, title, message) 53 | } 54 | -------------------------------------------------------------------------------- /core/common/redis.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "time" 7 | 8 | "github.com/labring/aiproxy/core/common/env" 9 | "github.com/redis/go-redis/v9" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ( 14 | RDB *redis.Client 15 | RedisEnabled = false 16 | ) 17 | 18 | // InitRedisClient This function is called after init() 19 | func InitRedisClient() (err error) { 20 | redisConn := env.String("REDIS", os.Getenv("REDIS_CONN_STRING")) 21 | if redisConn == "" { 22 | log.Info("REDIS not set, redis is not enabled") 23 | return nil 24 | } 25 | RedisEnabled = true 26 | log.Info("redis is enabled") 27 | opt, err := redis.ParseURL(redisConn) 28 | if err != nil { 29 | log.Fatal("failed to parse redis connection string: " + err.Error()) 30 | } 31 | RDB = redis.NewClient(opt) 32 | 33 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 34 | defer cancel() 35 | 36 | _, err = RDB.Ping(ctx).Result() 37 | if err != nil { 38 | log.Errorf("failed to ping redis: %s", err.Error()) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func RedisSet(key, value string, expiration time.Duration) error { 45 | ctx := context.Background() 46 | return RDB.Set(ctx, key, value, expiration).Err() 47 | } 48 | 49 | func RedisGet(key string) (string, error) { 50 | ctx := context.Background() 51 | return RDB.Get(ctx, key).Result() 52 | } 53 | 54 | func RedisDel(key string) error { 55 | ctx := context.Background() 56 | return RDB.Del(ctx, key).Err() 57 | } 58 | -------------------------------------------------------------------------------- /core/common/render/event.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labring/aiproxy/core/common/conv" 7 | ) 8 | 9 | type OpenAISSE struct { 10 | Data string 11 | } 12 | 13 | const ( 14 | nn = "\n\n" 15 | data = "data: " 16 | ) 17 | 18 | var ( 19 | nnBytes = conv.StringToBytes(nn) 20 | dataBytes = conv.StringToBytes(data) 21 | ) 22 | 23 | func (r *OpenAISSE) Render(w http.ResponseWriter) error { 24 | r.WriteContentType(w) 25 | 26 | for _, bytes := range [][]byte{ 27 | dataBytes, 28 | conv.StringToBytes(r.Data), 29 | nnBytes, 30 | } { 31 | // nosemgrep: 32 | // go.lang.security.audit.xss.no-direct-write-to-responsewriter.no-direct-write-to-responsewriter 33 | if _, err := w.Write(bytes); err != nil { 34 | return err 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func (r *OpenAISSE) WriteContentType(w http.ResponseWriter) { 41 | w.Header().Set("Content-Type", "text/event-stream") 42 | w.Header().Set("Cache-Control", "no-cache") 43 | w.Header().Set("Connection", "keep-alive") 44 | w.Header().Set("Transfer-Encoding", "chunked") 45 | w.Header().Set("X-Accel-Buffering", "no") 46 | } 47 | -------------------------------------------------------------------------------- /core/common/render/render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/bytedance/sonic" 8 | "github.com/gin-gonic/gin" 9 | "github.com/labring/aiproxy/core/common/conv" 10 | ) 11 | 12 | func StringData(c *gin.Context, str string) { 13 | if len(c.Errors) > 0 { 14 | return 15 | } 16 | if c.IsAborted() { 17 | return 18 | } 19 | c.Render(-1, &OpenAISSE{Data: str}) 20 | c.Writer.Flush() 21 | } 22 | 23 | func ObjectData(c *gin.Context, object any) error { 24 | if len(c.Errors) > 0 { 25 | return c.Errors.Last() 26 | } 27 | if c.IsAborted() { 28 | return errors.New("context aborted") 29 | } 30 | jsonData, err := sonic.Marshal(object) 31 | if err != nil { 32 | return fmt.Errorf("error marshalling object: %w", err) 33 | } 34 | c.Render(-1, &OpenAISSE{Data: conv.BytesToString(jsonData)}) 35 | c.Writer.Flush() 36 | return nil 37 | } 38 | 39 | const DONE = "[DONE]" 40 | 41 | func Done(c *gin.Context) { 42 | StringData(c, DONE) 43 | } 44 | -------------------------------------------------------------------------------- /core/common/splitter/think.go: -------------------------------------------------------------------------------- 1 | package splitter 2 | 3 | import "github.com/labring/aiproxy/core/common/conv" 4 | 5 | const ( 6 | NThinkHead = "\n\n" 7 | ThinkHead = "\n" 8 | ThinkTail = "\n" 9 | ) 10 | 11 | var ( 12 | nthinkHeadBytes = conv.StringToBytes(NThinkHead) 13 | thinkHeadBytes = conv.StringToBytes(ThinkHead) 14 | thinkTailBytes = conv.StringToBytes(ThinkTail) 15 | ) 16 | 17 | func NewThinkSplitter() *Splitter { 18 | return NewSplitter([][]byte{nthinkHeadBytes, thinkHeadBytes}, [][]byte{thinkTailBytes}) 19 | } 20 | -------------------------------------------------------------------------------- /core/common/tiktoken/assest.go: -------------------------------------------------------------------------------- 1 | package tiktoken 2 | 3 | import ( 4 | "embed" 5 | "encoding/base64" 6 | "errors" 7 | "os" 8 | "path" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/labring/aiproxy/core/common/conv" 13 | "github.com/pkoukk/tiktoken-go" 14 | ) 15 | 16 | //go:embed all:assets 17 | var assets embed.FS 18 | 19 | var ( 20 | _ tiktoken.BpeLoader = (*embedBpeLoader)(nil) 21 | defaultBpeLoader = tiktoken.NewDefaultBpeLoader() 22 | ) 23 | 24 | type embedBpeLoader struct{} 25 | 26 | func (e *embedBpeLoader) LoadTiktokenBpe(tiktokenBpeFile string) (map[string]int, error) { 27 | embedPath := path.Join("assets", path.Base(tiktokenBpeFile)) 28 | contents, err := assets.ReadFile(embedPath) 29 | if err != nil { 30 | if errors.Is(err, os.ErrNotExist) { 31 | return defaultBpeLoader.LoadTiktokenBpe(tiktokenBpeFile) 32 | } 33 | return nil, err 34 | } 35 | bpeRanks := make(map[string]int) 36 | for _, line := range strings.Split(conv.BytesToString(contents), "\n") { 37 | if line == "" { 38 | continue 39 | } 40 | parts := strings.Split(line, " ") 41 | token, err := base64.StdEncoding.DecodeString(parts[0]) 42 | if err != nil { 43 | return nil, err 44 | } 45 | rank, err := strconv.Atoi(parts[1]) 46 | if err != nil { 47 | return nil, err 48 | } 49 | bpeRanks[string(token)] = rank 50 | } 51 | return bpeRanks, nil 52 | } 53 | -------------------------------------------------------------------------------- /core/common/tiktoken/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labring/aiproxy/9e44a5cc922ecf98c93e1b68a5f5547ac6895a16/core/common/tiktoken/assets/.gitkeep -------------------------------------------------------------------------------- /core/common/tiktoken/tiktoken.go: -------------------------------------------------------------------------------- 1 | package tiktoken 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/pkoukk/tiktoken-go" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // tokenEncoderMap won't grow after initialization 12 | var ( 13 | tokenEncoderMap = map[string]*tiktoken.Tiktoken{} 14 | defaultTokenEncoder *tiktoken.Tiktoken 15 | tokenEncoderLock sync.RWMutex 16 | ) 17 | 18 | func init() { 19 | tiktoken.SetBpeLoader(&embedBpeLoader{}) 20 | gpt35TokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo") 21 | if err != nil { 22 | log.Fatal("failed to get gpt-3.5-turbo token encoder: " + err.Error()) 23 | } 24 | defaultTokenEncoder = gpt35TokenEncoder 25 | } 26 | 27 | func GetTokenEncoder(model string) *tiktoken.Tiktoken { 28 | tokenEncoderLock.RLock() 29 | tokenEncoder, ok := tokenEncoderMap[model] 30 | tokenEncoderLock.RUnlock() 31 | if ok { 32 | return tokenEncoder 33 | } 34 | 35 | tokenEncoderLock.Lock() 36 | defer tokenEncoderLock.Unlock() 37 | if tokenEncoder, ok := tokenEncoderMap[model]; ok { 38 | return tokenEncoder 39 | } 40 | 41 | tokenEncoder, err := tiktoken.EncodingForModel(model) 42 | if err != nil { 43 | if strings.Contains(err.Error(), "no encoding for model") { 44 | log.Warnf("no encoding for model %s, using encoder for gpt-3.5-turbo", model) 45 | tokenEncoderMap[model] = defaultTokenEncoder 46 | } else { 47 | log.Errorf("failed to get token encoder for model %s: %v", model, err) 48 | } 49 | return defaultTokenEncoder 50 | } 51 | 52 | tokenEncoderMap[model] = tokenEncoder 53 | return tokenEncoder 54 | } 55 | -------------------------------------------------------------------------------- /core/common/trunc.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "unicode/utf8" 5 | 6 | "github.com/labring/aiproxy/core/common/conv" 7 | ) 8 | 9 | func TruncateByRune[T ~string](s T, length int) T { 10 | total := 0 11 | for _, r := range s { 12 | runeLen := utf8.RuneLen(r) 13 | if runeLen == -1 || total+runeLen > length { 14 | return s[:total] 15 | } 16 | total += runeLen 17 | } 18 | return s[:total] 19 | } 20 | 21 | func TruncateBytesByRune(b []byte, length int) []byte { 22 | total := 0 23 | for _, r := range conv.BytesToString(b) { 24 | runeLen := utf8.RuneLen(r) 25 | if runeLen == -1 || total+runeLen > length { 26 | return b[:total] 27 | } 28 | total += runeLen 29 | } 30 | return b[:total] 31 | } 32 | -------------------------------------------------------------------------------- /core/common/trylock/lock.go: -------------------------------------------------------------------------------- 1 | package trylock 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/labring/aiproxy/core/common" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var memRecord = sync.Map{} 13 | 14 | func init() { 15 | go cleanMemLock() 16 | } 17 | 18 | func cleanMemLock() { 19 | ticker := time.NewTicker(30 * time.Second) 20 | defer ticker.Stop() 21 | 22 | for now := range ticker.C { 23 | memRecord.Range(func(key, value any) bool { 24 | exp, ok := value.(time.Time) 25 | if !ok || now.After(exp) { 26 | memRecord.CompareAndDelete(key, value) 27 | } 28 | return true 29 | }) 30 | } 31 | } 32 | 33 | func MemLock(key string, expiration time.Duration) bool { 34 | now := time.Now() 35 | newExpiration := now.Add(expiration) 36 | 37 | for { 38 | actual, loaded := memRecord.LoadOrStore(key, newExpiration) 39 | if !loaded { 40 | return true 41 | } 42 | oldExpiration, ok := actual.(time.Time) 43 | if !ok { 44 | memRecord.CompareAndDelete(key, actual) 45 | continue 46 | } 47 | if now.After(oldExpiration) { 48 | if memRecord.CompareAndSwap(key, actual, newExpiration) { 49 | return true 50 | } 51 | continue 52 | } 53 | return false 54 | } 55 | } 56 | 57 | func Lock(key string, expiration time.Duration) bool { 58 | if !common.RedisEnabled { 59 | return MemLock(key, expiration) 60 | } 61 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 62 | defer cancel() 63 | result, err := common.RDB.SetNX(ctx, key, true, expiration).Result() 64 | if err != nil { 65 | if MemLock("lockerror", time.Second*3) { 66 | log.Errorf("try notify error: %v", err) 67 | } 68 | return MemLock(key, expiration) 69 | } 70 | return result 71 | } 72 | -------------------------------------------------------------------------------- /core/common/trylock/lock_test.go: -------------------------------------------------------------------------------- 1 | package trylock_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/labring/aiproxy/core/common/trylock" 8 | ) 9 | 10 | func TestMemLock(t *testing.T) { 11 | if !trylock.MemLock("", time.Second) { 12 | t.Error("Expected true, Got false") 13 | } 14 | if trylock.MemLock("", time.Second) { 15 | t.Error("Expected false, Got true") 16 | } 17 | if trylock.MemLock("", time.Second) { 18 | t.Error("Expected false, Got true") 19 | } 20 | time.Sleep(time.Second) 21 | if !trylock.MemLock("", time.Second) { 22 | t.Error("Expected true, Got false") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/google/uuid" 7 | "github.com/labring/aiproxy/core/common/conv" 8 | ) 9 | 10 | func ShortUUID() string { 11 | var buf [32]byte 12 | bytes := uuid.New() 13 | hex.Encode(buf[:], bytes[:]) 14 | return conv.BytesToString(buf[:]) 15 | } 16 | -------------------------------------------------------------------------------- /core/controller/misc.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labring/aiproxy/core/common" 6 | "github.com/labring/aiproxy/core/middleware" 7 | ) 8 | 9 | type StatusData struct { 10 | StartTime int64 `json:"startTime"` 11 | } 12 | 13 | // GetStatus godoc 14 | // 15 | // @Summary Get status 16 | // @Description Returns the status of the server 17 | // @Tags misc 18 | // @Produce json 19 | // @Success 200 {object} middleware.APIResponse{data=StatusData} 20 | // @Router /api/status [get] 21 | func GetStatus(c *gin.Context) { 22 | middleware.SuccessResponse(c, &StatusData{ 23 | StartTime: common.StartTime, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /core/controller/utils.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func parsePageParams(c *gin.Context) (page, perPage int) { 10 | pageStr := c.Query("page") 11 | if pageStr == "" { 12 | pageStr = c.Query("p") 13 | } 14 | page, _ = strconv.Atoi(pageStr) 15 | perPage, _ = strconv.Atoi(c.Query("per_page")) 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /core/deploy/Kubefile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY registry registry 3 | COPY manifests manifests 4 | COPY scripts scripts 5 | 6 | ENV cloudDomain="127.0.0.1.nip.io" 7 | ENV cloudPort="" 8 | ENV certSecretName="wildcard-cert" 9 | 10 | ENV ADMIN_KEY="" 11 | ENV SEALOS_JWT_KEY="" 12 | ENV SQL_DSN="" 13 | ENV LOG_SQL_DSN="" 14 | ENV REDIS="" 15 | 16 | ENV BALANCE_SEALOS_CHECK_REAL_NAME_ENABLE="false" 17 | ENV BALANCE_SEALOS_NO_REAL_NAME_USED_AMOUNT_LIMIT="1" 18 | 19 | ENV SAVE_ALL_LOG_DETAIL="false" 20 | ENV LOG_DETAIL_REQUEST_BODY_MAX_SIZE="128" 21 | ENV LOG_DETAIL_RESPONSE_BODY_MAX_SIZE="128" 22 | 23 | CMD ["bash scripts/init.sh"] 24 | -------------------------------------------------------------------------------- /core/deploy/manifests/aiproxy-config.yaml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: aiproxy-env 5 | data: 6 | DEBUG: "false" 7 | DEBUG_SQL: "false" 8 | ADMIN_KEY: "{{ .ADMIN_KEY }}" 9 | SEALOS_JWT_KEY: "{{ .SEALOS_JWT_KEY }}" 10 | SQL_DSN: "{{ .SQL_DSN }}" 11 | LOG_SQL_DSN: "{{ .LOG_SQL_DSN }}" 12 | REDIS: "{{ .REDIS }}" 13 | BALANCE_SEALOS_CHECK_REAL_NAME_ENABLE: "{{ .BALANCE_SEALOS_CHECK_REAL_NAME_ENABLE }}" 14 | BALANCE_SEALOS_NO_REAL_NAME_USED_AMOUNT_LIMIT: "{{ .BALANCE_SEALOS_NO_REAL_NAME_USED_AMOUNT_LIMIT }}" 15 | SAVE_ALL_LOG_DETAIL: "{{ .SAVE_ALL_LOG_DETAIL }}" 16 | LOG_DETAIL_REQUEST_BODY_MAX_SIZE: "{{ .LOG_DETAIL_REQUEST_BODY_MAX_SIZE }}" 17 | LOG_DETAIL_RESPONSE_BODY_MAX_SIZE: "{{ .LOG_DETAIL_RESPONSE_BODY_MAX_SIZE }}" 18 | -------------------------------------------------------------------------------- /core/deploy/manifests/deploy.yaml.tmpl: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: aiproxy 5 | namespace: aiproxy-system 6 | labels: 7 | cloud.sealos.io/app-deploy-manager: aiproxy 8 | spec: 9 | ports: 10 | - port: 3000 11 | targetPort: 3000 12 | selector: 13 | app: aiproxy 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: aiproxy 19 | namespace: aiproxy-system 20 | annotations: 21 | originImageName: ghcr.io/labring/aiproxy:latest 22 | deploy.cloud.sealos.io/minReplicas: '3' 23 | deploy.cloud.sealos.io/maxReplicas: '3' 24 | labels: 25 | cloud.sealos.io/app-deploy-manager: aiproxy 26 | app: aiproxy 27 | spec: 28 | replicas: 3 29 | revisionHistoryLimit: 1 30 | selector: 31 | matchLabels: 32 | app: aiproxy 33 | strategy: 34 | type: RollingUpdate 35 | rollingUpdate: 36 | maxUnavailable: 0 37 | maxSurge: 1 38 | template: 39 | metadata: 40 | labels: 41 | app: aiproxy 42 | spec: 43 | containers: 44 | - name: aiproxy 45 | image: ghcr.io/labring/aiproxy:latest 46 | envFrom: 47 | - configMapRef: 48 | name: aiproxy-env 49 | resources: 50 | requests: 51 | cpu: 50m 52 | memory: 50Mi 53 | limits: 54 | cpu: 500m 55 | memory: 512Mi 56 | ports: 57 | - containerPort: 3000 58 | imagePullPolicy: Always 59 | startupProbe: 60 | httpGet: 61 | port: 3000 62 | path: /api/status 63 | initialDelaySeconds: 5 64 | periodSeconds: 3 65 | failureThreshold: 30 66 | successThreshold: 1 67 | timeoutSeconds: 1 68 | serviceAccountName: default 69 | automountServiceAccountToken: false 70 | -------------------------------------------------------------------------------- /core/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-contrib/cors" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func CORS() gin.HandlerFunc { 9 | config := cors.DefaultConfig() 10 | config.AllowAllOrigins = true 11 | config.AllowCredentials = true 12 | config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} 13 | config.AllowHeaders = []string{"*"} 14 | return cors.New(config) 15 | } 16 | -------------------------------------------------------------------------------- /core/middleware/ctxkey.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | const ( 4 | Channel = "channel" 5 | GroupModelTokenRPM = "group_model_token_rpm" 6 | GroupModelTokenRPS = "group_model_token_rps" 7 | GroupModelTokenTPM = "group_model_token_tpm" 8 | GroupModelTokenTPS = "group_model_token_tps" 9 | Group = "group" 10 | Token = "token" 11 | GroupBalance = "group_balance" 12 | RequestModel = "request_model" 13 | RequestUser = "request_user" 14 | RequestMetadata = "request_metadata" 15 | RequestAt = "request_at" 16 | RequestID = "request_id" 17 | ModelCaches = "model_caches" 18 | ModelConfig = "model_config" 19 | Mode = "mode" 20 | ) 21 | -------------------------------------------------------------------------------- /core/middleware/ipblack.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/common/ipblack" 8 | ) 9 | 10 | func IPBlock(c *gin.Context) { 11 | ip := c.ClientIP() 12 | isBlock := ipblack.GetIPIsBlockAnyWay(c.Request.Context(), ip) 13 | if isBlock { 14 | AbortLogWithMessage(c, http.StatusForbidden, "please try again later") 15 | c.Abort() 16 | return 17 | } 18 | c.Next() 19 | } 20 | -------------------------------------------------------------------------------- /core/middleware/reqid.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func GenRequestID(t time.Time) string { 11 | return strconv.FormatInt(t.UnixMicro(), 10) 12 | } 13 | 14 | const ( 15 | RequestIDHeader = "X-Request-Id" 16 | ) 17 | 18 | func SetRequestID(c *gin.Context, id string) { 19 | c.Set(RequestID, id) 20 | c.Header(RequestIDHeader, id) 21 | log := GetLogger(c) 22 | SetLogRequestIDField(log.Data, id) 23 | } 24 | 25 | func GetRequestID(c *gin.Context) string { 26 | return c.GetString(RequestID) 27 | } 28 | 29 | func RequestIDMiddleware(c *gin.Context) { 30 | now := time.Now() 31 | id := GenRequestID(now) 32 | SetRequestID(c, id) 33 | SetRequestAt(c, now) 34 | } 35 | 36 | func SetRequestAt(c *gin.Context, requestAt time.Time) { 37 | c.Set(RequestAt, requestAt) 38 | } 39 | 40 | func GetRequestAt(c *gin.Context) time.Time { 41 | return c.GetTime(RequestAt) 42 | } 43 | -------------------------------------------------------------------------------- /core/middleware/utils.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/relay/mode" 8 | relaymodel "github.com/labring/aiproxy/core/relay/model" 9 | ) 10 | 11 | func AbortLogWithMessageWithMode( 12 | m mode.Mode, 13 | c *gin.Context, 14 | statusCode int, 15 | message string, 16 | typ ...string, 17 | ) { 18 | GetLogger(c).Error(message) 19 | AbortWithMessageWithMode(m, c, statusCode, message, typ...) 20 | } 21 | 22 | func AbortWithMessageWithMode( 23 | m mode.Mode, 24 | c *gin.Context, 25 | statusCode int, 26 | message string, 27 | typ ...string, 28 | ) { 29 | c.JSON(statusCode, 30 | relaymodel.WrapperErrorWithMessage(m, statusCode, message, typ...), 31 | ) 32 | c.Abort() 33 | } 34 | 35 | func AbortLogWithMessage(c *gin.Context, statusCode int, message string, typ ...string) { 36 | GetLogger(c).Error(message) 37 | AbortWithMessage(c, statusCode, message, typ...) 38 | } 39 | 40 | func AbortWithMessage(c *gin.Context, statusCode int, message string, typ ...string) { 41 | c.JSON(statusCode, 42 | relaymodel.WrapperErrorWithMessage(GetMode(c), statusCode, message, typ...), 43 | ) 44 | c.Abort() 45 | } 46 | 47 | func GetMode(c *gin.Context) mode.Mode { 48 | m, exists := c.Get(Mode) 49 | if !exists { 50 | return mode.Unknown 51 | } 52 | v, ok := m.(mode.Mode) 53 | if !ok { 54 | panic(fmt.Sprintf("mode type error: %T, %v", v, v)) 55 | } 56 | return v 57 | } 58 | -------------------------------------------------------------------------------- /core/model/channeltest.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bytedance/sonic" 7 | "github.com/labring/aiproxy/core/relay/mode" 8 | ) 9 | 10 | type ChannelTest struct { 11 | TestAt time.Time `json:"test_at"` 12 | Model string `json:"model" gorm:"primaryKey"` 13 | ActualModel string `json:"actual_model"` 14 | Response string `json:"response" gorm:"type:text"` 15 | ChannelName string `json:"channel_name"` 16 | ChannelType ChannelType `json:"channel_type"` 17 | ChannelID int `json:"channel_id" gorm:"primaryKey"` 18 | Took float64 `json:"took"` 19 | Success bool `json:"success"` 20 | Mode mode.Mode `json:"mode"` 21 | Code int `json:"code"` 22 | } 23 | 24 | func (ct *ChannelTest) MarshalJSON() ([]byte, error) { 25 | type Alias ChannelTest 26 | return sonic.Marshal(&struct { 27 | *Alias 28 | TestAt int64 `json:"test_at"` 29 | }{ 30 | Alias: (*Alias)(ct), 31 | TestAt: ct.TestAt.UnixMilli(), 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /core/model/owner.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | //nolint:revive 4 | type ModelOwner string 5 | 6 | const ( 7 | ModelOwnerOpenAI ModelOwner = "openai" 8 | ModelOwnerAlibaba ModelOwner = "alibaba" 9 | ModelOwnerTencent ModelOwner = "tencent" 10 | ModelOwnerXunfei ModelOwner = "xunfei" 11 | ModelOwnerDeepSeek ModelOwner = "deepseek" 12 | ModelOwnerMoonshot ModelOwner = "moonshot" 13 | ModelOwnerMiniMax ModelOwner = "minimax" 14 | ModelOwnerBaidu ModelOwner = "baidu" 15 | ModelOwnerGoogle ModelOwner = "google" 16 | ModelOwnerBAAI ModelOwner = "baai" 17 | ModelOwnerFunAudioLLM ModelOwner = "funaudiollm" 18 | ModelOwnerDoubao ModelOwner = "doubao" 19 | ModelOwnerFishAudio ModelOwner = "fishaudio" 20 | ModelOwnerChatGLM ModelOwner = "chatglm" 21 | ModelOwnerStabilityAI ModelOwner = "stabilityai" 22 | ModelOwnerNetease ModelOwner = "netease" 23 | ModelOwnerAI360 ModelOwner = "ai360" 24 | ModelOwnerAnthropic ModelOwner = "anthropic" 25 | ModelOwnerMeta ModelOwner = "meta" 26 | ModelOwnerBaichuan ModelOwner = "baichuan" 27 | ModelOwnerMistral ModelOwner = "mistral" 28 | ModelOwnerOpenChat ModelOwner = "openchat" 29 | ModelOwnerMicrosoft ModelOwner = "microsoft" 30 | ModelOwnerDefog ModelOwner = "defog" 31 | ModelOwnerNexusFlow ModelOwner = "nexusflow" 32 | ModelOwnerCohere ModelOwner = "cohere" 33 | ModelOwnerHuggingFace ModelOwner = "huggingface" 34 | ModelOwnerLingyiWanwu ModelOwner = "lingyiwanwu" 35 | ModelOwnerStepFun ModelOwner = "stepfun" 36 | ModelOwnerXAI ModelOwner = "xai" 37 | ModelOwnerDoc2x ModelOwner = "doc2x" 38 | ModelOwnerJina ModelOwner = "jina" 39 | ) 40 | -------------------------------------------------------------------------------- /core/public/dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labring/aiproxy/9e44a5cc922ecf98c93e1b68a5f5547ac6895a16/core/public/dist/.gitkeep -------------------------------------------------------------------------------- /core/public/public.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | ) 7 | 8 | //go:embed all:dist 9 | var dist embed.FS 10 | 11 | var Public, _ = fs.Sub(dist, "dist") 12 | 13 | //go:embed all:templates 14 | var Templates embed.FS 15 | -------------------------------------------------------------------------------- /core/relay/adaptor/ai360/adaptor.go: -------------------------------------------------------------------------------- 1 | package ai360 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 6 | ) 7 | 8 | type Adaptor struct { 9 | openai.Adaptor 10 | } 11 | 12 | const baseURL = "https://ai.360.cn/v1" 13 | 14 | func (a *Adaptor) GetBaseURL() string { 15 | return baseURL 16 | } 17 | 18 | func (a *Adaptor) GetModelList() []model.ModelConfig { 19 | return ModelList 20 | } 21 | -------------------------------------------------------------------------------- /core/relay/adaptor/ai360/constants.go: -------------------------------------------------------------------------------- 1 | package ai360 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | var ModelList = []model.ModelConfig{ 9 | { 10 | Model: "360GPT_S2_V9", 11 | Type: mode.ChatCompletions, 12 | Owner: model.ModelOwnerAI360, 13 | }, 14 | { 15 | Model: "embedding-bert-512-v1", 16 | Type: mode.Embeddings, 17 | Owner: model.ModelOwnerAI360, 18 | }, 19 | { 20 | Model: "embedding_s1_v1", 21 | Type: mode.Embeddings, 22 | Owner: model.ModelOwnerAI360, 23 | }, 24 | { 25 | Model: "semantic_similarity_s1_v1", 26 | Type: mode.Embeddings, 27 | Owner: model.ModelOwnerAI360, 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /core/relay/adaptor/ali/config.go: -------------------------------------------------------------------------------- 1 | package ali 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /core/relay/adaptor/ali/fetures.go: -------------------------------------------------------------------------------- 1 | package ali 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "OpenAI compatibility", 10 | "Network search metering support", 11 | "Rerank support: https://help.aliyun.com/zh/model-studio/text-rerank-api", 12 | "STT support: https://help.aliyun.com/zh/model-studio/sambert-speech-synthesis/", 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/relay/adaptor/anthropic/config.go: -------------------------------------------------------------------------------- 1 | package anthropic 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /core/relay/adaptor/anthropic/event.go: -------------------------------------------------------------------------------- 1 | package anthropic 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/bytedance/sonic" 7 | "github.com/labring/aiproxy/core/common/conv" 8 | ) 9 | 10 | type Anthropic struct { 11 | Event string 12 | Data []byte 13 | } 14 | 15 | const ( 16 | n = "\n" 17 | nn = "\n\n" 18 | event = "event: " 19 | data = "data: " 20 | ) 21 | 22 | var ( 23 | nBytes = conv.StringToBytes(n) 24 | nnBytes = conv.StringToBytes(nn) 25 | eventBytes = conv.StringToBytes(event) 26 | dataBytes = conv.StringToBytes(data) 27 | ) 28 | 29 | func (r *Anthropic) Render(w http.ResponseWriter) error { 30 | r.WriteContentType(w) 31 | 32 | event := r.Event 33 | 34 | if event == "" { 35 | eventNode, err := sonic.Get(r.Data, "type") 36 | if err != nil { 37 | return err 38 | } 39 | event, err = eventNode.String() 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | for _, bytes := range [][]byte{ 46 | eventBytes, 47 | conv.StringToBytes(event), 48 | nBytes, 49 | dataBytes, 50 | r.Data, 51 | nnBytes, 52 | } { 53 | // nosemgrep: 54 | // go.lang.security.audit.xss.no-direct-write-to-responsewriter.no-direct-write-to-responsewriter 55 | if _, err := w.Write(bytes); err != nil { 56 | return err 57 | } 58 | } 59 | return nil 60 | } 61 | 62 | func (r *Anthropic) WriteContentType(w http.ResponseWriter) { 63 | w.Header().Set("Content-Type", "text/event-stream") 64 | w.Header().Set("Cache-Control", "no-cache") 65 | w.Header().Set("Connection", "keep-alive") 66 | w.Header().Set("Transfer-Encoding", "chunked") 67 | w.Header().Set("X-Accel-Buffering", "no") 68 | } 69 | -------------------------------------------------------------------------------- /core/relay/adaptor/anthropic/fetures.go: -------------------------------------------------------------------------------- 1 | package anthropic 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "Support native Endpoint: /v1/messages", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/relay/adaptor/anthropic/render.go: -------------------------------------------------------------------------------- 1 | package anthropic 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/bytedance/sonic" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func Data(c *gin.Context, data []byte) { 12 | if len(c.Errors) > 0 { 13 | return 14 | } 15 | if c.IsAborted() { 16 | return 17 | } 18 | c.Render(-1, &Anthropic{Data: data}) 19 | c.Writer.Flush() 20 | } 21 | 22 | func EventData(c *gin.Context, event string, data []byte) { 23 | if len(c.Errors) > 0 { 24 | return 25 | } 26 | if c.IsAborted() { 27 | return 28 | } 29 | c.Render(-1, &Anthropic{Event: event, Data: data}) 30 | c.Writer.Flush() 31 | } 32 | 33 | func ObjectData(c *gin.Context, object any) error { 34 | if len(c.Errors) > 0 { 35 | return c.Errors.Last() 36 | } 37 | if c.IsAborted() { 38 | return errors.New("context aborted") 39 | } 40 | jsonData, err := sonic.Marshal(object) 41 | if err != nil { 42 | return fmt.Errorf("error marshalling object: %w", err) 43 | } 44 | c.Render(-1, &Anthropic{Data: jsonData}) 45 | c.Writer.Flush() 46 | return nil 47 | } 48 | 49 | func EventObjectData(c *gin.Context, event string, object any) error { 50 | if len(c.Errors) > 0 { 51 | return c.Errors.Last() 52 | } 53 | if c.IsAborted() { 54 | return errors.New("context aborted") 55 | } 56 | jsonData, err := sonic.Marshal(object) 57 | if err != nil { 58 | return fmt.Errorf("error marshalling object: %w", err) 59 | } 60 | c.Render(-1, &Anthropic{Event: event, Data: jsonData}) 61 | c.Writer.Flush() 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /core/relay/adaptor/aws/claude/adapter.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/adaptor/anthropic" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | ) 12 | 13 | const ( 14 | ConvertedRequest = "convertedRequest" 15 | ) 16 | 17 | type Adaptor struct{} 18 | 19 | func (a *Adaptor) ConvertRequest( 20 | meta *meta.Meta, 21 | req *http.Request, 22 | ) (*adaptor.ConvertRequestResult, error) { 23 | r, err := anthropic.OpenAIConvertRequest(meta, req) 24 | if err != nil { 25 | return nil, err 26 | } 27 | meta.Set("stream", r.Stream) 28 | meta.Set(ConvertedRequest, r) 29 | return &adaptor.ConvertRequestResult{ 30 | Method: http.MethodPost, 31 | Header: nil, 32 | Body: nil, 33 | }, nil 34 | } 35 | 36 | func (a *Adaptor) DoResponse( 37 | meta *meta.Meta, 38 | c *gin.Context, 39 | ) (usage *model.Usage, err adaptor.Error) { 40 | if meta.GetBool("stream") { 41 | usage, err = StreamHandler(meta, c) 42 | } else { 43 | usage, err = Handler(meta, c) 44 | } 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /core/relay/adaptor/aws/claude/model.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor/anthropic" 4 | 5 | // Request is the request to AWS Claude 6 | // 7 | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html 8 | type Request struct { 9 | ToolChoice any `json:"tool_choice,omitempty"` 10 | Temperature *float64 `json:"temperature,omitempty"` 11 | TopP *float64 `json:"top_p,omitempty"` 12 | AnthropicVersion string `json:"anthropic_version"` 13 | System string `json:"system,omitempty"` 14 | Messages []anthropic.Message `json:"messages"` 15 | StopSequences []string `json:"stop_sequences,omitempty"` 16 | Tools []anthropic.Tool `json:"tools,omitempty"` 17 | MaxTokens int `json:"max_tokens,omitempty"` 18 | TopK int `json:"top_k,omitempty"` 19 | } 20 | -------------------------------------------------------------------------------- /core/relay/adaptor/aws/key.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/relay/adaptor" 5 | "github.com/labring/aiproxy/core/relay/adaptor/aws/utils" 6 | ) 7 | 8 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 9 | 10 | func (a *Adaptor) ValidateKey(key string) error { 11 | _, err := utils.GetAwsConfigFromKey(key) 12 | if err != nil { 13 | return err 14 | } 15 | return nil 16 | } 17 | 18 | func (a *Adaptor) KeyHelp() string { 19 | return "region|ak|sk" 20 | } 21 | -------------------------------------------------------------------------------- /core/relay/adaptor/aws/llama3/adapter.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/meta" 10 | relayutils "github.com/labring/aiproxy/core/relay/utils" 11 | ) 12 | 13 | const ( 14 | ConvertedRequest = "convertedRequest" 15 | ) 16 | 17 | type Adaptor struct{} 18 | 19 | func (a *Adaptor) ConvertRequest( 20 | meta *meta.Meta, 21 | req *http.Request, 22 | ) (*adaptor.ConvertRequestResult, error) { 23 | request, err := relayutils.UnmarshalGeneralOpenAIRequest(req) 24 | if err != nil { 25 | return nil, err 26 | } 27 | request.Model = meta.ActualModel 28 | meta.Set("stream", request.Stream) 29 | llamaReq := ConvertRequest(request) 30 | meta.Set(ConvertedRequest, llamaReq) 31 | return &adaptor.ConvertRequestResult{ 32 | Method: http.MethodPost, 33 | Header: nil, 34 | Body: nil, 35 | }, nil 36 | } 37 | 38 | func (a *Adaptor) DoResponse( 39 | meta *meta.Meta, 40 | c *gin.Context, 41 | ) (usage *model.Usage, err adaptor.Error) { 42 | if meta.GetBool("stream") { 43 | usage, err = StreamHandler(meta, c) 44 | } else { 45 | usage, err = Handler(meta, c) 46 | } 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /core/relay/adaptor/aws/llama3/main_test.go: -------------------------------------------------------------------------------- 1 | package aws_test 2 | 3 | import ( 4 | "testing" 5 | 6 | aws "github.com/labring/aiproxy/core/relay/adaptor/aws/llama3" 7 | model "github.com/labring/aiproxy/core/relay/model" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRenderPrompt(t *testing.T) { 12 | messages := []*model.Message{ 13 | { 14 | Role: "user", 15 | Content: "What's your name?", 16 | }, 17 | } 18 | prompt := aws.RenderPrompt(messages) 19 | expected := `<|begin_of_text|><|start_header_id|>user<|end_header_id|>What's your name?<|eot_id|><|start_header_id|>assistant<|end_header_id|> 20 | ` 21 | assert.Equal(t, expected, prompt) 22 | 23 | messages = []*model.Message{ 24 | { 25 | Role: "system", 26 | Content: "Your name is Kat. You are a detective.", 27 | }, 28 | { 29 | Role: "user", 30 | Content: "What's your name?", 31 | }, 32 | { 33 | Role: "assistant", 34 | Content: "Kat", 35 | }, 36 | { 37 | Role: "user", 38 | Content: "What's your job?", 39 | }, 40 | } 41 | prompt = aws.RenderPrompt(messages) 42 | expected = `<|begin_of_text|><|start_header_id|>system<|end_header_id|>Your name is Kat. You are a detective.<|eot_id|><|start_header_id|>user<|end_header_id|>What's your name?<|eot_id|><|start_header_id|>assistant<|end_header_id|>Kat<|eot_id|><|start_header_id|>user<|end_header_id|>What's your job?<|eot_id|><|start_header_id|>assistant<|end_header_id|> 43 | ` 44 | assert.Equal(t, expected, prompt) 45 | } 46 | -------------------------------------------------------------------------------- /core/relay/adaptor/aws/llama3/model.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | // Request is the request to AWS Llama3 4 | // 5 | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html 6 | type Request struct { 7 | Temperature *float64 `json:"temperature,omitempty"` 8 | TopP *float64 `json:"top_p,omitempty"` 9 | Prompt string `json:"prompt"` 10 | MaxGenLen int `json:"max_gen_len,omitempty"` 11 | } 12 | 13 | // Response is the response from AWS Llama3 14 | // 15 | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html 16 | type Response struct { 17 | Generation string `json:"generation"` 18 | StopReason string `json:"stop_reason"` 19 | PromptTokenCount int64 `json:"prompt_token_count"` 20 | GenerationTokenCount int64 `json:"generation_token_count"` 21 | } 22 | 23 | // {'generation': 'Hi', 'prompt_token_count': 15, 'generation_token_count': 1, 'stop_reason': None} 24 | type StreamResponse struct { 25 | Generation string `json:"generation"` 26 | StopReason string `json:"stop_reason"` 27 | PromptTokenCount int64 `json:"prompt_token_count"` 28 | GenerationTokenCount int64 `json:"generation_token_count"` 29 | } 30 | -------------------------------------------------------------------------------- /core/relay/adaptor/aws/registry.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | claude "github.com/labring/aiproxy/core/relay/adaptor/aws/claude" 6 | llama3 "github.com/labring/aiproxy/core/relay/adaptor/aws/llama3" 7 | "github.com/labring/aiproxy/core/relay/adaptor/aws/utils" 8 | ) 9 | 10 | type ModelType int 11 | 12 | const ( 13 | AwsClaude ModelType = iota + 1 14 | AwsLlama3 15 | ) 16 | 17 | type Model struct { 18 | config model.ModelConfig 19 | _type ModelType 20 | } 21 | 22 | var adaptors = map[string]Model{} 23 | 24 | func init() { 25 | for _, model := range claude.AwsModelIDMap { 26 | adaptors[model.Model] = Model{config: model.ModelConfig, _type: AwsClaude} 27 | } 28 | for _, model := range llama3.AwsModelIDMap { 29 | adaptors[model.Model] = Model{config: model.ModelConfig, _type: AwsLlama3} 30 | } 31 | } 32 | 33 | func GetAdaptor(model string) utils.AwsAdapter { 34 | adaptorType := adaptors[model] 35 | switch adaptorType._type { 36 | case AwsClaude: 37 | return &claude.Adaptor{} 38 | case AwsLlama3: 39 | return &llama3.Adaptor{} 40 | default: 41 | return nil 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/relay/adaptor/azure/key.go: -------------------------------------------------------------------------------- 1 | package azure 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | ) 9 | 10 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 11 | 12 | func (a *Adaptor) ValidateKey(key string) error { 13 | _, _, err := getTokenAndAPIVersion(key) 14 | if err != nil { 15 | return err 16 | } 17 | return nil 18 | } 19 | 20 | func (a *Adaptor) KeyHelp() string { 21 | return "key or key|api-version" 22 | } 23 | 24 | const defaultAPIVersion = "2024-12-01-preview" 25 | 26 | func getTokenAndAPIVersion(key string) (string, string, error) { 27 | split := strings.Split(key, "|") 28 | if len(split) == 1 { 29 | return key, defaultAPIVersion, nil 30 | } 31 | if len(split) != 2 { 32 | return "", "", errors.New("invalid key format") 33 | } 34 | return split[0], split[1], nil 35 | } 36 | -------------------------------------------------------------------------------- /core/relay/adaptor/baichuan/adaptor.go: -------------------------------------------------------------------------------- 1 | package baichuan 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 6 | ) 7 | 8 | type Adaptor struct { 9 | openai.Adaptor 10 | } 11 | 12 | const baseURL = "https://api.baichuan-ai.com/v1" 13 | 14 | func (a *Adaptor) GetBaseURL() string { 15 | return baseURL 16 | } 17 | 18 | func (a *Adaptor) GetModelList() []model.ModelConfig { 19 | return ModelList 20 | } 21 | -------------------------------------------------------------------------------- /core/relay/adaptor/baidu/error.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | relaymodel "github.com/labring/aiproxy/core/relay/model" 9 | ) 10 | 11 | // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/tlmyncueh 12 | 13 | func ErrorHandler(baiduError *Error) adaptor.Error { 14 | switch baiduError.ErrorCode { 15 | case 13, 14, 100, 110: 16 | return relaymodel.WrapperOpenAIErrorWithMessage( 17 | baiduError.ErrorMsg, 18 | "upstream_"+strconv.Itoa(baiduError.ErrorCode), 19 | http.StatusUnauthorized, 20 | ) 21 | case 17, 19, 111: 22 | return relaymodel.WrapperOpenAIErrorWithMessage( 23 | baiduError.ErrorMsg, 24 | "upstream_"+strconv.Itoa(baiduError.ErrorCode), 25 | http.StatusForbidden, 26 | ) 27 | case 336001, 336002, 336003, 28 | 336005, 336006, 336007, 29 | 336008, 336103, 336104, 30 | 336106, 336118, 336122, 31 | 336123, 336221, 337006, 32 | 337008, 337009: 33 | return relaymodel.WrapperOpenAIErrorWithMessage( 34 | baiduError.ErrorMsg, 35 | "upstream_"+strconv.Itoa(baiduError.ErrorCode), 36 | http.StatusBadRequest, 37 | ) 38 | case 4, 18, 336117, 336501, 336502, 39 | 336503, 336504, 336505, 40 | 336507: 41 | return relaymodel.WrapperOpenAIErrorWithMessage( 42 | baiduError.ErrorMsg, 43 | "upstream_"+strconv.Itoa(baiduError.ErrorCode), 44 | http.StatusTooManyRequests, 45 | ) 46 | } 47 | return relaymodel.WrapperOpenAIErrorWithMessage( 48 | baiduError.ErrorMsg, 49 | "upstream_"+strconv.Itoa(baiduError.ErrorCode), 50 | http.StatusInternalServerError, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /core/relay/adaptor/baidu/key.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | ) 9 | 10 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 11 | 12 | func (a *Adaptor) ValidateKey(key string) error { 13 | _, _, err := getClientIDAndSecret(key) 14 | return err 15 | } 16 | 17 | func (a *Adaptor) KeyHelp() string { 18 | return "client_id|client_secret" 19 | } 20 | 21 | // key格式: client_id|client_secret 22 | func getClientIDAndSecret(key string) (string, string, error) { 23 | parts := strings.Split(key, "|") 24 | if len(parts) != 2 { 25 | return "", "", errors.New("invalid key format") 26 | } 27 | return parts[0], parts[1], nil 28 | } 29 | -------------------------------------------------------------------------------- /core/relay/adaptor/baidu/model.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/relay/model" 5 | ) 6 | 7 | type Error struct { 8 | ErrorMsg string `json:"error_msg"` 9 | ErrorCode int `json:"error_code"` 10 | } 11 | 12 | type ErrorResponse struct { 13 | *Error `json:"error"` 14 | ID string `json:"id"` 15 | } 16 | 17 | type ChatResponse struct { 18 | Usage *model.Usage `json:"usage"` 19 | *Error `json:"error"` 20 | ID string `json:"id"` 21 | Object string `json:"object"` 22 | Result string `json:"result"` 23 | Created int64 `json:"created"` 24 | IsTruncated bool `json:"is_truncated"` 25 | NeedClearHistory bool `json:"need_clear_history"` 26 | } 27 | 28 | type ChatStreamResponse struct { 29 | ChatResponse 30 | SentenceID int `json:"sentence_id"` 31 | IsEnd bool `json:"is_end"` 32 | } 33 | -------------------------------------------------------------------------------- /core/relay/adaptor/baiduv2/key.go: -------------------------------------------------------------------------------- 1 | package baiduv2 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | ) 9 | 10 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 11 | 12 | func (a *Adaptor) ValidateKey(key string) error { 13 | _, _, err := getAKAndSK(key) 14 | return err 15 | } 16 | 17 | func (a *Adaptor) KeyHelp() string { 18 | return "ak|sk" 19 | } 20 | 21 | // key格式: ak|sk 22 | func getAKAndSK(key string) (string, string, error) { 23 | parts := strings.Split(key, "|") 24 | if len(parts) != 2 { 25 | return "", "", errors.New("invalid key format") 26 | } 27 | return parts[0], parts[1], nil 28 | } 29 | -------------------------------------------------------------------------------- /core/relay/adaptor/cloudflare/adaptor.go: -------------------------------------------------------------------------------- 1 | package cloudflare 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 9 | "github.com/labring/aiproxy/core/relay/meta" 10 | "github.com/labring/aiproxy/core/relay/mode" 11 | ) 12 | 13 | type Adaptor struct { 14 | openai.Adaptor 15 | } 16 | 17 | const baseURL = "https://api.cloudflare.com" 18 | 19 | func (a *Adaptor) GetBaseURL() string { 20 | return baseURL 21 | } 22 | 23 | // WorkerAI cannot be used across accounts with AIGateWay 24 | // https://developers.cloudflare.com/ai-gateway/providers/workersai/#openai-compatible-endpoints 25 | // https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/workers-ai 26 | func isAIGateWay(baseURL string) bool { 27 | return strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") && 28 | strings.HasSuffix(baseURL, "/workers-ai") 29 | } 30 | 31 | func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { 32 | u := meta.Channel.BaseURL 33 | isAIGateWay := isAIGateWay(u) 34 | var urlPrefix string 35 | if isAIGateWay { 36 | urlPrefix = u 37 | } else { 38 | urlPrefix = fmt.Sprintf("%s/client/v4/accounts/%s/ai", u, meta.Channel.Key) 39 | } 40 | 41 | switch meta.Mode { 42 | case mode.ChatCompletions: 43 | return urlPrefix + "/v1/chat/completions", nil 44 | case mode.Embeddings: 45 | return urlPrefix + "/v1/embeddings", nil 46 | default: 47 | if isAIGateWay { 48 | return fmt.Sprintf("%s/%s", urlPrefix, meta.ActualModel), nil 49 | } 50 | return fmt.Sprintf("%s/run/%s", urlPrefix, meta.ActualModel), nil 51 | } 52 | } 53 | 54 | func (a *Adaptor) GetModelList() []model.ModelConfig { 55 | return ModelList 56 | } 57 | -------------------------------------------------------------------------------- /core/relay/adaptor/cohere/constant.go: -------------------------------------------------------------------------------- 1 | package cohere 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | var ModelList = []model.ModelConfig{ 9 | { 10 | Model: "command", 11 | Type: mode.ChatCompletions, 12 | Owner: model.ModelOwnerCohere, 13 | }, 14 | { 15 | Model: "command-nightly", 16 | Type: mode.ChatCompletions, 17 | Owner: model.ModelOwnerCohere, 18 | }, 19 | { 20 | Model: "command-light", 21 | Type: mode.ChatCompletions, 22 | Owner: model.ModelOwnerCohere, 23 | }, 24 | { 25 | Model: "command-light-nightly", 26 | Type: mode.ChatCompletions, 27 | Owner: model.ModelOwnerCohere, 28 | }, 29 | { 30 | Model: "command-r", 31 | Type: mode.ChatCompletions, 32 | Owner: model.ModelOwnerCohere, 33 | }, 34 | { 35 | Model: "command-r-plus", 36 | Type: mode.ChatCompletions, 37 | Owner: model.ModelOwnerCohere, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /core/relay/adaptor/coze/constant/contenttype/define.go: -------------------------------------------------------------------------------- 1 | package contenttype 2 | 3 | const ( 4 | Text = "text" 5 | ) 6 | -------------------------------------------------------------------------------- /core/relay/adaptor/coze/constant/event/define.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | const ( 4 | Message = "message" 5 | Done = "done" 6 | Error = "error" 7 | ) 8 | -------------------------------------------------------------------------------- /core/relay/adaptor/coze/constant/messagetype/define.go: -------------------------------------------------------------------------------- 1 | package messagetype 2 | 3 | const ( 4 | Answer = "answer" 5 | FollowUp = "follow_up" 6 | ) 7 | -------------------------------------------------------------------------------- /core/relay/adaptor/coze/constants.go: -------------------------------------------------------------------------------- 1 | package coze 2 | 3 | import "github.com/labring/aiproxy/core/model" 4 | 5 | var ModelList = []model.ModelConfig{} 6 | -------------------------------------------------------------------------------- /core/relay/adaptor/coze/key.go: -------------------------------------------------------------------------------- 1 | package coze 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | ) 9 | 10 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 11 | 12 | func (a *Adaptor) ValidateKey(key string) error { 13 | _, _, err := getTokenAndUserID(key) 14 | if err != nil { 15 | return err 16 | } 17 | return nil 18 | } 19 | 20 | func (a *Adaptor) KeyHelp() string { 21 | return "token|user_id" 22 | } 23 | 24 | func getTokenAndUserID(key string) (string, string, error) { 25 | split := strings.Split(key, "|") 26 | if len(split) != 2 { 27 | return "", "", errors.New("invalid key format") 28 | } 29 | return split[0], split[1], nil 30 | } 31 | -------------------------------------------------------------------------------- /core/relay/adaptor/coze/model.go: -------------------------------------------------------------------------------- 1 | package coze 2 | 3 | type Message struct { 4 | Role string `json:"role"` 5 | Type string `json:"type"` 6 | Content string `json:"content"` 7 | ContentType string `json:"content_type"` 8 | } 9 | 10 | type ErrorInformation struct { 11 | Msg string `json:"msg"` 12 | Code int `json:"code"` 13 | } 14 | 15 | type Request struct { 16 | ConversationID string `json:"conversation_id,omitempty"` 17 | BotID string `json:"bot_id"` 18 | User string `json:"user"` 19 | Query string `json:"query"` 20 | ChatHistory []Message `json:"chat_history,omitempty"` 21 | Stream bool `json:"stream"` 22 | } 23 | 24 | type Response struct { 25 | ConversationID string `json:"conversation_id,omitempty"` 26 | Msg string `json:"msg,omitempty"` 27 | Messages []Message `json:"messages,omitempty"` 28 | Code int `json:"code,omitempty"` 29 | } 30 | 31 | type StreamResponse struct { 32 | Message *Message `json:"message,omitempty"` 33 | Event string `json:"event,omitempty"` 34 | ConversationID string `json:"conversation_id,omitempty"` 35 | Index int `json:"index,omitempty"` 36 | IsFinish bool `json:"is_finish,omitempty"` 37 | } 38 | -------------------------------------------------------------------------------- /core/relay/adaptor/deepseek/adaptor.go: -------------------------------------------------------------------------------- 1 | package deepseek 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor" 6 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 7 | ) 8 | 9 | var _ adaptor.Adaptor = (*Adaptor)(nil) 10 | 11 | type Adaptor struct { 12 | openai.Adaptor 13 | } 14 | 15 | const baseURL = "https://api.deepseek.com/v1" 16 | 17 | func (a *Adaptor) GetBaseURL() string { 18 | return baseURL 19 | } 20 | 21 | func (a *Adaptor) GetModelList() []model.ModelConfig { 22 | return ModelList 23 | } 24 | -------------------------------------------------------------------------------- /core/relay/adaptor/deepseek/balance.go: -------------------------------------------------------------------------------- 1 | package deepseek 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/bytedance/sonic" 10 | "github.com/labring/aiproxy/core/model" 11 | "github.com/labring/aiproxy/core/relay/adaptor" 12 | ) 13 | 14 | var _ adaptor.Balancer = (*Adaptor)(nil) 15 | 16 | func (a *Adaptor) GetBalance(channel *model.Channel) (float64, error) { 17 | u := channel.BaseURL 18 | if u == "" { 19 | u = baseURL 20 | } 21 | url := u + "/user/balance" 22 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 23 | if err != nil { 24 | return 0, err 25 | } 26 | req.Header.Set("Authorization", "Bearer "+channel.Key) 27 | resp, err := http.DefaultClient.Do(req) 28 | if err != nil { 29 | return 0, err 30 | } 31 | defer resp.Body.Close() 32 | var usage UsageResponse 33 | if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&usage); err != nil { 34 | return 0, err 35 | } 36 | index := -1 37 | for i, balanceInfo := range usage.BalanceInfos { 38 | if balanceInfo.Currency == "CNY" { 39 | index = i 40 | break 41 | } 42 | } 43 | if index == -1 { 44 | return 0, errors.New("currency CNY not found") 45 | } 46 | balance, err := strconv.ParseFloat(usage.BalanceInfos[index].TotalBalance, 64) 47 | if err != nil { 48 | return 0, err 49 | } 50 | return balance, nil 51 | } 52 | 53 | type UsageResponse struct { 54 | BalanceInfos []struct { 55 | Currency string `json:"currency"` 56 | TotalBalance string `json:"total_balance"` 57 | GrantedBalance string `json:"granted_balance"` 58 | ToppedUpBalance string `json:"topped_up_balance"` 59 | } `json:"balance_infos"` 60 | IsAvailable bool `json:"is_available"` 61 | } 62 | -------------------------------------------------------------------------------- /core/relay/adaptor/deepseek/constants.go: -------------------------------------------------------------------------------- 1 | package deepseek 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | var ModelList = []model.ModelConfig{ 9 | { 10 | Model: "deepseek-chat", 11 | Type: mode.ChatCompletions, 12 | Owner: model.ModelOwnerDeepSeek, 13 | Price: model.Price{ 14 | InputPrice: 0.001, 15 | OutputPrice: 0.002, 16 | }, 17 | RPM: 10000, 18 | Config: model.NewModelConfig( 19 | model.WithModelConfigMaxContextTokens(64000), 20 | model.WithModelConfigMaxOutputTokens(8192), 21 | model.WithModelConfigToolChoice(true), 22 | ), 23 | }, 24 | 25 | { 26 | Model: "deepseek-reasoner", 27 | Type: mode.ChatCompletions, 28 | Owner: model.ModelOwnerDeepSeek, 29 | Price: model.Price{ 30 | InputPrice: 0.004, 31 | OutputPrice: 0.016, 32 | }, 33 | RPM: 10000, 34 | Config: model.NewModelConfig( 35 | model.WithModelConfigMaxContextTokens(64000), 36 | model.WithModelConfigMaxOutputTokens(8192), 37 | ), 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /core/relay/adaptor/doc2x/config.go: -------------------------------------------------------------------------------- 1 | package doc2x 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /core/relay/adaptor/doc2x/constants.go: -------------------------------------------------------------------------------- 1 | package doc2x 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | var ModelList = []model.ModelConfig{ 9 | { 10 | Model: "pdf", 11 | Type: mode.ParsePdf, 12 | Owner: model.ModelOwnerDoc2x, 13 | Price: model.Price{ 14 | InputPrice: 20, 15 | }, 16 | RPM: 10, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /core/relay/adaptor/doc2x/html2md_test.go: -------------------------------------------------------------------------------- 1 | package doc2x_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/labring/aiproxy/core/relay/adaptor/doc2x" 7 | ) 8 | 9 | func TestHTMLTable2Md(t *testing.T) { 10 | t.Parallel() 11 | 12 | tables := []struct { 13 | name string 14 | html string 15 | expected string 16 | }{ 17 | { 18 | name: "basic table", 19 | html: `
sadsa
sadasdsasad
dsadsadsa
`, 20 | expected: `| sadsa | | | 21 | | --- | --- | --- | 22 | | | sadasdsa | sad | 23 | | | | dsadsadsa | 24 | | | | |`, 25 | }, 26 | { 27 | name: "simple table", 28 | html: `
Header 1Header 2
Data 1Data 2
`, 29 | expected: `| Header 1 | Header 2 | 30 | | --- | --- | 31 | | Data 1 | Data 2 |`, 32 | }, 33 | { 34 | name: "empty table", 35 | html: `
`, 36 | expected: `| | | 37 | | --- | --- | 38 | | | |`, 39 | }, 40 | } 41 | 42 | for _, tc := range tables { 43 | t.Run(tc.name, func(t *testing.T) { 44 | t.Parallel() 45 | result := doc2x.HTMLTable2Md(tc.html) 46 | 47 | if result != tc.expected { 48 | t.Errorf("Expected:\n%s\nGot:\n%s", tc.expected, result) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | // var htmlImage = `` 56 | 57 | // func TestInlineMdImage(t *testing.T) { 58 | // t.Parallel() 59 | // result := doc2x.InlineMdImage(context.Background(), htmlImage) 60 | // t.Log(result) 61 | // } 62 | -------------------------------------------------------------------------------- /core/relay/adaptor/doubao/fetures.go: -------------------------------------------------------------------------------- 1 | package doubao 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "Bot support", 10 | "Network search metering support", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/relay/adaptor/doubaoaudio/fetures.go: -------------------------------------------------------------------------------- 1 | package doubaoaudio 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "https://www.volcengine.com/docs/6561/1257543", 10 | "TTS support", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/relay/adaptor/doubaoaudio/key.go: -------------------------------------------------------------------------------- 1 | package doubaoaudio 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | ) 9 | 10 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 11 | 12 | func (a *Adaptor) ValidateKey(key string) error { 13 | _, _, err := getAppIDAndToken(key) 14 | return err 15 | } 16 | 17 | func (a *Adaptor) KeyHelp() string { 18 | return "app_id|app_token" 19 | } 20 | 21 | // key格式: app_id|app_token 22 | func getAppIDAndToken(key string) (string, string, error) { 23 | parts := strings.Split(key, "|") 24 | if len(parts) != 2 { 25 | return "", "", errors.New("invalid key format") 26 | } 27 | return parts[0], parts[1], nil 28 | } 29 | -------------------------------------------------------------------------------- /core/relay/adaptor/gemini/config.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/labring/aiproxy/core/relay/adaptor" 7 | ) 8 | 9 | var _ adaptor.Config = (*Adaptor)(nil) 10 | 11 | type Config struct { 12 | Safety string `json:"safety"` 13 | } 14 | 15 | var ConfigTemplates = adaptor.ConfigTemplates{ 16 | "safety": { 17 | Name: "Safety", 18 | Description: "Safety settings: https://ai.google.dev/gemini-api/docs/safety-settings, default is BLOCK_NONE", 19 | Example: "BLOCK_NONE", 20 | Type: adaptor.ConfigTypeString, 21 | Validator: func(a any) error { 22 | s, ok := a.(string) 23 | if !ok { 24 | return fmt.Errorf("invalid safety settings type: %v, must be a string", a) 25 | } 26 | switch s { 27 | case "BLOCK_NONE", 28 | "BLOCK_ONLY_HIGH", 29 | "BLOCK_MEDIUM_AND_ABOVE", 30 | "BLOCK_LOW_AND_ABOVE", 31 | "HARM_BLOCK_THRESHOLD_UNSPECIFIED": 32 | return nil 33 | default: 34 | return fmt.Errorf( 35 | "invalid safety settings: %s, must be one of: BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE, HARM_BLOCK_THRESHOLD_UNSPECIFIED", 36 | s, 37 | ) 38 | } 39 | }, 40 | }, 41 | } 42 | 43 | func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates { 44 | return ConfigTemplates 45 | } 46 | -------------------------------------------------------------------------------- /core/relay/adaptor/gemini/fetures.go: -------------------------------------------------------------------------------- 1 | package gemini 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "https://ai.google.dev", 10 | "Chat、Embeddings、Image generation Support", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/relay/adaptor/geminiopenai/adaptor.go: -------------------------------------------------------------------------------- 1 | package geminiopenai 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor/gemini" 6 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 7 | ) 8 | 9 | type Adaptor struct { 10 | openai.Adaptor 11 | } 12 | 13 | const baseURL = "https://generativelanguage.googleapis.com/v1beta/openai" 14 | 15 | func (a *Adaptor) GetBaseURL() string { 16 | return baseURL 17 | } 18 | 19 | func (a *Adaptor) GetModelList() []model.ModelConfig { 20 | return gemini.ModelList 21 | } 22 | -------------------------------------------------------------------------------- /core/relay/adaptor/geminiopenai/fetures.go: -------------------------------------------------------------------------------- 1 | package geminiopenai 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "https://ai.google.dev/gemini-api/docs/openai", 10 | "OpenAI compatibility", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/relay/adaptor/groq/adaptor.go: -------------------------------------------------------------------------------- 1 | package groq 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 6 | ) 7 | 8 | type Adaptor struct { 9 | openai.Adaptor 10 | } 11 | 12 | const baseURL = "https://api.groq.com/openai/v1" 13 | 14 | func (a *Adaptor) GetBaseURL() string { 15 | return baseURL 16 | } 17 | 18 | func (a *Adaptor) GetModelList() []model.ModelConfig { 19 | return ModelList 20 | } 21 | -------------------------------------------------------------------------------- /core/relay/adaptor/jina/adaptor.go: -------------------------------------------------------------------------------- 1 | package jina 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | "github.com/labring/aiproxy/core/relay/mode" 12 | ) 13 | 14 | type Adaptor struct { 15 | openai.Adaptor 16 | } 17 | 18 | const baseURL = "https://api.jina.ai/v1" 19 | 20 | func (a *Adaptor) GetBaseURL() string { 21 | return baseURL 22 | } 23 | 24 | func (a *Adaptor) ConvertRequest( 25 | meta *meta.Meta, 26 | req *http.Request, 27 | ) (*adaptor.ConvertRequestResult, error) { 28 | switch meta.Mode { 29 | case mode.Embeddings: 30 | return ConvertEmbeddingsRequest(meta, req) 31 | default: 32 | return a.Adaptor.ConvertRequest(meta, req) 33 | } 34 | } 35 | 36 | func (a *Adaptor) DoResponse( 37 | meta *meta.Meta, 38 | c *gin.Context, 39 | resp *http.Response, 40 | ) (usage *model.Usage, err adaptor.Error) { 41 | switch meta.Mode { 42 | case mode.Rerank: 43 | return RerankHandler(meta, c, resp) 44 | default: 45 | return a.Adaptor.DoResponse(meta, c, resp) 46 | } 47 | } 48 | 49 | func (a *Adaptor) GetModelList() []model.ModelConfig { 50 | return ModelList 51 | } 52 | -------------------------------------------------------------------------------- /core/relay/adaptor/jina/constants.go: -------------------------------------------------------------------------------- 1 | package jina 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | var ModelList = []model.ModelConfig{ 9 | { 10 | Model: "jina-reranker-v2-base-multilingual", 11 | Type: mode.Rerank, 12 | Owner: model.ModelOwnerJina, 13 | Price: model.Price{ 14 | InputPrice: 0.06, 15 | }, 16 | RPM: 120, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /core/relay/adaptor/jina/embeddings.go: -------------------------------------------------------------------------------- 1 | package jina 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/bytedance/sonic" 8 | "github.com/labring/aiproxy/core/common" 9 | "github.com/labring/aiproxy/core/relay/adaptor" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | ) 12 | 13 | // 14 | //nolint:gocritic 15 | func ConvertEmbeddingsRequest( 16 | meta *meta.Meta, 17 | req *http.Request, 18 | ) (*adaptor.ConvertRequestResult, error) { 19 | reqMap := make(map[string]any) 20 | err := common.UnmarshalBodyReusable(req, &reqMap) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | reqMap["model"] = meta.ActualModel 26 | 27 | switch v := reqMap["input"].(type) { 28 | case string: 29 | reqMap["input"] = []string{v} 30 | } 31 | 32 | delete(reqMap, "encoding_format") 33 | 34 | jsonData, err := sonic.Marshal(reqMap) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return &adaptor.ConvertRequestResult{ 39 | Method: http.MethodPost, 40 | Header: nil, 41 | Body: bytes.NewReader(jsonData), 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /core/relay/adaptor/jina/error.go: -------------------------------------------------------------------------------- 1 | package jina 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/bytedance/sonic" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | relaymodel "github.com/labring/aiproxy/core/relay/model" 10 | ) 11 | 12 | type Detail struct { 13 | Loc []string `json:"loc"` 14 | Msg string `json:"msg"` 15 | Type string `json:"type"` 16 | } 17 | 18 | func ErrorHanlder(resp *http.Response) adaptor.Error { 19 | defer resp.Body.Close() 20 | 21 | body, err := io.ReadAll(resp.Body) 22 | if err != nil { 23 | return relaymodel.WrapperOpenAIError(err, "read_response_body_failed", resp.StatusCode) 24 | } 25 | 26 | detailValue, err := sonic.Get(body, "detail") 27 | if err != nil { 28 | return relaymodel.WrapperOpenAIError(err, "unmarshal_response_body_failed", resp.StatusCode) 29 | } 30 | 31 | errorMessage := "unknown error" 32 | errorType := relaymodel.ErrorTypeUpstream 33 | 34 | if detailStr, err := detailValue.String(); err == nil { 35 | errorMessage = detailStr 36 | } else { 37 | var details []Detail 38 | detailsData, _ := detailValue.Raw() 39 | if err := sonic.Unmarshal([]byte(detailsData), &details); err == nil && len(details) > 0 { 40 | errorMessage = details[0].Msg 41 | if details[0].Type != "" { 42 | errorType = details[0].Type 43 | } 44 | } 45 | } 46 | 47 | return relaymodel.NewOpenAIError(resp.StatusCode, relaymodel.OpenAIError{ 48 | Message: errorMessage, 49 | Type: errorType, 50 | Code: resp.StatusCode, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /core/relay/adaptor/jina/fetures.go: -------------------------------------------------------------------------------- 1 | package jina 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "https://jina.ai", 10 | "Embeddings、Rerank Support", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/relay/adaptor/lingyiwanwu/adaptor.go: -------------------------------------------------------------------------------- 1 | package lingyiwanwu 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor" 6 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 7 | ) 8 | 9 | type Adaptor struct { 10 | openai.Adaptor 11 | } 12 | 13 | const baseURL = "https://api.lingyiwanwu.com/v1" 14 | 15 | func (a *Adaptor) GetBaseURL() string { 16 | return baseURL 17 | } 18 | 19 | func (a *Adaptor) GetModelList() []model.ModelConfig { 20 | return ModelList 21 | } 22 | 23 | func (a *Adaptor) GetBalance(_ *model.Channel) (float64, error) { 24 | return 0, adaptor.ErrGetBalanceNotImplemented 25 | } 26 | -------------------------------------------------------------------------------- /core/relay/adaptor/lingyiwanwu/constants.go: -------------------------------------------------------------------------------- 1 | package lingyiwanwu 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | // https://platform.lingyiwanwu.com/docs 9 | 10 | var ModelList = []model.ModelConfig{ 11 | { 12 | Model: "yi-lightning", 13 | Type: mode.ChatCompletions, 14 | Owner: model.ModelOwnerLingyiWanwu, 15 | Price: model.Price{ 16 | InputPrice: 0.00099, 17 | OutputPrice: 0.00099, 18 | }, 19 | RPM: 60, 20 | Config: model.NewModelConfig( 21 | model.WithModelConfigMaxContextTokens(16384), 22 | model.WithModelConfigToolChoice(true), 23 | ), 24 | }, 25 | { 26 | Model: "yi-vision-v2", 27 | Type: mode.ChatCompletions, 28 | Owner: model.ModelOwnerLingyiWanwu, 29 | Price: model.Price{ 30 | InputPrice: 0.006, 31 | OutputPrice: 0.006, 32 | }, 33 | RPM: 60, 34 | Config: model.NewModelConfig( 35 | model.WithModelConfigMaxContextTokens(16384), 36 | model.WithModelConfigVision(true), 37 | model.WithModelConfigToolChoice(true), 38 | ), 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /core/relay/adaptor/minimax/fetures.go: -------------------------------------------------------------------------------- 1 | package minimax 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "Chat、Embeddings、TTS(need group id) Support", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/relay/adaptor/minimax/key.go: -------------------------------------------------------------------------------- 1 | package minimax 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | ) 9 | 10 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 11 | 12 | func (a *Adaptor) ValidateKey(key string) error { 13 | _, _, err := GetAPIKeyAndGroupID(key) 14 | if err != nil { 15 | return err 16 | } 17 | return nil 18 | } 19 | 20 | func (a *Adaptor) KeyHelp() string { 21 | return "api_key|group_id" 22 | } 23 | 24 | func GetAPIKeyAndGroupID(key string) (string, string, error) { 25 | keys := strings.Split(key, "|") 26 | if len(keys) != 2 { 27 | return "", "", errors.New("invalid key format") 28 | } 29 | return keys[0], keys[1], nil 30 | } 31 | -------------------------------------------------------------------------------- /core/relay/adaptor/mistral/adaptor.go: -------------------------------------------------------------------------------- 1 | package mistral 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 6 | ) 7 | 8 | type Adaptor struct { 9 | openai.Adaptor 10 | } 11 | 12 | const baseURL = "https://api.mistral.ai/v1" 13 | 14 | func (a *Adaptor) GetBaseURL() string { 15 | return baseURL 16 | } 17 | 18 | func (a *Adaptor) GetModelList() []model.ModelConfig { 19 | return ModelList 20 | } 21 | -------------------------------------------------------------------------------- /core/relay/adaptor/mistral/constants.go: -------------------------------------------------------------------------------- 1 | package mistral 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | var ModelList = []model.ModelConfig{ 9 | { 10 | Model: "open-mistral-7b", 11 | Type: mode.ChatCompletions, 12 | Owner: model.ModelOwnerMistral, 13 | }, 14 | { 15 | Model: "open-mixtral-8x7b", 16 | Type: mode.ChatCompletions, 17 | Owner: model.ModelOwnerMistral, 18 | }, 19 | { 20 | Model: "mistral-small-latest", 21 | Type: mode.ChatCompletions, 22 | Owner: model.ModelOwnerMistral, 23 | }, 24 | { 25 | Model: "mistral-medium-latest", 26 | Type: mode.ChatCompletions, 27 | Owner: model.ModelOwnerMistral, 28 | }, 29 | { 30 | Model: "mistral-large-latest", 31 | Type: mode.ChatCompletions, 32 | Owner: model.ModelOwnerMistral, 33 | }, 34 | { 35 | Model: "mistral-embed", 36 | Type: mode.Embeddings, 37 | Owner: model.ModelOwnerMistral, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /core/relay/adaptor/moonshot/adaptor.go: -------------------------------------------------------------------------------- 1 | package moonshot 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 6 | ) 7 | 8 | type Adaptor struct { 9 | openai.Adaptor 10 | } 11 | 12 | const baseURL = "https://api.moonshot.cn/v1" 13 | 14 | func (a *Adaptor) GetBaseURL() string { 15 | return baseURL 16 | } 17 | 18 | func (a *Adaptor) GetModelList() []model.ModelConfig { 19 | return ModelList 20 | } 21 | -------------------------------------------------------------------------------- /core/relay/adaptor/moonshot/balance.go: -------------------------------------------------------------------------------- 1 | package moonshot 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/bytedance/sonic" 9 | "github.com/labring/aiproxy/core/model" 10 | "github.com/labring/aiproxy/core/relay/adaptor" 11 | ) 12 | 13 | var _ adaptor.Balancer = (*Adaptor)(nil) 14 | 15 | func (a *Adaptor) GetBalance(channel *model.Channel) (float64, error) { 16 | u := channel.BaseURL 17 | if u == "" { 18 | u = baseURL 19 | } 20 | url := u + "/users/me/balance" 21 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 22 | if err != nil { 23 | return 0, err 24 | } 25 | req.Header.Set("Authorization", "Bearer "+channel.Key) 26 | resp, err := http.DefaultClient.Do(req) 27 | if err != nil { 28 | return 0, err 29 | } 30 | defer resp.Body.Close() 31 | 32 | var response BalanceResponse 33 | if err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&response); err != nil { 34 | return 0, err 35 | } 36 | 37 | if response.Error != nil { 38 | return 0, fmt.Errorf("type: %s, message: %s", response.Error.Type, response.Error.Message) 39 | } 40 | 41 | return response.Data.AvailableBalance, nil 42 | } 43 | 44 | type BalanceResponse struct { 45 | Error *BalanceError `json:"error"` 46 | Data BalanceData `json:"data"` 47 | } 48 | 49 | type BalanceData struct { 50 | AvailableBalance float64 `json:"available_balance"` 51 | } 52 | 53 | type BalanceError struct { 54 | Message string `json:"message"` 55 | Type string `json:"type"` 56 | } 57 | -------------------------------------------------------------------------------- /core/relay/adaptor/novita/adaptor.go: -------------------------------------------------------------------------------- 1 | package novita 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 6 | ) 7 | 8 | type Adaptor struct { 9 | openai.Adaptor 10 | } 11 | 12 | const baseURL = "https://api.novita.ai/v3/openai" 13 | 14 | func (a *Adaptor) GetBaseURL() string { 15 | return baseURL 16 | } 17 | 18 | func (a *Adaptor) GetModelList() []model.ModelConfig { 19 | return ModelList 20 | } 21 | -------------------------------------------------------------------------------- /core/relay/adaptor/novita/constants.go: -------------------------------------------------------------------------------- 1 | package novita 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | // https://novita.ai/llm-api 9 | 10 | var ModelList = []model.ModelConfig{ 11 | { 12 | Model: "meta-llama/llama-3-8b-instruct", 13 | Type: mode.ChatCompletions, 14 | Owner: model.ModelOwnerMeta, 15 | }, 16 | { 17 | Model: "meta-llama/llama-3-70b-instruct", 18 | Type: mode.ChatCompletions, 19 | Owner: model.ModelOwnerMeta, 20 | }, 21 | { 22 | Model: "nousresearch/hermes-2-pro-llama-3-8b", 23 | Type: mode.ChatCompletions, 24 | Owner: model.ModelOwnerMeta, 25 | }, 26 | { 27 | Model: "nousresearch/nous-hermes-llama2-13b", 28 | Type: mode.ChatCompletions, 29 | Owner: model.ModelOwnerMeta, 30 | }, 31 | { 32 | Model: "mistralai/mistral-7b-instruct", 33 | Type: mode.ChatCompletions, 34 | Owner: model.ModelOwnerMistral, 35 | }, 36 | { 37 | Model: "teknium/openhermes-2.5-mistral-7b", 38 | Type: mode.ChatCompletions, 39 | Owner: model.ModelOwnerMistral, 40 | }, 41 | { 42 | Model: "microsoft/wizardlm-2-8x22b", 43 | Type: mode.ChatCompletions, 44 | Owner: model.ModelOwnerMicrosoft, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /core/relay/adaptor/ollama/constants.go: -------------------------------------------------------------------------------- 1 | package ollama 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | var ModelList = []model.ModelConfig{ 9 | { 10 | Model: "codellama:7b-instruct", 11 | Type: mode.ChatCompletions, 12 | Owner: model.ModelOwnerMeta, 13 | }, 14 | { 15 | Model: "llama2:7b", 16 | Type: mode.ChatCompletions, 17 | Owner: model.ModelOwnerMeta, 18 | }, 19 | { 20 | Model: "llama2:latest", 21 | Type: mode.ChatCompletions, 22 | Owner: model.ModelOwnerMeta, 23 | }, 24 | { 25 | Model: "llama3:latest", 26 | Type: mode.ChatCompletions, 27 | Owner: model.ModelOwnerMeta, 28 | }, 29 | { 30 | Model: "phi3:latest", 31 | Type: mode.ChatCompletions, 32 | Owner: model.ModelOwnerMicrosoft, 33 | }, 34 | { 35 | Model: "qwen:0.5b-chat", 36 | Type: mode.ChatCompletions, 37 | Owner: model.ModelOwnerAlibaba, 38 | }, 39 | { 40 | Model: "qwen:7b", 41 | Type: mode.ChatCompletions, 42 | Owner: model.ModelOwnerAlibaba, 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /core/relay/adaptor/ollama/error.go: -------------------------------------------------------------------------------- 1 | package ollama 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/bytedance/sonic" 8 | "github.com/labring/aiproxy/core/common/conv" 9 | "github.com/labring/aiproxy/core/relay/adaptor" 10 | relaymodel "github.com/labring/aiproxy/core/relay/model" 11 | ) 12 | 13 | type errorResponse struct { 14 | Error string `json:"error"` 15 | } 16 | 17 | func ErrorHandler(resp *http.Response) adaptor.Error { 18 | defer resp.Body.Close() 19 | 20 | data, err := io.ReadAll(resp.Body) 21 | if err != nil { 22 | return relaymodel.WrapperOpenAIErrorWithMessage( 23 | "read response body error: "+err.Error(), 24 | nil, 25 | http.StatusInternalServerError, 26 | ) 27 | } 28 | 29 | var er errorResponse 30 | err = sonic.Unmarshal(data, &er) 31 | if err != nil { 32 | return relaymodel.WrapperOpenAIErrorWithMessage( 33 | conv.BytesToString(data), 34 | nil, 35 | http.StatusInternalServerError, 36 | ) 37 | } 38 | return relaymodel.WrapperOpenAIErrorWithMessage(er.Error, nil, resp.StatusCode) 39 | } 40 | -------------------------------------------------------------------------------- /core/relay/adaptor/ollama/fetures.go: -------------------------------------------------------------------------------- 1 | package ollama 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "Chat、Embeddings Support", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/relay/adaptor/openai/config.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /core/relay/adaptor/openai/embeddings.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/bytedance/sonic" 8 | "github.com/labring/aiproxy/core/common" 9 | "github.com/labring/aiproxy/core/relay/adaptor" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | ) 12 | 13 | // 14 | //nolint:gocritic 15 | func ConvertEmbeddingsRequest( 16 | meta *meta.Meta, 17 | req *http.Request, 18 | inputToSlices bool, 19 | ) (*adaptor.ConvertRequestResult, error) { 20 | reqMap := make(map[string]any) 21 | err := common.UnmarshalBodyReusable(req, &reqMap) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | reqMap["model"] = meta.ActualModel 27 | 28 | if inputToSlices { 29 | switch v := reqMap["input"].(type) { 30 | case string: 31 | reqMap["input"] = []string{v} 32 | } 33 | } 34 | 35 | jsonData, err := sonic.Marshal(reqMap) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &adaptor.ConvertRequestResult{ 40 | Method: http.MethodPost, 41 | Header: nil, 42 | Body: bytes.NewReader(jsonData), 43 | }, nil 44 | } 45 | -------------------------------------------------------------------------------- /core/relay/adaptor/openai/fetures.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "OpenAI compatibility", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/relay/adaptor/openai/helper.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/relay/model" 8 | ) 9 | 10 | func ResponseText2Usage(responseText, modeName string, promptTokens int64) *model.Usage { 11 | usage := &model.Usage{ 12 | PromptTokens: promptTokens, 13 | CompletionTokens: CountTokenText(responseText, modeName), 14 | } 15 | usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens 16 | return usage 17 | } 18 | 19 | func GetFullRequestURL(baseURL, requestURL string) string { 20 | fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) 21 | 22 | if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") { 23 | fullRequestURL = fmt.Sprintf( 24 | "%s%s", 25 | baseURL, 26 | strings.TrimPrefix(requestURL, "/openai/deployments"), 27 | ) 28 | } 29 | return fullRequestURL 30 | } 31 | -------------------------------------------------------------------------------- /core/relay/adaptor/openai/id.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import "github.com/labring/aiproxy/core/common" 4 | 5 | func ChatCompletionID() string { 6 | return "chatcmpl-" + common.ShortUUID() 7 | } 8 | 9 | func CallID() string { 10 | return "call_" + common.ShortUUID() 11 | } 12 | -------------------------------------------------------------------------------- /core/relay/adaptor/openrouter/fetures.go: -------------------------------------------------------------------------------- 1 | package openrouter 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "The `reasoning` field is converted to `reasoning_content`", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/relay/adaptor/siliconflow/adaptor.go: -------------------------------------------------------------------------------- 1 | package siliconflow 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | "github.com/labring/aiproxy/core/relay/mode" 12 | ) 13 | 14 | var _ adaptor.Adaptor = (*Adaptor)(nil) 15 | 16 | type Adaptor struct { 17 | openai.Adaptor 18 | } 19 | 20 | const baseURL = "https://api.siliconflow.cn/v1" 21 | 22 | func (a *Adaptor) GetBaseURL() string { 23 | return baseURL 24 | } 25 | 26 | func (a *Adaptor) GetModelList() []model.ModelConfig { 27 | return ModelList 28 | } 29 | 30 | // 31 | //nolint:gocritic 32 | func (a *Adaptor) DoResponse( 33 | meta *meta.Meta, 34 | c *gin.Context, 35 | resp *http.Response, 36 | ) (usage *model.Usage, err adaptor.Error) { 37 | usage, err = a.Adaptor.DoResponse(meta, c, resp) 38 | if err != nil { 39 | return nil, err 40 | } 41 | switch meta.Mode { 42 | case mode.AudioSpeech: 43 | size := c.Writer.Size() 44 | usage = &model.Usage{ 45 | OutputTokens: model.ZeroNullInt64(size), 46 | TotalTokens: model.ZeroNullInt64(size), 47 | } 48 | } 49 | return usage, nil 50 | } 51 | -------------------------------------------------------------------------------- /core/relay/adaptor/siliconflow/image.go: -------------------------------------------------------------------------------- 1 | package siliconflow 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/bytedance/sonic" 9 | "github.com/labring/aiproxy/core/common" 10 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 11 | "github.com/labring/aiproxy/core/relay/meta" 12 | ) 13 | 14 | type ImageRequest struct { 15 | Model string `json:"model"` 16 | Prompt string `json:"prompt"` 17 | NegativePrompt string `json:"negative_prompt"` 18 | ImageSize string `json:"image_size"` 19 | BatchSize int `json:"batch_size"` 20 | Seed int64 `json:"seed"` 21 | NumInferenceSteps int `json:"num_inference_steps"` 22 | GuidanceScale int `json:"guidance_scale"` 23 | PromptEnhancement bool `json:"prompt_enhancement"` 24 | } 25 | 26 | func ConvertImageRequest(meta *meta.Meta, request *http.Request) (http.Header, io.Reader, error) { 27 | var reqMap map[string]any 28 | err := common.UnmarshalBodyReusable(request, &reqMap) 29 | if err != nil { 30 | return nil, nil, err 31 | } 32 | 33 | meta.Set(openai.MetaResponseFormat, reqMap["response_format"]) 34 | 35 | reqMap["model"] = meta.ActualModel 36 | reqMap["batch_size"] = reqMap["n"] 37 | delete(reqMap, "n") 38 | if _, ok := reqMap["steps"]; ok { 39 | reqMap["num_inference_steps"] = reqMap["steps"] 40 | delete(reqMap, "steps") 41 | } 42 | if _, ok := reqMap["scale"]; ok { 43 | reqMap["guidance_scale"] = reqMap["scale"] 44 | delete(reqMap, "scale") 45 | } 46 | reqMap["image_size"] = reqMap["size"] 47 | delete(reqMap, "size") 48 | 49 | data, err := sonic.Marshal(&reqMap) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | return http.Header{}, bytes.NewReader(data), nil 54 | } 55 | -------------------------------------------------------------------------------- /core/relay/adaptor/stepfun/adaptor.go: -------------------------------------------------------------------------------- 1 | package stepfun 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labring/aiproxy/core/model" 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 9 | "github.com/labring/aiproxy/core/relay/meta" 10 | "github.com/labring/aiproxy/core/relay/mode" 11 | ) 12 | 13 | type Adaptor struct { 14 | openai.Adaptor 15 | } 16 | 17 | const baseURL = "https://api.stepfun.com/v1" 18 | 19 | func (a *Adaptor) GetBaseURL() string { 20 | return baseURL 21 | } 22 | 23 | func (a *Adaptor) ConvertRequest( 24 | meta *meta.Meta, 25 | req *http.Request, 26 | ) (*adaptor.ConvertRequestResult, error) { 27 | switch meta.Mode { 28 | case mode.AudioSpeech: 29 | return openai.ConvertTTSRequest(meta, req, "cixingnansheng") 30 | default: 31 | return a.Adaptor.ConvertRequest(meta, req) 32 | } 33 | } 34 | 35 | func (a *Adaptor) GetModelList() []model.ModelConfig { 36 | return ModelList 37 | } 38 | 39 | func (a *Adaptor) GetBalance(_ *model.Channel) (float64, error) { 40 | return 0, adaptor.ErrGetBalanceNotImplemented 41 | } 42 | -------------------------------------------------------------------------------- /core/relay/adaptor/tencent/adaptor.go: -------------------------------------------------------------------------------- 1 | package tencent 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/adaptor" 6 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 7 | ) 8 | 9 | // https://cloud.tencent.com/document/api/1729/101837 10 | 11 | type Adaptor struct { 12 | openai.Adaptor 13 | } 14 | 15 | const baseURL = "https://api.hunyuan.cloud.tencent.com/v1" 16 | 17 | func (a *Adaptor) GetBaseURL() string { 18 | return baseURL 19 | } 20 | 21 | func (a *Adaptor) GetModelList() []model.ModelConfig { 22 | return ModelList 23 | } 24 | 25 | func (a *Adaptor) GetBalance(_ *model.Channel) (float64, error) { 26 | return 0, adaptor.ErrGetBalanceNotImplemented 27 | } 28 | -------------------------------------------------------------------------------- /core/relay/adaptor/text-embeddings-inference/constants.go: -------------------------------------------------------------------------------- 1 | package textembeddingsinference 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | // maybe we should use a list of models from 9 | // https://github.com/huggingface/text-embeddings-inference?tab=readme-ov-file#supported-models 10 | var ModelList = []model.ModelConfig{ 11 | { 12 | Model: "bge-reranker-v2-m3", 13 | Type: mode.Rerank, 14 | Owner: model.ModelOwnerBAAI, 15 | Price: model.Price{ 16 | InputPrice: 0.015, 17 | OutputPrice: 0.015, 18 | }, 19 | Config: model.NewModelConfig( 20 | model.WithModelConfigMaxContextTokens(32768), 21 | ), 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /core/relay/adaptor/text-embeddings-inference/embeddings.go: -------------------------------------------------------------------------------- 1 | package textembeddingsinference 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | ) 12 | 13 | func EmbeddingsHandler( 14 | meta *meta.Meta, 15 | c *gin.Context, 16 | resp *http.Response, 17 | ) (*model.Usage, adaptor.Error) { 18 | if resp.StatusCode != http.StatusOK { 19 | return nil, EmbeddingsErrorHanlder(resp) 20 | } 21 | return openai.DoResponse(meta, c, resp) 22 | } 23 | -------------------------------------------------------------------------------- /core/relay/adaptor/text-embeddings-inference/error.go: -------------------------------------------------------------------------------- 1 | package textembeddingsinference 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/bytedance/sonic" 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | relaymodel "github.com/labring/aiproxy/core/relay/model" 9 | ) 10 | 11 | type RerankErrorResponse struct { 12 | Error string `json:"error"` 13 | ErrorType string `json:"error_type"` 14 | } 15 | 16 | func RerankErrorHanlder(resp *http.Response) adaptor.Error { 17 | defer resp.Body.Close() 18 | 19 | errResp := RerankErrorResponse{} 20 | err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&errResp) 21 | if err != nil { 22 | return relaymodel.WrapperOpenAIError( 23 | err, 24 | "read_response_body_failed", 25 | http.StatusInternalServerError, 26 | ) 27 | } 28 | 29 | return relaymodel.WrapperOpenAIErrorWithMessage( 30 | errResp.Error, 31 | errResp.ErrorType, 32 | resp.StatusCode, 33 | ) 34 | } 35 | 36 | type EmbeddingsErrorResponse struct { 37 | Type string `json:"type"` 38 | Message string `json:"message"` 39 | } 40 | 41 | func EmbeddingsErrorHanlder(resp *http.Response) adaptor.Error { 42 | defer resp.Body.Close() 43 | 44 | errResp := EmbeddingsErrorResponse{} 45 | err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&errResp) 46 | if err != nil { 47 | return relaymodel.WrapperOpenAIError( 48 | err, 49 | "read_response_body_failed", 50 | http.StatusInternalServerError, 51 | ) 52 | } 53 | 54 | return relaymodel.WrapperOpenAIErrorWithMessage(errResp.Message, errResp.Type, resp.StatusCode) 55 | } 56 | -------------------------------------------------------------------------------- /core/relay/adaptor/text-embeddings-inference/fetures.go: -------------------------------------------------------------------------------- 1 | package textembeddingsinference 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "https://github.com/huggingface/text-embeddings-inference", 10 | "Embeddings、Rerank Support", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/relay/adaptor/vertexai/claude/constants.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/model" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | var ModelList = []model.ModelConfig{ 9 | { 10 | Model: "claude-3-haiku@20240307", 11 | Type: mode.ChatCompletions, 12 | Owner: model.ModelOwnerAnthropic, 13 | }, 14 | { 15 | Model: "claude-3-sonnet@20240229", 16 | Type: mode.ChatCompletions, 17 | Owner: model.ModelOwnerAnthropic, 18 | }, 19 | { 20 | Model: "claude-3-opus@20240229", 21 | Type: mode.ChatCompletions, 22 | Owner: model.ModelOwnerAnthropic, 23 | }, 24 | { 25 | Model: "claude-3-5-sonnet@20240620", 26 | Type: mode.ChatCompletions, 27 | Owner: model.ModelOwnerAnthropic, 28 | }, 29 | { 30 | Model: "claude-3-5-sonnet-v2@20241022", 31 | Type: mode.ChatCompletions, 32 | Owner: model.ModelOwnerAnthropic, 33 | }, 34 | { 35 | Model: "claude-3-5-haiku@20241022", 36 | Type: mode.ChatCompletions, 37 | Owner: model.ModelOwnerAnthropic, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /core/relay/adaptor/vertexai/claude/model.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor/anthropic" 4 | 5 | type Request struct { 6 | AnthropicVersion string `json:"anthropic_version"` 7 | *anthropic.Request 8 | } 9 | -------------------------------------------------------------------------------- /core/relay/adaptor/vertexai/config.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/relay/adaptor" 5 | "github.com/labring/aiproxy/core/relay/adaptor/gemini" 6 | ) 7 | 8 | func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates { 9 | return gemini.ConfigTemplates 10 | } 11 | -------------------------------------------------------------------------------- /core/relay/adaptor/vertexai/fetures.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | var _ adaptor.Features = (*Adaptor)(nil) 6 | 7 | func (a *Adaptor) Features() []string { 8 | return []string{ 9 | "Claude support native Endpoint: /v1/messages", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/relay/adaptor/vertexai/gemini/adapter.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/adaptor/gemini" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | "github.com/labring/aiproxy/core/relay/mode" 12 | "github.com/labring/aiproxy/core/relay/utils" 13 | ) 14 | 15 | type Adaptor struct{} 16 | 17 | func (a *Adaptor) ConvertRequest( 18 | meta *meta.Meta, 19 | request *http.Request, 20 | ) (*adaptor.ConvertRequestResult, error) { 21 | return gemini.ConvertRequest(meta, request) 22 | } 23 | 24 | func (a *Adaptor) DoResponse( 25 | meta *meta.Meta, 26 | c *gin.Context, 27 | resp *http.Response, 28 | ) (usage *model.Usage, err adaptor.Error) { 29 | switch meta.Mode { 30 | case mode.Embeddings: 31 | usage, err = gemini.EmbeddingHandler(meta, c, resp) 32 | default: 33 | if utils.IsStreamResponse(resp) { 34 | usage, err = gemini.StreamHandler(meta, c, resp) 35 | } else { 36 | usage, err = gemini.Handler(meta, c, resp) 37 | } 38 | } 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /core/relay/adaptor/vertexai/key.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/bytedance/sonic" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | ) 10 | 11 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 12 | 13 | func (a *Adaptor) ValidateKey(key string) error { 14 | _, err := getConfigFromKey(key) 15 | if err != nil { 16 | return err 17 | } 18 | return nil 19 | } 20 | 21 | func (a *Adaptor) KeyHelp() string { 22 | return "region|adcJSON" 23 | } 24 | 25 | // region|adcJSON 26 | func getConfigFromKey(key string) (Config, error) { 27 | region, adcJSON, ok := strings.Cut(key, "|") 28 | if !ok { 29 | return Config{}, errors.New("invalid key format") 30 | } 31 | node, err := sonic.GetFromString(adcJSON, "project_id") 32 | if err != nil { 33 | return Config{}, err 34 | } 35 | projectID, err := node.String() 36 | if err != nil { 37 | return Config{}, err 38 | } 39 | return Config{ 40 | Region: region, 41 | ProjectID: projectID, 42 | ADCJSON: adcJSON, 43 | }, nil 44 | } 45 | -------------------------------------------------------------------------------- /core/relay/adaptor/vertexai/registry.go: -------------------------------------------------------------------------------- 1 | package vertexai 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/labring/aiproxy/core/model" 9 | "github.com/labring/aiproxy/core/relay/adaptor" 10 | "github.com/labring/aiproxy/core/relay/adaptor/gemini" 11 | vertexclaude "github.com/labring/aiproxy/core/relay/adaptor/vertexai/claude" 12 | vertexgemini "github.com/labring/aiproxy/core/relay/adaptor/vertexai/gemini" 13 | "github.com/labring/aiproxy/core/relay/meta" 14 | ) 15 | 16 | type ModelType int 17 | 18 | const ( 19 | VerterAIClaude ModelType = iota + 1 20 | VerterAIGemini 21 | ) 22 | 23 | var modelList = []model.ModelConfig{} 24 | 25 | func init() { 26 | modelList = append(modelList, vertexclaude.ModelList...) 27 | 28 | modelList = append(modelList, gemini.ModelList...) 29 | } 30 | 31 | type innerAIAdapter interface { 32 | ConvertRequest(meta *meta.Meta, request *http.Request) (*adaptor.ConvertRequestResult, error) 33 | DoResponse( 34 | meta *meta.Meta, 35 | c *gin.Context, 36 | resp *http.Response, 37 | ) (usage *model.Usage, err adaptor.Error) 38 | } 39 | 40 | func GetAdaptor(model string) innerAIAdapter { 41 | switch { 42 | case strings.Contains(model, "claude"): 43 | return &vertexclaude.Adaptor{} 44 | case strings.Contains(model, "gemini"): 45 | return &vertexgemini.Adaptor{} 46 | default: 47 | return nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/relay/adaptor/xai/adaptor.go: -------------------------------------------------------------------------------- 1 | package xai 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | ) 12 | 13 | type Adaptor struct { 14 | openai.Adaptor 15 | } 16 | 17 | const baseURL = "https://api.x.ai/v1" 18 | 19 | func (a *Adaptor) GetBaseURL() string { 20 | return baseURL 21 | } 22 | 23 | func (a *Adaptor) DoResponse( 24 | meta *meta.Meta, 25 | c *gin.Context, 26 | resp *http.Response, 27 | ) (usage *model.Usage, err adaptor.Error) { 28 | if resp.StatusCode != http.StatusOK { 29 | return nil, ErrorHandler(resp) 30 | } 31 | 32 | return a.Adaptor.DoResponse(meta, c, resp) 33 | } 34 | 35 | func (a *Adaptor) GetModelList() []model.ModelConfig { 36 | return ModelList 37 | } 38 | -------------------------------------------------------------------------------- /core/relay/adaptor/xai/error.go: -------------------------------------------------------------------------------- 1 | package xai 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/bytedance/sonic" 9 | "github.com/labring/aiproxy/core/common/conv" 10 | "github.com/labring/aiproxy/core/relay/adaptor" 11 | relaymodel "github.com/labring/aiproxy/core/relay/model" 12 | ) 13 | 14 | type errorResponse struct { 15 | Error string `json:"error"` 16 | Code string `json:"code"` 17 | } 18 | 19 | func ErrorHandler(resp *http.Response) adaptor.Error { 20 | defer resp.Body.Close() 21 | 22 | data, err := io.ReadAll(resp.Body) 23 | if err != nil { 24 | return relaymodel.WrapperOpenAIErrorWithMessage( 25 | "read response body error: "+err.Error(), 26 | nil, 27 | http.StatusInternalServerError, 28 | ) 29 | } 30 | 31 | var er errorResponse 32 | err = sonic.Unmarshal(data, &er) 33 | if err != nil { 34 | return relaymodel.WrapperOpenAIErrorWithMessage( 35 | conv.BytesToString(data), 36 | nil, 37 | http.StatusInternalServerError, 38 | ) 39 | } 40 | 41 | statusCode := resp.StatusCode 42 | 43 | if strings.Contains(er.Error, "Incorrect API key provided") { 44 | statusCode = http.StatusUnauthorized 45 | } 46 | 47 | return relaymodel.WrapperOpenAIErrorWithMessage(er.Error, er.Code, statusCode) 48 | } 49 | -------------------------------------------------------------------------------- /core/relay/adaptor/xunfei/adaptor.go: -------------------------------------------------------------------------------- 1 | package xunfei 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labring/aiproxy/core/model" 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 9 | "github.com/labring/aiproxy/core/relay/meta" 10 | ) 11 | 12 | type Adaptor struct { 13 | openai.Adaptor 14 | } 15 | 16 | func (a *Adaptor) GetBaseURL() string { 17 | return baseURL 18 | } 19 | 20 | const baseURL = "https://spark-api-open.xf-yun.com/v1" 21 | 22 | func (a *Adaptor) ConvertRequest( 23 | meta *meta.Meta, 24 | req *http.Request, 25 | ) (*adaptor.ConvertRequestResult, error) { 26 | domain := getXunfeiDomain(meta.ActualModel) 27 | model := meta.ActualModel 28 | meta.ActualModel = domain 29 | defer func() { 30 | meta.ActualModel = model 31 | }() 32 | return a.Adaptor.ConvertRequest(meta, req) 33 | } 34 | 35 | func (a *Adaptor) GetModelList() []model.ModelConfig { 36 | return ModelList 37 | } 38 | 39 | func (a *Adaptor) GetBalance(_ *model.Channel) (float64, error) { 40 | return 0, adaptor.ErrGetBalanceNotImplemented 41 | } 42 | -------------------------------------------------------------------------------- /core/relay/adaptor/xunfei/key.go: -------------------------------------------------------------------------------- 1 | package xunfei 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/labring/aiproxy/core/relay/adaptor" 8 | ) 9 | 10 | var _ adaptor.KeyValidator = (*Adaptor)(nil) 11 | 12 | func (a *Adaptor) ValidateKey(key string) error { 13 | if strings.Contains(key, ":") { 14 | return nil 15 | } 16 | return errors.New("invalid key format") 17 | } 18 | 19 | func (a *Adaptor) KeyHelp() string { 20 | return "xxx:xxx" 21 | } 22 | -------------------------------------------------------------------------------- /core/relay/adaptor/xunfei/main.go: -------------------------------------------------------------------------------- 1 | package xunfei 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // https://console.xfyun.cn/services/cbm 8 | // https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html 9 | 10 | func getXunfeiDomain(modelName string) string { 11 | _, s, _ := strings.Cut(modelName, "-") 12 | switch strings.ToLower(s) { 13 | case "lite": 14 | return "lite" 15 | case "pro": 16 | return "generalv3" 17 | case "pro-128k": 18 | return "pro-128k" 19 | case "max": 20 | return "generalv3.5" 21 | case "max-32k": 22 | return "max-32k" 23 | case "4.0-ultra": 24 | return "4.0Ultra" 25 | default: 26 | return modelName 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/relay/adaptor/zhipu/adaptor.go: -------------------------------------------------------------------------------- 1 | package zhipu 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 10 | "github.com/labring/aiproxy/core/relay/meta" 11 | "github.com/labring/aiproxy/core/relay/mode" 12 | ) 13 | 14 | type Adaptor struct { 15 | openai.Adaptor 16 | } 17 | 18 | const baseURL = "https://open.bigmodel.cn/api/paas/v4" 19 | 20 | func (a *Adaptor) GetBaseURL() string { 21 | return baseURL 22 | } 23 | 24 | func (a *Adaptor) DoResponse( 25 | meta *meta.Meta, 26 | c *gin.Context, 27 | resp *http.Response, 28 | ) (usage *model.Usage, err adaptor.Error) { 29 | switch meta.Mode { 30 | case mode.Embeddings: 31 | usage, err = EmbeddingsHandler(c, resp) 32 | default: 33 | usage, err = openai.DoResponse(meta, c, resp) 34 | } 35 | return 36 | } 37 | 38 | func (a *Adaptor) GetModelList() []model.ModelConfig { 39 | return ModelList 40 | } 41 | 42 | func (a *Adaptor) GetBalance(_ *model.Channel) (float64, error) { 43 | return 0, adaptor.ErrGetBalanceNotImplemented 44 | } 45 | -------------------------------------------------------------------------------- /core/relay/adaptor/zhipu/model.go: -------------------------------------------------------------------------------- 1 | package zhipu 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/relay/model" 5 | ) 6 | 7 | type Request struct { 8 | Temperature *float64 `json:"temperature,omitempty"` 9 | TopP *float64 `json:"top_p,omitempty"` 10 | RequestID string `json:"request_id,omitempty"` 11 | Prompt []*model.Message `json:"prompt"` 12 | Incremental bool `json:"incremental,omitempty"` 13 | } 14 | 15 | type EmbeddingRequest struct { 16 | Input any `json:"input"` 17 | Model string `json:"model"` 18 | } 19 | 20 | type EmbeddingResponse struct { 21 | Model string `json:"model"` 22 | Object string `json:"object"` 23 | Embeddings []EmbeddingData `json:"data"` 24 | model.Usage `json:"usage"` 25 | } 26 | 27 | type EmbeddingData struct { 28 | Object string `json:"object"` 29 | Embedding []float64 `json:"embedding"` 30 | Index int `json:"index"` 31 | } 32 | 33 | type ImageRequest struct { 34 | Model string `json:"model"` 35 | Prompt string `json:"prompt"` 36 | UserID string `json:"user_id,omitempty"` 37 | } 38 | -------------------------------------------------------------------------------- /core/relay/controller/anthropic.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labring/aiproxy/core/model" 6 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 7 | "github.com/labring/aiproxy/core/relay/utils" 8 | ) 9 | 10 | func GetAnthropicRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) { 11 | return mc.Price, nil 12 | } 13 | 14 | func GetAnthropicRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) { 15 | textRequest, err := utils.UnmarshalAnthropicMessageRequest(c.Request) 16 | if err != nil { 17 | return model.Usage{}, err 18 | } 19 | 20 | return model.Usage{ 21 | InputTokens: model.ZeroNullInt64(openai.CountTokenMessages( 22 | textRequest.Messages, 23 | textRequest.Model, 24 | )), 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /core/relay/controller/chat.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labring/aiproxy/core/model" 6 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 7 | "github.com/labring/aiproxy/core/relay/utils" 8 | ) 9 | 10 | func GetChatRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) { 11 | return mc.Price, nil 12 | } 13 | 14 | func GetChatRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) { 15 | textRequest, err := utils.UnmarshalGeneralOpenAIRequest(c.Request) 16 | if err != nil { 17 | return model.Usage{}, err 18 | } 19 | 20 | return model.Usage{ 21 | InputTokens: model.ZeroNullInt64(openai.CountTokenMessages( 22 | textRequest.Messages, 23 | textRequest.Model, 24 | )), 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /core/relay/controller/completions.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labring/aiproxy/core/model" 6 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 7 | "github.com/labring/aiproxy/core/relay/utils" 8 | ) 9 | 10 | func GetCompletionsRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) { 11 | return mc.Price, nil 12 | } 13 | 14 | func GetCompletionsRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) { 15 | textRequest, err := utils.UnmarshalGeneralOpenAIRequest(c.Request) 16 | if err != nil { 17 | return model.Usage{}, err 18 | } 19 | 20 | return model.Usage{ 21 | InputTokens: model.ZeroNullInt64(openai.CountTokenInput( 22 | textRequest.Prompt, 23 | textRequest.Model, 24 | )), 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /core/relay/controller/edits.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/labring/aiproxy/core/model" 9 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 10 | ) 11 | 12 | func GetImagesEditsRequestPrice(c *gin.Context, mc model.ModelConfig) (model.Price, error) { 13 | size := c.PostForm("size") 14 | quality := c.PostForm("quality") 15 | 16 | imageCostPrice, ok := GetImagesOutputPrice(mc, size, quality) 17 | if !ok { 18 | return model.Price{}, fmt.Errorf("invalid image size `%s` or quality `%s`", size, quality) 19 | } 20 | 21 | return model.Price{ 22 | PerRequestPrice: mc.Price.PerRequestPrice, 23 | InputPrice: mc.Price.InputPrice, 24 | InputPriceUnit: mc.Price.InputPriceUnit, 25 | ImageInputPrice: mc.Price.ImageInputPrice, 26 | ImageInputPriceUnit: mc.Price.ImageInputPriceUnit, 27 | OutputPrice: model.ZeroNullFloat64(imageCostPrice), 28 | OutputPriceUnit: mc.Price.OutputPriceUnit, 29 | }, nil 30 | } 31 | 32 | func GetImagesEditsRequestUsage(c *gin.Context, mc model.ModelConfig) (model.Usage, error) { 33 | mutliForms, err := c.MultipartForm() 34 | if err != nil { 35 | return model.Usage{}, err 36 | } 37 | images := int64(len(mutliForms.File["image"])) 38 | 39 | prompt := c.PostForm("prompt") 40 | nStr := c.PostForm("n") 41 | n := 1 42 | if nStr != "" { 43 | n, err = strconv.Atoi(nStr) 44 | if err != nil { 45 | return model.Usage{}, err 46 | } 47 | } 48 | 49 | return model.Usage{ 50 | InputTokens: model.ZeroNullInt64(openai.CountTokenInput( 51 | prompt, 52 | mc.Model, 53 | )), 54 | ImageInputTokens: model.ZeroNullInt64(images), 55 | OutputTokens: model.ZeroNullInt64(n), 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /core/relay/controller/embed.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labring/aiproxy/core/model" 6 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 7 | "github.com/labring/aiproxy/core/relay/utils" 8 | ) 9 | 10 | func GetEmbedRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) { 11 | return mc.Price, nil 12 | } 13 | 14 | func GetEmbedRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) { 15 | textRequest, err := utils.UnmarshalGeneralOpenAIRequest(c.Request) 16 | if err != nil { 17 | return model.Usage{}, err 18 | } 19 | 20 | return model.Usage{ 21 | InputTokens: model.ZeroNullInt64(openai.CountTokenInput( 22 | textRequest.Input, 23 | textRequest.Model, 24 | )), 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /core/relay/controller/handle.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labring/aiproxy/core/common/config" 6 | "github.com/labring/aiproxy/core/middleware" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/meta" 10 | ) 11 | 12 | // HandleResult contains all the information needed for consumption recording 13 | type HandleResult struct { 14 | Error adaptor.Error 15 | Usage model.Usage 16 | Detail *RequestDetail 17 | } 18 | 19 | func Handle(adaptor adaptor.Adaptor, c *gin.Context, meta *meta.Meta) *HandleResult { 20 | log := middleware.GetLogger(c) 21 | 22 | usage, detail, respErr := DoHelper(adaptor, c, meta) 23 | if respErr != nil { 24 | var logDetail *RequestDetail 25 | if detail != nil && config.DebugEnabled { 26 | logDetail = detail 27 | log.Errorf( 28 | "handle failed: %+v\nrequest detail:\n%s\nresponse detail:\n%s", 29 | respErr, 30 | logDetail.RequestBody, 31 | logDetail.ResponseBody, 32 | ) 33 | } else { 34 | log.Errorf("handle failed: %+v", respErr) 35 | } 36 | 37 | return &HandleResult{ 38 | Error: respErr, 39 | Usage: usage, 40 | Detail: detail, 41 | } 42 | } 43 | 44 | return &HandleResult{ 45 | Usage: usage, 46 | Detail: detail, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/relay/controller/pdf.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labring/aiproxy/core/model" 6 | ) 7 | 8 | func GetPdfRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) { 9 | return mc.Price, nil 10 | } 11 | 12 | func GetPdfRequestUsage(_ *gin.Context, _ model.ModelConfig) (model.Usage, error) { 13 | return model.Usage{}, nil 14 | } 15 | -------------------------------------------------------------------------------- /core/relay/controller/rerank.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor/openai" 9 | relaymodel "github.com/labring/aiproxy/core/relay/model" 10 | "github.com/labring/aiproxy/core/relay/utils" 11 | ) 12 | 13 | func getRerankRequest(c *gin.Context) (*relaymodel.RerankRequest, error) { 14 | rerankRequest, err := utils.UnmarshalRerankRequest(c.Request) 15 | if err != nil { 16 | return nil, err 17 | } 18 | if rerankRequest.Model == "" { 19 | return nil, errors.New("model parameter must be provided") 20 | } 21 | if rerankRequest.Query == "" { 22 | return nil, errors.New("query must not be empty") 23 | } 24 | if len(rerankRequest.Documents) == 0 { 25 | return nil, errors.New("document list must not be empty") 26 | } 27 | 28 | return rerankRequest, nil 29 | } 30 | 31 | func rerankPromptTokens(rerankRequest *relaymodel.RerankRequest) int64 { 32 | tokens := openai.CountTokenInput(rerankRequest.Query, rerankRequest.Model) 33 | for _, d := range rerankRequest.Documents { 34 | tokens += openai.CountTokenInput(d, rerankRequest.Model) 35 | } 36 | return tokens 37 | } 38 | 39 | func GetRerankRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) { 40 | return mc.Price, nil 41 | } 42 | 43 | func GetRerankRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) { 44 | rerankRequest, err := getRerankRequest(c) 45 | if err != nil { 46 | return model.Usage{}, err 47 | } 48 | return model.Usage{ 49 | InputTokens: model.ZeroNullInt64(rerankPromptTokens(rerankRequest)), 50 | }, nil 51 | } 52 | -------------------------------------------------------------------------------- /core/relay/controller/tts.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "unicode/utf8" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/utils" 9 | ) 10 | 11 | func GetTTSRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) { 12 | return mc.Price, nil 13 | } 14 | 15 | func GetTTSRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) { 16 | ttsRequest, err := utils.UnmarshalTTSRequest(c.Request) 17 | if err != nil { 18 | return model.Usage{}, err 19 | } 20 | 21 | return model.Usage{ 22 | InputTokens: model.ZeroNullInt64(utf8.RuneCountInString(ttsRequest.Input)), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /core/relay/mode/define.go: -------------------------------------------------------------------------------- 1 | package mode 2 | 3 | import "fmt" 4 | 5 | type Mode int 6 | 7 | func (m Mode) String() string { 8 | switch m { 9 | case Unknown: 10 | return "Unknown" 11 | case ChatCompletions: 12 | return "ChatCompletions" 13 | case Completions: 14 | return "Completions" 15 | case Embeddings: 16 | return "Embeddings" 17 | case Moderations: 18 | return "Moderations" 19 | case ImagesGenerations: 20 | return "ImagesGenerations" 21 | case ImagesEdits: 22 | return "ImagesEdits" 23 | case AudioSpeech: 24 | return "AudioSpeech" 25 | case AudioTranscription: 26 | return "AudioTranscription" 27 | case AudioTranslation: 28 | return "AudioTranslation" 29 | case Rerank: 30 | return "Rerank" 31 | case ParsePdf: 32 | return "ParsePdf" 33 | case Anthropic: 34 | return "Anthropic" 35 | default: 36 | return fmt.Sprintf("Mode(%d)", m) 37 | } 38 | } 39 | 40 | const ( 41 | Unknown Mode = iota 42 | ChatCompletions 43 | Completions 44 | Embeddings 45 | Moderations 46 | ImagesGenerations 47 | ImagesEdits 48 | AudioSpeech 49 | AudioTranscription 50 | AudioTranslation 51 | Rerank 52 | ParsePdf 53 | Anthropic 54 | ) 55 | -------------------------------------------------------------------------------- /core/relay/model/anthropic.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/labring/aiproxy/core/relay/adaptor" 4 | 5 | type AnthropicMessageRequest struct { 6 | Model string `json:"model,omitempty"` 7 | Messages []*Message `json:"messages,omitempty"` 8 | } 9 | 10 | type AnthropicError struct { 11 | Type string `json:"type"` 12 | Message string `json:"message"` 13 | } 14 | 15 | type AnthropicErrorResponse struct { 16 | Type string `json:"type"` 17 | Error AnthropicError `json:"error"` 18 | } 19 | 20 | func NewAnthropicError(statusCode int, err AnthropicError) adaptor.Error { 21 | return adaptor.NewError(statusCode, AnthropicErrorResponse{ 22 | Type: "error", 23 | Error: err, 24 | }) 25 | } 26 | 27 | func WrapperAnthropicError(err error, typ string, statusCode int) adaptor.Error { 28 | return WrapperAnthropicErrorWithMessage(err.Error(), typ, statusCode) 29 | } 30 | 31 | func WrapperAnthropicErrorWithMessage(message, typ string, statusCode int) adaptor.Error { 32 | return NewAnthropicError(statusCode, AnthropicError{ 33 | Type: typ, 34 | Message: message, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /core/relay/model/constant.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | const ( 4 | ContentTypeText = "text" 5 | ContentTypeImageURL = "image_url" 6 | ContentTypeInputAudio = "input_audio" 7 | ) 8 | 9 | const ( 10 | ChatCompletionChunk = "chat.completion.chunk" 11 | ChatCompletion = "chat.completion" 12 | ) 13 | 14 | type FinishReason = string 15 | 16 | const ( 17 | FinishReasonStop FinishReason = "stop" 18 | FinishReasonLength FinishReason = "length" 19 | FinishReasonContentFilter FinishReason = "content_filter" 20 | FinishReasonToolCalls FinishReason = "tool_calls" 21 | FinishReasonFunctionCall FinishReason = "function_call" 22 | ) 23 | -------------------------------------------------------------------------------- /core/relay/model/embed.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type EmbeddingRequest struct { 4 | Input string `json:"input"` 5 | Model string `json:"model"` 6 | EncodingFormat string `json:"encoding_format"` 7 | Dimensions int `json:"dimensions"` 8 | } 9 | 10 | type EmbeddingResponseItem struct { 11 | Object string `json:"object"` 12 | Embedding []float64 `json:"embedding"` 13 | Index int `json:"index"` 14 | } 15 | 16 | type EmbeddingResponse struct { 17 | Object string `json:"object"` 18 | Model string `json:"model"` 19 | Data []*EmbeddingResponseItem `json:"data"` 20 | Usage `json:"usage"` 21 | } 22 | -------------------------------------------------------------------------------- /core/relay/model/errors.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/labring/aiproxy/core/relay/adaptor" 5 | "github.com/labring/aiproxy/core/relay/mode" 6 | ) 7 | 8 | const ( 9 | ErrorTypeAIPROXY = "aiproxy_error" 10 | ErrorTypeUpstream = "upstream_error" 11 | ErrorCodeBadResponse = "bad_response" 12 | ) 13 | 14 | func WrapperError(m mode.Mode, statusCode int, err error, typ ...string) adaptor.Error { 15 | return WrapperErrorWithMessage(m, statusCode, err.Error(), typ...) 16 | } 17 | 18 | func WrapperErrorWithMessage( 19 | m mode.Mode, 20 | statusCode int, 21 | message string, 22 | typ ...string, 23 | ) adaptor.Error { 24 | respType := ErrorTypeAIPROXY 25 | if len(typ) > 0 { 26 | respType = typ[0] 27 | } 28 | switch m { 29 | case mode.Anthropic: 30 | return NewAnthropicError(statusCode, AnthropicError{ 31 | Message: message, 32 | Type: respType, 33 | }) 34 | default: 35 | return NewOpenAIError(statusCode, OpenAIError{ 36 | Message: message, 37 | Type: respType, 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/relay/model/pdf.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ParsePdfResponse struct { 4 | Pages int64 `json:"pages"` 5 | Markdown string `json:"markdown"` 6 | } 7 | 8 | type ParsePdfListResponse struct { 9 | Markdowns []string `json:"markdowns"` 10 | } 11 | -------------------------------------------------------------------------------- /core/relay/model/rerank.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type RerankRequest struct { 4 | TopN *int `json:"top_n,omitempty"` 5 | MaxChunksPerDoc *int `json:"max_chunks_per_doc,omitempty"` 6 | ReturnDocuments *bool `json:"return_documents,omitempty"` 7 | OverlapTokens *int `json:"overlap_tokens,omitempty"` 8 | Model string `json:"model"` 9 | Query string `json:"query"` 10 | Documents []string `json:"documents"` 11 | } 12 | 13 | type Document struct { 14 | Text string `json:"text"` 15 | } 16 | 17 | type RerankResult struct { 18 | Document *Document `json:"document,omitempty"` 19 | Index int `json:"index"` 20 | RelevanceScore float64 `json:"relevance_score"` 21 | } 22 | 23 | type RerankMetaTokens struct { 24 | InputTokens int64 `json:"input_tokens"` 25 | OutputTokens int64 `json:"output_tokens"` 26 | } 27 | 28 | type RerankMeta struct { 29 | Tokens *RerankMetaTokens `json:"tokens,omitempty"` 30 | Model string `json:"model,omitempty"` 31 | } 32 | 33 | type RerankResponse struct { 34 | Meta RerankMeta `json:"meta"` 35 | ID string `json:"id"` 36 | Results []*RerankResult `json:"results"` 37 | } 38 | 39 | type SlimRerankResponse struct { 40 | Meta RerankMeta `json:"meta"` 41 | } 42 | -------------------------------------------------------------------------------- /core/relay/model/stt.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type SttJSONResponse struct { 4 | Text string `json:"text,omitempty"` 5 | } 6 | 7 | type SttVerboseJSONResponse struct { 8 | Task string `json:"task,omitempty"` 9 | Language string `json:"language,omitempty"` 10 | Text string `json:"text,omitempty"` 11 | Segments []*Segment `json:"segments,omitempty"` 12 | Duration float64 `json:"duration,omitempty"` 13 | } 14 | 15 | type Segment struct { 16 | Text string `json:"text"` 17 | Tokens []int `json:"tokens"` 18 | ID int `json:"id"` 19 | Seek int `json:"seek"` 20 | Start float64 `json:"start"` 21 | End float64 `json:"end"` 22 | Temperature float64 `json:"temperature"` 23 | AvgLogprob float64 `json:"avg_logprob"` 24 | CompressionRatio float64 `json:"compression_ratio"` 25 | NoSpeechProb float64 `json:"no_speech_prob"` 26 | } 27 | -------------------------------------------------------------------------------- /core/relay/model/tool.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Tool struct { 4 | ID string `json:"id,omitempty"` 5 | Type string `json:"type,omitempty"` 6 | Function Function `json:"function"` 7 | } 8 | 9 | type Function struct { 10 | Parameters any `json:"parameters,omitempty"` 11 | Arguments string `json:"arguments,omitempty"` 12 | Description string `json:"description,omitempty"` 13 | Name string `json:"name,omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /core/relay/model/tts.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type TextToSpeechRequest struct { 4 | Model string `json:"model" binding:"required"` 5 | Input string `json:"input" binding:"required"` 6 | Voice string `json:"voice" binding:"required"` 7 | ResponseFormat string `json:"response_format"` 8 | Speed float64 `json:"speed"` 9 | } 10 | -------------------------------------------------------------------------------- /core/relay/plugin/cache/config.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | type Config struct { 4 | Enable bool `json:"enable"` 5 | TTL int `json:"ttl"` 6 | ItemMaxSize int `json:"item_max_size"` 7 | AddCacheHitHeader bool `json:"add_cache_hit_header"` 8 | CacheHitHeader string `json:"cache_hit_header"` 9 | } 10 | -------------------------------------------------------------------------------- /core/relay/plugin/noop/noop.go: -------------------------------------------------------------------------------- 1 | package noop 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/model" 8 | "github.com/labring/aiproxy/core/relay/adaptor" 9 | "github.com/labring/aiproxy/core/relay/meta" 10 | "github.com/labring/aiproxy/core/relay/plugin" 11 | ) 12 | 13 | var _ plugin.Plugin = (*Noop)(nil) 14 | 15 | type Noop struct{} 16 | 17 | func (n *Noop) GetRequestURL(meta *meta.Meta, do adaptor.GetRequestURL) (string, error) { 18 | return do.GetRequestURL(meta) 19 | } 20 | 21 | func (n *Noop) SetupRequestHeader( 22 | meta *meta.Meta, 23 | c *gin.Context, 24 | req *http.Request, 25 | do adaptor.SetupRequestHeader, 26 | ) error { 27 | return do.SetupRequestHeader(meta, c, req) 28 | } 29 | 30 | func (n *Noop) ConvertRequest( 31 | meta *meta.Meta, 32 | req *http.Request, 33 | do adaptor.ConvertRequest, 34 | ) (*adaptor.ConvertRequestResult, error) { 35 | return do.ConvertRequest(meta, req) 36 | } 37 | 38 | func (n *Noop) DoRequest( 39 | meta *meta.Meta, 40 | c *gin.Context, 41 | req *http.Request, 42 | do adaptor.DoRequest, 43 | ) (*http.Response, error) { 44 | return do.DoRequest(meta, c, req) 45 | } 46 | 47 | func (n *Noop) DoResponse( 48 | meta *meta.Meta, 49 | c *gin.Context, 50 | resp *http.Response, 51 | do adaptor.DoResponse, 52 | ) (*model.Usage, adaptor.Error) { 53 | return do.DoResponse(meta, c, resp) 54 | } 55 | -------------------------------------------------------------------------------- /core/relay/plugin/thinksplit/config.go: -------------------------------------------------------------------------------- 1 | package thinksplit 2 | 3 | // Config represents the plugin configuration 4 | type Config struct { 5 | Enable bool `json:"enable"` 6 | } 7 | -------------------------------------------------------------------------------- /core/relay/plugin/web-search/prompts/chinese-internet.md: -------------------------------------------------------------------------------- 1 | # 目标 2 | 你需要分析**用户发送的消息**,是否需要查询中文搜索引擎,并按照如下情况回复相应内容: 3 | 4 | ## 情况一:不需要查询搜索引擎 5 | ### 情况举例: 6 | 1. **用户发送的消息**不是在提问或寻求帮助 7 | 2. **用户发送的消息**是要求翻译文字 8 | 9 | ### 思考过程 10 | 根据上面的**情况举例**,如果符合,则按照下面**回复内容示例**进行回复,注意不要输出思考过程 11 | 12 | ### 回复内容示例: 13 | none 14 | 15 | ## 情况二:需要查询搜索引擎 16 | ### 情况举例: 17 | 1. 答复**用户发送的消息**,需依赖互联网上最新的资料 18 | 2. 答复**用户发送的消息**,需依赖论文等专业资料 19 | 3. 通过查询资料,可以更好地答复**用户发送的消息** 20 | 21 | ### 思考过程 22 | 根据上面的**情况举例**,以及其他需要查询资料的情况,如果符合,按照以下步骤思考,并按照下面**回复内容示例**进行回复,注意不要输出思考过程: 23 | 1. What: 分析要答复**用户发送的消息**,需要了解什么知识和资料 24 | 2. How: 分析对于要查询的知识和资料,应该提出什么样的问题 25 | 3. Adjust: 明确查询什么问题后,用一句话概括问题,并且针对搜索引擎做问题优化 26 | 4. Final: 按照下面**回复内容示例**进行回复,注意: 27 | - 不要输出思考过程 28 | - 可以查询多次,多个查询用换行分隔,总查询次数控制在{max_count}次以内 29 | - 需要以"internet:"开头 30 | - 即使**用户发送的消息**使用了中文以外的其他语言,也用中文向搜索引擎查询问题,但注意不要翻译专有名词 31 | 32 | ### 回复内容示例: 33 | 34 | #### 查询多次搜索引擎 35 | internet: 黄金价格走势 36 | internet: 历史黄金价格高点 37 | 38 | # 用户发送的消息为: 39 | {question} 40 | -------------------------------------------------------------------------------- /core/relay/plugin/web-search/prompts/internet.md: -------------------------------------------------------------------------------- 1 | # 目标 2 | 你需要分析**用户发送的消息**,是否需要查询搜索引擎(Google/Bing),并按照如下情况回复相应内容: 3 | 4 | ## 情况一:不需要查询搜索引擎 5 | ### 情况举例: 6 | 1. **用户发送的消息**不是在提问或寻求帮助 7 | 2. **用户发送的消息**是要求翻译文字 8 | 9 | ### 思考过程 10 | 根据上面的**情况举例**,如果符合,则按照下面**回复内容示例**进行回复,注意不要输出思考过程 11 | 12 | ### 回复内容示例: 13 | none 14 | 15 | ## 情况二:需要查询搜索引擎 16 | ### 情况举例: 17 | 1. 答复**用户发送的消息**,需依赖互联网上最新的资料 18 | 2. 答复**用户发送的消息**,需依赖论文等专业资料 19 | 3. 通过查询资料,可以更好地答复**用户发送的消息** 20 | 21 | ### 思考过程 22 | 根据上面的**情况举例**,以及其他需要查询资料的情况,如果符合,按照以下步骤思考,并按照下面**回复内容示例**进行回复,注意不要输出思考过程: 23 | 1. What: 分析要答复**用户发送的消息**,需要了解什么知识和资料 24 | 2. How: 分析对于要查询的知识和资料,应该提出什么样的问题 25 | 3. Adjust: 明确查询什么问题后,用一句话概括问题,并且针对搜索引擎做问题优化 26 | 4. Final: 按照下面**回复内容示例**进行回复,注意: 27 | - 不要输出思考过程 28 | - 可以查询多次,多个查询用换行分隔,总查询次数控制在{max_count}次以内 29 | - 需要以"internet:"开头 30 | - 尽量满足**用户发送的消息**中的搜索要求,例如用户要求用英文搜索,则需用英文表述问题和关键词 31 | - 用户如果没有要求搜索语言,则用和**用户发送的消息**一致的语言表述问题和关键词 32 | - 如果**用户发送的消息**使用中文,至少要有一条向搜索引擎查询的中文问题 33 | 34 | ### 回复内容示例: 35 | 36 | #### 用不同语言查询多次搜索引擎 37 | internet: 黄金价格走势 38 | internet: The trend of gold prices 39 | 40 | # 用户发送的消息为: 41 | {question} 42 | -------------------------------------------------------------------------------- /core/relay/plugin/web-search/prompts/private.md: -------------------------------------------------------------------------------- 1 | # 目标 2 | 你需要分析**用户发送的消息**,是否需要查询搜索引擎(Google/Bing)/私有知识库,并按照如下情况回复相应内容: 3 | 4 | ## 情况一:不需要查询搜索引擎/私有知识库 5 | ### 情况举例: 6 | 1. **用户发送的消息**不是在提问或寻求帮助 7 | 2. **用户发送的消息**是要求翻译文字 8 | 9 | ### 思考过程 10 | 根据上面的**情况举例**,如果符合,则按照下面**回复内容示例**进行回复,注意不要输出思考过程 11 | 12 | ### 回复内容示例: 13 | none 14 | 15 | ## 情况二:需要查询搜索引擎/私有知识库 16 | ### 情况举例: 17 | 1. 答复**用户发送的消息**,需依赖互联网上最新的资料 18 | 2. 答复**用户发送的消息**,需依赖论文等专业资料 19 | 3. 通过查询资料,可以更好地答复**用户发送的消息** 20 | 21 | ### 思考过程 22 | 根据上面的**情况举例**,以及其他需要查询资料的情况,如果符合,按照以下步骤思考,并按照下面**回复内容示例**进行回复,注意不要输出思考过程: 23 | 1. What: 分析要答复**用户发送的消息**,需要了解什么知识和资料 24 | 2. Where: 判断了解这个知识和资料要向Google等搜索引擎提问,还是向私有知识库进行查询,或者需要同时查询多个地方 25 | 3. How: 分析对于要查询的知识和资料,应该提出什么样的问题 26 | 4. Adjust: 明确要向什么地方查询什么问题后,按下面方式对问题进行调整 27 | 4.1. 向搜索引擎提问:用一句话概括问题,并且针对搜索引擎做问题优化 28 | 4.2. 向私有知识库提问:用一句话概括问题,私有知识库不需要对关键词进行拆分 29 | 5. Final: 按照下面**回复内容示例**进行回复,注意: 30 | - 不要输出思考过程 31 | - 可以向多个查询目标分别查询多次,多个查询用换行分隔,总查询次数控制在{max_count}次以内 32 | - 查询搜索引擎时,需要以"internet:"开头 33 | - 查询私有知识库时,需要以"private:"开头 34 | - 当用多个关键词查询时,关键词之间用","分隔 35 | - 尽量满足**用户发送的消息**中的搜索要求,例如用户要求用英文搜索,则需用英文表述问题和关键词 36 | - 用户如果没有要求搜索语言,则用和**用户发送的消息**一致的语言表述问题和关键词 37 | - 如果**用户发送的消息**使用中文,至少要有一条向搜索引擎查询的中文问题 38 | 39 | ### 回复内容示例: 40 | 41 | #### 用不同语言查询多次搜索引擎 42 | internet: 黄金价格走势 43 | internet: The trend of gold prices 44 | 45 | #### 向多个查询目标查询多次 46 | internet: 中国未来房价趋势 47 | internet: 最新中国经济政策 48 | private: 财务状况 49 | 50 | # 用户发送的消息为: 51 | {question} 52 | -------------------------------------------------------------------------------- /core/router/main.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | func SetRouter(router *gin.Engine) { 8 | SetAPIRouter(router) 9 | SetRelayRouter(router) 10 | SetMCPRouter(router) 11 | SetStaticFileRouter(router) 12 | SetSwaggerRouter(router) 13 | } 14 | -------------------------------------------------------------------------------- /core/router/mcp.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labring/aiproxy/core/controller" 6 | "github.com/labring/aiproxy/core/middleware" 7 | ) 8 | 9 | func SetMCPRouter(router *gin.Engine) { 10 | mcpRoute := router.Group("/mcp", middleware.MCPAuth) 11 | 12 | mcpRoute.GET("/public/:id/sse", controller.PublicMCPSseServer) 13 | mcpRoute.POST("/public/message", controller.PublicMCPMessage) 14 | mcpRoute.GET("/public/:id/streamable", controller.PublicMCPStreamable) 15 | mcpRoute.POST("/public/:id/streamable", controller.PublicMCPStreamable) 16 | mcpRoute.DELETE("/public/:id/streamable", controller.PublicMCPStreamable) 17 | 18 | mcpRoute.GET("/group/:id/sse", controller.GroupMCPSseServer) 19 | mcpRoute.POST("/group/message", controller.GroupMCPMessage) 20 | mcpRoute.GET("/group/:id/streamable", controller.GroupMCPStreamable) 21 | mcpRoute.POST("/group/:id/streamable", controller.GroupMCPStreamable) 22 | mcpRoute.DELETE("/group/:id/streamable", controller.GroupMCPStreamable) 23 | } 24 | -------------------------------------------------------------------------------- /core/router/swagger.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/labring/aiproxy/core/docs" 8 | swaggerfiles "github.com/swaggo/files" 9 | ginSwagger "github.com/swaggo/gin-swagger" 10 | "github.com/swaggo/swag" 11 | ) 12 | 13 | func cloneSwaggerInfo(spec *swag.Spec) *swag.Spec { 14 | newSpec := *spec 15 | return &newSpec 16 | } 17 | 18 | func SetSwaggerRouter(router *gin.Engine) { 19 | docs.SwaggerInfo.BasePath = "/" 20 | router.GET("/doc.json", func(ctx *gin.Context) { 21 | ctx.Header("Content-Type", "application/json; charset=utf-8") 22 | if ctx.Request.Host == "" { 23 | ctx.String(http.StatusOK, docs.SwaggerInfo.ReadDoc()) 24 | return 25 | } 26 | swagInfo := cloneSwaggerInfo(docs.SwaggerInfo) 27 | swagInfo.Host = ctx.Request.Host 28 | ctx.String(http.StatusOK, swagInfo.ReadDoc()) 29 | }) 30 | router.GET("/swagger", func(ctx *gin.Context) { 31 | ctx.Redirect(http.StatusMovedPermanently, "/swagger/index.html") 32 | }) 33 | router.GET("/swagger/*any", 34 | ginSwagger.WrapHandler( 35 | swaggerfiles.Handler, 36 | ginSwagger.URL("/doc.json"), 37 | ), 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /core/scripts/swag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | swag init --parseDependency --parseInternal 4 | swag fmt -------------------------------------------------------------------------------- /core/scripts/tiktoken.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | ASSETS=$( 6 | cat < 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { AppRouter } from "@/routes" 2 | import { ThemeProvider } from "./handler/ThemeProvider" 3 | 4 | function App() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default App -------------------------------------------------------------------------------- /web/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { get } from './index' 2 | import { AxiosRequestConfig } from 'axios' 3 | import { ChannelTypeMeta } from '@/types/channel' 4 | 5 | // Auth API endpoints 6 | export const authApi = { 7 | 8 | // Get channel type metas 9 | getChannelTypeMetas: (token?: string): Promise => { 10 | const config: AxiosRequestConfig = {} 11 | 12 | if (token) { 13 | config.headers = { 14 | Authorization: `${token}` 15 | } 16 | } 17 | 18 | return get('/channels/type_metas', config) 19 | }, 20 | 21 | } -------------------------------------------------------------------------------- /web/src/api/channel.ts: -------------------------------------------------------------------------------- 1 | // src/api/channel.ts 2 | import { get, post, put, del } from './index' 3 | import { 4 | ChannelTypeMetaMap, 5 | ChannelsResponse, 6 | ChannelCreateRequest, 7 | ChannelUpdateRequest, 8 | ChannelStatusRequest 9 | } from '@/types/channel' 10 | 11 | export const channelApi = { 12 | getTypeMetas: async (): Promise => { 13 | const response = await get('channels/type_metas') 14 | return response 15 | }, 16 | 17 | getChannels: async (page: number, perPage: number): Promise => { 18 | const response = await get('channels/search', { 19 | params: { 20 | p: page, 21 | per_page: perPage 22 | } 23 | }) 24 | return response 25 | }, 26 | 27 | createChannel: async (data: ChannelCreateRequest): Promise => { 28 | await post('channel/', data) 29 | return 30 | }, 31 | 32 | updateChannel: async (id: number, data: ChannelUpdateRequest): Promise => { 33 | await put(`channel/${id}`, data) 34 | return 35 | }, 36 | 37 | deleteChannel: async (id: number): Promise => { 38 | await del(`channel/${id}`) 39 | return 40 | }, 41 | 42 | updateChannelStatus: async (id: number, status: ChannelStatusRequest): Promise => { 43 | await post(`channel/${id}/status`, status) 44 | return 45 | } 46 | } -------------------------------------------------------------------------------- /web/src/api/model.ts: -------------------------------------------------------------------------------- 1 | // src/api/model.ts 2 | import { get, post, del } from './index' 3 | import { ModelConfig, ModelCreateRequest } from '@/types/model' 4 | 5 | 6 | export const modelApi = { 7 | getModels: async (): Promise => { 8 | const response = await get('model_configs/all') 9 | return response 10 | }, 11 | 12 | getModel: async (model: string): Promise => { 13 | const response = await get(`model_config/${model}`) 14 | return response 15 | }, 16 | 17 | createModel: async (data: ModelCreateRequest): Promise => { 18 | await post('model_config/', data) 19 | return 20 | }, 21 | 22 | deleteModel: async (model: string): Promise => { 23 | await del(`model_config/${model}`) 24 | return 25 | } 26 | } -------------------------------------------------------------------------------- /web/src/api/services.ts: -------------------------------------------------------------------------------- 1 | export { authApi } from './auth' 2 | export { channelApi } from './channel' 3 | export { modelApi } from './model' 4 | export { tokenApi } from './token' 5 | -------------------------------------------------------------------------------- /web/src/api/token.ts: -------------------------------------------------------------------------------- 1 | // src/api/token.ts 2 | import { get, post, del } from './index' 3 | import { TokensResponse, Token, TokenStatusRequest } from '@/types/token' 4 | 5 | export const tokenApi = { 6 | getTokens: async (page: number, perPage: number): Promise => { 7 | const response = await get('tokens/search', { 8 | params: { 9 | p: page, 10 | per_page: perPage 11 | } 12 | }) 13 | return response 14 | }, 15 | 16 | createToken: async (name: string): Promise => { 17 | // 重要:group的值与name保持一致,创建时使用auto_create_group=true 18 | const response = await post(`token/${name}?auto_create_group=true`, { 19 | name 20 | }) 21 | return response 22 | }, 23 | 24 | deleteToken: async (id: number): Promise => { 25 | await del(`tokens/${id}`) 26 | return 27 | }, 28 | 29 | updateTokenStatus: async (id: number, status: TokenStatusRequest): Promise => { 30 | await post(`tokens/${id}/status`, status) 31 | return 32 | } 33 | } -------------------------------------------------------------------------------- /web/src/components/common/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react" 2 | import { useTheme } from "@/handler/ThemeContext" 3 | 4 | import { Switch } from "@/components/ui/switch" 5 | 6 | export function ThemeToggle() { 7 | const { theme, setTheme } = useTheme() 8 | 9 | const toggleTheme = () => { 10 | setTheme(theme === "light" ? "dark" : "light") 11 | } 12 | 13 | return ( 14 |
15 | 19 | 25 | 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /web/src/components/layout/RootLayOut.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { Outlet } from "react-router" 3 | import { Sidebar } from "./SideBar" 4 | import { cn } from "@/lib/utils" 5 | 6 | export function RootLayout() { 7 | const [collapsed, setCollapsed] = useState(false) 8 | 9 | return ( 10 |
11 | setCollapsed(!collapsed)} 23 | /> 24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /web/src/components/ui/animation/components/animated-button.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ui/animation/components/animated-button.tsx 2 | import { motion } from "motion/react" 3 | import React, { forwardRef } from "react" 4 | import { 5 | buttonAnimation, 6 | primaryButtonAnimation, 7 | secondaryButtonAnimation, 8 | ghostButtonAnimation, 9 | destructiveButtonAnimation 10 | } from "../button-animation" 11 | 12 | interface AnimatedButtonProps { 13 | children: React.ReactNode 14 | className?: string 15 | animationVariant?: "default" | "primary" | "secondary" | "ghost" | "destructive" 16 | } 17 | 18 | export const AnimatedButton = forwardRef( 19 | ({ 20 | children, 21 | className = "", 22 | animationVariant = "default", 23 | ...props 24 | }, ref) => { 25 | // 根据动画变体选择动画属性 26 | const getAnimationProps = () => { 27 | switch (animationVariant) { 28 | case "primary": 29 | return primaryButtonAnimation 30 | case "secondary": 31 | return secondaryButtonAnimation 32 | case "ghost": 33 | return ghostButtonAnimation 34 | case "destructive": 35 | return destructiveButtonAnimation 36 | case "default": 37 | default: 38 | return buttonAnimation 39 | } 40 | } 41 | 42 | return ( 43 | 49 | {children} 50 | 51 | ) 52 | } 53 | ) 54 | 55 | AnimatedButton.displayName = "AnimatedButton" -------------------------------------------------------------------------------- /web/src/components/ui/animation/components/animated-container.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "motion/react" 2 | import React, { forwardRef } from "react" 3 | import { 4 | containerAnimation, 5 | smoothContainerAnimation, 6 | scrollContainerAnimation, 7 | layoutRootAnimation 8 | } from "../container-animation" 9 | 10 | interface AnimatedContainerProps { 11 | children: React.ReactNode 12 | className?: string 13 | variant?: "default" | "smooth" | "scroll" | "root" 14 | layoutId?: string 15 | layoutDependency?: unknown 16 | } 17 | 18 | export const AnimatedContainer = forwardRef( 19 | ({ 20 | children, 21 | className = "", 22 | variant = "default", 23 | layoutId, 24 | layoutDependency, 25 | ...props 26 | }, ref) => { 27 | // 根据变体选择动画属性 28 | const getAnimationProps = () => { 29 | switch (variant) { 30 | case "smooth": 31 | return smoothContainerAnimation 32 | case "scroll": 33 | return scrollContainerAnimation 34 | case "root": 35 | return layoutRootAnimation 36 | case "default": 37 | default: 38 | return containerAnimation 39 | } 40 | } 41 | 42 | return ( 43 | 51 | {children} 52 | 53 | ) 54 | } 55 | ) 56 | 57 | AnimatedContainer.displayName = "AnimatedContainer" -------------------------------------------------------------------------------- /web/src/components/ui/animation/components/collapse.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "motion/react" 2 | import React from "react" 3 | import { collapseAnimation, collapseScaleAnimation, collapseLightAnimation } from "../collapse-animation" 4 | 5 | interface CollapseProps { 6 | isOpen: boolean 7 | children: React.ReactNode 8 | className?: string 9 | animationType?: "default" | "scale" | "light" 10 | initial?: boolean 11 | } 12 | 13 | export function Collapse({ 14 | isOpen, 15 | children, 16 | className = "", 17 | animationType = "default", 18 | initial = false 19 | }: CollapseProps) { 20 | // Select the appropriate animation based on the animationType 21 | const getAnimationProps = () => { 22 | switch (animationType) { 23 | case "scale": 24 | return collapseScaleAnimation 25 | case "light": 26 | return collapseLightAnimation 27 | case "default": 28 | default: 29 | return collapseAnimation 30 | } 31 | } 32 | 33 | return ( 34 | 35 | {isOpen && ( 36 | 41 | {children} 42 | 43 | )} 44 | 45 | ) 46 | } -------------------------------------------------------------------------------- /web/src/components/ui/animation/components/display.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "motion/react" 2 | import React from "react" 3 | import { fadeAnimation, slideAnimation, scaleAnimation } from "../display-animation" 4 | 5 | interface DisplayProps { 6 | visible: boolean 7 | children: React.ReactNode 8 | className?: string 9 | animationType?: "fade" | "slide" | "scale" 10 | initial?: boolean 11 | mode?: "sync" | "wait" | "popLayout" 12 | } 13 | 14 | export function Display({ 15 | visible, 16 | children, 17 | className = "", 18 | animationType = "fade", 19 | initial = false, 20 | mode = "sync" 21 | }: DisplayProps) { 22 | // 根据动画类型选择适当的动画属性 23 | const getAnimationProps = () => { 24 | switch (animationType) { 25 | case "slide": 26 | return slideAnimation 27 | case "scale": 28 | return scaleAnimation 29 | case "fade": 30 | default: 31 | return fadeAnimation 32 | } 33 | } 34 | 35 | return ( 36 | 37 | {visible && ( 38 | 43 | {children} 44 | 45 | )} 46 | 47 | ) 48 | } -------------------------------------------------------------------------------- /web/src/components/ui/animation/components/tab-animation.tsx: -------------------------------------------------------------------------------- 1 | import { tabFadeAnimation, tabScaleAnimation, tabContentAnimation } from "../tab-animation" 2 | import React from "react" 3 | import { AnimatePresence, motion } from "motion/react" 4 | 5 | interface TabsAnimationProviderProps { 6 | children: React.ReactNode 7 | currentView: string 8 | animationVariant?: "slide" | "fade" | "scale" 9 | } 10 | 11 | export function TabsAnimationProvider({ 12 | children, 13 | currentView, 14 | animationVariant = "slide" 15 | }: TabsAnimationProviderProps) { 16 | // 根据选择的变体选择相应的动画 17 | const getAnimationProps = () => { 18 | switch (animationVariant) { 19 | case "fade": 20 | return tabFadeAnimation 21 | case "scale": 22 | return tabScaleAnimation 23 | case "slide": 24 | default: 25 | return tabContentAnimation 26 | } 27 | } 28 | 29 | return ( 30 | 31 | 35 | {children} 36 | 37 | 38 | ) 39 | } -------------------------------------------------------------------------------- /web/src/components/ui/animation/container-animation.ts: -------------------------------------------------------------------------------- 1 | import { HTMLMotionProps } from "motion/react" 2 | 3 | // 基础容器动画 - 使用弹性动画效果 4 | export const containerAnimation: HTMLMotionProps<"div"> = { 5 | layout: true, 6 | transition: { 7 | layout: { 8 | type: "spring", 9 | stiffness: 300, 10 | damping: 30 11 | } 12 | } 13 | } 14 | 15 | // 平滑容器动画 - 使用补间动画,更均匀的过渡 16 | export const smoothContainerAnimation: HTMLMotionProps<"div"> = { 17 | layout: true, 18 | transition: { 19 | layout: { 20 | duration: 0.4, 21 | ease: [0.4, 0, 0.2, 1], 22 | type: "tween" 23 | } 24 | } 25 | } 26 | 27 | // 优化的滚动容器动画 - 减少过渡时间,提高响应性 28 | export const scrollContainerAnimation: HTMLMotionProps<"div"> = { 29 | layout: true, 30 | layoutRoot: true, 31 | transition: { 32 | layout: { 33 | duration: 0.25, 34 | ease: [0.25, 0.1, 0.25, 1.0], 35 | type: "tween" 36 | } 37 | } 38 | } 39 | 40 | // 高性能布局根动画 - 适用于复杂列表和表格 41 | export const layoutRootAnimation: HTMLMotionProps<"div"> = { 42 | layout: true, 43 | layoutRoot: true, 44 | transition: { 45 | layout: { 46 | type: "tween", 47 | duration: 0.2 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /web/src/components/ui/animation/grid-animation.ts: -------------------------------------------------------------------------------- 1 | import { HTMLMotionProps } from "motion/react" 2 | 3 | // 布局动画配置 - 用于处理元素添加、删除和重排序的动画效果 4 | export const layoutAnimationProps = { 5 | layout: true, 6 | initial: { opacity: 0, scale: 0.8 }, 7 | animate: { opacity: 1, scale: 1 }, 8 | exit: { opacity: 0, scale: 0.8 }, 9 | transition: { 10 | layout: { 11 | type: "spring", 12 | damping: 25, 13 | stiffness: 300, 14 | mass: 0.8 15 | }, 16 | opacity: { duration: 0.3 }, 17 | scale: { 18 | type: "spring", 19 | damping: 15, 20 | stiffness: 200 21 | } 22 | } 23 | } 24 | 25 | // 保留网格项目的动画配置,因为它在SiteCard中使用 26 | export const gridItemAnimation: HTMLMotionProps<"div"> = { 27 | variants: { 28 | initial: { 29 | opacity: 0, 30 | y: 20, 31 | scale: 0.95 32 | }, 33 | animate: { 34 | opacity: 1, 35 | y: 0, 36 | scale: 1, 37 | transition: { 38 | type: "spring", 39 | damping: 15, 40 | stiffness: 200 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /web/src/components/ui/animation/tab-animation.ts: -------------------------------------------------------------------------------- 1 | // src/components/ui/animations/tabs-animations.tsx 2 | import { HTMLMotionProps } from "motion/react" 3 | 4 | // 标签内容切换动画 - 水平滑动效果 5 | export const tabContentAnimation: HTMLMotionProps<"div"> = { 6 | initial: { 7 | opacity: 0, 8 | x: 10 9 | }, 10 | animate: { 11 | opacity: 1, 12 | x: 0, 13 | transition: { 14 | duration: 0.3, 15 | ease: [0.22, 1, 0.36, 1] 16 | } 17 | }, 18 | exit: { 19 | opacity: 0, 20 | x: -10, 21 | transition: { 22 | duration: 0.2 23 | } 24 | } 25 | } 26 | 27 | // 标签内容淡入淡出动画 - 没有位移只有透明度变化 28 | export const tabFadeAnimation: HTMLMotionProps<"div"> = { 29 | initial: { 30 | opacity: 0 31 | }, 32 | animate: { 33 | opacity: 1, 34 | transition: { 35 | duration: 0.25 36 | } 37 | }, 38 | exit: { 39 | opacity: 0, 40 | transition: { 41 | duration: 0.2 42 | } 43 | } 44 | } 45 | 46 | // 标签内容缩放动画 - 带有轻微的缩放效果 47 | export const tabScaleAnimation: HTMLMotionProps<"div"> = { 48 | initial: { 49 | opacity: 0, 50 | scale: 0.98 51 | }, 52 | animate: { 53 | opacity: 1, 54 | scale: 1, 55 | transition: { 56 | duration: 0.25, 57 | ease: [0.25, 1, 0.5, 1] 58 | } 59 | }, 60 | exit: { 61 | opacity: 0, 62 | scale: 0.98, 63 | transition: { 64 | duration: 0.2 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /web/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Avatar({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | function AvatarImage({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 32 | ) 33 | } 34 | 35 | function AvatarFallback({ 36 | className, 37 | ...props 38 | }: React.ComponentProps) { 39 | return ( 40 | 48 | ) 49 | } 50 | 51 | export { Avatar, AvatarImage, AvatarFallback } 52 | -------------------------------------------------------------------------------- /web/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ) 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span" 36 | 37 | return ( 38 | 43 | ) 44 | } 45 | 46 | export { Badge, badgeVariants } 47 | -------------------------------------------------------------------------------- /web/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | function Collapsible({ 4 | ...props 5 | }: React.ComponentProps) { 6 | return 7 | } 8 | 9 | function CollapsibleTrigger({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 17 | ) 18 | } 19 | 20 | function CollapsibleContent({ 21 | ...props 22 | }: React.ComponentProps) { 23 | return ( 24 | 28 | ) 29 | } 30 | 31 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 32 | -------------------------------------------------------------------------------- /web/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /web/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /web/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Separator({ 7 | className, 8 | orientation = "horizontal", 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | export { Separator } 27 | -------------------------------------------------------------------------------- /web/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /web/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes" 2 | import { Toaster as Sonner, ToasterProps } from "sonner" 3 | 4 | const Toaster = ({ ...props }: ToasterProps) => { 5 | const { theme = "system" } = useTheme() 6 | 7 | return ( 8 | 26 | ) 27 | } 28 | 29 | export { Toaster } 30 | -------------------------------------------------------------------------------- /web/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitive from "@radix-ui/react-switch" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Switch({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | 25 | 26 | ) 27 | } 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /web/src/feature/auth/components/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useNavigate, useLocation, Outlet } from 'react-router' 3 | import useAuthStore from '@/store/auth' 4 | 5 | export function ProtectedRoute() { 6 | const { isAuthenticated } = useAuthStore() 7 | const navigate = useNavigate() 8 | const location = useLocation() 9 | 10 | useEffect(() => { 11 | if (!isAuthenticated) { 12 | // Redirect to login, but save the current location 13 | navigate('/login', { state: { from: location } }) 14 | } 15 | }, [isAuthenticated, navigate, location]) 16 | 17 | // If authenticated, render children 18 | return isAuthenticated ? : null 19 | } -------------------------------------------------------------------------------- /web/src/feature/auth/hooks.ts: -------------------------------------------------------------------------------- 1 | // src/feature/auth/hooks.ts 2 | import { useMutation } from '@tanstack/react-query' 3 | import { useNavigate, useLocation } from 'react-router' 4 | import { authApi } from '@/api/services' 5 | import { useAuthStore } from '@/store/auth' 6 | import { toast } from 'sonner' 7 | import { ApiError } from '@/api/index' 8 | 9 | export function useLoginMutation() { 10 | const navigate = useNavigate() 11 | const location = useLocation() 12 | const { login } = useAuthStore() 13 | 14 | // get redirect url from location 15 | const from = (location.state as { from?: { pathname: string } })?.from?.pathname || '/' 16 | 17 | return useMutation({ 18 | mutationFn: async (token: string) => { 19 | const result = await authApi.getChannelTypeMetas(token) 20 | return { token, result } 21 | }, 22 | onSuccess: ({ token }) => { 23 | // login success, save token 24 | login(token) 25 | toast.success('login success') 26 | // redirect to previous page or home page 27 | navigate(from, { replace: true }) 28 | }, 29 | onError: (error: unknown) => { 30 | if (error instanceof ApiError) { 31 | if (error.code === 401) { 32 | toast.error('Token无效,请重新输入') 33 | } else { 34 | toast.error(`API错误 (${error.code}): ${error.message}`) 35 | } 36 | } else { 37 | toast.error('登录失败,请重试') 38 | } 39 | } 40 | }) 41 | } -------------------------------------------------------------------------------- /web/src/handler/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react" 2 | 3 | export type Theme = "dark" | "light" | "system" 4 | 5 | export type ThemeProviderState = { 6 | theme: Theme 7 | setTheme: (theme: Theme) => void 8 | } 9 | 10 | export const initialState: ThemeProviderState = { 11 | theme: "system", 12 | setTheme: () => null, 13 | } 14 | 15 | export const ThemeProviderContext = createContext(initialState) 16 | 17 | export const useTheme = () => { 18 | const context = useContext(ThemeProviderContext) 19 | 20 | if (context === undefined) 21 | throw new Error("useTheme must be used within a ThemeProvider") 22 | 23 | return context 24 | } -------------------------------------------------------------------------------- /web/src/handler/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { ThemeProviderContext, Theme } from "./ThemeContext" 3 | 4 | type ThemeProviderProps = { 5 | children: React.ReactNode 6 | defaultTheme?: Theme 7 | storageKey?: string 8 | } 9 | 10 | export function ThemeProvider({ 11 | children, 12 | defaultTheme = "system", 13 | storageKey = "vite-ui-theme", 14 | ...props 15 | }: ThemeProviderProps) { 16 | const [theme, setTheme] = useState( 17 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 18 | ) 19 | 20 | useEffect(() => { 21 | const root = window.document.documentElement 22 | 23 | root.classList.remove("light", "dark") 24 | 25 | if (theme === "system") { 26 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 27 | .matches 28 | ? "dark" 29 | : "light" 30 | 31 | root.classList.add(systemTheme) 32 | return 33 | } 34 | 35 | root.classList.add(theme) 36 | }, [theme]) 37 | 38 | const value = { 39 | theme, 40 | setTheme: (theme: Theme) => { 41 | localStorage.setItem(storageKey, theme) 42 | setTheme(theme) 43 | }, 44 | } 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /web/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import Backend from 'i18next-http-backend' 4 | import LanguageDetector from 'i18next-browser-languagedetector' 5 | import { ENV } from './utils/env' 6 | 7 | i18n 8 | .use(Backend) 9 | .use(LanguageDetector) 10 | .use(initReactI18next) 11 | .init({ 12 | fallbackLng: 'en', 13 | debug: ENV.isDevelopment, 14 | interpolation: { 15 | escapeValue: false, 16 | }, 17 | backend: { 18 | loadPath: '/locales/{{lng}}/{{ns}}.json', 19 | } 20 | }) 21 | 22 | export default i18n -------------------------------------------------------------------------------- /web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode, Suspense } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 4 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools' 5 | import './index.css' 6 | import './i18n' 7 | import App from './App.tsx' 8 | import { ErrorBoundary } from './handler/ErrorBoundary' 9 | import { ENV } from './utils/env.ts' 10 | import { Toaster } from '@/components/ui/sonner' 11 | import { ConstantCategory } from './constant/index.ts' 12 | import { getConstant } from './constant/index.ts' 13 | import { I18nextProvider } from 'react-i18next' 14 | import i18n from './i18n' 15 | import { LoadingFallback } from './components/common/LoadingFallBack' 16 | 17 | const queryClient = new QueryClient({ 18 | defaultOptions: { 19 | queries: { 20 | staleTime: getConstant(ConstantCategory.FEATURE, 'QUERY_STALE_TIME', 5 * 60 * 1000), 21 | retry: getConstant(ConstantCategory.FEATURE, 'DEFAULT_QUERY_RETRY', 1), 22 | refetchOnWindowFocus: false 23 | } 24 | } 25 | }) 26 | 27 | 28 | 29 | createRoot(document.getElementById('root')!).render( 30 | 31 | 32 | 33 | 34 | }> 35 | 36 | 37 | 38 | 39 | {ENV.isDevelopment && } 40 | 41 | 42 | , 43 | ) 44 | -------------------------------------------------------------------------------- /web/src/pages/channel/page.tsx: -------------------------------------------------------------------------------- 1 | // src/pages/channel/page.tsx 2 | import { AnimatedRoute } from '@/components/layout/AnimatedRoute' 3 | import { ChannelTable } from '@/feature/channel/components/ChannelTable' 4 | 5 | export default function ChannelPage() { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /web/src/pages/model/page.tsx: -------------------------------------------------------------------------------- 1 | // src/pages/model/page.tsx 2 | import { AnimatedRoute } from '@/components/layout/AnimatedRoute' 3 | import { ModelTable } from '@/feature/model/components/ModelTable' 4 | 5 | export default function ModelPage() { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /web/src/pages/token/page.tsx: -------------------------------------------------------------------------------- 1 | // src/pages/token/page.tsx 2 | import { TokenTable } from '@/feature/token/components/TokenTable' 3 | import { AnimatedRoute } from '@/components/layout/AnimatedRoute' 4 | 5 | export default function TokenPage() { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /web/src/routes/constants.ts: -------------------------------------------------------------------------------- 1 | export const BASE_PATH = '/' as const 2 | 3 | export const ROUTES = { 4 | MONITOR: "/monitor", 5 | KEY: "/key", 6 | CHANNEL: "/channel", 7 | MODEL: "/model", 8 | LOG: "/log", 9 | } as const 10 | 11 | export type RouteKey = keyof typeof ROUTES 12 | export type RoutePath = typeof ROUTES[RouteKey] 13 | 14 | // get route path by key 15 | export const getRoute = (key: RouteKey): RoutePath => ROUTES[key] -------------------------------------------------------------------------------- /web/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | // src/routes/AppRouter.tsx 2 | import { createBrowserRouter, RouteObject, RouterProvider } from "react-router" 3 | import { useRoutes } from "./config" 4 | import { RouteErrorBoundary } from "@/handler/ErrorBoundary" 5 | 6 | export function AppRouter() { 7 | // use existing routes config 8 | const routes = useRoutes() 9 | 10 | // iterate routes and add errorElement 11 | const routesWithErrorHandling = addErrorElementToRoutes(routes) 12 | 13 | // create router 14 | const router = createBrowserRouter(routesWithErrorHandling) 15 | 16 | return 17 | } 18 | 19 | // recursive add error handling 20 | function addErrorElementToRoutes(routes: RouteObject[]) { 21 | return routes.map(route => { 22 | // add error element to each route 23 | const updatedRoute = { 24 | ...route, 25 | errorElement: 26 | } 27 | 28 | // recursive handle sub routes 29 | if (route.children) { 30 | updatedRoute.children = addErrorElementToRoutes(route.children) 31 | } 32 | 33 | return updatedRoute 34 | }) 35 | } -------------------------------------------------------------------------------- /web/src/store/auth.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { persist } from 'zustand/middleware' 3 | 4 | export interface AuthState { 5 | token: string | null 6 | isAuthenticated: boolean 7 | isAuthenticating: boolean 8 | login: (token: string) => void 9 | logout: () => void 10 | } 11 | 12 | export const useAuthStore = create()( 13 | persist( 14 | (set) => ({ 15 | token: null, 16 | isAuthenticated: false, 17 | isAuthenticating: false, 18 | 19 | login: (token: string) => { 20 | set({ 21 | token, 22 | isAuthenticated: true, 23 | }) 24 | }, 25 | 26 | logout: () => { 27 | set({ 28 | token: null, 29 | isAuthenticated: false, 30 | }) 31 | }, 32 | 33 | setToken: (token: string) => { 34 | set({ 35 | token, 36 | }) 37 | }, 38 | }), 39 | { 40 | name: 'auth-storage', 41 | // Only persist these fields 42 | partialize: (state) => ({ 43 | token: state.token, 44 | isAuthenticated: state.isAuthenticated, 45 | }), 46 | } 47 | ) 48 | ) 49 | 50 | export default useAuthStore -------------------------------------------------------------------------------- /web/src/types/channel.ts: -------------------------------------------------------------------------------- 1 | // src/types/channel.ts 2 | export interface Channel { 3 | id: number 4 | type: number 5 | name: string 6 | key: string 7 | base_url: string 8 | models: string[] 9 | model_mapping: Record | null 10 | request_count: number 11 | status: number 12 | created_at: number 13 | priority: number 14 | balance?: number 15 | used_amount?: number 16 | } 17 | 18 | export interface ChannelTypeMeta { 19 | name: string 20 | keyHelp: string 21 | defaultBaseUrl: string 22 | } 23 | 24 | export type ChannelTypeMetaMap = Record 25 | 26 | export interface ChannelsResponse { 27 | channels: Channel[] 28 | total: number 29 | } 30 | 31 | export interface ChannelCreateRequest { 32 | type: number 33 | name: string 34 | key: string 35 | base_url: string 36 | models: string[] 37 | model_mapping?: Record 38 | } 39 | 40 | export interface ChannelUpdateRequest { 41 | type: number 42 | name: string 43 | key: string 44 | base_url: string 45 | models: string[] 46 | model_mapping?: Record 47 | } 48 | 49 | export interface ChannelStatusRequest { 50 | status: number 51 | } -------------------------------------------------------------------------------- /web/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | type ApiError = import('../api').ApiError 3 | } 4 | 5 | // this is required to make the file a module 6 | export { } -------------------------------------------------------------------------------- /web/src/types/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import 'i18next' 2 | 3 | import type translation from '../../public/locales/en/translation.json' 4 | 5 | declare module 'i18next' { 6 | interface CustomTypeOptions { 7 | defaultNS: 'translation' 8 | resources: { 9 | translation: typeof translation 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /web/src/types/model.ts: -------------------------------------------------------------------------------- 1 | // src/types/model.ts 2 | export interface ModelConfigDetail { 3 | max_input_tokens?: number 4 | max_output_tokens?: number 5 | max_context_tokens?: number 6 | vision?: boolean 7 | tool_choice?: boolean 8 | support_formats?: string[] 9 | support_voices?: string[] 10 | } 11 | 12 | export interface ModelPrice { 13 | input_price: number 14 | output_price: number 15 | per_request_price: number 16 | cache_creation_price?: number 17 | cache_creation_price_unit?: number 18 | cached_price?: number 19 | cached_price_unit?: number 20 | image_input_price?: number 21 | image_input_price_unit?: number 22 | image_output_price?: number 23 | image_output_price_unit?: number 24 | web_search_price?: number 25 | web_search_price_unit?: number 26 | } 27 | 28 | export interface ModelConfig { 29 | config?: ModelConfigDetail 30 | created_at: number 31 | updated_at: number 32 | image_prices: number[] | null 33 | model: string 34 | owner: string 35 | image_batch_size?: number 36 | type: number 37 | price: ModelPrice 38 | rpm: number 39 | tpm?: number 40 | } 41 | 42 | export interface ModelCreateRequest { 43 | model: string 44 | type: number 45 | } -------------------------------------------------------------------------------- /web/src/types/token.ts: -------------------------------------------------------------------------------- 1 | // src/types/token.ts 2 | export interface Token { 3 | key: string 4 | name: string 5 | group: string 6 | subnets: string[] | null 7 | models: string[] | null 8 | status: number 9 | id: number 10 | quota: number 11 | used_amount: number 12 | request_count: number 13 | created_at: number 14 | expired_at: number 15 | accessed_at: number 16 | } 17 | 18 | export interface TokensResponse { 19 | tokens: Token[] 20 | total: number 21 | } 22 | 23 | export interface TokenCreateRequest { 24 | name: string 25 | } 26 | 27 | export interface TokenStatusRequest { 28 | status: number 29 | } -------------------------------------------------------------------------------- /web/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export const ENV = { 2 | NODE_ENV: import.meta.env.MODE || 'development', 3 | isDevelopment: import.meta.env.DEV, 4 | isProduction: import.meta.env.PROD, 5 | API_BASE_URL: import.meta.env.VITE_API_BASE_URL, 6 | API_TIMEOUT: import.meta.env.VITE_API_TIMEOUT, 7 | } -------------------------------------------------------------------------------- /web/src/validation/auth.ts: -------------------------------------------------------------------------------- 1 | // src/validation/auth.ts 2 | import { z } from 'zod' 3 | 4 | export const loginSchema = z.object({ 5 | token: z 6 | .string() 7 | .min(1, { message: '请输入Token' }) 8 | .min(6, { message: 'Token长度不能少于6个字符' }) 9 | }) 10 | 11 | export type LoginFormValues = z.infer -------------------------------------------------------------------------------- /web/src/validation/channel.ts: -------------------------------------------------------------------------------- 1 | // src/validation/channel.ts 2 | import { z } from 'zod' 3 | 4 | export const channelCreateSchema = z.object({ 5 | type: z.number().min(1, '厂商不能为空'), 6 | name: z.string().min(1, '名称不能为空'), 7 | key: z.string().min(1, '密钥不能为空'), 8 | base_url: z.string().min(1, '代理地址不能为空'), 9 | models: z.array(z.string()).min(1, '至少选择一个模型'), 10 | model_mapping: z.record(z.string(), z.string()).optional() 11 | }) 12 | 13 | export type ChannelCreateForm = z.infer -------------------------------------------------------------------------------- /web/src/validation/model.ts: -------------------------------------------------------------------------------- 1 | // src/validation/model.ts 2 | import { z } from 'zod' 3 | 4 | export const modelCreateSchema = z.object({ 5 | model: z.string().min(1, 'Model name is required'), 6 | type: z.number().min(0, 'Type is required'), 7 | }) 8 | 9 | export type ModelCreateForm = z.infer -------------------------------------------------------------------------------- /web/src/validation/token.ts: -------------------------------------------------------------------------------- 1 | // src/validation/token.ts 2 | import { z } from 'zod' 3 | 4 | export const tokenCreateSchema = z.object({ 5 | name: z.string() 6 | .min(1, '名称不能为空') 7 | .regex(/^[a-zA-Z0-9_]+$/, '名称只能包含字母、数字和下划线') 8 | .max(20, '名称长度不能超过20个字符'), 9 | }) 10 | 11 | export type TokenCreateForm = z.infer -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": [ 6 | "./src/*" 7 | ] 8 | }, 9 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 10 | "target": "ES2020", 11 | "useDefineForClassFields": true, 12 | "lib": [ 13 | "ES2020", 14 | "DOM", 15 | "DOM.Iterable" 16 | ], 17 | "module": "ESNext", 18 | "skipLibCheck": true, 19 | /* Bundler mode */ 20 | "moduleResolution": "bundler", 21 | "allowImportingTsExtensions": true, 22 | "isolatedModules": true, 23 | "moduleDetection": "force", 24 | "noEmit": true, 25 | "jsx": "react-jsx", 26 | /* Linting */ 27 | "strict": true, 28 | "noUnusedLocals": true, 29 | "noUnusedParameters": true, 30 | "noFallthroughCasesInSwitch": true, 31 | "noUncheckedSideEffectImports": true 32 | }, 33 | "include": [ 34 | "src" 35 | ] 36 | } -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ], 11 | "compilerOptions": { 12 | "baseUrl": ".", 13 | "paths": { 14 | "@/*": [ 15 | "./src/*" 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import tailwindcss from "@tailwindcss/vite" 3 | import { defineConfig } from 'vite' 4 | import react from '@vitejs/plugin-react-swc' 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), tailwindcss()], 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "./src"), 12 | }, 13 | }, 14 | }) 15 | --------------------------------------------------------------------------------