From 8ae7dcb84ab2349306c157188e80517a5d61be41 Mon Sep 17 00:00:00 2001 From: Ethan Liu Date: Sat, 12 Aug 2023 00:29:37 +0800 Subject: [PATCH] feat: Replaced Google search with Serper API --- .env.local.demo | 7 +- CHANGE_LOG.md | 4 +- CHANGE_LOG.zh_CN.md | 4 +- package.json | 4 +- pnpm-lock.yaml | 109 ++++---- src/app/api/azure/function_call.ts | 341 +++++-------------------- src/app/api/azure/regular.ts | 173 ++++--------- src/app/api/azure/route.ts | 151 +---------- src/app/api/azure/types.ts | 7 + src/app/api/openai/function_call.ts | 332 ++++-------------------- src/app/api/openai/regular.ts | 170 ++++-------- src/app/api/openai/route.ts | 18 +- src/app/api/openai/types.ts | 8 + src/components/plugin/googleSearch.tsx | 27 +- src/hooks/usePlugin/index.ts | 2 - src/hooks/usePlugin/types.ts | 1 - src/lib/stream/google_search.ts | 38 +++ src/lib/stream/index.ts | 244 ++++++++++++++++++ src/locales/en.json | 5 +- src/locales/zh-CN.json | 5 +- 20 files changed, 614 insertions(+), 1036 deletions(-) create mode 100644 src/app/api/azure/types.ts create mode 100644 src/app/api/openai/types.ts create mode 100644 src/lib/stream/google_search.ts create mode 100644 src/lib/stream/index.ts diff --git a/.env.local.demo b/.env.local.demo index 33bb22b..7499573 100644 --- a/.env.local.demo +++ b/.env.local.demo @@ -39,6 +39,9 @@ NEXTPUBLIC_UMAMI_WEBSITE_ID= # Vercel Cron Secret CRON_SECRET= -# Google Search +# Google Search (Temporarily abandoned) GOOGLE_SEARCH_ENGINE_ID= -GOOGLE_SEARCH_API_KEY= \ No newline at end of file +GOOGLE_SEARCH_API_KEY= + +# SERPER API KEY +SERPER_API_KEY= \ No newline at end of file diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index ebc4c09..d661e35 100755 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -2,11 +2,12 @@ ## v0.8.3 -> 2023-08-11 +> 2023-08-12 ### Fixed - Fixed mobile session content obscuring the bottom input box +- Refactored function calling invocation logic and fixed bugs ### Add @@ -17,6 +18,7 @@ ### Changed - Adjusted the text input box for editing chat content to Textarea +- Replaced Google search with [Serper API](https://serper.dev/), which is easier to configure ## v0.8.2 diff --git a/CHANGE_LOG.zh_CN.md b/CHANGE_LOG.zh_CN.md index 6af0a63..6d1e350 100755 --- a/CHANGE_LOG.zh_CN.md +++ b/CHANGE_LOG.zh_CN.md @@ -2,11 +2,12 @@ ## v0.8.3 -> 2023-08-11 +> 2023-08-12 ### 修复 - 修复移动端会话内容遮挡底部输入框的问题 +- 重构 function calling 的调用逻辑,修复 bug ### 新增 @@ -17,6 +18,7 @@ ### 调整 - 调整编辑聊天内容的文本输入框为 Textarea +- 将谷歌搜索 由官方 API 更换为 [Serper API](https://serper.dev/),配置更方便 ## v0.8.2 diff --git a/package.json b/package.json index 922af4f..8dca62c 100755 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "clsx": "2.0.0", "decimal.js": "10.4.3", "echarts": "5.4.3", - "eslint": "8.46.0", + "eslint": "8.47.0", "eslint-config-next": "13.4.13", "file-saver": "2.0.5", "framer-motion": "10.15.1", @@ -40,7 +40,7 @@ "math-random": "2.0.1", "microsoft-cognitiveservices-speech-sdk": "1.31.0", "next": "13.4.13", - "next-auth": "4.22.5", + "next-auth": "4.23.0", "next-intl": "2.19.1", "next-themes": "0.2.1", "nodemailer": "6.9.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cdd1a5..866277d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ dependencies: version: 0.1.12(@babel/core@7.22.9)(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.3) '@next-auth/prisma-adapter': specifier: 1.0.7 - version: 1.0.7(@prisma/client@5.1.1)(next-auth@4.22.5) + version: 1.0.7(@prisma/client@5.1.1)(next-auth@4.23.0) '@prisma/client': specifier: 5.1.1 version: 5.1.1(prisma@5.1.1) @@ -54,11 +54,11 @@ dependencies: specifier: 5.4.3 version: 5.4.3 eslint: - specifier: 8.46.0 - version: 8.46.0 + specifier: 8.47.0 + version: 8.47.0 eslint-config-next: specifier: 13.4.13 - version: 13.4.13(eslint@8.46.0)(typescript@5.1.6) + version: 13.4.13(eslint@8.47.0)(typescript@5.1.6) file-saver: specifier: 2.0.5 version: 2.0.5 @@ -84,8 +84,8 @@ dependencies: specifier: 13.4.13 version: 13.4.13(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) next-auth: - specifier: 4.22.5 - version: 4.22.5(next@13.4.13)(nodemailer@6.9.4)(react-dom@18.2.0)(react@18.2.0) + specifier: 4.23.0 + version: 4.23.0(next@13.4.13)(nodemailer@6.9.4)(react-dom@18.2.0)(react@18.2.0) next-intl: specifier: 2.19.1 version: 2.19.1(next@13.4.13)(react@18.2.0) @@ -1564,14 +1564,14 @@ packages: resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} dev: false - /@eslint-community/eslint-utils@4.4.0(eslint@8.46.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.47.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.46.0 - eslint-visitor-keys: 3.4.2 + eslint: 8.47.0 + eslint-visitor-keys: 3.4.3 dev: false /@eslint-community/regexpp@4.6.2: @@ -1579,8 +1579,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: false - /@eslint/eslintrc@2.1.1: - resolution: {integrity: sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==} + /@eslint/eslintrc@2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 @@ -1596,8 +1596,8 @@ packages: - supports-color dev: false - /@eslint/js@8.46.0: - resolution: {integrity: sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==} + /@eslint/js@8.47.0: + resolution: {integrity: sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false @@ -1921,14 +1921,14 @@ packages: '@napi-rs/simple-git-win32-x64-msvc': 0.1.8 dev: false - /@next-auth/prisma-adapter@1.0.7(@prisma/client@5.1.1)(next-auth@4.22.5): + /@next-auth/prisma-adapter@1.0.7(@prisma/client@5.1.1)(next-auth@4.23.0): resolution: {integrity: sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==} peerDependencies: '@prisma/client': '>=2.26.0 || >=3' next-auth: ^4 dependencies: '@prisma/client': 5.1.1(prisma@5.1.1) - next-auth: 4.22.5(next@13.4.13)(nodemailer@6.9.4)(react-dom@18.2.0)(react@18.2.0) + next-auth: 4.23.0(next@13.4.13)(nodemailer@6.9.4)(react-dom@18.2.0)(react@18.2.0) dev: false /@next/env@13.4.13: @@ -3389,7 +3389,7 @@ packages: resolution: {integrity: sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==} dev: true - /@typescript-eslint/parser@5.62.0(eslint@8.46.0)(typescript@5.1.6): + /@typescript-eslint/parser@5.62.0(eslint@8.47.0)(typescript@5.1.6): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3403,7 +3403,7 @@ packages: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) debug: 4.3.4 - eslint: 8.46.0 + eslint: 8.47.0 typescript: 5.1.6 transitivePeerDependencies: - supports-color @@ -4797,7 +4797,7 @@ packages: engines: {node: '>=12'} dev: false - /eslint-config-next@13.4.13(eslint@8.46.0)(typescript@5.1.6): + /eslint-config-next@13.4.13(eslint@8.47.0)(typescript@5.1.6): resolution: {integrity: sha512-EXAh5h1yG/YTNa5YdskzaSZncBjKjvFe2zclMCi2KXyTsXha22wB6MPs/U7idB6a2qjpBdbZcruQY1TWjfNMZw==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 @@ -4808,14 +4808,14 @@ packages: dependencies: '@next/eslint-plugin-next': 13.4.13 '@rushstack/eslint-patch': 1.3.2 - '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.1.6) - eslint: 8.46.0 + '@typescript-eslint/parser': 5.62.0(eslint@8.47.0)(typescript@5.1.6) + eslint: 8.47.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.46.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) - eslint-plugin-jsx-a11y: 6.7.1(eslint@8.46.0) - eslint-plugin-react: 7.33.0(eslint@8.46.0) - eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.46.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.47.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.47.0) + eslint-plugin-jsx-a11y: 6.7.1(eslint@8.47.0) + eslint-plugin-react: 7.33.0(eslint@8.47.0) + eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.47.0) typescript: 5.1.6 transitivePeerDependencies: - eslint-import-resolver-webpack @@ -4832,7 +4832,7 @@ packages: - supports-color dev: false - /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.46.0): + /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.47.0): resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -4841,9 +4841,9 @@ packages: dependencies: debug: 4.3.4 enhanced-resolve: 5.15.0 - eslint: 8.46.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) + eslint: 8.47.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.47.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.47.0) get-tsconfig: 4.6.2 globby: 13.2.2 is-core-module: 2.12.1 @@ -4856,7 +4856,7 @@ packages: - supports-color dev: false - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.47.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -4877,16 +4877,16 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.1.6) + '@typescript-eslint/parser': 5.62.0(eslint@8.47.0)(typescript@5.1.6) debug: 3.2.7 - eslint: 8.46.0 + eslint: 8.47.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.46.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.47.0) transitivePeerDependencies: - supports-color dev: false - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0): + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.47.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} peerDependencies: @@ -4896,15 +4896,15 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.1.6) + '@typescript-eslint/parser': 5.62.0(eslint@8.47.0)(typescript@5.1.6) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.46.0 + eslint: 8.47.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.46.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.47.0) has: 1.0.3 is-core-module: 2.12.1 is-glob: 4.0.3 @@ -4919,7 +4919,7 @@ packages: - supports-color dev: false - /eslint-plugin-jsx-a11y@6.7.1(eslint@8.46.0): + /eslint-plugin-jsx-a11y@6.7.1(eslint@8.47.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} engines: {node: '>=4.0'} peerDependencies: @@ -4934,7 +4934,7 @@ packages: axobject-query: 3.2.1 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.46.0 + eslint: 8.47.0 has: 1.0.3 jsx-ast-utils: 3.3.4 language-tags: 1.0.5 @@ -4944,16 +4944,16 @@ packages: semver: 6.3.1 dev: false - /eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.46.0): + /eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.47.0): resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.46.0 + eslint: 8.47.0 dev: false - /eslint-plugin-react@7.33.0(eslint@8.46.0): + /eslint-plugin-react@7.33.0(eslint@8.47.0): resolution: {integrity: sha512-qewL/8P34WkY8jAqdQxsiL82pDUeT7nhs8IsuXgfgnsEloKCT4miAV9N9kGtx7/KM9NH/NCGUE7Edt9iGxLXFw==} engines: {node: '>=4'} peerDependencies: @@ -4963,7 +4963,7 @@ packages: array.prototype.flatmap: 1.3.1 array.prototype.tosorted: 1.1.1 doctrine: 2.1.0 - eslint: 8.46.0 + eslint: 8.47.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.4 minimatch: 3.1.2 @@ -4990,15 +4990,20 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false - /eslint@8.46.0: - resolution: {integrity: sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==} + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /eslint@8.47.0: + resolution: {integrity: sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.47.0) '@eslint-community/regexpp': 4.6.2 - '@eslint/eslintrc': 2.1.1 - '@eslint/js': 8.46.0 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.47.0 '@humanwhocodes/config-array': 0.11.10 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -5009,7 +5014,7 @@ packages: doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.2 + eslint-visitor-keys: 3.4.3 espree: 9.6.1 esquery: 1.5.0 esutils: 2.0.3 @@ -5042,7 +5047,7 @@ packages: dependencies: acorn: 8.10.0 acorn-jsx: 5.3.2(acorn@8.10.0) - eslint-visitor-keys: 3.4.2 + eslint-visitor-keys: 3.4.3 dev: false /esprima@4.0.1: @@ -7076,8 +7081,8 @@ packages: engines: {node: '>= 0.6'} dev: false - /next-auth@4.22.5(next@13.4.13)(nodemailer@6.9.4)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-zPVEpqDp4cx1y0HbonCSrz2sA4qw0grTMd/S0PezUKXvDzmVemtsJnfNK/xo5pO2sz5ilM541+EVCTp9QoRLbA==} + /next-auth@4.23.0(next@13.4.13)(nodemailer@6.9.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-RgukcJkBdvsJwEfA+B80Wcowvtgy6tk8KKWffb7CMCdzcLO4fCCA6aB6sp/DZ2I0ISvWGnbVcO5KXmlan71igw==} peerDependencies: next: ^12.2.5 || ^13 nodemailer: ^6.6.5 diff --git a/src/app/api/azure/function_call.ts b/src/app/api/azure/function_call.ts index 300fcff..5869522 100644 --- a/src/app/api/azure/function_call.ts +++ b/src/app/api/azure/function_call.ts @@ -1,293 +1,67 @@ -import { ResErr, isUndefined, sleep, function_call_maps } from "@/lib"; -import { calcTokens } from "@/lib/calcTokens"; +import { ResErr, isUndefined, function_call_maps } from "@/lib"; +import { stream } from "@/lib/stream"; import type { supportModelType } from "@/lib/calcTokens/gpt-tokens"; -import { prisma } from "@/lib/prisma"; -import { BASE_PRICE } from "@/utils/constant"; +import type { IFetchAzureOpenAI } from "./types"; type fn_call = keyof typeof function_call_maps; -interface IFunctionCall { - plugins: fn_call[]; - fetchURL: string; - Authorization: string; - temperature?: number; - max_tokens?: number; +interface IFunctionCall extends IFetchAzureOpenAI { modelLabel: supportModelType; - messages: any[]; - session: any; - headerApiKey?: string | null; + userId?: string; + headerApiKey?: string; } -const stream = async ( - readable: ReadableStream, - writable: WritableStream, - fetchURL: string, - messages: any[], - model: supportModelType, - tools: fn_call[], - Authorization: string, - temperature: number, - max_tokens: number, - userId?: string, - headerApiKey?: string | null -) => { - const reader = readable.getReader(); - const writer = writable.getWriter(); - - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const newline = "\n"; - const delimiter = "\n\n"; - const encodedNewline = encoder.encode(newline); - - let streamBuffer = ""; - let content = ""; - let resultContent = ""; - let is_function_call = false; - let function_call_name = ""; - let function_call_arguments = ""; - - while (true) { - let resultText = ""; - let function_call_result = ""; - const { value, done } = await reader.read(); - if (done) break; - - content += decoder.decode(value); - streamBuffer += decoder.decode(value, { stream: true }); - - const contentLines = content.split(newline).filter((item) => item?.trim()); - const lines = streamBuffer.split(delimiter); - - for (const contentLine of contentLines) { - const message = contentLine.replace(/^data: /, ""); - if (message !== "[DONE]") { - try { - const delta = JSON.parse(message).choices[0].delta; - if (delta.content) resultText += delta.content; - if (delta.function_call) { - is_function_call = true; - function_call_result += delta.function_call.arguments || ""; - } - if (delta.function_call?.name) { - function_call_name = delta.function_call.name; - } - } catch {} - } - } - - if (!is_function_call) { - // Loop through all but the last line, which may be incomplete. - for (let i = 0; i < lines.length - 1; i++) { - await writer.write(encoder.encode(lines[i] + delimiter)); - await sleep(20); - } - } - - resultContent = resultText; - streamBuffer = lines[lines.length - 1]; - function_call_arguments = function_call_result; - } - - // If use own key, no need to calculate tokens - if (userId && !headerApiKey) { - const final = [...messages, { role: "assistant", content: resultContent }]; - - const { usedTokens, usedUSD } = calcTokens(final, model); - - const findUser = await prisma.user.findUnique({ - where: { id: userId }, - }); - - if (findUser) { - const costTokens = findUser.costTokens + usedTokens; - const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); - const availableTokens = - findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); - - await prisma.user.update({ - where: { id: userId }, - data: { - costTokens, - costUSD, - availableTokens, - }, - }); - } - } - - if (!is_function_call) { - if (streamBuffer) await writer.write(encoder.encode(streamBuffer)); - await writer.write(encodedNewline); - await writer.close(); - } else { - const { keyword } = JSON.parse(function_call_arguments); - - const GOOGLE_SEARCH_ENGINE_ID = process.env.GOOGLE_SEARCH_ENGINE_ID; - const GOOGLE_SEARCH_API_KEY = process.env.GOOGLE_SEARCH_API_KEY; - - const url = - "https://www.googleapis.com/customsearch/v1?" + - `key=${GOOGLE_SEARCH_API_KEY}` + - `&cx=${GOOGLE_SEARCH_ENGINE_ID}` + - `&q=${encodeURIComponent(keyword)}`; - - const data = await fetch(url).then((res) => res.json()); - const results = - data.items?.map((item: any) => ({ - title: item.title, - link: item.link, - snippet: item.snippet, - })) ?? []; - - const newMessages = [ - ...messages, - { - role: "assistant", - content: null, - function_call: { - name: function_call_name, - arguments: function_call_arguments, - }, - }, - { - role: "function", - name: function_call_name, - content: JSON.stringify({ result: JSON.stringify(results) }), - }, - ]; - - let function_call_streamBuffer = ""; - let function_call_content = ""; - let function_call_resultContent = ""; - - const function_call_response = await fetch(fetchURL, { - headers: { - "Content-Type": "application/json", - "api-key": Authorization, - "X-Accel-Buffering": "no", - }, - method: "POST", - body: JSON.stringify({ - frequency_penalty: 0, - presence_penalty: 0, - stream: true, - temperature, - max_tokens, - messages: newMessages, - stop: null, - functions: [function_call_maps[tools[0]]], - }), - }); - - const function_call_reader = ( - function_call_response.body as ReadableStream - ).getReader(); - - while (true) { - let function_call_resultText = ""; - const { value, done } = await function_call_reader.read(); - if (done) break; - - function_call_content += decoder.decode(value); - function_call_streamBuffer += decoder.decode(value, { stream: true }); - - const lines = function_call_streamBuffer.split(delimiter); - const contentLines = function_call_content - .split(newline) - .filter((item) => item?.trim()); - - for (const contentLine of contentLines) { - const message = contentLine.replace(/^data: /, ""); - if (message !== "[DONE]") { - try { - const content = JSON.parse(message).choices[0].delta.content; - if (content) function_call_resultText += content; - } catch {} - } - } - - for (let i = 0; i < lines.length - 1; i++) { - await writer.write(encoder.encode(lines[i] + delimiter)); - await sleep(20); - } - - function_call_resultContent = function_call_resultText; - function_call_streamBuffer = lines[lines.length - 1]; - } - - // If use own key, no need to calculate tokens - if (userId && !headerApiKey) { - const final = [ - ...messages, - { role: "assistant", content: function_call_resultContent }, - ]; - - const { usedTokens, usedUSD } = calcTokens(final, model); - - const findUser = await prisma.user.findUnique({ - where: { id: userId }, - }); - - if (findUser) { - const costTokens = findUser.costTokens + usedTokens; - const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); - const availableTokens = - findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); - - await prisma.user.update({ - where: { id: userId }, - data: { - costTokens, - costUSD, - availableTokens, - }, - }); - } - } - - if (function_call_streamBuffer) { - await writer.write(encoder.encode(function_call_streamBuffer)); - } - - await writer.write(encodedNewline); - await writer.close(); - } -}; - -export const function_call = async ({ - plugins, +const fetchAzureOpenAI = async ({ fetchURL, Authorization, temperature, max_tokens, messages, + plugins, +}: IFetchAzureOpenAI & { plugins: fn_call[] }) => { + return await fetch(fetchURL, { + headers: { + "Content-Type": "application/json", + "api-key": Authorization, + "X-Accel-Buffering": "no", + }, + method: "POST", + body: JSON.stringify({ + frequency_penalty: 0, + presence_penalty: 0, + stream: true, + temperature, + max_tokens, + messages, + stop: null, + // Temporary support for individual tools. + functions: [function_call_maps[plugins[0]]], + }), + }); +}; + +export const function_call = async ({ + temperature: p_temperature, + max_tokens: p_max_tokens, + fetchURL, + Authorization, modelLabel, - session, + messages, + plugins, + userId, headerApiKey, -}: IFunctionCall) => { +}: IFunctionCall & { plugins: fn_call[] }) => { try { - const params_temperature = isUndefined(temperature) ? 1 : temperature; - const params_max_tokens = isUndefined(max_tokens) ? 2000 : max_tokens; + const temperature = isUndefined(p_temperature) ? 1 : p_temperature; + const max_tokens = isUndefined(p_max_tokens) ? 2000 : p_max_tokens; - const response = await fetch(fetchURL, { - headers: { - "Content-Type": "application/json", - "api-key": Authorization, - "X-Accel-Buffering": "no", - }, - method: "POST", - body: JSON.stringify({ - frequency_penalty: 0, - presence_penalty: 0, - stream: true, - temperature: params_temperature, - max_tokens: params_max_tokens, - messages, - stop: null, - // Temporary support for individual tools. - functions: [function_call_maps[plugins[0]]], - }), + const response = await fetchAzureOpenAI({ + fetchURL, + Authorization, + temperature, + max_tokens, + messages, + plugins, }); if (response.status !== 200) { @@ -296,23 +70,24 @@ export const function_call = async ({ const { readable, writable } = new TransformStream(); - stream( - response.body as ReadableStream, + stream({ + readable: response.body as ReadableStream, writable, - fetchURL, + userId, + headerApiKey, messages, modelLabel, plugins, + fetchURL, Authorization, - params_temperature, - params_max_tokens, - session?.user.id, - headerApiKey - ); + temperature, + max_tokens, + fetchFn: fetchAzureOpenAI, + }); return new Response(readable, response); } catch (error: any) { - console.log(error, "openai error"); + console.log(error, "azure openai function_call error"); return ResErr({ msg: error?.message || "Error" }); } }; diff --git a/src/app/api/azure/regular.ts b/src/app/api/azure/regular.ts index bb805a0..805b6ca 100644 --- a/src/app/api/azure/regular.ts +++ b/src/app/api/azure/regular.ts @@ -1,140 +1,62 @@ import { ResErr, isUndefined } from "@/lib"; +import { stream } from "@/lib/stream"; import type { supportModelType } from "@/lib/calcTokens/gpt-tokens"; -import { prisma } from "@/lib/prisma"; -import { calcTokens } from "@/lib/calcTokens"; -import { BASE_PRICE } from "@/utils/constant"; +import type { IFetchAzureOpenAI } from "./types"; -interface IRegular { - messages: any[]; - fetchURL: string; - Authorization: string; - temperature?: number; - max_tokens?: number; - modelLabel: supportModelType; - session: any; - headerApiKey?: string | null; +interface IRegular extends IFetchAzureOpenAI { prompt?: string; + model: string; + modelLabel: supportModelType; + userId?: string; + headerApiKey?: string; } -const sleep = (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; - -// support printer mode and add newline -const stream = async ( - readable: ReadableStream, - writable: WritableStream, - messages: any[], - model: supportModelType, - userId?: string, - headerApiKey?: string | null -) => { - const reader = readable.getReader(); - const writer = writable.getWriter(); - - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const newline = "\n"; - const delimiter = "\n\n"; - const encodedNewline = encoder.encode(newline); - - let buffer = ""; - let content = ""; - let resultContent = ""; - while (true) { - let resultText = ""; - let { value, done } = await reader.read(); - if (done) break; - - content += decoder.decode(value); - buffer += decoder.decode(value, { stream: true }); // stream: true is important here,fix the bug of incomplete line - let lines = buffer.split(delimiter); - let contentLines = content.split(newline).filter((item) => item?.trim()); - - // Loop through all but the last line, which may be incomplete. - for (let i = 0; i < lines.length - 1; i++) { - await writer.write(encoder.encode(lines[i] + delimiter)); - await sleep(20); - } - - for (const contentLine of contentLines) { - const message = contentLine.replace(/^data: /, ""); - if (message !== "[DONE]") { - try { - const content = JSON.parse(message).choices[0].delta.content; - if (content) resultText += content; - } catch {} - } - } - - resultContent = resultText; - buffer = lines[lines.length - 1]; - } - - // If use own key, no need to calculate tokens - if (userId && !headerApiKey) { - const final = [...messages, { role: "assistant", content: resultContent }]; - - const { usedTokens, usedUSD } = calcTokens(final, model); - - const findUser = await prisma.user.findUnique({ - where: { id: userId }, - }); - if (findUser) { - const costTokens = findUser.costTokens + usedTokens; - const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); - const availableTokens = - findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); - - await prisma.user.update({ - where: { id: userId }, - data: { - costTokens, - costUSD, - availableTokens, - }, - }); - } - } - - if (buffer) { - await writer.write(encoder.encode(buffer)); - } - - await writer.write(encodedNewline); - await writer.close(); +const fetchAzureOpenAI = async ({ + fetchURL, + Authorization, + temperature, + max_tokens, + messages, +}: IFetchAzureOpenAI) => { + return await fetch(fetchURL, { + headers: { + "Content-Type": "application/json", + "api-key": Authorization, + }, + method: "POST", + body: JSON.stringify({ + frequency_penalty: 0, + presence_penalty: 0, + stream: true, + temperature: isUndefined(temperature) ? 1 : temperature, + max_tokens: isUndefined(max_tokens) ? 2000 : max_tokens, + messages, + stop: null, + }), + }); }; export const regular = async ({ + prompt, messages, fetchURL, Authorization, + model, + modelLabel, temperature, max_tokens, - modelLabel, - session, + userId, headerApiKey, - prompt, }: IRegular) => { if (prompt) messages.unshift({ role: "system", content: prompt }); try { - const response = await fetch(fetchURL, { - headers: { - "Content-Type": "application/json", - "api-key": Authorization, - "X-Accel-Buffering": "no", - }, - method: "POST", - body: JSON.stringify({ - frequency_penalty: 0, - presence_penalty: 0, - stream: true, - temperature: isUndefined(temperature) ? 1 : temperature, - max_tokens: isUndefined(max_tokens) ? 2000 : max_tokens, - messages, - stop: null, - }), + const response = await fetchAzureOpenAI({ + fetchURL, + Authorization, + temperature, + max_tokens, + messages, }); if (response.status !== 200) { @@ -142,17 +64,20 @@ export const regular = async ({ } const { readable, writable } = new TransformStream(); - stream( - response.body as ReadableStream, + + stream({ + readable: response.body as ReadableStream, writable, + userId, + headerApiKey, messages, + model, modelLabel, - session?.user.id, - headerApiKey - ); + }); + return new Response(readable, response); } catch (error: any) { - console.log(error, "openai error"); + console.log(error, "azure openai regular error"); return ResErr({ msg: error?.message || "Error" }); } }; diff --git a/src/app/api/azure/route.ts b/src/app/api/azure/route.ts index b1ba541..0439885 100644 --- a/src/app/api/azure/route.ts +++ b/src/app/api/azure/route.ts @@ -1,116 +1,24 @@ import { headers } from "next/headers"; import { getServerSession } from "next-auth/next"; -import { calcTokens } from "@/lib/calcTokens"; import { authOptions } from "@/utils/plugin/auth"; -import type { supportModelType } from "@/lib/calcTokens/gpt-tokens"; import { prisma } from "@/lib/prisma"; -import { ResErr, isUndefined } from "@/lib"; +import { ResErr } from "@/lib"; import { PREMIUM_MODELS } from "@/hooks/useLLM"; -import { BASE_PRICE } from "@/utils/constant"; import { regular } from "./regular"; import { function_call } from "./function_call"; -const sleep = (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; - -// support printer mode and add newline -const stream = async ( - readable: ReadableStream, - writable: WritableStream, - messages: any[], - model: supportModelType, - userId?: string, - headerApiKey?: string | null -) => { - const reader = readable.getReader(); - const writer = writable.getWriter(); - - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const newline = "\n"; - const delimiter = "\n\n"; - const encodedNewline = encoder.encode(newline); - - let buffer = ""; - let content = ""; - let resultContent = ""; - while (true) { - let resultText = ""; - let { value, done } = await reader.read(); - if (done) { - break; - } - - content += decoder.decode(value); - buffer += decoder.decode(value, { stream: true }); // stream: true is important here,fix the bug of incomplete line - let lines = buffer.split(delimiter); - let contentLines = content.split(newline).filter((item) => item?.trim()); - - // Loop through all but the last line, which may be incomplete. - for (let i = 0; i < lines.length - 1; i++) { - await writer.write(encoder.encode(lines[i] + delimiter)); - await sleep(20); - } - - for (const contentLine of contentLines) { - const message = contentLine.replace(/^data: /, ""); - if (message !== "[DONE]") { - try { - const content = JSON.parse(message).choices[0].delta.content; - if (content) resultText += content; - } catch {} - } - } - - resultContent = resultText; - buffer = lines[lines.length - 1]; - } - - // If use own key, no need to calculate tokens - if (userId && !headerApiKey) { - const final = [...messages, { role: "assistant", content: resultContent }]; - - const { usedTokens, usedUSD } = calcTokens(final, model); - - const findUser = await prisma.user.findUnique({ - where: { id: userId }, - }); - if (findUser) { - const costTokens = findUser.costTokens + usedTokens; - const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); - const availableTokens = - findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); - - await prisma.user.update({ - where: { id: userId }, - data: { - costTokens, - costUSD, - availableTokens, - }, - }); - } - } - - if (buffer) { - await writer.write(encoder.encode(buffer)); - } - - await writer.write(encodedNewline); - await writer.close(); -}; - export async function POST(request: Request) { const session = await getServerSession(authOptions); const headersList = headers(); - const headerApiKey = headersList.get("Authorization"); + const headerApiKey = headersList.get("Authorization") || ""; const ENV_API_KEY = process.env.NEXT_PUBLIC_AZURE_OPENAI_API_KEY; const ENV_API_VERSION = process.env.NEXT_AZURE_OPENAI_API_VERSION || "2023-07-01-preview"; const { + // model 用于接口发送给 OpenAI 或者其他大语言模型的请求参数 model, + // modelLabel 用于 Token 计算 modelLabel, temperature, max_tokens, @@ -164,17 +72,21 @@ export async function POST(request: Request) { const messages = [...chat_list]; + const userId = session?.user.id; + // Without using plugins, we will proceed with a regular conversation. if (!plugins?.length) { return await regular({ + prompt, messages, fetchURL, Authorization, + model, + modelLabel, temperature, max_tokens, - modelLabel, - session, - prompt, + userId, + headerApiKey, }); } @@ -182,46 +94,11 @@ export async function POST(request: Request) { plugins, fetchURL, Authorization, + modelLabel, temperature, max_tokens, - modelLabel, messages, - session, + userId, + headerApiKey, }); - - if (prompt) messages.unshift({ role: "system", content: prompt }); - - try { - const response = await fetch(fetchURL, { - headers: { - "Content-Type": "application/json", - "api-key": Authorization, - "X-Accel-Buffering": "no", - }, - method: "POST", - body: JSON.stringify({ - frequency_penalty: 0, - max_tokens: isUndefined(max_tokens) ? 2000 : max_tokens, - messages, - presence_penalty: 0, - stop: null, - stream: true, - temperature: isUndefined(temperature) ? 1 : temperature, - }), - }); - - const { readable, writable } = new TransformStream(); - stream( - response.body as ReadableStream, - writable, - messages, - modelLabel, - session?.user.id, - headerApiKey - ); - return new Response(readable, response); - } catch (error: any) { - console.log(error, "openai error"); - return ResErr({ msg: error?.message || "Error" }); - } } diff --git a/src/app/api/azure/types.ts b/src/app/api/azure/types.ts new file mode 100644 index 0000000..b2fcc04 --- /dev/null +++ b/src/app/api/azure/types.ts @@ -0,0 +1,7 @@ +export interface IFetchAzureOpenAI { + messages: any[]; + fetchURL: string; + Authorization: string; + temperature?: number; + max_tokens?: number; +} diff --git a/src/app/api/openai/function_call.ts b/src/app/api/openai/function_call.ts index f6ad1da..4e40dbd 100644 --- a/src/app/api/openai/function_call.ts +++ b/src/app/api/openai/function_call.ts @@ -1,290 +1,68 @@ -import { ResErr, isUndefined, sleep, function_call_maps } from "@/lib"; -import { calcTokens } from "@/lib/calcTokens"; +import { ResErr, isUndefined, function_call_maps } from "@/lib"; import type { supportModelType } from "@/lib/calcTokens/gpt-tokens"; -import { prisma } from "@/lib/prisma"; -import { BASE_PRICE } from "@/utils/constant"; +import { stream } from "@/lib/stream"; +import type { IFetchOpenAI } from "./types"; type fn_call = keyof typeof function_call_maps; -interface IFunctionCall { +interface IFunctionCall extends IFetchOpenAI { plugins: fn_call[]; - fetchURL: string; - Authorization: string; - model: supportModelType; - temperature?: number; - max_tokens?: number; modelLabel: supportModelType; - messages: any[]; - session: any; - headerApiKey?: string | null; + userId?: string; + headerApiKey?: string; } -const stream = async ( - readable: ReadableStream, - writable: WritableStream, - fetchURL: string, - messages: any[], - model: supportModelType, - tools: fn_call[], - Authorization: string, - temperature: number, - max_tokens: number, - userId?: string, - headerApiKey?: string | null -) => { - const reader = readable.getReader(); - const writer = writable.getWriter(); - - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - const newline = "\n"; - const delimiter = "\n\n"; - const encodedNewline = encoder.encode(newline); - - let streamBuffer = ""; - let content = ""; - let resultContent = ""; - let is_function_call = false; - let function_call_name = ""; - let function_call_arguments = ""; - - while (true) { - let resultText = ""; - let function_call_result = ""; - const { value, done } = await reader.read(); - if (done) break; - - content += decoder.decode(value); - streamBuffer += decoder.decode(value, { stream: true }); - - const contentLines = content.split(newline).filter((item) => item?.trim()); - const lines = streamBuffer.split(delimiter); - - for (const contentLine of contentLines) { - const message = contentLine.replace(/^data: /, ""); - if (message !== "[DONE]") { - try { - const delta = JSON.parse(message).choices[0].delta; - if (delta.content) resultText += delta.content; - if (delta.function_call) { - is_function_call = true; - function_call_result += delta.function_call.arguments || ""; - } - if (delta.function_call?.name) { - function_call_name = delta.function_call.name; - } - } catch {} - } - } - - if (!is_function_call) { - // Loop through all but the last line, which may be incomplete. - for (let i = 0; i < lines.length - 1; i++) { - await writer.write(encoder.encode(lines[i] + delimiter)); - await sleep(20); - } - } - - resultContent = resultText; - streamBuffer = lines[lines.length - 1]; - function_call_arguments = function_call_result; - } - - // If use own key, no need to calculate tokens - if (userId && !headerApiKey) { - const final = [...messages, { role: "assistant", content: resultContent }]; - - const { usedTokens, usedUSD } = calcTokens(final, model); - - const findUser = await prisma.user.findUnique({ - where: { id: userId }, - }); - - if (findUser) { - const costTokens = findUser.costTokens + usedTokens; - const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); - const availableTokens = - findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); - - await prisma.user.update({ - where: { id: userId }, - data: { - costTokens, - costUSD, - availableTokens, - }, - }); - } - } - - if (!is_function_call) { - if (streamBuffer) await writer.write(encoder.encode(streamBuffer)); - await writer.write(encodedNewline); - await writer.close(); - } else { - const { keyword } = JSON.parse(function_call_arguments); - - const GOOGLE_SEARCH_ENGINE_ID = process.env.GOOGLE_SEARCH_ENGINE_ID; - const GOOGLE_SEARCH_API_KEY = process.env.GOOGLE_SEARCH_API_KEY; - - const url = - "https://www.googleapis.com/customsearch/v1?" + - `key=${GOOGLE_SEARCH_API_KEY}` + - `&cx=${GOOGLE_SEARCH_ENGINE_ID}` + - `&q=${encodeURIComponent(keyword)}`; - - const data = await fetch(url).then((res) => res.json()); - const results = - data.items?.map((item: any) => ({ - title: item.title, - link: item.link, - snippet: item.snippet, - })) ?? []; - - const newMessages = [ - ...messages, - { - role: "assistant", - content: null, - function_call: { - name: function_call_name, - arguments: function_call_arguments, - }, - }, - { - role: "function", - name: function_call_name, - content: JSON.stringify({ result: JSON.stringify(results) }), - }, - ]; - - let function_call_streamBuffer = ""; - let function_call_content = ""; - let function_call_resultContent = ""; - - const function_call_response = await fetch(fetchURL, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${Authorization}`, - }, - method: "POST", - body: JSON.stringify({ - stream: true, - model, - temperature, - max_tokens, - messages: newMessages, - functions: [function_call_maps[tools[0]]], - }), - }); - - const function_call_reader = ( - function_call_response.body as ReadableStream - ).getReader(); - - while (true) { - let function_call_resultText = ""; - const { value, done } = await function_call_reader.read(); - if (done) break; - - function_call_content += decoder.decode(value); - function_call_streamBuffer += decoder.decode(value, { stream: true }); - - const lines = function_call_streamBuffer.split(delimiter); - const contentLines = function_call_content - .split(newline) - .filter((item) => item?.trim()); - - for (const contentLine of contentLines) { - const message = contentLine.replace(/^data: /, ""); - if (message !== "[DONE]") { - try { - const content = JSON.parse(message).choices[0].delta.content; - if (content) function_call_resultText += content; - } catch {} - } - } - - for (let i = 0; i < lines.length - 1; i++) { - await writer.write(encoder.encode(lines[i] + delimiter)); - await sleep(20); - } - - function_call_resultContent = function_call_resultText; - function_call_streamBuffer = lines[lines.length - 1]; - } - - // If use own key, no need to calculate tokens - if (userId && !headerApiKey) { - const final = [ - ...messages, - { role: "assistant", content: function_call_resultContent }, - ]; - - const { usedTokens, usedUSD } = calcTokens(final, model); - - const findUser = await prisma.user.findUnique({ - where: { id: userId }, - }); - - if (findUser) { - const costTokens = findUser.costTokens + usedTokens; - const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); - const availableTokens = - findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); - - await prisma.user.update({ - where: { id: userId }, - data: { - costTokens, - costUSD, - availableTokens, - }, - }); - } - } - - if (function_call_streamBuffer) { - await writer.write(encoder.encode(function_call_streamBuffer)); - } - - await writer.write(encodedNewline); - await writer.close(); - } +const fetchOpenAI = async ({ + fetchURL, + Authorization, + model, + temperature, + max_tokens, + messages, + plugins, +}: IFetchOpenAI & { plugins: fn_call[] }) => { + return await fetch(fetchURL, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${Authorization}`, + }, + method: "POST", + body: JSON.stringify({ + stream: true, + model, + temperature, + max_tokens, + messages, + // Temporary support for individual tools. + functions: [function_call_maps[plugins[0]]], + }), + }); }; export const function_call = async ({ plugins, + messages, fetchURL, Authorization, model, - temperature, - max_tokens, - messages, modelLabel, - session, + temperature: p_temperature, + max_tokens: p_max_tokens, + userId, headerApiKey, }: IFunctionCall) => { try { - const params_temperature = isUndefined(temperature) ? 1 : temperature; - const params_max_tokens = isUndefined(max_tokens) ? 2000 : max_tokens; + const temperature = isUndefined(p_temperature) ? 1 : p_temperature; + const max_tokens = isUndefined(p_max_tokens) ? 2000 : p_max_tokens; - const response = await fetch(fetchURL, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${Authorization}`, - }, - method: "POST", - body: JSON.stringify({ - stream: true, - model, - temperature: params_temperature, - max_tokens: params_max_tokens, - messages, - // Temporary support for individual tools. - functions: [function_call_maps[plugins[0]]], - }), + const response = await fetchOpenAI({ + fetchURL, + Authorization, + model, + temperature, + max_tokens, + messages, + plugins, }); if (response.status !== 200) { @@ -293,23 +71,25 @@ export const function_call = async ({ const { readable, writable } = new TransformStream(); - stream( - response.body as ReadableStream, + stream({ + readable: response.body as ReadableStream, writable, - fetchURL, + userId, + headerApiKey, messages, + model, modelLabel, plugins, + fetchURL, Authorization, - params_temperature, - params_max_tokens, - session?.user.id, - headerApiKey - ); + temperature, + max_tokens, + fetchFn: fetchOpenAI, + }); return new Response(readable, response); } catch (error: any) { - console.log(error, "openai error"); + console.log(error, "openai function_call error"); return ResErr({ msg: error?.message || "Error" }); } }; diff --git a/src/app/api/openai/regular.ts b/src/app/api/openai/regular.ts index 38c33a5..6ce4125 100644 --- a/src/app/api/openai/regular.ts +++ b/src/app/api/openai/regular.ts @@ -1,141 +1,61 @@ import { ResErr, isUndefined } from "@/lib"; +import { stream } from "@/lib/stream"; import type { supportModelType } from "@/lib/calcTokens/gpt-tokens"; -import { prisma } from "@/lib/prisma"; -import { calcTokens } from "@/lib/calcTokens"; -import { BASE_PRICE } from "@/utils/constant"; +import type { IFetchOpenAI } from "./types"; -interface IRegular { - messages: any[]; - fetchURL: string; - Authorization: string; - model: string; - temperature?: number; - max_tokens?: number; - modelLabel: supportModelType; - session: any; - headerApiKey?: string | null; +interface IRegular extends IFetchOpenAI { prompt?: string; + modelLabel: supportModelType; + userId?: string; + headerApiKey?: string; } -const sleep = (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; - -// support printer mode and add newline -const stream = async ( - readable: ReadableStream, - writable: WritableStream, - messages: any[], - model: supportModelType, - userId?: string, - headerApiKey?: string | null -) => { - const reader = readable.getReader(); - const writer = writable.getWriter(); - - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const newline = "\n"; - const delimiter = "\n\n"; - const encodedNewline = encoder.encode(newline); - - let buffer = ""; - let content = ""; - let resultContent = ""; - while (true) { - let resultText = ""; - let { value, done } = await reader.read(); - if (done) { - break; - } - - content += decoder.decode(value); - buffer += decoder.decode(value, { stream: true }); // stream: true is important here,fix the bug of incomplete line - let lines = buffer.split(delimiter); - let contentLines = content.split(newline).filter((item) => item?.trim()); - - // Loop through all but the last line, which may be incomplete. - for (let i = 0; i < lines.length - 1; i++) { - await writer.write(encoder.encode(lines[i] + delimiter)); - await sleep(20); - } - - for (const contentLine of contentLines) { - const message = contentLine.replace(/^data: /, ""); - if (message !== "[DONE]") { - try { - const content = JSON.parse(message).choices[0].delta.content; - if (content) resultText += content; - } catch {} - } - } - - resultContent = resultText; - buffer = lines[lines.length - 1]; - } - - // If use own key, no need to calculate tokens - if (userId && !headerApiKey) { - const final = [...messages, { role: "assistant", content: resultContent }]; - - const { usedTokens, usedUSD } = calcTokens(final, model); - - const findUser = await prisma.user.findUnique({ - where: { id: userId }, - }); - if (findUser) { - const costTokens = findUser.costTokens + usedTokens; - const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); - const availableTokens = - findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); - - await prisma.user.update({ - where: { id: userId }, - data: { - costTokens, - costUSD, - availableTokens, - }, - }); - } - } - - if (buffer) { - await writer.write(encoder.encode(buffer)); - } - - await writer.write(encodedNewline); - await writer.close(); +const fetchOpenAI = async ({ + fetchURL, + Authorization, + model, + temperature, + max_tokens, + messages, +}: IFetchOpenAI) => { + return await fetch(fetchURL, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${Authorization}`, + }, + method: "POST", + body: JSON.stringify({ + stream: true, + model, + temperature: isUndefined(temperature) ? 1 : temperature, + max_tokens: isUndefined(max_tokens) ? 2000 : max_tokens, + messages, + }), + }); }; export const regular = async ({ + prompt, messages, fetchURL, Authorization, model, + modelLabel, temperature, max_tokens, - modelLabel, - session, + userId, headerApiKey, - prompt, }: IRegular) => { if (prompt) messages.unshift({ role: "system", content: prompt }); try { - const response = await fetch(fetchURL, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${Authorization}`, - }, - method: "POST", - body: JSON.stringify({ - stream: true, - model, - temperature: isUndefined(temperature) ? 1 : temperature, - max_tokens: isUndefined(max_tokens) ? 2000 : max_tokens, - messages, - }), + const response = await fetchOpenAI({ + fetchURL, + Authorization, + model, + temperature, + max_tokens, + messages, }); if (response.status !== 200) { @@ -143,18 +63,20 @@ export const regular = async ({ } const { readable, writable } = new TransformStream(); - stream( - response.body as ReadableStream, + + stream({ + readable: response.body as ReadableStream, writable, + userId, + headerApiKey, messages, + model, modelLabel, - session?.user.id, - headerApiKey - ); + }); return new Response(readable, response); } catch (error: any) { - console.log(error, "openai error"); + console.log(error, "openai regular error"); return ResErr({ msg: error?.message || "Error" }); } }; diff --git a/src/app/api/openai/route.ts b/src/app/api/openai/route.ts index ee594bf..4ce94f6 100644 --- a/src/app/api/openai/route.ts +++ b/src/app/api/openai/route.ts @@ -18,11 +18,13 @@ const getEnvProxyUrl = () => { export async function POST(request: Request) { const session = await getServerSession(authOptions); const headersList = headers(); - const headerApiKey = headersList.get("Authorization"); + const headerApiKey = headersList.get("Authorization") || ""; const NEXT_PUBLIC_OPENAI_API_KEY = process.env.NEXT_PUBLIC_OPENAI_API_KEY; const { + // model 用于接口发送给 OpenAI 或者其他大语言模型的请求参数 model, + // modelLabel 用于 Token 计算 modelLabel, proxy: proxyUrl, temperature, @@ -69,18 +71,21 @@ export async function POST(request: Request) { const messages = [...chat_list]; + const userId = session?.user.id; + // Without using plugins, we will proceed with a regular conversation. if (!plugins?.length) { return await regular({ + prompt, messages, fetchURL, Authorization, model, + modelLabel, temperature, max_tokens, - modelLabel, - session, - prompt, + userId, + headerApiKey, }); } @@ -89,10 +94,11 @@ export async function POST(request: Request) { fetchURL, Authorization, model, + modelLabel, temperature, max_tokens, - modelLabel, messages, - session, + userId, + headerApiKey, }); } diff --git a/src/app/api/openai/types.ts b/src/app/api/openai/types.ts new file mode 100644 index 0000000..f872bb4 --- /dev/null +++ b/src/app/api/openai/types.ts @@ -0,0 +1,8 @@ +export interface IFetchOpenAI { + messages: any[]; + fetchURL: string; + Authorization: string; + model: string; + temperature?: number; + max_tokens?: number; +} diff --git a/src/components/plugin/googleSearch.tsx b/src/components/plugin/googleSearch.tsx index d8d569b..dd009dd 100644 --- a/src/components/plugin/googleSearch.tsx +++ b/src/components/plugin/googleSearch.tsx @@ -9,9 +9,7 @@ export default function GoogleSearch() { const tPlugin = useTranslations("plugin"); const tCommon = useTranslations("common"); - const { enable, engine_id, api_key } = usePluginStore( - (state) => state.google_search - ); + const { enable, api_key } = usePluginStore((state) => state.google_search); const updateGoogleSearch = usePluginStore( (state) => state.updateGoogleSearch @@ -19,28 +17,28 @@ export default function GoogleSearch() { const resetPlugin = useChannelStore((state) => state.resetPlugin); const onChangeEnable = (value: boolean) => { - // if (value && (!engine_id?.trim() || !api_key?.trim())) { + // if (value && !api_key?.trim()) { // return console.log("请先配置"); // } if (!value) resetPlugin("google_search"); onChange("enable", value); }; - const onChange = (key: "enable" | "engine_id" | "api_key", value: any) => { - const params: any = { enable, engine_id, api_key }; + const onChange = (key: "enable" | "api_key", value: any) => { + const params: any = { enable, api_key }; params[key] = value; updateGoogleSearch(params); }; return ( -
-
+
+
{tPlugin("enable-google-search")}
{tPlugin("google-search-free")}
-
{tPlugin("introduction")}
+
{tPlugin("introduction")}
-
{tPlugin("engine-id")}
- onChange("engine_id", value)} - /> -
-
-
{tPlugin("api-key")}
+
{tPlugin("api-key")}
( (set) => ({ google_search: { enable: false, - engine_id: "", api_key: "", }, @@ -36,7 +35,6 @@ export const usePluginInit = () => { const init = () => { const local_google_search = getStorage("google_search") || { enable: false, - engine_id: "", api_key: "", }; diff --git a/src/hooks/usePlugin/types.ts b/src/hooks/usePlugin/types.ts index 49f4d08..4533af1 100644 --- a/src/hooks/usePlugin/types.ts +++ b/src/hooks/usePlugin/types.ts @@ -1,6 +1,5 @@ type GoogleSearch = { enable: boolean; - engine_id: string; api_key: string; }; diff --git a/src/lib/stream/google_search.ts b/src/lib/stream/google_search.ts new file mode 100644 index 0000000..bdb33f4 --- /dev/null +++ b/src/lib/stream/google_search.ts @@ -0,0 +1,38 @@ +export const google_search = async (keyword: string) => { + let result = "No good search result found"; + try { + const res = await fetch("https://google.serper.dev/search", { + method: "POST", + headers: { + "X-API-KEY": process.env.SERPER_API_KEY || "", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + q: keyword, + hl: "zh-cn", + }), + }).then((res) => res.json()); + + let result = "No good search result found"; + + if (res.answerBox && res.answerBox instanceof Array) { + res.answerBox = res.answerBox[0]; + } + + if (res.answerBox?.answer) { + result = res.answerBox.answer; + } else if (res.answerBox?.snippet) { + result = res.answerBox.snippet; + } else if (res.answerBox?.snippetHighlightedWords) { + result = res.answerBox.snippetHighlightedWords[0]; + } else if (res.sports_results?.gameSpotlight) { + result = res.sports_results.gameSpotlight; + } else if (res.knowledge_graph?.description) { + result = res.knowledge_graph.description; + } + + return "search result:\n" + result; + } catch { + return "search result:\n" + result; + } +}; diff --git a/src/lib/stream/index.ts b/src/lib/stream/index.ts new file mode 100644 index 0000000..ebc8049 --- /dev/null +++ b/src/lib/stream/index.ts @@ -0,0 +1,244 @@ +import { calcTokens } from "@/lib/calcTokens"; +import type { supportModelType } from "@/lib/calcTokens/gpt-tokens"; +import { function_call_maps } from "@/lib"; +import { prisma } from "@/lib/prisma"; +import { BASE_PRICE } from "@/utils/constant"; +import { google_search } from "./google_search"; + +type fn_call = keyof typeof function_call_maps; + +interface StreamProps { + readable: ReadableStream; + writable: WritableStream; + userId?: string; + headerApiKey?: string; + messages: any[]; + model?: string; + modelLabel: supportModelType; + plugins?: fn_call[]; + fetchURL?: string; + Authorization?: string; + temperature?: number; + max_tokens?: number; + fetchFn?: any; +} + +const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +export const stream = async ({ + readable, + writable, + userId, + headerApiKey, + messages, + model, + modelLabel, + plugins = [], + fetchURL, + Authorization, + temperature, + max_tokens, + fetchFn, +}: StreamProps) => { + const reader = readable.getReader(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const newline = "\n"; + const delimiter = "\n\n"; + const encodedNewline = encoder.encode(newline); + + let streamBuffer = ""; + let streamContent = ""; + let tokenContext = ""; + + let is_function_call = false; + let function_call_name = ""; + let function_call_arguments = ""; + + while (true) { + let context = ""; + let function_call_result = ""; + // Read until the end tag, then exit the loop. + const { value, done } = await reader.read(); + if (done) break; + + streamBuffer += decoder.decode(value, { stream: true }); + streamContent += decoder.decode(value); + + const contextLines = streamContent + .split(newline) + .filter((item) => item?.trim()); + const streamLines = streamBuffer.split(delimiter); + + for (const contextLine of contextLines) { + const message = contextLine.replace(/^data: /, ""); + if (message !== "[DONE]") { + try { + const delta = JSON.parse(message).choices[0].delta; + if (delta.content) context += delta.content; + if (delta.function_call) { + is_function_call = true; + function_call_result += delta.function_call.arguments || ""; + } + if (delta.function_call?.name) { + function_call_name = delta.function_call.name; + } + } catch {} + } + } + + if (!is_function_call) { + // Loop through all but the last line, which may be incomplete. + for (let i = 0; i < streamLines.length - 1; i++) { + await writer.write(encoder.encode(streamLines[i] + delimiter)); + await sleep(20); + } + } + + tokenContext = context; + streamBuffer = streamLines[streamLines.length - 1]; + function_call_arguments = function_call_result; + } + + // If use own key, no need to calculate tokens + if (userId && !headerApiKey) { + const final = [...messages, { role: "assistant", content: tokenContext }]; + + const { usedTokens, usedUSD } = calcTokens(final, modelLabel); + + const findUser = await prisma.user.findUnique({ + where: { id: userId }, + }); + if (findUser) { + const costTokens = findUser.costTokens + usedTokens; + const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); + const availableTokens = + findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); + + await prisma.user.update({ + where: { id: userId }, + data: { + costTokens, + costUSD, + availableTokens, + }, + }); + } + } + + if (!is_function_call) { + if (streamBuffer) await writer.write(encoder.encode(streamBuffer)); + await writer.write(encodedNewline); + await writer.close(); + } else { + const { keyword } = JSON.parse(function_call_arguments); + const results = await google_search(keyword); + + const newMessages = [ + ...messages, + { + role: "assistant", + content: null, + function_call: { + name: function_call_name, + arguments: function_call_arguments, + }, + }, + { + role: "function", + name: function_call_name, + content: results, + }, + ]; + + let function_call_streamBuffer = ""; + let function_call_content = ""; + let function_call_resultContent = ""; + + const function_call_response = await fetchFn({ + fetchURL, + Authorization, + model, + temperature, + max_tokens, + messages: newMessages, + plugins, + }); + + const function_call_reader = ( + function_call_response.body as ReadableStream + ).getReader(); + + while (true) { + let function_call_resultText = ""; + const { value, done } = await function_call_reader.read(); + if (done) break; + + function_call_content += decoder.decode(value); + function_call_streamBuffer += decoder.decode(value, { stream: true }); + + const lines = function_call_streamBuffer.split(delimiter); + const contentLines = function_call_content + .split(newline) + .filter((item) => item?.trim()); + + for (const contentLine of contentLines) { + const message = contentLine.replace(/^data: /, ""); + if (message !== "[DONE]") { + try { + const content = JSON.parse(message).choices[0].delta.content; + if (content) function_call_resultText += content; + } catch {} + } + } + + for (let i = 0; i < lines.length - 1; i++) { + await writer.write(encoder.encode(lines[i] + delimiter)); + await sleep(20); + } + + function_call_resultContent = function_call_resultText; + function_call_streamBuffer = lines[lines.length - 1]; + } + + // If use own key, no need to calculate tokens + if (userId && !headerApiKey) { + const final = [ + ...messages, + { role: "assistant", content: function_call_resultContent }, + ]; + + const { usedTokens, usedUSD } = calcTokens(final, modelLabel); + + const findUser = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (findUser) { + const costTokens = findUser.costTokens + usedTokens; + const costUSD = Number((findUser.costUSD + usedUSD).toFixed(5)); + const availableTokens = + findUser.availableTokens - Math.ceil(usedUSD * BASE_PRICE); + + await prisma.user.update({ + where: { id: userId }, + data: { + costTokens, + costUSD, + availableTokens, + }, + }); + } + } + + if (function_call_streamBuffer) { + await writer.write(encoder.encode(function_call_streamBuffer)); + } + + await writer.write(encodedNewline); + await writer.close(); + } +}; diff --git a/src/locales/en.json b/src/locales/en.json index 91c2779..faa35ff 100755 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -136,11 +136,10 @@ "check-configure": "Check how to configure.", "config": "Plugin Configuration", "enable-google-search": "Enable Google Search", - "engine-id": "Google Search engine ID", "google-search": "Google Search", - "google-search-free": "During the testing period, you can use it for free without configuring an ID and API KEY.", + "google-search-free": "During the testing period, you can use it for free without configuring API KEY.", "introduction": "Google Search Introduction", - "introduction-1": "This plugin allows the AI assistant to search for information in real-time from the internet using Google. Note: You will need to have a Google API key and a Google Custom Search Engine ID in order to use this plugin.", + "introduction-1": "This plugin allows the AI assistant to search for information in real-time from the internet using the Google Search API.", "introduction-2": "Example: When did Twitter change its name to 'X'? How is the weather in Chengdu now?", "plugins": "Plugins" }, diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index c47404e..4386c21 100755 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -136,11 +136,10 @@ "check-configure": "点击查看如何配置", "config": "插件配置", "enable-google-search": "启用谷歌搜索", - "engine-id": "谷歌搜索 引擎 ID", "google-search": "谷歌搜索", - "google-search-free": "测试期间可免费试用,无需配置 ID 和 API KEY", + "google-search-free": "测试期间可免费试用,无需配置 API KEY", "introduction": "谷歌搜索简介", - "introduction-1": "这个插件允许AI助手实时从互联网上使用Google搜索来查找信息。注意:您需要拥有一个Google API密钥和一个Google自定义搜索引擎ID才能使用此插件。", + "introduction-1": "这个插件允许AI助手实时从互联网上使用Google搜索 API 来查找信息。", "introduction-2": "示例用法:Twitter 是何时改名为'X'的?现在成都天气如何?", "plugins": "插件" },